Compare commits

...

649 commits

Author SHA1 Message Date
WebStreamr
720c9bb651
docs: mention PixelServer / pixeldrain limits 2026-03-27 22:10:38 +00:00
webstreamr
f05cf5040c
chore(release): release v0.69.1 (#801) 2026-03-27 22:05:18 +00:00
WebStreamr
65191cfc8d
fix(source): exclude ad videos in HDHub4u 2026-03-27 22:03:14 +00:00
WebStreamr
bd2dec0392
fix(source): remove stray console.log 2026-03-27 21:59:07 +00:00
webstreamr
5f9fbbb30d
chore(release): release v0.69.0 (#799) 2026-03-27 22:48:26 +01:00
WebStreamr
43f04455f1
feat(source): add HDHub4u 2026-03-27 21:42:10 +00:00
WebStreamr
43637a2ff4
refactor(source): extract hd hub link redirect resolver 2026-03-27 20:17:03 +00:00
WebStreamr
b87bf62362
chore: remove duplicate URLs 2026-03-27 19:40:27 +00:00
WebStreamr
89e57d48d9
chore(extractor): detect languages from HubCloud title 2026-03-27 19:30:47 +00:00
renovate[bot]
ad47aecea3
chore(deps): update dependency axios to v1.14.0 (#800)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 19:28:52 +00:00
WebStreamr
f192fd0b75
chore(extractor): detect resolution from HubCloud title 2026-03-27 19:26:31 +00:00
WebStreamr
557efd2079
fix(extractor): don't fail for HubDrive if HubCloud link is missing 2026-03-27 18:02:48 +00:00
webstreamr
373c6c1d4c
chore(release): release v0.68.0 (#798) 2026-03-26 15:57:28 +01:00
WebStreamr
4c7da5dc20
feat: exclude resolution config options 2026-03-26 14:46:07 +00:00
webstreamr
bed5b00300
chore(release): release v0.67.0 (#796) 2026-03-25 10:03:12 +01:00
WebStreamr
e36d460d57
feat(extractor): support FSLv2 server in HubCloud 2026-03-25 09:01:25 +00:00
renovate[bot]
5cffa112ba
chore(deps): update dependency typescript-eslint to v8.57.2 (#795)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 17:59:06 +00:00
webstreamr
66ae60ca44
chore(release): release v0.66.16 (#794) 2026-03-23 18:57:05 +01:00
webstreamr
b60496bf22
docs: remove mention of XDMovies 2026-03-23 18:17:28 +01:00
WebStreamr
ff39c59475
fix(source): remove XDMovies
was fun while it lasted, I won't implement a 15s websocket wait
2026-03-23 16:55:48 +00:00
webstreamr
b6b67568b5
chore(release): release v0.66.15 (#790) 2026-03-23 15:07:39 +00:00
WebStreamr
eecf9c42c5
fix(extractor): hard-code referer for Dropload and SuperVideo for now 2026-03-23 15:06:05 +00:00
renovate[bot]
fc3d61b05a
chore(deps): lock file maintenance (#792)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 00:42:51 +00:00
renovate[bot]
b2b9760445
chore(deps): update dependency cacheable to v2.3.4 (#789)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 01:43:07 +00:00
webstreamr
7afff18629
chore(release): release v0.66.14 (#785) 2026-03-17 20:53:49 +00:00
WebStreamr
ee3effaf00
chore(extractor): add cacheVersion and bump it for HubCloud and HubDrive 2026-03-17 20:51:57 +00:00
WebStreamr
296f0478b0
fix(extractor): reduce HubCloud + HubDrive ttl to 12h 2026-03-17 20:46:17 +00:00
WebStreamr
693a77e73e
fix(extractor): add PixelDrain referer to HubCloud extractor 2026-03-17 20:34:26 +00:00
renovate[bot]
b769cd0aa1
chore(deps): update dependency typescript-eslint to v8.57.1 (#786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-16 17:47:10 +00:00
renovate[bot]
9eca05efc5
chore(deps): lock file maintenance (#784)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-16 01:43:07 +00:00
webstreamr
ca0110cdbc
chore(release): release v0.66.13 (#781) 2026-03-14 12:56:14 +00:00
WebStreamr
66ee72baf0
fix: downgrade sqlite for now 2026-03-14 12:54:37 +00:00
webstreamr
3c08d69bc3
chore(release): release v0.66.12 (#778) 2026-03-14 08:33:01 +00:00
WebStreamr
0f004dc878
fix: require sqlite with alpine fix 2026-03-14 08:31:36 +00:00
renovate[bot]
b7093ed4ae
chore(deps): lock file maintenance (#779)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-14 08:29:13 +00:00
renovate[bot]
20289eaeaf
chore(deps): update dependency slugify to v1.6.8 (#777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-14 00:36:58 +00:00
webstreamr
3b16fe0390
chore(release): release v0.66.11 (#762) 2026-03-13 16:53:54 +01:00
WebStreamr
c78c1ad628
fix(extractor): add missing VOE domain 2026-03-13 15:52:08 +00:00
WebStreamr
8fe4cc5a9b
fix(source): switch to new Frembed URL and resolve link redirects 2026-03-13 15:50:08 +00:00
renovate[bot]
930e8191be
fix(deps): update dependency sqlite3 to v6 (#772)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-13 10:00:31 +01:00
renovate[bot]
2734fb4d0d
chore(deps): update dependency tough-cookie to v6.0.1 (#773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 21:54:29 +00:00
renovate[bot]
b5223148c7
chore(deps): update jest monorepo to v30.3.0 (#767)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-10 02:42:27 +00:00
renovate[bot]
ad3d0b317e
chore(deps): update dependency typescript-eslint to v8.57.0 (#766)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 17:24:18 +00:00
renovate[bot]
e90b5583a7
chore(deps): update dependency express-rate-limit to v8.3.1 (#765)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 15:15:19 +00:00
renovate[bot]
394dc91bfa
chore(deps): lock file maintenance (#764)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 02:14:06 +00:00
renovate[bot]
7de046838d
chore(deps): update dependency @types/node to v24.12.0 (#763)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 02:09:48 +00:00
renovate[bot]
955872a825
chore(deps): update eslint monorepo to v9.39.4 (#761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 22:42:15 +00:00
webstreamr
d17780f0a7
chore(release): release v0.66.10 (#751) 2026-03-06 14:05:01 +01:00
renovate[bot]
737a8063a8
chore(deps): update docker/build-push-action action to v7 (#759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 13:03:13 +00:00
WebStreamr
423abf2479
fix(extractor): use Dropload hosting URL origin as Referer 2026-03-06 13:01:24 +00:00
renovate[bot]
4e436509bf
chore(deps): update dependency @stylistic/eslint-plugin to v5.10.0 (#760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 05:58:20 +00:00
renovate[bot]
f58e0e1f4f
chore(deps): update dependency express-rate-limit to v8.3.0 (#758)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-05 19:17:41 +00:00
renovate[bot]
d2e72b0756
chore(deps): update docker/setup-buildx-action action to v4 (#757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-05 12:08:53 +01:00
renovate[bot]
edebd860aa
chore(deps): update dependency axios-cache-interceptor to v1.12.0 (#755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-05 01:18:54 +00:00
renovate[bot]
6b8363bd9c
chore(deps): update docker/login-action action to v4 (#750)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 13:57:09 +01:00
webstreamr
d0c40ffa00
chore(release): release v0.66.9 (#747) 2026-03-04 12:05:03 +00:00
renovate[bot]
b36e1484eb
chore(deps): update docker/setup-qemu-action action to v4 (#749)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 13:03:46 +01:00
WebStreamr
a5e429fea3
chore: add referral 2026-03-04 09:05:50 +00:00
WebStreamr
6d5870ccb2
docs: use default branch for tests badge 2026-03-02 13:28:27 +00:00
webstreamr
6cc82520a0
chore(release): release v0.66.8 (#746) 2026-03-02 13:14:43 +00:00
WebStreamr
7dfc599de9
fix(extractor): use Dropload playlist headers also internally 2026-03-02 13:02:34 +00:00
WebStreamr
aa820e1f64
docs: add more limitations 2026-03-02 12:59:17 +00:00
WebStreamr
38d0ac5305
docs: add more references 2026-03-02 12:54:32 +00:00
WebStreamr
438387faee
docs: add known issues / limitations section 2026-03-02 12:52:00 +00:00
webstreamr
5a1786d3e5
chore(release): release v0.66.7 (#738) 2026-03-02 11:42:22 +01:00
WebStreamr
bbb80f3cf0
fix(extractor): add referer as proxy header for Dropload 2026-03-02 10:26:46 +00:00
renovate[bot]
8ea6d4fe37
chore(deps): lock file maintenance (#745)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 01:36:34 +00:00
renovate[bot]
282b3d05b3
chore(deps): update dependency @types/node to v24.11.0 (#744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 01:33:17 +00:00
renovate[bot]
168d0280dc
chore(deps): update dependency cacheable to v2.3.3 (#737)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 18:17:33 +00:00
renovate[bot]
50e024a3ea
chore(deps): update dependency axios to v1.13.6 (#736)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 18:13:55 +00:00
webstreamr
e1d582036a
chore(release): release v0.66.6 (#732) 2026-02-26 22:11:16 +01:00
renovate[bot]
354c2731d8
chore(deps): update dependency minimatch to v10.2.4 (#735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 19:50:19 +00:00
renovate[bot]
ce4637ea80
chore(deps): update dependency minimatch to v10.2.3 (#715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 05:50:31 +00:00
renovate[bot]
73ce5f372d
chore(deps): update dependency typescript-eslint to v8.56.1 (#734)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 18:38:21 +00:00
WebStreamr
221fa989f4
chore(fetcher): cache whole flaresolverr solution and also use status from it 2026-02-23 12:08:01 +00:00
webstreamr
a4dfad6235
chore(release): release v0.66.5 (#731) 2026-02-23 13:01:01 +01:00
WebStreamr
ae10810caa
perf: cache all non-error requests for 15m, tune axios caching 2026-02-23 11:57:07 +00:00
WebStreamr
964e979469
fix(source): add support for old XDMovies uploads without URL protector 2026-02-23 11:14:53 +00:00
WebStreamr
556b20e669
fix(extractor): remove StreamUp 2026-02-23 10:58:55 +00:00
renovate[bot]
6c102362fd
chore(deps): lock file maintenance (#730)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 01:12:44 +00:00
webstreamr
49faa5d116
chore(release): release v0.66.4 (#700) 2026-02-21 16:16:16 +01:00
WebStreamr
cf8fb4cfa4
fix(source): handle new XDMovies URL protection 2026-02-21 15:10:11 +00:00
renovate[bot]
3bb7a800e3
chore(deps): update dependency nodemon to v3.1.14 (#728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-21 01:03:22 +00:00
renovate[bot]
26b6143152
chore(deps): update eslint monorepo to v9.39.3 (#727)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 14:13:46 +00:00
renovate[bot]
728ed7ec06
chore(deps): update dependency nodemon to v3.1.13 (#726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 02:29:35 +00:00
renovate[bot]
5bfbd94e51
chore(deps): update dependency nodemon to v3.1.12 (#725)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-19 22:36:51 +00:00
renovate[bot]
23db19d073
chore(deps): update dependency glob to v13.0.6 (#724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-19 18:32:50 +00:00
renovate[bot]
bd160eb32c
chore(deps): update dependency @stylistic/eslint-plugin to v5.9.0 (#723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-19 14:16:22 +00:00
renovate[bot]
fb354b4746
chore(deps): update dependency glob to v13.0.5 (#721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 19:57:41 +00:00
renovate[bot]
245dcf6007
chore(deps): update dependency glob to v13.0.4 (#720)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 05:37:00 +00:00
renovate[bot]
94f5c7438d
chore(deps): update dependency typescript-eslint to v8.56.0 (#719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 17:38:37 +00:00
renovate[bot]
d0f3e0f931
chore(deps): lock file maintenance (#718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 01:13:23 +00:00
renovate[bot]
524630d671
chore(deps): update dependency @types/node to v24.10.13 (#717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 01:12:34 +00:00
renovate[bot]
65a0e57ab9
chore(deps): update dependency glob to v13.0.3 (#714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-12 23:33:37 +00:00
renovate[bot]
eccea520b7
chore(deps): update dependency glob to v13.0.2 (#711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 17:53:29 +00:00
renovate[bot]
7ec9c5b147
chore(deps): update dependency typescript-eslint to v8.55.0 (#710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 18:33:43 +00:00
renovate[bot]
1ae12fa0f7
chore(deps): update dependency @stylistic/eslint-plugin to v5.8.0 (#709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 11:04:50 +00:00
renovate[bot]
ccdd4600bb
chore(deps): lock file maintenance (#708)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 01:13:41 +00:00
renovate[bot]
3f35ecf6f3
chore(deps): update dependency @types/node to v24.10.12 (#707)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 01:12:39 +00:00
renovate[bot]
de13209709
chore(deps): update dependency axios to v1.13.5 (#705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 13:10:52 +00:00
WebStreamr
34500ea3fc
chore(extractor): adapt mixdrop host check 2026-02-06 14:04:40 +00:00
renovate[bot]
72c191fbc1
chore(deps): update dependency glob to v13.0.1 (#698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 18:54:11 +00:00
webstreamr
8c2897a0b1
chore(release): release v0.66.3 (#697) 2026-02-03 09:58:35 +00:00
WebStreamr
31da8aa92c
perf(fetcher): simplify FlareSolverr integration by just using a request per host 2026-02-03 09:57:09 +00:00
webstreamr
92c45d553e
chore(release): release v0.66.2 (#696) 2026-02-03 08:51:58 +01:00
WebStreamr
ae5829dcb0
perf(fetcher): improve FlareSolverr integration
- multiple sessions which are destroyed after being idle for 60 minutes
- fix timeout handling
- pass cookies to share between sessions
- disable media
2026-02-03 07:41:02 +00:00
webstreamr
3f5a82d663
chore(release): release v0.66.1 (#695) 2026-02-02 15:39:00 +00:00
WebStreamr
2612860f15
fix(source): make XDMovies a fallback source to avoid hubcloud and many duplicates 2026-02-02 15:37:15 +00:00
webstreamr
e6adf6549a
chore(release): release v0.66.0 (#693) 2026-02-02 14:44:23 +00:00
WebStreamr
c23ffa382d
fix(fetcher): never block flaresolverr timeouts 2026-02-02 14:42:53 +00:00
WebStreamr
1f4d81bfc9
feat(source): add XDMovies 2026-02-02 14:42:53 +00:00
renovate[bot]
455d84d5b1
chore(deps): lock file maintenance (#692)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 01:14:33 +00:00
webstreamr
72c19e7f59
chore(release): release v0.65.10 (#684) 2026-01-28 18:13:53 +00:00
WebStreamr
e52cee2125
chore(extractor): increase StreamEmbed TTL to 3d 2026-01-28 18:12:03 +00:00
renovate[bot]
5c87c5d9e2
chore(deps): update dependency axios to v1.13.4 (#689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 19:35:09 +00:00
renovate[bot]
8f8ecb868e
chore(deps): update dependency typescript-eslint to v8.54.0 (#687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 19:36:45 +00:00
WebStreamr
a876c9b144
chore(extractor): increase Fsst TTL to 3 hours 2026-01-26 19:33:10 +00:00
WebStreamr
2ba6e4fa35
fix(extractor): avoid caching results with empty meta, add some self-healing 2026-01-26 14:12:10 +00:00
WebStreamr
50a7e5559b
refactor(extractor): introduce internal type to reduce common code 2026-01-26 13:47:55 +00:00
WebStreamr
db860c25c6
fix(extractor): ensure every result has a ttl 2026-01-26 13:37:26 +00:00
renovate[bot]
074311107a
chore(deps): lock file maintenance (#686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 00:33:39 +00:00
renovate[bot]
19a73b3866
chore(deps): update dependency axios to v1.13.3 (#685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-25 18:30:04 +00:00
WebStreamr
75c9509793
chore(extractor): add new VOE domain 2026-01-25 07:02:47 +00:00
webstreamr
bc98d64969
chore(release): release v0.65.9 (#679) 2026-01-23 22:14:09 +00:00
WebStreamr
a90d7b1cad
fix(extractor): use full cached meta to avoid invalid cache states
The lazy extraction does not specify meta the same way the stream
resolver does. this leads to partial meta being inserted in both the
short-term and long-term cache and consecutive requests have then
missing meta info like e.g. countries
2026-01-23 22:12:50 +00:00
renovate[bot]
43e16f5bfa
chore(deps): update dependency cheerio to v1.2.0 (#683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 19:02:36 +00:00
renovate[bot]
622b1b3bcc
chore(deps): update dependency @stylistic/eslint-plugin to v5.7.1 (#682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 14:48:29 +00:00
WebStreamr
12ea135feb
refactor(cache): extract clearCache function 2026-01-22 15:16:03 +00:00
renovate[bot]
e5197fdb8b
chore(deps): update dependency @keyv/sqlite to v4.0.8 (#677)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 15:03:48 +00:00
WebStreamr
a5539bcf59
refactor(cache): make code more dry and return in-memory cache for test env 2026-01-22 15:01:00 +00:00
renovate[bot]
d3db3fc512
chore(deps): update dependency axios-cache-interceptor to v1.11.4 (#678)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-21 10:13:10 +00:00
webstreamr
9945ed4615
chore(release): release v0.65.8 (#675) 2026-01-20 21:13:14 +00:00
WebStreamr
28a83e2f29
chore: npm audit fix 2026-01-20 21:11:27 +00:00
WebStreamr
93fe1ae871
chore: add env var for one-time cache file deletion
kind of a hack to be able to do that on environments without FS access
2026-01-20 21:10:29 +00:00
WebStreamr
1ddc8d99d6
fix(extractor): never use external URL results for lazy URL caching to avoid clashes 2026-01-20 20:57:01 +00:00
WebStreamr
3c979e77cc
chore(extractor): add lauradaydo.com to VOE 2026-01-20 20:52:19 +00:00
GSTAR
a00fa2c3d6
fix(extractor): use MediaFlow Proxy for DoodStream (#676)
Co-authored-by: WebStreamr <210764791+webstreamr@users.noreply.github.com>
2026-01-20 21:49:37 +01:00
renovate[bot]
7c886583c9
chore(deps): update dependency typescript-eslint to v8.53.1 (#674)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 18:03:42 +00:00
webstreamr
92a4ed5578
chore(release): release v0.65.7 (#673) 2026-01-19 12:01:50 +01:00
WebStreamr
e8ed29eb9d
fix: pass empty array instead of undefined if meta has no countries 2026-01-19 10:58:49 +00:00
webstreamr
ef2cc7f16b
chore(release): release v0.65.6 (#670) 2026-01-19 09:42:17 +00:00
WebStreamr
646e75d5f8
fix: log all unhandled errors inside next handlers, terminate app properly for unhandled error outside 2026-01-19 09:40:28 +00:00
WebStreamr
d259587491
fix(extractor): use correct referer for rgshows, treat vidzee response as block 2026-01-19 09:12:45 +00:00
renovate[bot]
7c99e4d1c1
chore(deps): lock file maintenance (#669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 01:31:30 +00:00
renovate[bot]
aeb15d8640
chore(deps): update dependency @types/node to v24.10.9 (#668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 01:30:43 +00:00
webstreamr
771d664e5a
chore(release): release v0.65.5 (#665) 2026-01-18 13:22:45 +00:00
WebStreamr
7b708e2385
chore: add priority for sorting, move VixSrc up and RgShows down 2026-01-18 13:21:17 +00:00
WebStreamr
a9c281c8c4
fix(extractor): only use bytes and height from meta cache to avoid clashes 2026-01-18 12:57:52 +00:00
renovate[bot]
8aa1344fa5
chore(deps): update dependency cacheable to v2.3.2 (#667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 20:33:42 +00:00
webstreamr
5fd62028ce
chore: add issue templates 2026-01-17 11:38:19 +01:00
WebStreamr
5b49dd0735
chore(fetcher): tune FlareSolverr timeouts and use other cookie variants too 2026-01-16 19:53:18 +00:00
webstreamr
7bb38fda3e
chore(release): release v0.65.4 (#664) 2026-01-16 14:23:59 +01:00
WebStreamr
507b96014c
chore(extractor): simplify cache expiration handling 2026-01-16 13:21:38 +00:00
WebStreamr
986db2cc67
chore: extract HubDrive handling into own extractor re-using HubCloud for better caching, increase both TTLs 2026-01-16 13:16:07 +00:00
webstreamr
546dee9799
chore(release): release v0.65.3 (#663) 2026-01-16 12:47:27 +01:00
WebStreamr
4cfbf731b8
fix(fetcher): more FlareSolverr related stats, fix clearance cookie expiration 2026-01-16 11:45:49 +00:00
WebStreamr
231a12f685
docs: add tmp mount for persistent cache to Docker run example 2026-01-16 10:09:29 +00:00
WebStreamr
e54b54bfb3
docs: mention FlareSolverr support / env var 2026-01-16 10:09:25 +00:00
webstreamr
58966ccff1
chore(release): release v0.65.2 (#659) 2026-01-16 10:36:11 +01:00
WebStreamr
59c84952ed
fix(fetcher): make FlareSolverr work again, use single session and pass proxy 2026-01-16 09:03:19 +00:00
renovate[bot]
90c56debfe
chore(deps): update dependency @tsconfig/node24 to v24.0.4 (#662)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 06:16:54 +00:00
WebStreamr
9cf9c4498c
fix: remove leftover logging 2026-01-15 21:01:10 +00:00
webstreamr
31ca9597e5
chore(release): release v0.65.1 (#658) 2026-01-15 20:35:32 +00:00
WebStreamr
17e70c169b
fix(extractor): use milliseconds instead of incorrect seconds for ttl
🤦‍♂️
2026-01-15 20:33:45 +00:00
WebStreamr
79c8e7fc32
fix(source): be resilient against missing heights in 4KHDHub 2026-01-15 20:05:03 +00:00
WebStreamr
26278aadba
fix(extractor): use lazy url/meta cache also for extractors without a ttl 2026-01-15 19:48:34 +00:00
WebStreamr
5ef6119294
fix: calculate fallback count properly, only use VidSrc when no other normal source provided results 2026-01-15 19:43:55 +00:00
webstreamr
96c4b1f16b
chore(release): release v0.65.0 (#651) 2026-01-15 15:05:59 +01:00
WebStreamr
4cf08fc6d5
feat: add fetcher http stats 2026-01-15 14:03:51 +00:00
WebStreamr
bad1170421
feat: introduce fallback sources and mark VidSrc and RgShows as such
fallback sources are only considered when not enough results are found.
the idea is to use it for sources which are very useful as fallback but
not everytime either because the quality is not good or querying them is
resource intensive.
2026-01-15 14:03:03 +00:00
WebStreamr
7593d3d62e
chore: make cache immutable, add rate limit 2026-01-15 13:37:28 +00:00
WebStreamr
ce11a80ba9
fix(extractor): make VixSrc fall back to multi instead of it if language cannot be determined 2026-01-15 09:49:21 +00:00
webstreamr
5103be82a4
chore(release): release v0.64.0 (#649) 2026-01-14 14:52:35 +01:00
WebStreamr
9ee91dff31
feat(extractor): add back VidSrc
This reverts commit f5f47403cc.
2026-01-14 13:38:09 +00:00
WebStreamr
381fb89cfb
feat(source): bring back VidSrc as multi source
This reverts commit 1bd85fb4ad.
2026-01-14 13:11:40 +00:00
webstreamr
9354c0ee9b
chore(release): release v0.63.1 (#648) 2026-01-14 13:02:01 +00:00
WebStreamr
51aa127a33
chore: regularly clean-up expired items from sqlite cache 2026-01-14 13:00:42 +00:00
WebStreamr
9d6d138fff
chore(extractor): make lazyUrlResultCache non-blocking and add stats 2026-01-14 11:58:42 +00:00
WebStreamr
a189cddd37
chore: do not ignore errors on height guessing 2026-01-14 11:51:32 +00:00
WebStreamr
f7cf9da686
fix(source): make 4KHDHub title matching just a tiny bit less strict
To make the guy happy who wants to sleep with my mother
2026-01-14 11:15:40 +00:00
webstreamr
d00cc8a94a
chore(release): release v0.63.0 (#641) 2026-01-12 19:58:06 +00:00
renovate[bot]
20ce2ac831
chore(deps): lock file maintenance (#640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: WebStreamr <210764791+webstreamr@users.noreply.github.com>
2026-01-12 19:56:35 +00:00
WebStreamr
c6a6a70eaf
feat: introduce lazy extract for all non-HLS streams 2026-01-12 19:48:59 +00:00
renovate[bot]
9e256186aa
chore(deps): update dependency typescript-eslint to v8.53.0 (#645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 19:48:59 +00:00
WebStreamr
f047287fe9
feat: introduce generic meta cache 2026-01-12 19:48:54 +00:00
renovate[bot]
816123ab83
chore(deps): update dependency @types/node to v24.10.7 (#639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 01:49:56 +00:00
webstreamr
815d3b6b1e
chore(release): release v0.62.3 (#638) 2026-01-11 20:35:58 +01:00
WebStreamr
05b80a2d63
chore: configure more Indian languages, add Tamil and Telugu to 4KHDHub 2026-01-11 19:29:55 +00:00
WebStreamr
de79dc014f
fix(source): RgShows is multi 2026-01-11 19:13:14 +00:00
WebStreamr
1a99a9e99b
chore: enable nonBlocking secondary cache writes 2026-01-11 19:12:09 +00:00
WebStreamr
f49769f3c4
chore(extractor): return only best Fsst result
it's too flaky to deal with all
2026-01-11 19:11:43 +00:00
webstreamr
77cf6767b1
chore(release): release v0.62.2 (#634) 2026-01-10 22:29:01 +00:00
WebStreamr
d1fcd671c9
fix(extractor): support non-HLS streams in RgShows 2026-01-10 22:27:44 +00:00
webstreamr
5cb4944413
chore(release): release v0.62.1 (#633) 2026-01-10 20:44:56 +00:00
WebStreamr
559ee6ed4e
chore(source): simplifly Frembed domain detection again
no clue why, but audin213.com is timing out on the official instance
2026-01-10 20:43:35 +00:00
webstreamr
cc1e96f3cf
chore(release): release v0.62.0 (#632) 2026-01-10 20:30:11 +00:00
WebStreamr
b273e3a71d
chore(source): improve 4KHDHub search matching
- search without year, otherwise incorrect results might be returned
  only
- do a less stricter levenshtein if name is fully included
2026-01-10 20:28:49 +00:00
WebStreamr
66fb0e2e6d
chore: remove all proxy black magic
can be configured per env via PROXY_CONFIG instead
2026-01-10 20:11:03 +00:00
GSTAR
baf5c9143d
feat(extractor): add StreamUP (#621)
Co-authored-by: WebStreamr <210764791+webstreamr@users.noreply.github.com>
2026-01-10 06:39:19 +00:00
WebStreamr
28a8a1554d
chore: do not fail resolution if height guessing fails 2026-01-09 20:00:46 +00:00
webstreamr
515ca0a98c
chore(release): release v0.61.0 (#625) 2026-01-09 19:50:01 +01:00
WebStreamr
dcad7db29c
feat: add RgShows 2026-01-09 15:34:04 +00:00
renovate[bot]
4bec9e556f
chore(deps): update dependency axios-cache-interceptor to v1.11.3 (#626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 15:04:15 +00:00
WebStreamr
da7397fe75
chore: enforce https:// prefix in mediaflow proxy url and support config without scheme to work aroud elfhosted weirdness 2026-01-09 15:00:29 +00:00
renovate[bot]
86eb3d498c
chore(deps): update dependency @stylistic/eslint-plugin to v5.7.0 (#624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 08:50:05 +00:00
webstreamr
ca8caa7795
chore(release): release v0.60.0 (#619) 2026-01-06 20:19:55 +01:00
WebStreamr
f6f71b043b
feat(extractor): switch KinoGer from cf to source, add kinoger.p2pplay.pro support 2026-01-06 19:10:15 +00:00
WebStreamr
281fe00a35
chore(fetcher): disable TLS verification
to better deal with hoster quirks
2026-01-06 19:08:16 +00:00
webstreamr
e5d68c5214
chore(release): release v0.59.1 (#618) 2026-01-06 15:41:23 +00:00
WebStreamr
04f3fc42e0
fix(source): adapt MegaKino domain
they came up with something new which might not work with redirects any
more. let's see..
2026-01-06 15:39:54 +00:00
webstreamr
ed50cb2f6a
chore(release): release v0.59.0 (#612) 2026-01-06 16:23:26 +01:00
WebStreamr
e1e8f37862
feat(extractor): bring back Fsst 2026-01-06 15:20:12 +00:00
WebStreamr
7146c6cf11
fix(source): handle MegaKino relative page links 2026-01-06 14:03:28 +00:00
WebStreamr
41bc53b9da
chore(source): do not filter out any KinoGer links 2026-01-06 13:49:46 +00:00
WebStreamr
bfe8c36e87
chore(extractor): add crystaltreatmenteast.com to VOE 2026-01-06 13:40:07 +00:00
renovate[bot]
430e5b7fce
chore(deps): update dependency typescript-eslint to v8.52.0 (#616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-05 19:01:18 +00:00
renovate[bot]
8d1991ed84
chore(deps): lock file maintenance (#615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-05 02:53:09 +00:00
WebStreamr
c075fe9481
fix(extractor): handle FileLions deleted by administration 2026-01-02 15:00:50 +00:00
webstreamr
7e4891a93f
chore(release): release v0.58.12 (#610) 2026-01-02 10:18:16 +01:00
WebStreamr
f8f62db174
chore(source): determine latest 4KHDHub domain more dynamically 2026-01-02 09:15:30 +00:00
WebStreamr
93a098e438
chore(source): add 4KHDHub test for stranger things s05e08 2026-01-02 08:58:11 +00:00
WebStreamr
1f4d7c752d
chore(source): update 4KHDHub TLD and re-generate fixtures 2026-01-02 08:47:12 +00:00
WebStreamr
9b3c7c76cf
fix: run npm audit fix to fix security issues 2026-01-02 08:38:55 +00:00
webstreamr
ac4cc8a346
chore(release): release v0.58.11 (#598) 2026-01-01 19:08:25 +00:00
WebStreamr
e7b1e2a4fc
fix(fetcher): increase queue limits 5 times 2026-01-01 19:06:25 +00:00
renovate[bot]
f8b63f4cce
chore(deps): update dependency typescript-eslint to v8.51.0 (#608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 21:45:33 +00:00
renovate[bot]
30c60261af
chore(deps): update dependency axios-cache-interceptor to v1.11.2 (#607)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 05:08:12 +00:00
renovate[bot]
9079feebbd
chore(deps): lock file maintenance (#606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 02:13:17 +00:00
renovate[bot]
a7d93590c6
chore(deps): update dependency axios-cache-interceptor to v1.11.1 (#605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-27 16:36:28 +00:00
renovate[bot]
d20c568e85
chore(deps): update dependency typescript-eslint to v8.50.1 (#599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 20:43:02 +00:00
WebStreamr
c8e38d73ff
fix(extractor): add new FileLions domain 2025-12-22 13:35:44 +00:00
WebStreamr
135d90a233
fix(extractor): use last FileMoon iframe url matcher 2025-12-22 13:34:55 +00:00
WebStreamr
9f5ac02ec6
fix(extractor): deal better with cases where video ID URLs end with a slash 2025-12-22 13:32:33 +00:00
WebStreamr
6f24b7928d
fix(test): improve stability of MegaKino test by dirty mocking getFinalRedirectUrl 2025-12-22 13:31:30 +00:00
webstreamr
b5fc29f150
chore(release): release v0.58.10 (#575) 2025-12-22 10:38:44 +00:00
WebStreamr
6cc24c0332
fix(extractor): update FileLion domains, remove kinoger.be 2025-12-22 10:37:16 +00:00
WebStreamr
924265b964
fix(source): determine Frembed domain dynamically 2025-12-22 10:34:18 +00:00
WebStreamr
7264fd2ed4
chore(source): useless debug log 2025-12-22 10:29:14 +00:00
WebStreamr
0610b8556a
fix: return 400 for invalid ID instead of raising Error 2025-12-22 10:21:24 +00:00
renovate[bot]
9fd563450e
chore(deps): replace dependency @tsconfig/node22 with @tsconfig/node24 (#581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: WebStreamr <210764791+webstreamr@users.noreply.github.com>
2025-12-22 10:21:02 +01:00
WebStreamr
ddf4f227b3
fix: use "Mozilla" instead of "Mozilla/5.0" as default user agent 2025-12-22 09:04:36 +00:00
renovate[bot]
36a15f0091
chore(deps): lock file maintenance (#595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 00:38:29 +00:00
renovate[bot]
58bd6250c9
chore(deps): update dependency axios-cache-interceptor to v1.10.0 (#592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 01:16:25 +00:00
renovate[bot]
10081a2276
chore(deps): update dependency cacheable to v2.3.1 (#587)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 02:04:17 +00:00
renovate[bot]
fa6bee4c83
chore(deps): update dependency axios-cache-interceptor to v1.9.1 (#584)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 02:13:07 +00:00
renovate[bot]
40f9f9ec8c
chore(deps): update dependency typescript-eslint to v8.50.0 (#583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 21:52:15 +00:00
renovate[bot]
01649955b1
chore(deps): lock file maintenance (#580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 02:08:33 +00:00
renovate[bot]
5d412b759f
chore(deps): update dependency @types/node to v24.10.4 (#579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 02:07:35 +00:00
renovate[bot]
4f7a9d725e
chore(deps): update eslint monorepo to v9.39.2 (#574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 03:07:21 +00:00
webstreamr
e3ea65d7f4
chore(release): release v0.58.9 (#572) 2025-12-12 17:58:18 +01:00
WebStreamr
d71aa240e7
fix(extractor): use extractorId per HubCloud sub hoster again for better bingeGroup 2025-12-12 16:56:10 +00:00
WebStreamr
e438f3cbbd
fix(extractor): fix HubCloud picking up incorrect FSL URLs 2025-12-12 16:48:46 +00:00
WebStreamr
d44e0a2588
fix(extractor): handle FileMoon with missing height info in packed 2025-12-12 16:26:43 +00:00
WebStreamr
258e35c94d
fix(source): resolve MegaKino dynamically and cache for an hour 2025-12-12 16:12:41 +00:00
webstreamr
d607b7fb6b
chore(release): release v0.58.8 (#560) 2025-12-11 16:21:18 +00:00
WebStreamr
42b9cbe38f
fix(source): update MegaKino domain 2025-12-11 16:19:26 +00:00
WebStreamr
d7edb51698
refactor: add generic source and extractor ids to meta and generalize bingeGroup generation 2025-12-11 16:02:13 +00:00
WebStreamr
1bd85fb4ad
chore(source): remove VidSrc which does not add value atm 2025-12-11 15:47:55 +00:00
renovate[bot]
580845ae77
chore(deps): update dependency typescript-eslint to v8.49.0 (#567)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 20:16:18 +00:00
renovate[bot]
32b49d49a9
chore(deps): update dependency axios-cache-interceptor to v1.9.0 (#566)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 20:12:24 +00:00
renovate[bot]
5ff8a49be5
chore(deps): lock file maintenance (#562)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 01:53:09 +00:00
renovate[bot]
3955820eb9
chore(deps): update dependency winston to v3.19.0 (#559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-07 09:55:19 +00:00
webstreamr
609bd23cd8
chore(release): release v0.58.7 (#557) 2025-12-07 00:06:36 +01:00
WebStreamr
0f7bc7c07f
fix(source): update MegaKino domain 2025-12-06 23:01:39 +00:00
WebStreamr
cf0be4712e
fix(source): return all KinoGer domains, fixes VOE results 2025-12-06 23:01:39 +00:00
WebStreamr
50c5d3a1cd
chore(extractor): add kinoger.ru support to VOE 2025-12-06 23:01:39 +00:00
WebStreamr
8dae887eca
revert: fix(source): remove KinoGer
This reverts commit e5a5c62106.
2025-12-06 23:01:28 +00:00
webstreamr
3be37008b7
chore(release): release v0.58.6 (#548) 2025-12-04 21:31:29 +00:00
WebStreamr
ae00c43106
fix(extractor): handle DoodStream Not Found 2025-12-04 21:29:46 +00:00
renovate[bot]
d7392c09d2
chore(deps): update dependency typescript-eslint to v8.48.1 (#550)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 14:44:16 +00:00
renovate[bot]
f549a32dd2
chore(deps): update dependency express to v5.2.1 (#549)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 22:05:45 +00:00
renovate[bot]
e092d7247d
chore(deps): update dependency express to v5.2.0 (#547)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 19:57:06 +00:00
renovate[bot]
d3dbedf18a
chore(deps): update dependency ts-jest to v29.4.6 (#546)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 19:53:35 +00:00
webstreamr
90ab3d0a31
chore(release): release v0.58.5 (#543) 2025-12-01 12:24:06 +01:00
WebStreamr
cba3c8bbae
chore(extractor): add new VOE domains 2025-12-01 10:57:14 +00:00
WebStreamr
4a14ad7fb0
chore(extractor): match new DoodStream domain 2025-12-01 10:54:00 +00:00
WebStreamr
5c04104ad2
chore: introduce helper for better and central season/episode formatting 2025-12-01 10:51:44 +00:00
WebStreamr
ef2b6721c7
refactor(extractor): move label format handling into central place 2025-12-01 08:20:14 +00:00
renovate[bot]
d7e82ceff3
chore(deps): lock file maintenance (#542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 01:33:44 +00:00
webstreamr
973eb06584
chore(release): release v0.58.4 (#537) 2025-11-30 11:20:36 +01:00
WebStreamr
825d97adc4
chore: noop 2025-11-30 10:17:27 +00:00
webstreamr
8835219c26
chore(release): release v0.58.3 (#536) 2025-11-30 07:31:17 +00:00
WebStreamr
1dd9d324e6
chore: adapt default user agent slightly 2025-11-30 07:29:27 +00:00
WebStreamr
afbc16a3ec
revert: "chore: remove proxy headers in requests"
This reverts commit 6f6a3ae2fd.
2025-11-30 07:24:28 +00:00
webstreamr
98b9d5a81d
chore(release): release v0.58.2 (#533) 2025-11-29 18:31:14 +00:00
WebStreamr
6f6a3ae2fd
chore: remove proxy headers in requests 2025-11-29 18:29:51 +00:00
webstreamr
5300e6c6e7
chore(release): release v0.58.1 (#532) 2025-11-29 17:26:52 +01:00
WebStreamr
9f7155f42e
chore: switch to non-alpine based node image
to hopefully improve the tls fingerprint
2025-11-29 16:14:20 +00:00
webstreamr
362bef9f57
chore(release): release v0.58.0 (#480) 2025-11-29 13:23:10 +00:00
WebStreamr
4cd1e09466
chore(extractor): shorten mediaflow proxy label 2025-11-29 13:21:57 +00:00
WebStreamr
1765311586
chore(source): use more consistent meta title format 2025-11-29 12:54:34 +00:00
WebStreamr
68cfa65135
fix: use correct fetch for FlareSolverr 2025-11-29 12:37:35 +00:00
WebStreamr
b7930014ab
fix: use axios proxy config properly 2025-11-29 12:31:31 +00:00
WebStreamr
67d0508fd7
fix: ensure that VixSrc results have a title 2025-11-28 16:15:44 +00:00
WebStreamr
280abc4dea
chore: do not use proxy for MFP urls 2025-11-28 16:06:44 +00:00
GSTAR
e1c727b7de
Add all domains to streamtape extractoe (#489)
Co-authored-by: webstreamr <210764791+webstreamr@users.noreply.github.com>
2025-11-28 16:56:30 +01:00
WebStreamr
d07d31d94a
fix(source): update frembed domain and add referer for API requests 2025-11-28 15:51:48 +00:00
WebStreamr
09f795b862
feat: switch to axios to avoid Cloudflare TLS fingerprinting issues 2025-11-28 15:51:47 +00:00
WebStreamr
f5f47403cc
chore(extractor): remove non-working VidSrc 2025-11-28 15:27:51 +00:00
WebStreamr
31e46c5c56
fix: npm audit fix 2025-11-27 14:53:50 +00:00
renovate[bot]
219cb4b5f6
chore(deps): update dependency cacheable to v2.3.0 (#526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 02:56:56 +00:00
renovate[bot]
683e26e32a
chore(deps): update actions/checkout action to v6 (#515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 10:12:43 +09:00
renovate[bot]
9bec5b5f3b
chore(deps): update dependency typescript-eslint to v8.48.0 (#525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 17:37:03 +00:00
renovate[bot]
4815054fed
chore(deps): lock file maintenance (#520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 01:06:04 +00:00
renovate[bot]
c50e095781
chore(deps): update dependency @stylistic/eslint-plugin to v5.6.1 (#513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 07:51:47 +00:00
renovate[bot]
b4ba098158
chore(deps): update dependency @stylistic/eslint-plugin to v5.6.0 (#512)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 23:02:34 +00:00
renovate[bot]
836021c57e
chore(deps): update dependency @tsconfig/node22 to v22.0.5 (#511)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 06:17:39 +00:00
renovate[bot]
0a85b0dc1e
chore(deps): update dependency typescript-eslint to v8.47.0 (#510)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 23:34:19 +00:00
renovate[bot]
3b78186702
chore(deps): lock file maintenance (#508)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 01:49:15 +00:00
renovate[bot]
dbbf310661
chore(deps): update dependency @types/node to v24.10.1 (#507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 01:48:15 +00:00
renovate[bot]
7eb760b5d6
chore(deps): update dependency cacheable to v2.2.0 (#506)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 22:47:32 +00:00
renovate[bot]
e2268a325a
chore(deps): update dependency @tsconfig/node22 to v22.0.4 (#504)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 06:11:43 +00:00
renovate[bot]
3f0c661dae
chore(deps): update dependency @tsconfig/node22 to v22.0.3 (#502)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 06:08:08 +00:00
renovate[bot]
7dca36fa83
chore(deps): update dependency nodemon to v3.1.11 (#499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 20:14:01 +00:00
renovate[bot]
159be1cac4
chore(deps): update dependency typescript-eslint to v8.46.4 (#497)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 17:41:26 +00:00
renovate[bot]
e478a45bc0
chore(deps): lock file maintenance (#495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 01:17:33 +00:00
WebStreamr
d80ca63dad
ci: run build and test actions for PRs 2025-11-07 14:01:57 +00:00
renovate[bot]
57bc4640e5
chore(deps): lock file maintenance (#473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 00:40:06 +00:00
renovate[bot]
33dbeb9776
chore(deps): update eslint monorepo to v9.39.1 (#468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 00:37:04 +00:00
renovate[bot]
3c398d4a1d
chore(deps): update dependency typescript-eslint to v8.46.3 (#479)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 20:30:05 +00:00
webstreamr
943f4c3453
chore(release): release v0.57.2 (#471) 2025-11-03 16:32:49 +01:00
WebStreamr
8e1ea10c5a
fix(extractor): fix KinoGer and add new domains 2025-11-03 11:12:14 +00:00
renovate[bot]
a7bc742035
chore(deps): update dependency @types/express to v5.0.5 (#472)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 01:15:06 +00:00
WebStreamr
82b810ed36
chore(extractor): register Vidora extractor 2025-11-02 21:57:50 +00:00
webstreamr
6eceaf474d
chore(release): release v0.57.1 (#470) 2025-11-02 22:56:23 +01:00
WebStreamr
88a1fe10a1
fix(source): switch to new Frembed domain 2025-11-02 21:54:42 +00:00
webstreamr
240504816c
chore(release): release v0.57.0 (#467) 2025-11-01 21:32:46 +01:00
WebStreamr
053dce0729
fix(extractor): remove flaky HubCloud head requests
potentially non-working URLs might be returned now from time to time, but that's still better than not returning working ones because of flakyness or blocking. should also improve performance slightly.

reverts changes from c819dd6
2025-11-01 20:29:47 +00:00
WebStreamr
a36d152b61
fix(extractor): support embed-only VOE urls 2025-11-01 20:22:00 +00:00
WebStreamr
e5a5c62106
fix(source): remove KinoGer
protected via CloudFlare challenge
2025-11-01 19:49:43 +00:00
webstreamr
db25f98251
feat(extractor): add Vidora extractor (#466) 2025-10-31 23:14:48 +01:00
webstreamr
ad7c9cc545
chore(release): release v0.56.8 (#462) 2025-10-29 16:07:14 +01:00
WebStreamr
331e5bd5d1
fix(source): add fallback search for english title to Kokoshka 2025-10-29 13:30:58 +00:00
WebStreamr
c4bf32f991
chore: allow Node 22 still 2025-10-29 13:30:58 +00:00
renovate[bot]
20c43a3ea1
chore(deps): update node.js to v24 2025-10-29 13:30:58 +00:00
WebStreamr
172edaf0fc
fix(source): fine-tune Kokoshka result matching 2025-10-29 13:30:53 +00:00
WebStreamr
4bc5e42e50
chore(extractor): cache not found results for 12 hours 2025-10-29 11:08:15 +00:00
WebStreamr
88b6a5ef45
fix(source): switch to working MegaKino domain 2025-10-29 11:04:08 +00:00
renovate[bot]
5078609c86
chore(deps): update dependency minimatch to v10.1.1 (#461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 03:41:36 +00:00
webstreamr
e6ef4b870a
chore(release): release v0.56.7 (#457) 2025-10-28 15:11:54 +00:00
WebStreamr
68dcab39fb
ci: build also for arm64
Co-authored-by: kapdon <94782486+kapdon@users.noreply.github.com>
2025-10-28 15:10:27 +00:00
WebStreamr
79c16d530d
ci: add workflow_dispatch trigger for build and tests 2025-10-28 15:03:35 +00:00
renovate[bot]
365c2de4e5
chore(deps): update dependency @types/node to v22.18.12 (#456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 01:06:48 +00:00
renovate[bot]
7cabb4d4ab
chore(deps): update dependency @types/express to v5.0.4 (#455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 01:04:00 +00:00
webstreamr
73476779ae
chore(release): release v0.56.6 (#454) 2025-10-24 14:57:43 +00:00
WebStreamr
bd1e184023
fix(extractor): handle Fastream deleted or expired file 2025-10-24 14:56:19 +00:00
webstreamr
cc7b0be150
chore(release): release v0.56.5 (#450) 2025-10-24 16:46:12 +02:00
WebStreamr
e2eaa9143b
chore(extractor): add VOE domain 2025-10-24 14:38:35 +00:00
WebStreamr
23da8dc637
chore(extractor): add KinoGer domains 2025-10-24 14:37:46 +00:00
WebStreamr
6203863523
chore: remove proxyConfig option 2025-10-24 14:33:39 +00:00
WebStreamr
e6d8d5076c
chore: remove noCache option 2025-10-24 14:30:57 +00:00
WebStreamr
cc3d30be2f
chore: add host info of relevant URL to all errors 2025-10-24 14:26:54 +00:00
WebStreamr
77164dfaa7
chore(extractor): do not make any requests for external URLs 2025-10-24 14:09:12 +00:00
WebStreamr
56e72b73fe
chore: cache playlist heights for a month 2025-10-24 14:07:04 +00:00
renovate[bot]
c8b6cf2732
chore(deps): update dependency typescript-eslint to v8.46.2 (#451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 20:48:46 +00:00
renovate[bot]
a9b81d00c0
chore(deps): lock file maintenance (#449)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 01:05:31 +00:00
renovate[bot]
03bb9fea79
chore(deps): update dependency @types/node to v22.18.11 (#448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 01:04:42 +00:00
webstreamr
bb733fca96
chore(release): release v0.56.4 (#443) 2025-10-18 20:10:42 +00:00
WebStreamr
ae7b784783
chore: mark VidSrc as being multi instead of English 2025-10-18 20:09:17 +00:00
WebStreamr
d59658fb7b
chore: switch to new VidSrc domains, pass-through TMDB IDs if given 2025-10-18 19:57:31 +00:00
webstreamr
20a4eb2f94
chore(release): release v0.56.3 (#440) 2025-10-18 18:53:31 +00:00
WebStreamr
ad61be8626
chore(extractor): add goofy-banana.com to Voe 2025-10-18 18:52:16 +00:00
renovate[bot]
209ec2673f
chore(deps): update dependency @stylistic/eslint-plugin to v5.5.0 (#441)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-18 05:47:04 +00:00
renovate[bot]
6fffb57a8d
chore(deps): update dependency cacheable to v2.1.1 (#438)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-18 01:56:36 +00:00
renovate[bot]
5d6a8145c3
chore(deps): update eslint monorepo to v9.38.0 (#439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-18 01:54:04 +00:00
webstreamr
00fcda4f12
chore(release): release v0.56.2 (#436) 2025-10-17 15:04:16 +00:00
WebStreamr
e989fc05d5
fix: only push lates tag, no need for invalid version tags 2025-10-17 15:02:53 +00:00
webstreamr
ea2c128937
chore(release): release v0.56.1 (#435) 2025-10-17 17:00:58 +02:00
WebStreamr
a93799036d
fix: push latest for release tags 2025-10-17 14:57:26 +00:00
webstreamr
ce2fac6c4c
chore(release): release v0.56.0 (#434) 2025-10-17 14:53:05 +00:00
WebStreamr
bcd868850d
chore: full remove Fsst 2025-10-17 14:51:43 +00:00
WebStreamr
eaa95e8cd9
docs: add more infos about proxy config 2025-10-17 14:50:01 +00:00
WebStreamr
a76e7254ff
docs: add info about MediaFlow Proxy 2025-10-17 14:48:03 +00:00
WebStreamr
14da572830
docs: default self hosting docs to official Docker image 2025-10-17 14:40:50 +00:00
WebStreamr
c7b507ca6b
docs: document environment variables 2025-10-17 14:27:19 +00:00
WebStreamr
6f60c49bd1
chore: throw explicit error if required TMDB_ACCESS_TOKEN env var is not set 2025-10-17 14:25:30 +00:00
WebStreamr
0018ccc929
feat: push docker latest build for tags 2025-10-17 14:02:14 +00:00
WebStreamr
177739e7cd
chore: tag 4KHDHub with Indian flag 2025-10-17 13:36:13 +00:00
webstreamr
5bf8096158
chore(release): release v0.55.1 (#433) 2025-10-16 21:01:00 +02:00
WebStreamr
066424e087
fix(source): query TMDB for Albanian info for Kokoshka 2025-10-16 18:57:14 +00:00
webstreamr
7e08a8090c
chore(release): release v0.55.0 (#431) 2025-10-16 14:58:21 +00:00
webstreamr
a4fb495e28
feat: add Albanian source Kokoshka (#430)
Co-authored-by: GSTAR <44748406+GLlgGL@users.noreply.github.com>
2025-10-16 16:57:07 +02:00
webstreamr
9ab69d8461
chore(release): release v0.54.4 (#421) 2025-10-15 20:27:38 +00:00
WebStreamr
1a56d7ef91
fix: add manual imdb -> tmdb mapping for "Monsters: The Lyle and Erik Menendez Story (2024)" 2025-10-15 20:26:16 +00:00
WebStreamr
c88b3798f0
fix(source): switch from imdb to tmdb search for CineHDPlus 2025-10-15 20:16:56 +00:00
WebStreamr
076f853a5b
fix(source): switch from imdb to tmdb search for StreamKiste 2025-10-15 20:16:32 +00:00
WebStreamr
405a8efad3
fix: add manual imdb -> tmdb mapping for "Monster: The Ed Gein Story(2025)" 2025-10-15 19:52:20 +00:00
WebStreamr
6f3233eefd
ci: remove explicit caching of node modules 2025-10-14 11:29:46 +00:00
renovate[bot]
3007698b1b
chore(deps): update actions/setup-node action to v6 (#426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 09:54:26 +00:00
WebStreamr
0830635947
chore: configure devEngines in package.json 2025-10-14 09:49:35 +00:00
renovate[bot]
c7040e9936
chore(deps): update dependency typescript-eslint to v8.46.1 (#425)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 20:31:06 +00:00
WebStreamr
da24e96fb4
chore: fully remove soaper 2025-10-13 10:36:15 +00:00
WebStreamr
a42edcd438
fix(source): remove PrimeWire
they started to limit redirect url resolve requests to one per second
2025-10-13 10:33:16 +00:00
renovate[bot]
cf1f3e9eed
chore(deps): lock file maintenance (#424)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 01:55:30 +00:00
renovate[bot]
c5c4c1a41c
chore(deps): update dependency @types/node to v22.18.10 (#423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 01:54:28 +00:00
renovate[bot]
fbffe3aa67
chore(deps): update dependency ts-jest to v29.4.5 (#422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 14:15:02 +00:00
renovate[bot]
8761eee22f
chore(deps): update dependency @keyv/sqlite to v4.0.6 (#420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 10:01:45 +00:00
webstreamr
068017941b
chore(release): release v0.54.3 (#417) 2025-10-07 22:44:24 +02:00
WebStreamr
a3b90bf19a
chore: increase MFP related timeouts 2025-10-07 20:37:42 +00:00
WebStreamr
22c6754efc
refactor: use .at() instead of .slice() construct 2025-10-07 19:07:42 +00:00
WebStreamr
5e1c3eea24
perf(extractor): do not guess size from mp4 for Uqload 2025-10-07 19:01:41 +00:00
WebStreamr
e11cf8986e
perf(extractor): do not guess size from mp4 for Streamtape 2025-10-07 18:54:36 +00:00
WebStreamr
b8c5c1a4f3
perf(extractor): do not guess size from mp4 for DoodStream 2025-10-07 18:54:36 +00:00
WebStreamr
06e0ba23d7
fix(extractor): support Dropload with missing height/size info 2025-10-07 18:54:36 +00:00
WebStreamr
8fb3320868
perf(extractor): do not guess resolution via MFP playlist for Voe 2025-10-07 18:54:36 +00:00
WebStreamr
b3db546e00
perf(extractor): do not guess resolution via MFP playlist for LuluStream 2025-10-07 18:54:36 +00:00
WebStreamr
312f60660a
perf(extractor): do not guess resolution via MFP playlist for FileMoon 2025-10-07 18:54:36 +00:00
WebStreamr
bab0da2a8f
chore(extractor): use MEDIAFLOW_DEFAULT_INIT where it was forgotten 2025-10-07 18:54:36 +00:00
WebStreamr
a7360b68f0
perf(extractor): do not guess resolution via MFP playlist for FileLions 2025-10-07 18:54:36 +00:00
webstreamr
f1c00e2684
chore(release): release v0.54.2 (#416) 2025-10-07 11:41:49 +00:00
WebStreamr
298ac96558
fix(extractor): do not pass referer for Uqload MFP requests
deals with error_wrong_ip responses we got before
2025-10-07 11:39:47 +00:00
webstreamr
a54ae2b151
chore(release): release v0.54.1 (#415) 2025-10-06 18:56:39 +00:00
WebStreamr
d0958e8771
fix(extractor): handle another LuluStream file not found case 2025-10-06 18:55:08 +00:00
renovate[bot]
8b4aef94be
chore(deps): update dependency typescript-eslint to v8.46.0 (#414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 18:46:53 +00:00
renovate[bot]
c19d46f406
chore(deps): update dependency cacheable to v2.1.0 (#413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 18:44:13 +00:00
webstreamr
5170dde1d6
chore(release): release v0.54.0 (#410) 2025-10-06 13:17:43 +00:00
WebStreamr
c27c1f9541
chore(extractor): add KinoGer domain 2025-10-06 13:16:19 +00:00
WebStreamr
424c2fd1e0
fix(extractor): handle StreamEmbed video is not ready 2025-10-06 12:55:48 +00:00
WebStreamr
bb21bbc7fa
feat(extractor): add new MediaFlow Proxy extractors FileMoon, LuluStream and Voe 2025-10-06 12:52:59 +00:00
WebStreamr
d1493cbb57
fix(extractor): handle FileLions Not Found case 2025-10-06 09:43:42 +00:00
WebStreamr
30a709d619
fix(source): use PrimeWire page instead of API as Referer 2025-10-06 09:32:52 +00:00
WebStreamr
c610a995c2
chore(extractor): work around VidSrc blocking by generating random IPs 2025-10-06 08:23:04 +00:00
WebStreamr
78125c7f37
chore(extractor): add FileLions domain 2025-10-06 08:18:56 +00:00
WebStreamr
7191c59b8a
chore(extractor): re-use and optimise mediaflow default fetch init slightly 2025-10-06 08:00:43 +00:00
WebStreamr
eea8cac018
fix(extractor): remove referer header from Mixdrop 2025-10-06 07:48:55 +00:00
renovate[bot]
5a4f28e50a
chore(deps): lock file maintenance (#408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 01:51:33 +00:00
renovate[bot]
f7bb497432
chore(deps): update dependency @types/node to v22.18.8 (#407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 01:50:56 +00:00
renovate[bot]
b8360405dd
chore(deps): update eslint monorepo to v9.37.0 (#404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 20:36:35 +00:00
webstreamr
753e90bbe2
chore(release): release v0.53.7 (#402) 2025-10-02 15:40:42 +00:00
WebStreamr
ac7d2480ea
fix(extractor): bring back VixSrc language exclusion 2025-10-02 15:39:36 +00:00
webstreamr
26120d1662
chore(release): release v0.53.6 (#398) 2025-10-02 17:13:42 +02:00
WebStreamr
5ac359f1eb
chore(fetcher): retry unexpected exceptions 2025-10-02 15:07:57 +00:00
WebStreamr
34b7fddfc7
chore(extractor): configure new VidSrc TLDs 2025-10-02 15:05:01 +00:00
renovate[bot]
390adea9d4
chore(deps): update dependency cacheable to v2.0.3 (#400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 21:36:50 +00:00
renovate[bot]
7e4657c6ad
chore(deps): update dependency winston to v3.18.3 (#397)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 03:07:10 +00:00
renovate[bot]
04069d83ea
chore(deps): update dependency typescript to v5.9.3 (#396)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 03:05:13 +00:00
webstreamr
0c0c2987e8
chore(release): release v0.53.5 (#395) 2025-09-30 22:17:35 +02:00
WebStreamr
012b1648e1
chore(extractor): make VixSrc work without MediaFlow proxy 2025-09-30 20:13:12 +00:00
WebStreamr
04b8987c30
chore: use concurrency of 4 for mediaflow proxy requests
by default mediaflow also runs with 4 workers, this should avoid
pressure
2025-09-30 19:29:11 +00:00
renovate[bot]
9f46b7fffa
chore(deps): update dependency winston to v3.18.2 (#394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 17:42:37 +00:00
webstreamr
b5d7e95a92
chore(release): release v0.53.4 (#393) 2025-09-30 14:53:57 +00:00
renovate[bot]
c3721c428e
chore(deps): update dependency winston to v3.18.1 (#392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 14:52:29 +00:00
WebStreamr
d6fb4c971b
fix(source): dynamically determine MegaKino base URL 2025-09-30 14:49:26 +00:00
WebStreamr
5466472b21
chore(fetcher): add generic fetch also returning response url 2025-09-30 14:45:47 +00:00
WebStreamr
f0a66e1984
fix(extractor): handle Uqload File Not Found 2025-09-30 08:10:30 +00:00
webstreamr
4598151034
chore(release): release v0.53.3 (#391) 2025-09-29 20:30:16 +00:00
WebStreamr
8f2e8e492f
chore(source): improve Eurostreaming search by trimming some special chars and supporting similarity search 2025-09-29 20:27:49 +00:00
WebStreamr
e327f3c734
chore(source): support similar match search in HomeCine 2025-09-29 20:15:23 +00:00
WebStreamr
20648ae4cd
chore(source): use original title as search fallback in HomeCine and don't enforce year check 2025-09-29 19:50:39 +00:00
renovate[bot]
4bf2cf632b
chore(deps): update dependency typescript-eslint to v8.45.0 (#389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 19:49:12 +00:00
WebStreamr
0879531478
test: fix slugger length check in FetcherMock 2025-09-29 19:46:27 +00:00
webstreamr
4bb8e132f5
chore(release): release v0.53.2 (#386) 2025-09-29 10:51:23 +00:00
WebStreamr
fb689bf7da
fix(source): adapt MegaKino for new search 2025-09-29 10:50:04 +00:00
renovate[bot]
6c44fccf66
chore(deps): lock file maintenance (#385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 01:09:41 +00:00
webstreamr
b670fc7109
chore(release): release v0.53.1 (#383) 2025-09-28 22:48:38 +02:00
WebStreamr
d8a4375783
chore: bring back mediaflow proxy redirect url generation
reverts parts of 25b326ee48
2025-09-28 20:47:01 +00:00
WebStreamr
60530343b4
fix: do not write out label twice if the same for source and extractor 2025-09-28 20:16:54 +00:00
WebStreamr
fcced2e122
chore(fetcher): add referer to fetch logging 2025-09-28 20:15:15 +00:00
WebStreamr
af5c3f2ae6
fix(source): improve Eurostream partial keyword matching 2025-09-28 20:04:38 +00:00
WebStreamr
be7c19a54f
chore: add referer explicitly to sources 2025-09-28 19:49:37 +00:00
renovate[bot]
708d3d785d
chore(deps): update jest monorepo to v30.2.0 (#384)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-28 10:00:15 +00:00
WebStreamr
25b326ee48
chore: rework referer header passing and mediaflow proxy URL creation 2025-09-25 20:01:02 +00:00
webstreamr
447153ca3c
chore(release): release v0.53.0 (#381) 2025-09-25 16:52:50 +02:00
WebStreamr
1fb7d0c66b
feat: add source info 2025-09-25 14:51:26 +00:00
WebStreamr
ae342845f8
fix(source): use iframe src from API as referer for Movix 2025-09-25 14:32:58 +00:00
webstreamr
0ff0758b74
chore: lock to latest node major only (#382) 2025-09-25 09:11:35 +02:00
renovate[bot]
7760c13ea9
chore(deps): update node.js to v22.20.0 (#380)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 18:57:42 +00:00
webstreamr
3757a3514b
chore(release): release v0.52.5 (#379) 2025-09-24 18:55:24 +00:00
WebStreamr
6214df7e8f
fix(source): set correct referer for Movix 2025-09-24 18:53:31 +00:00
WebStreamr
42d46accb6
fix: prefer using source referer for extractors if it exists 2025-09-24 18:53:07 +00:00
WebStreamr
ddc8133c5b
fix: always move errors up and external urls down 2025-09-24 18:34:53 +00:00
WebStreamr
a96d015a0c
refactor(fetcher): introduce json method 2025-09-24 18:34:53 +00:00
WebStreamr
b1244086cd
chore: add web emoji as icon 2025-09-24 18:15:51 +00:00
WebStreamr
da556d3523
chore(extractor): add DoodStream host 2025-09-24 18:06:51 +00:00
WebStreamr
6f412d736b
chore(extractor): add FileLions domain 2025-09-24 18:04:35 +00:00
WebStreamr
4697d150cf
chore(fetcher): double queue limit and timeouts 2025-09-24 18:04:00 +00:00
webstreamr
4c5f5a3514
chore(release): release v0.52.4 (#378) 2025-09-23 08:52:50 +00:00
WebStreamr
0a0bedacb4
chore(fetcher): complete proxy headers 2025-09-23 08:50:56 +00:00
WebStreamr
7e771c2644
revert: Revert "chore(fetcher): try to remove the proxy headers (again)"
This reverts commit 00bd11495f.
2025-09-23 08:32:59 +00:00
renovate[bot]
9709c31bcd
chore(deps): update dependency typescript-eslint to v8.44.1 (#376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 18:33:39 +00:00
renovate[bot]
2e3b6602af
fix(deps): update dependency cacheable to v2.0.2 (#377)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 18:31:07 +00:00
WebStreamr
6d5a511cb4
chore: remove XPrime
backend API responses are encrypted and use webassembly for decryption
2025-09-22 09:30:14 +00:00
renovate[bot]
1a2b40d6f2
chore(deps): lock file maintenance (#374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 02:38:48 +00:00
renovate[bot]
b7714d7d60
chore(deps): update dependency @types/node to v22.18.6 (#373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 02:38:02 +00:00
renovate[bot]
953a7914c9
chore(deps): update dependency @stylistic/eslint-plugin to v5.4.0 (#371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 17:59:00 +00:00
renovate[bot]
092d53ef9d
chore(deps): update eslint monorepo to v9.36.0 (#369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 14:44:16 +00:00
renovate[bot]
48b0924bd8
chore(deps): update dependency ts-jest to v29.4.4 (#368)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 14:41:57 +00:00
webstreamr
a2af1ae21e
chore(release): release v0.52.3 (#363) 2025-09-18 08:54:27 +00:00
renovate[bot]
833c4870f8
chore(deps): update dependency ts-jest to v29.4.3 (#365)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 08:52:59 +00:00
WebStreamr
a30b271a57
fix(source): improve 4KHDHub year and title matching 2025-09-18 08:50:44 +00:00
renovate[bot]
f9d8dc0b86
fix(deps): update dependency cacheable to v2.0.1 (#364)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 19:44:25 +00:00
WebStreamr
60f44c9603
revert: Revert "fix: remove turnstyle detection to check if it causes false-positives"
This reverts commit c10ca29fda.
2025-09-16 19:41:59 +00:00
webstreamr
1433d032f3
chore(release): release v0.52.2 (#360) 2025-09-16 14:50:54 +00:00
WebStreamr
d90cd8f984
fix(fetcher): only retry 503 and 504 2025-09-16 14:49:13 +00:00
WebStreamr
c10ca29fda
fix: remove turnstyle detection to check if it causes false-positives 2025-09-16 13:05:03 +00:00
WebStreamr
c819dd6bb7
fix(extractor): filter out non-accessible HubCloud streams 2025-09-16 12:40:01 +00:00
WebStreamr
d06c2934a8
fix: do not return cache headers when noCache is set 2025-09-16 12:09:42 +00:00
renovate[bot]
261897c699
chore(deps): update dependency typescript-eslint to v8.44.0 (#362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 11:19:48 +00:00
renovate[bot]
6ef80d3166
chore(deps): update dependency ts-jest to v29.4.2 (#361)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 10:43:52 +00:00
WebStreamr
aeb7ce5f54
fix(extractor): normalize Streamtape URLs 2025-09-15 14:02:37 +00:00
renovate[bot]
1028c663eb
chore(deps): lock file maintenance (#359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 00:26:42 +00:00
renovate[bot]
e33aa42b19
chore(deps): update dependency @types/node to v22.18.3 (#358)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 00:25:54 +00:00
webstreamr
48b6f9efb0
chore(release): release v0.52.1 (#357) 2025-09-14 23:12:05 +02:00
WebStreamr
f4d905833f
fix(source): re-implement PrimeWire in safer and more efficient way 2025-09-14 20:51:28 +00:00
webstreamr
65c73e8b6e
chore(release): release v0.52.0 (#355) 2025-09-14 21:31:32 +02:00
WebStreamr
737bf6aa94
feat: allow to disable extractors via env var 2025-09-14 19:30:32 +00:00
WebStreamr
f89d90619a
feat: allow to disable sources via env var 2025-09-14 19:29:22 +00:00
webstreamr
a3c25adc7a
chore(release): release v0.51.2 (#354) 2025-09-14 18:19:23 +00:00
WebStreamr
ae32376758
fix(fetcher): re-use proxy agents 2025-09-14 18:18:09 +00:00
WebStreamr
aded562304
fix(source): close jsdom window immediately after getting HTML 2025-09-14 18:12:41 +00:00
WebStreamr
8b479be6c5
fix: log more infos for uncaught errors 2025-09-14 18:12:28 +00:00
WebStreamr
8acbd37df3
revert: Revert recent reverts again
This reverts commit 78db179957.
2025-09-14 17:38:46 +00:00
webstreamr
cf6946bc4a
chore(release): release v0.51.1 (#351) 2025-09-14 15:40:25 +02:00
WebStreamr
78db179957
revert: Revert "chore(fetcher): log more infos about responses"
This reverts commit 731bd33470.
2025-09-14 13:37:17 +00:00
WebStreamr
9a279b53a4
revert: Revert "feat: allow to disable cache"
This reverts commit 4078f0c404.
2025-09-14 13:37:14 +00:00
WebStreamr
2aa9ad270e
revert: Revert "chore: log error count in final result log"
This reverts commit 2421913587.
2025-09-14 13:37:11 +00:00
WebStreamr
4ca87f0ff1
revert: Revert "chore(fetcher): log proxy usage, allow to disable proxy via config"
This reverts commit b67e1b85d6.
2025-09-14 13:37:08 +00:00
WebStreamr
dc610bfa64
revert: Revert "fix(deps): update dependency jsdom to v27 (#350)"
This reverts commit 58b50b49de.
2025-09-14 13:37:04 +00:00
renovate[bot]
58b50b49de
fix(deps): update dependency jsdom to v27 (#350)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 18:31:17 +00:00
webstreamr
a79b0bdfdd
chore(release): release v0.51.0 (#349) 2025-09-13 20:28:43 +02:00
WebStreamr
b67e1b85d6
chore(fetcher): log proxy usage, allow to disable proxy via config 2025-09-13 18:27:17 +00:00
WebStreamr
2421913587
chore: log error count in final result log 2025-09-13 17:59:21 +00:00
WebStreamr
4078f0c404
feat: allow to disable cache 2025-09-13 17:51:12 +00:00
WebStreamr
731bd33470
chore(fetcher): log more infos about responses 2025-09-13 17:41:28 +00:00
WebStreamr
4351ddc00d
revert: Revert "fix(extractor): use SuperVideo embed URls which are not triggering CF challenges"
This reverts commit 3a2178eb55.
2025-09-12 18:49:00 +00:00
webstreamr
941a61fb74
chore(release): release v0.50.4 (#348) 2025-09-12 20:36:18 +02:00
WebStreamr
a729dc85e0
chore: add url info to blocked error 2025-09-12 18:35:22 +00:00
webstreamr
491595934f
chore(release): release v0.50.3 (#347) 2025-09-12 20:27:19 +02:00
WebStreamr
d087318286
chore(fetcher): cache only 2xx and 404 responses 2025-09-12 18:24:19 +00:00
WebStreamr
5429b0cb7a
chore(extractor): set VidSrc referer again 2025-09-12 18:17:10 +00:00
WebStreamr
69da82b139
chore(fetcher): bring back error retry 2025-09-12 17:53:37 +00:00
WebStreamr
fdb9bff86b
chore: slightly clean-up extractor and source logging 2025-09-12 17:49:14 +00:00
WebStreamr
cb256de9f8
chore(source): disable Soaper 2025-09-12 17:41:18 +00:00
WebStreamr
b27126fd8e
fix(extractor): cache FileLions 2025-09-12 17:39:47 +00:00
webstreamr
b9c13b8a89
chore(release): release v0.50.2 (#346) 2025-09-12 13:22:31 +00:00
WebStreamr
2a47d97d5c
chore(fetcher): switch back to undici fetch 🤡 2025-09-12 13:20:59 +00:00
webstreamr
0f00aecde5
chore(release): release v0.50.1 (#345) 2025-09-12 11:54:32 +02:00
WebStreamr
7ee87ceb9e
chore(fetcher): remove interceptors and retries 2025-09-12 09:50:07 +00:00
webstreamr
81208ab99b
chore(release): release v0.50.0 (#344) 2025-09-12 10:15:35 +02:00
WebStreamr
b513a8926b
feat: add support for proxy per host using minimatch 2025-09-12 08:13:18 +00:00
WebStreamr
2f370d1b9c
refactor: create dispatcher per request 2025-09-12 07:30:27 +00:00
WebStreamr
ffd62a5d16
chore(extractor): remove Referer from follow-up VidSrc requests completely 2025-09-12 07:10:51 +00:00
webstreamr
7249c1fd96
chore(release): release v0.49.7 (#343) 2025-09-11 16:51:45 +02:00
WebStreamr
87893819b0
chore(fetcher): switch back to native fetch 🤡 2025-09-11 14:34:26 +00:00
webstreamr
c29ec82d6a
chore(release): release v0.49.6 (#342) 2025-09-11 15:33:53 +02:00
WebStreamr
98c53a15f2
fix(extractor): do not pass Referer for cloudnestra requests
seems to avoid some blocking interestingly 🙈
2025-09-11 12:42:57 +00:00
webstreamr
d190446187
chore(release): release v0.49.5 (#341) 2025-09-10 19:43:32 +00:00
WebStreamr
7f264b2503
chore: include imdb and tmdb IDs in XPrime requests 2025-09-10 19:42:18 +00:00
webstreamr
128c144ebf
chore(release): release v0.49.4 (#340) 2025-09-10 21:29:47 +02:00
WebStreamr
531ad7562a
fix(extractor): use correct xprime referer 2025-09-10 18:42:01 +00:00
webstreamr
c9e4dfce68
chore(release): release v0.49.3 (#339) 2025-09-10 15:25:51 +02:00
WebStreamr
057b2346dc
chore(extractor): clean-up header/referer passing 2025-09-10 13:21:36 +00:00
WebStreamr
7f2cf10021
fix(extractor): improve messy meta type 2025-09-10 13:21:02 +00:00
webstreamr
904f5de3e0
chore(release): release v0.49.2 (#338) 2025-09-10 11:26:52 +00:00
WebStreamr
00bd11495f
chore(fetcher): try to remove the proxy headers (again) 2025-09-10 11:25:16 +00:00
webstreamr
03b3512380
chore(release): release v0.49.1 (#337) 2025-09-10 10:13:59 +00:00
WebStreamr
c5da7c1997
fix(extractor): grab VixSrc height from playlist 2025-09-10 10:12:41 +00:00
renovate[bot]
8b5588936e
fix(deps): update dependency cacheable to v2 (#336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: WebStreamr <210764791+webstreamr@users.noreply.github.com>
2025-09-10 10:11:52 +00:00
WebStreamr
3a2178eb55
fix(extractor): use SuperVideo embed URls which are not triggering CF challenges
we unfortunately loose size and title info though
2025-09-10 10:01:33 +00:00
renovate[bot]
78a83c536e
fix(deps): update dependency undici to v7.16.0 (#335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 01:31:56 +00:00
webstreamr
17e99101bc
chore(release): release v0.49.0 (#334) 2025-09-09 14:02:59 +02:00
WebStreamr
8430e5d0d1
feat(extractor): add FileLions extractor 2025-09-09 11:51:20 +00:00
renovate[bot]
0bfced503c
chore(deps): update dependency typescript-eslint to v8.43.0 (#333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 18:12:47 +00:00
webstreamr
39681dc7f6
chore(release): release v0.48.0 (#332) 2025-09-08 09:34:47 +00:00
WebStreamr
39850dccf7
feat(extractor): support PixelServer in HubCloud
looks like it will limit download speed after a while, but some sources
only have that one..
2025-09-08 09:33:31 +00:00
webstreamr
327d297d58
chore(release): release v0.47.2 (#331) 2025-09-08 09:12:24 +00:00
WebStreamr
68232363ae
fix(extractor): find more FSL links 2025-09-08 09:11:07 +00:00
webstreamr
41f78b50e7
chore(release): release v0.47.1 (#330) 2025-09-08 09:03:57 +00:00
WebStreamr
c2983f0551
fix(source): handle missing 4KHDHub episodes more gracefully 2025-09-08 09:02:19 +00:00
WebStreamr
6577eda289
chore: add Norwegian, sort language list 2025-09-08 08:50:23 +00:00
webstreamr
ad4eea68ce
chore(release): release v0.47.0 (#325) 2025-09-08 08:33:35 +00:00
WebStreamr
e1b5e7bce1
chore(tmdb): use mutex to avoid double tmdb requests 2025-09-08 08:31:44 +00:00
WebStreamr
3b8481ed3f
chore: add infos about sources and extractors to description 2025-09-08 08:17:12 +00:00
WebStreamr
9f15a03a5a
chore: add more details to /live / ipcheck endpoint, add cloudnestra 2025-09-08 08:04:08 +00:00
WebStreamr
3380185449
chore: drop uuid in favor of node:crypto randomUUID 2025-09-08 07:38:24 +00:00
WebStreamr
b60f91a96a
chore: configure a bunch of more language mappings 2025-09-08 07:35:31 +00:00
WebStreamr
1ecb088b60
feat(extractor): add HubCloud with FSL 2025-09-08 07:35:31 +00:00
WebStreamr
8a1645fb81
feat(source): add 4KHDHub 2025-09-08 07:35:31 +00:00
renovate[bot]
224b241cd6
chore(deps): lock file maintenance (#329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 01:47:05 +00:00
renovate[bot]
907a150574
chore(deps): update dependency @types/node to v22.18.1 (#328)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 01:46:17 +00:00
WebStreamr
48c7735165
fix(extractor): gracefully handle deleted files for SaveFiles 2025-09-06 19:31:21 +00:00
renovate[bot]
55983990fb
chore(deps): update eslint monorepo to v9.35.0 (#324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 16:37:06 +00:00
webstreamr
120f10139c
chore(release): release v0.46.5 (#323) 2025-09-05 08:54:32 +00:00
WebStreamr
b589c25bb5
chore(extractor): do only one VidSrc request per domain at a time 2025-09-05 08:52:57 +00:00
WebStreamr
211babec37
chore(fetcher): wait out short rate limits and retry 2025-09-05 08:52:36 +00:00
WebStreamr
e846900384
fix(fetcher): fix TTL for rate limit cache 2025-09-05 07:35:06 +00:00
webstreamr
ad7444ed97
chore(release): release v0.46.4 (#322) 2025-09-04 11:37:50 +00:00
WebStreamr
af4d0010fb
chore(source): retry also blocking errors with different VidSrc TLD 2025-09-04 11:36:37 +00:00
webstreamr
fde6128757
chore(release): release v0.46.3 (#321) 2025-09-04 10:30:23 +02:00
WebStreamr
cf79473fad
chore: allow forcing the IP check in the /live endpoint 2025-09-04 08:07:55 +00:00
webstreamr
ff62ea0597
chore(release): release v0.46.2 (#319) 2025-09-04 07:43:24 +00:00
WebStreamr
ad7e480213
fix(extractor): remove non-working VidSrc pm TLD 2025-09-04 07:42:16 +00:00
renovate[bot]
139cb64ca5
chore(deps): update actions/setup-node action to v5 (#320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 06:40:25 +02:00
WebStreamr
cfcf81b665
refactor: simplify passing around meta 2025-09-03 19:55:58 +00:00
WebStreamr
4002301fa6
fix(source): VixSrc is not an English source 2025-09-03 17:44:07 +00:00
webstreamr
b0a251c21e
chore(release): release v0.46.1 (#318) 2025-09-03 16:49:34 +02:00
WebStreamr
756cd01ab9
chore: decrease fetcher and extractor memory cache sizes 2025-09-03 14:39:27 +00:00
webstreamr
c7e87e14b9
chore(release): release v0.46.0 (#316) 2025-09-03 15:02:24 +02:00
WebStreamr
fd0900e053
feat: add stats endpoint with cache stats 2025-09-03 13:00:07 +00:00
WebStreamr
5ea34f66dd
fix(source): make cache prop static 2025-09-03 12:48:21 +00:00
WebStreamr
5547fd5486
fix(source): move countryCode filtering into central place 2025-09-03 12:18:56 +00:00
WebStreamr
12ac42ef5f
chore: no special sorting for errors 2025-09-03 11:05:16 +00:00
WebStreamr
89790609db
feat(source): persist PrimeWire redirect URL cache 2025-09-03 11:03:03 +00:00
WebStreamr
2b7e9c403d
feat: configure sqlite as secondary source and extractor cache 2025-09-03 10:56:13 +00:00
WebStreamr
94abcd3f0a
chore: add ipStatus to /live endpoint 2025-09-03 10:29:00 +00:00
WebStreamr
83021fb773
chore(fetcher): decrease timeout to 5 seconds 2025-09-03 10:28:48 +00:00
webstreamr
63ada6099a
chore(release): release v0.45.3 (#315) 2025-09-02 15:08:17 +00:00
WebStreamr
833043143f
chore(extractor): cache YouTube for 6h 2025-09-02 15:06:55 +00:00
WebStreamr
41aacf84db
chore(extractor): cache ExternalUrl for 6h 2025-09-02 15:05:51 +00:00
WebStreamr
1b89cadab5
chore: always return integers for Cache-Control max-age 2025-09-02 14:51:31 +00:00
WebStreamr
969ab8d0d7
chore(extractor): start refreshing results randomly after at least 2/3 of the TTL passed 2025-09-02 14:41:55 +00:00
renovate[bot]
896ec74959
chore(deps): update dependency jest to v30.1.3 (#314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 14:30:31 +00:00
WebStreamr
316fd4621b
chore(extractor): cache KinoGer for 6h 2025-09-02 14:28:45 +00:00
WebStreamr
bb95118d2b
chore(extractor): cache SaveFiles for 6h 2025-09-02 14:28:04 +00:00
WebStreamr
fc17545860
chore(extractor): cache XPrime for 6h 2025-09-02 14:27:23 +00:00
WebStreamr
a250440670
chore(extractor): cache Doodstream for 6h 2025-09-02 14:26:25 +00:00
WebStreamr
1e100e6d2e
chore(extractor): cache Soaper for 12h 2025-09-02 13:48:01 +00:00
WebStreamr
ccf8006f13
chore(extractor): cache Dropload for 3h 2025-09-02 13:40:22 +00:00
WebStreamr
7ac71837b3
chore(extractor): cache SuperVideo for 3h 2025-09-02 13:39:39 +00:00
WebStreamr
c68570d2bc
chore(extractor): cache VidSrc for 3h 2025-09-02 13:38:51 +00:00
WebStreamr
bc06cb5750
chore(source): cache sources for 12h 2025-09-02 13:37:26 +00:00
WebStreamr
dc093f7109
refactor: consistently use cacheable for caching 2025-09-02 13:33:51 +00:00
renovate[bot]
50764b3602
chore(deps): update dependency typescript-eslint to v8.42.0 (#312)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 12:32:14 +00:00
webstreamr
bd38329fd8
chore(release): release v0.45.2 (#310) 2025-09-01 22:12:06 +02:00
WebStreamr
ceb520d49d
fix(source): consider season and episode for cache key 2025-09-01 20:10:30 +00:00
WebStreamr
b47a512587
fix: catch all timeouts 2025-09-01 18:33:42 +00:00
WebStreamr
2468fbde4e
chore(source): remove outdated comment 2025-09-01 18:33:42 +00:00
WebStreamr
e109637e48
refactor: move locks into StreamController 2025-09-01 10:58:05 +00:00
WebStreamr
4d05ac859f
chore: handle NotFoundError / 404 inside source and cache it also 2025-09-01 10:53:59 +00:00
renovate[bot]
285746a4a9
chore(deps): update dependency @stylistic/eslint-plugin to v5.3.1 (#309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 10:37:17 +00:00
webstreamr
8154527533
chore(release): release v0.45.1 (#307) 2025-09-01 12:35:45 +02:00
renovate[bot]
4414324a08
chore(deps): update jest monorepo to v30.1.2 (#308)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 08:20:06 +00:00
WebStreamr
c47f48c64b
chore(fetcher): log retries 2025-09-01 08:17:59 +00:00
WebStreamr
0fc7675560
chore(fetcher): retry timeouts once 2025-08-31 20:12:04 +00:00
WebStreamr
3c4507b198
test: disable console logging to reduce noise 2025-08-31 20:06:33 +00:00
WebStreamr
c556d9500b
chore(extractor): support caching via MediaFlow Proxy 2025-08-31 20:06:33 +00:00
WebStreamr
33ee150915
fix(extractor): set missing viaMediaFlowProxy in Streamtape 2025-08-31 19:22:48 +00:00
WebStreamr
c82a7ab756
chore(source): cache source results for 3 hours 2025-08-31 19:14:43 +00:00
WebStreamr
9da7c07dc6
refactor(source): introduce abstract parent class 2025-08-31 18:52:42 +00:00
renovate[bot]
3044e41d9b
chore(deps): update node.js to v22.19.0 (#306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-31 10:02:09 +00:00
webstreamr
75363c72cf
chore(release): release v0.45.0 (#293) 2025-08-28 21:21:55 +02:00
WebStreamr
18d8ee6b79
chore(extractor): use guessSizeFromMp4 in Streamtape, Uqload and XPrime 2025-08-28 19:17:21 +00:00
WebStreamr
4a1a3b19aa
refactor: introduce guessSizeFromMp4 2025-08-28 19:01:14 +00:00
WebStreamr
cefc80fa55
chore(extractor): move Fetcher into parent, guess missing heights for SuperVideo 2025-08-28 18:58:15 +00:00
renovate[bot]
ada3ff1926
chore(deps): update jest monorepo to v30.1.1 (#305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 19:42:12 +00:00
renovate[bot]
b4dca46e5e
chore(deps): update jest monorepo to v30.1.0 (#304)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 06:52:02 +00:00
renovate[bot]
0449150874
chore(deps): update dependency typescript-eslint to v8.41.0 (#302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 19:07:18 +00:00
renovate[bot]
2381a2c2ad
chore(deps): lock file maintenance (#300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 14:30:00 +00:00
WebStreamr
c8cd2ec794
chore(extractor): add more KinoGer domains 2025-08-25 14:29:58 +00:00
696 changed files with 126992 additions and 35781 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -0,0 +1,23 @@
---
name: Request source/extractor
about: Request a source or extractor to get URLs from
title: Request to add <source/extractor>
labels: enhancement
assignees: ''
---
**What is the site you want to get added?**
A link to the source or embed hoster
**What is the benefit of adding this?**
More content is not a good reason. If it's already covered via an existing source/extractor, why should we add another one?
**Are you aware of any protection the site uses?**
E.g. rate-limits or Cloudflare captchas and such
**What languages does this relate to?**
E.g. multi, German, ..
**Example**
A movie/series IMDb/TMDB ID + source link or embed URL to test with.

28
.github/workflows/build-tag.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Build and push latest
on:
push:
tags:
- 'v*'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Set up QEMU (for cross-arch)
uses: docker/setup-qemu-action@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
-
name: Build and push
uses: docker/build-push-action@v7
with:
push: true
platforms: linux/amd64,linux/arm64
tags: webstreamr/webstreamr:latest

View file

@ -1,15 +1,18 @@
name: Build
on: push
on:
push:
pull_request:
workflow_dispatch:
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
-
name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
push: false
tags: webstreamr/webstreamr:latest

View file

@ -1,26 +1,17 @@
name: Tests
on: push
on:
push:
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "22"
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
node-version: "24"
- name: Install Dependencies
run: npm install

View file

@ -1,3 +1,3 @@
{
".": "0.44.1"
".": "0.69.1"
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
FROM node:22.18-alpine AS builder
FROM node:24 AS builder
WORKDIR /app
COPY package*.json ./
@ -10,7 +10,7 @@ RUN npm run build
RUN npm ci --only=production
FROM node:22.18-alpine
FROM node:24
WORKDIR /app
COPY --from=builder /app/dist ./dist

View file

@ -1,15 +1,47 @@
# WebStreamr
[![Tests](https://github.com/webstreamr/webstreamr/workflows/Tests/badge.svg)](https://github.com/webstreamr/webstreamr/actions/workflows/tests.yml)
[![Tests](https://github.com/webstreamr/webstreamr/actions/workflows/tests.yml/badge.svg)](https://github.com/webstreamr/webstreamr/actions/workflows/tests.yml)
[![GitHub release](https://img.shields.io/github/v/release/webstreamr/webstreamr)](https://github.com/webstreamr/webstreamr/releases)
![GitHub License](https://img.shields.io/github/license/webstreamr/webstreamr)
[Stremio](https://www.stremio.com/) add-on which provides HTTP URLs from streaming websites.
HTTP streams have limitations.
For a better experience, I'd advise using a Debrid service and WebStreamr as fallback.
[TorBox](https://torbox.app/subscription?referral=f22eb00d-27ce-4e20-85fc-68da3d018b99) is working very well.
## Public instance
A public instance is available at https://webstreamr.hayd.uk. Hosting infrastructure for this instance is donated by [ElfHosted](https://elfhosted.com), and independently maintained by [Hayduk](https://hayd.uk).
## Known issues / limitations
- PixelServer / pixeldrain has a daily limit of 6 GB per IP: https://pixeldrain.dev
- Dropload and SuperVideo on Android do not work because Stremio does not use the `Referer` header properly via HLS playlists: https://github.com/Stremio/stremio-bugs/issues/2389, maybe https://github.com/Stremio/stremio-bugs/issues/1579
- MediaFlow proxy has to be used in an inefficient way because Stremio on Android or its players cannot deal with HLS playlist with redirects: https://github.com/Stremio/stremio-bugs/issues/1574
- FlareSolverr cookies cannot be used because Cloudflare does techniques like TLS fingerprinting most likely. But FlareSolverr uses a session per host and should be quick.
- VidSrc works but rate limits heavily and is therefore only queried as fallback if nothing else is found.
- RgShows detects shared usage and blocks IPs. It therefore only works on private instances.
## MediaFlow Proxy
[MediaFlow Proxy](https://github.com/mhdzumair/mediaflow-proxy/) can be added when configuration the add-on to gain access to a couple of more file hosters.
It depends highly on the language / source used if that unlocks more streams or not.
MediaFlow proxy is needed because some hosters ip-lock streams and the add-on does not run on the same device that will stream the video.
The following hosters can be used only with MediaFlow Proxy:
- Fastream
- FileLions
- FileMoon
- LuluStream
- Mixdrop
- Streamtape
- VOE
MediaFlow proxy can either be self-hosted or acquired via bundle from [ElfHosted](https://docs.elfhosted.com/app/mediaflow-proxy/).
## Hosting
Don't want to use the public instance, or concerned about reliability during periods of high use? It's open-source, you can host it yourself!
@ -22,12 +54,64 @@ Self-hosting to stressful? ElfHosted offer [ready-to-go, turn-key WebStreamr ins
### Self-Hosting
You can build an image using the [Dockerfile](./Dockerfile) and run it.
Alternatively, you can also start WebStreamr directly via
You can run the latest WebStreamr via Docker. E.g.
```shell
npm install
npm run build
npm start
docker run \
--detach=true \
--name webstreamr \
--rm \
--pull always \
--publish 51546:51546 \
--env TMDB_ACCESS_TOKEN="YOUR_TOKEN" \
--volume /tmp:/tmp \
webstreamr/webstreamr
```
### Environment variables
#### `CACHE_DIR`
Optional. Directory for persistent caches using SQLite files. Default: OS tmp dir.
#### `CONFIGURATION_DESCRIPTION`
Optional. To customize the description shown on the configuration page.
#### `DISABLED_EXTRACTORS`
Optional. Comma separated list of extractors which should be disabled. E.g. `doodstream,vidsrc`
#### `DISABLED_SOURCES`
Optional. Comma separated list of sources which should be disabled. E.g. `frembed,vidsrc`
#### `FLARESOLVERR_ENDPOINT`
Optional. If domains show Cloudflare challenges, FlareSolverr can be used to work around them. E.g. `http://flaresolverr:8191`
Proxy configuration is passed-through and only a single session is used to save resources. Byparr is not supported.
#### `MANIFEST_ID`
Optional. Add-on manifest ID. Default: `webstreamr`
#### `MANIFEST_NAME`
Optional. Add-on manifest name. Default: `WebStreamr`
#### `PORT`
Optional. Port of the node web server. Default: `51546`
#### `PROXY_CONFIG`
Optional. Proxies which should be used based on domain. Supports minimatch. E.g. `dood.to:http://USERNAME:PASSWORD@IP:PORT,*:socks5://172.17.0.1:1080` would use an http proxy for dood.to and a socks5 proxy for all other domains.
Some hosters are a bit picky when it comes to IPs. Best case is if you use a residential IP.
If you can't do that, then I suggest to use a VPN / proxy like Cloudflare WARP.
DoodStream is not working with WARP.
Free Webshare proxies seem to work with it.
#### `TMDB_ACCESS_TOKEN`
**Required**. TMDB access token to get information like title and year for content. Use the [API Read Access Token](https://www.themoviedb.org/settings/api).

View file

@ -13,6 +13,7 @@ const config: Config = {
coverageDirectory: '<rootDir>/coverage',
coveragePathIgnorePatterns: [
'/src/controller/',
'/src/utils/dispatcher.ts',
],
coverageProvider: 'babel',
coverageThreshold: {

View file

@ -1,5 +1,10 @@
import { socksDispatcher } from 'fetch-socks';
import { Agent, Dispatcher, interceptors, ProxyAgent, setGlobalDispatcher } from 'undici';
process.env['CACHE_DIR'] = '/dev/null';
process.env['HOST'] = 'example.test';
process.env['PROTOCOL'] = 'https';
if (!process.env['TMDB_ACCESS_TOKEN']) {
process.env['TMDB_ACCESS_TOKEN'] = 'some access token';
}
jest.mock('randomstring', () => ({
generate: jest.fn(() => 'mocked-random-string'),
@ -9,19 +14,4 @@ beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => 639837296000);
});
let dispatcher: Dispatcher;
if (process.env['ALL_PROXY']) {
const proxyUrl = new URL(process.env['ALL_PROXY']);
if (proxyUrl.protocol === 'socks5:') {
dispatcher = socksDispatcher({ type: 5, host: proxyUrl.hostname, port: parseInt(proxyUrl.port) }, { allowH2: true });
} else {
dispatcher = new ProxyAgent({ uri: proxyUrl.href, allowH2: true });
}
} else {
dispatcher = new Agent({ allowH2: true });
}
dispatcher.compose(
interceptors.dns(),
interceptors.retry({ maxRetries: 3 }),
);
setGlobalDispatcher(dispatcher);
console.log = console.warn = console.error = console.info = console.debug = () => { /* disable in favor of logger */ };

5520
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "webstreamr",
"description": "Provides HTTP URLs from streaming websites.",
"version": "0.44.1",
"version": "0.69.1",
"type": "commonjs",
"scripts": {
"analyse": "tsc --noEmit --project ./tsconfig.dev.json",
@ -28,37 +28,55 @@
"url": "https://github.com/webstreamr/webstreamr/issues"
},
"engines": {
"node": "^22.0.0"
"node": "^22.0.0 || ^24.0.0"
},
"devEngines": {
"runtime": {
"name": "node",
"onFail": "error"
},
"packageManager": {
"name": "npm",
"onFail": "error"
}
},
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@types/jsdom": "^21.1.7",
"@keyv/sqlite": "^4.0.5",
"async-mutex": "^0.5.0",
"axios": "^1.13.2",
"axios-cache-interceptor": "^1.8.3",
"axios-mock-adapter": "^2.1.0",
"axios-retry": "^4.5.0",
"bytes": "^3.1.2",
"cacheable": "^2.0.0",
"cheerio": "^1.0.0",
"express": "^5.1.0",
"fetch-socks": "^1.3.2",
"http-cache-semantics": "^4.2.0",
"jsdom": "^26.1.0",
"lru-cache": "^11.1.0",
"express-rate-limit": "^8.2.1",
"fast-levenshtein": "^3.0.0",
"glob": "^13.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"memoizee": "^0.4.17",
"minimatch": "^10.0.3",
"randomstring": "^1.3.1",
"rot13-cipher": "^1.0.0",
"slugify": "^1.6.6",
"socks-proxy-agent": "^8.0.5",
"sqlite3": "^5.1.7",
"tough-cookie": "^6.0.0",
"undici": "^7.10.0",
"unpacker": "^1.0.1",
"uuid": "^11.1.0",
"winston": "^3.17.0"
},
"devDependencies": {
"@eslint/js": "^9.26.0",
"@stylistic/eslint-plugin": "^5.0.0",
"@tsconfig/node22": "^22.0.2",
"@tsconfig/node24": "^24.0.0",
"@types/bytes": "^3.1.5",
"@types/express": "^5.0.1",
"@types/http-cache-semantics": "^4.0.4",
"@types/fast-levenshtein": "^0.0.4",
"@types/jest": "^30.0.0",
"@types/make-fetch-happen": "^10.0.4",
"@types/node": "^22.15.3",
"@types/memoizee": "^0.4.12",
"@types/node": "^24.0.0",
"@types/randomstring": "^1.3.0",
"@types/stremio-addon-sdk": "^1.6.11",
"babel-jest": "^30.0.0",

View file

@ -22,7 +22,7 @@ export class ConfigureController {
}
private getConfigure(req: Request, res: Response) {
const config: Config = JSON.parse(req.params['config'] || JSON.stringify(getDefaultConfig()));
const config: Config = JSON.parse(req.params['config'] as string || JSON.stringify(getDefaultConfig()));
// Convenience preset for ElfHosted WebStreamr bundle including Media Flow Proxy
if (!req.params['config'] && isElfHostedInstance(req)) {

View file

@ -0,0 +1,53 @@
import { Mutex } from 'async-mutex';
import { Request, Response, Router } from 'express';
import winston from 'winston';
import { ExtractorRegistry } from '../extractor';
import { contextFromRequestAndResponse, Fetcher } from '../utils';
export class ExtractController {
public readonly router: Router;
private readonly logger: winston.Logger;
private readonly extractorRegistry: ExtractorRegistry;
private readonly locks = new Map<string, Mutex>();
public constructor(logger: winston.Logger, _fetcher: Fetcher, extractorRegistry: ExtractorRegistry) {
this.router = Router();
this.logger = logger;
this.extractorRegistry = extractorRegistry;
this.router.get('/extract', this.extract.bind(this));
}
private async extract(req: Request, res: Response) {
if (req.method !== 'GET') {
res.status(405).send('Method Not Allowed');
return;
}
const ctx = contextFromRequestAndResponse(req, res);
const index = parseInt(req.query['index'] as string);
const url = new URL(req.query['url'] as string);
this.logger.info(`Lazy extract index ${index} of URL ${url} for ip ${ctx.ip}`, ctx);
let mutex = this.locks.get(url.href);
if (!mutex) {
mutex = new Mutex();
this.locks.set(url.href, mutex);
}
await mutex.runExclusive(async () => {
const urlResults = await this.extractorRegistry.handle(ctx, url);
res.redirect(urlResults[index]?.url.href as string);
});
if (!mutex.isLocked()) {
this.locks.delete(url.href);
}
};
}

View file

@ -21,7 +21,7 @@ export class ManifestController {
}
private getManifest(req: Request, res: Response) {
const config: Config = JSON.parse(req.params['config'] || JSON.stringify(getDefaultConfig()));
const config: Config = JSON.parse(req.params['config'] as string || JSON.stringify(getDefaultConfig()));
const manifest = buildManifest(this.sources, this.extractors, config);

View file

@ -5,8 +5,6 @@ import winston from 'winston';
import { Source } from '../source';
import { contextFromRequestAndResponse, envIsProd, Id, ImdbId, StreamResolver, TmdbId } from '../utils';
const locks = new Map<string, Mutex>();
export class StreamController {
public readonly router: Router;
@ -14,6 +12,8 @@ export class StreamController {
private readonly sources: Source[];
private readonly streamResolver: StreamResolver;
private readonly locks = new Map<string, Mutex>();
public constructor(logger: winston.Logger, sources: Source[], streams: StreamResolver) {
this.router = Router();
@ -27,7 +27,7 @@ export class StreamController {
private async getStream(req: Request, res: Response) {
const type: ContentType = (req.params['type'] || '') as ContentType;
const rawId: string = req.params['id'] || '';
const rawId: string = req.params['id'] as string || '';
let id: Id;
if (rawId.startsWith('tmdb:')) {
@ -35,7 +35,9 @@ export class StreamController {
} else if (rawId.startsWith('tt')) {
id = ImdbId.fromString(rawId);
} else {
throw new Error(`Unsupported ID: ${rawId}`);
res.status(400).send(`Unsupported ID: ${rawId}`);
return;
}
const ctx = contextFromRequestAndResponse(req, res);
@ -44,17 +46,17 @@ export class StreamController {
const sources = this.sources.filter(source => source.countryCodes.filter(countryCode => countryCode in ctx.config).length);
let mutex = locks.get(rawId);
let mutex = this.locks.get(rawId);
if (!mutex) {
mutex = new Mutex();
locks.set(rawId, mutex);
this.locks.set(rawId, mutex);
}
await mutex.runExclusive(async () => {
const { streams, ttl } = await this.streamResolver.resolve(ctx, sources, type, id);
if (ttl && envIsProd()) {
res.setHeader('Cache-Control', `max-age=${ttl / 1000}, public`);
res.setHeader('Cache-Control', `public, max-age=${Math.floor(ttl / 1000)}, immutable`);
}
res.setHeader('Content-Type', 'application/json');
@ -62,7 +64,7 @@ export class StreamController {
});
if (!mutex.isLocked()) {
locks.delete(rawId);
this.locks.delete(rawId);
}
};
}

View file

@ -1,3 +1,4 @@
export * from './ExtractController';
export * from './ConfigureController';
export * from './ManifestController';
export * from './StreamController';

View file

@ -1,12 +1,15 @@
import { AxiosResponse } from 'axios';
import { BlockedReason } from '../types';
export class BlockedError extends Error {
public readonly url: URL;
public readonly reason: BlockedReason;
public readonly headers: Record<string, string[] | string | undefined>;
public readonly headers: AxiosResponse['headers'];
public constructor(reason: BlockedReason, headers: Record<string, string[] | string | undefined>) {
public constructor(url: URL, reason: BlockedReason, headers: AxiosResponse['headers']) {
super();
this.url = url;
this.reason = reason;
this.headers = headers;
}

View file

@ -1,11 +1,15 @@
import { AxiosResponse } from 'axios';
export class HttpError extends Error {
public readonly url: URL;
public readonly status: number;
public readonly statusText: string;
public readonly headers: Record<string, string[] | string | undefined>;
public readonly headers: AxiosResponse['headers'];
public constructor(status: number, statusText: string, headers: Record<string, string[] | string | undefined>) {
public constructor(url: URL, status: number, statusText: string, headers: AxiosResponse['headers']) {
super();
this.url = url;
this.status = status;
this.statusText = statusText;
this.headers = headers;

View file

@ -1 +1,9 @@
export class QueueIsFullError extends Error {}
export class QueueIsFullError extends Error {
public readonly url: URL;
public constructor(url: URL) {
super();
this.url = url;
}
}

View file

@ -1 +1,9 @@
export class TimeoutError extends Error {}
export class TimeoutError extends Error {
public readonly url: URL;
public constructor(url: URL) {
super();
this.url = url;
}
}

View file

@ -1,9 +1,11 @@
export class TooManyRequestsError extends Error {
public readonly url: URL;
public readonly retryAfter: number;
public constructor(retryAfter: number) {
public constructor(url: URL, retryAfter: number) {
super();
this.url = url;
this.retryAfter = retryAfter;
}
}

View file

@ -1 +1,9 @@
export class TooManyTimeoutsError extends Error {}
export class TooManyTimeoutsError extends Error {
public readonly url: URL;
public constructor(url: URL) {
super();
this.url = url;
}
}

View file

@ -17,51 +17,54 @@ export * from './TooManyTimeoutsError';
export const logErrorAndReturnNiceString = (ctx: Context, logger: winston.Logger, source: string, error: unknown): string => {
if (error instanceof BlockedError) {
if (error.reason === BlockedReason.cloudflare_challenge) {
logger.warn(`${source}: Request was blocked via Cloudflare challenge.`, ctx);
} else if (error.reason === BlockedReason.cloudflare_censor) {
logger.warn(`${source}: Request was censored by Cloudflare.`, ctx);
} else if (error.reason === BlockedReason.media_flow_proxy_auth) {
if (error.reason === BlockedReason.media_flow_proxy_auth) {
return '⚠️ MediaFlow Proxy authentication failed. Please set the correct password.';
} else {
logger.warn(`${source}: Request was blocked, headers: ${JSON.stringify(error.headers)}.`, ctx);
}
return `⚠️ Request was blocked. Reason: ${error.reason}`;
logger.warn(`${source}: Request to ${error.url} was blocked, reason: ${error.reason}, headers: ${JSON.stringify(error.headers)}.`, ctx);
return `⚠️ Request to ${error.url.host} was blocked. Reason: ${error.reason}`;
}
if (error instanceof TooManyRequestsError) {
logger.warn(`${source}: Rate limited for ${error.retryAfter} seconds.`, ctx);
logger.warn(`${source}: Request to ${error.url} was rate limited for ${error.retryAfter} seconds.`, ctx);
return '🚦 Request was rate-limited. Please try again later or consider self-hosting.';
return `🚦 Request to ${error.url.host} was rate-limited. Please try again later or consider self-hosting.`;
}
if (error instanceof TooManyTimeoutsError) {
logger.warn(`${source}: Too many timeouts.`, ctx);
logger.warn(`${source}: Too many timeouts when requesting ${error.url}.`, ctx);
return '🚦 Too many recent timeouts. Please try again later.';
return `🚦 Too many recent timeouts when requesting ${error.url.host}. Please try again later.`;
}
if (error instanceof TimeoutError) {
logger.warn(`${source}: Request to ${error.url} timed out.`, ctx);
return `🐢 Request to ${error.url.host} timed out.`;
}
if (error instanceof DOMException && ['AbortError', 'TimeoutError'].includes(error.name)) {
// sometimes this gets through, no idea why..
logger.warn(`${source}: Request timed out.`, ctx);
return '🐢 Request timed out.';
}
if (error instanceof QueueIsFullError) {
logger.warn(`${source}: Request queue is full.`, ctx);
logger.warn(`${source}: Request queue for ${error.url.host} is full.`, ctx);
return '⏳ Request queue is full. Please try again later or consider self-hosting.';
return `⏳ Request queue for ${error.url.host} is full. Please try again later or consider self-hosting.`;
}
if (error instanceof HttpError) {
logger.error(`${source}: HTTP status ${error.status} (${error.statusText}), headers: ${JSON.stringify(error.headers)}, stack: ${error.stack}.`, ctx);
logger.error(`${source}: Error when requesting url ${error.url}, HTTP status ${error.status} (${error.statusText}), headers: ${JSON.stringify(error.headers)}, stack: ${error.stack}.`, ctx);
if (error.status >= 500) {
return `❌ Remote server has issues. We can't fix this, please try later again.`;
return `❌ Remote server ${error.url.host} has issues. We can't fix this, please try later again.`;
}
return `❌ Request failed with status ${error.status} (${error.statusText}). Request-id: ${ctx.id}.`;
return `❌ Request to ${error.url.host} failed with status ${error.status} (${error.statusText}). Request-id: ${ctx.id}.`;
}
const cause = (error as Error & { cause?: unknown }).cause;

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { DoodStream } from './DoodStream';
import { ExtractorRegistry } from './ExtractorRegistry';
@ -8,26 +7,18 @@ import { ExtractorRegistry } from './ExtractorRegistry';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new DoodStream(new FetcherMock(`${__dirname}/__fixtures__/DoodStream`))]);
const ctx = createTestContext();
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('DoodStream', () => {
test('dood.to', async () => {
expect(await extractorRegistry.handle(ctx, new URL('http://dood.to/e/sk1m9eumzyjj'), CountryCode.de)).toMatchSnapshot();
});
test('doodster', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dooodster.com/e/1cfcevn6dg8shrfvht22odxw2lty18hr'), CountryCode.de)).toMatchSnapshot();
});
test('missing pass_md5 -> not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dood.to/e/gy8l8mb2i311'), CountryCode.mx)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('http://dood.to/e/sk1m9eumzyjj'))).toMatchSnapshot();
});
test('can guess height from title', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://do7go.com/e/dfx8me4un4ul'), CountryCode.fr)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://do7go.com/e/dfx8me4un4ul'))).toMatchSnapshot();
});
test('cloudflare storage', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://doodstream.com/e/wfpwtsgyr1xi'), CountryCode.mx)).toMatchSnapshot();
test('not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://myvidplay.com/e/cz7cus0bvlzr'))).toMatchSnapshot();
});
});

View file

@ -1,8 +1,8 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import randomstring from 'randomstring';
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class DoodStream extends Extractor {
@ -10,66 +10,44 @@ export class DoodStream extends Extractor {
public readonly label = 'DoodStream';
private readonly fetcher: Fetcher;
public override readonly ttl: number = 21600000; // 6h
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public override viaMediaFlowProxy = true;
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/doodstream.py */
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/dood|do[0-9]go|doood|dooood|ds2play|ds2video|d0o0d|do0od|d0000d|d000d|vidply|all3do|doply|vide0|vvide0|d-s/);
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/dood|do[0-9]go|doood|dooood|ds2play|ds2video|dsvplay|d0o0d|do0od|d0000d|d000d|myvidplay|vidply|all3do|doply|vide0|vvide0|d-s/) && supportsMediaFlowProxy(ctx);
};
public override normalize(url: URL): URL {
const videoId = url.pathname.split('/').slice(-1)[0] as string;
const videoId = url.pathname.replace(/\/+$/, '').split('/').at(-1) as string;
return new URL(`http://dood.to/e/${videoId}`);
};
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const passMd5Match = html.match(/\/pass_md5\/[\w-]+\/([\w-]+)/);
if (!passMd5Match) {
const html = await this.fetcher.text(ctx, url, { headers });
if (/Video not found/.test(html)) {
throw new NotFoundError();
}
const token = passMd5Match[1] as string;
const baseUrl = await this.fetcher.text(ctx, new URL(passMd5Match[0], url.origin));
const $ = cheerio.load(html);
const title = $('title').text().trim().replace(/ - DoodStream$/, '').trim();
let mp4Url: URL;
let bytes: number | undefined;
if (baseUrl.includes('cloudflarestorage')) {
mp4Url = new URL(baseUrl);
} else {
mp4Url = new URL(`${baseUrl}${randomstring.generate(10)}?token=${token}&expiry=${Date.now()}`);
const mp4Head = await this.fetcher.head(ctx, mp4Url, { headers: { Referer: url.origin } });
if (mp4Head['content-length']) {
bytes = parseInt(mp4Head['content-length'] as string);
}
}
const downloadHtml = await this.fetcher.text(ctx, new URL(url.href.replace('/e/', '/d/')));
const sizeMatch = downloadHtml.match(/([\d.]+ ?[GM]B)/);
return [
{
url: mp4Url,
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Doodstream', url, headers),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title,
...(bytes && { bytes }),
},
requestHeaders: {
Referer: url.origin,
...(sizeMatch && { bytes: bytes.parse(sizeMatch[1] as string) as number }),
},
},
];

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { Dropload } from './Dropload';
import { ExtractorRegistry } from './ExtractorRegistry';
@ -12,18 +11,22 @@ const ctx = createTestContext();
describe('Dropload', () => {
test('dropload.io', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/embed-lyo2h1snpe5c.html'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/embed-lyo2h1snpe5c.html'))).toMatchSnapshot();
});
test('dropload unknown height and size', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/gf2apij3ql37'))).toMatchSnapshot();
});
test('file not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'))).toMatchSnapshot();
});
test('processing / internal problem', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/7xtxuac84xyh.html'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/7xtxuac84xyh.html'))).toMatchSnapshot();
});
test('download URL', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/d/xhcmgcc2txnv'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/d/xhcmgcc2txnv'))).toMatchSnapshot();
});
});

View file

@ -1,8 +1,8 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { extractUrlFromPacked, Fetcher } from '../utils';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
export class Dropload extends Extractor {
@ -10,47 +10,48 @@ export class Dropload extends Extractor {
public readonly label = 'Dropload';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public override readonly ttl: number = 10800000; // 3h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/dropload/);
return null !== url.host.match(/dropload|dr0pstream/);
}
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/d/', '/').replace('/e/', '/').replace('/embed-', '/'));
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (html.includes('File Not Found') || html.includes('Pending in queue')) {
throw new NotFoundError();
}
const heightMatch = html.match(/\d{3,}x(\d{3,}),/) as string[];
const playlistUrl = extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]);
const playlistHeaders = { Referer: 'https://dr0pstream.com/' };
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/) as string[];
const heightMatch = html.match(/\d{3,}x(\d{3,}),/);
const height = heightMatch
? parseInt(heightMatch[1] as string)
: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers: playlistHeaders });
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/);
const size = sizeMatch ? bytes.parse(sizeMatch[1] as string) as number : undefined;
const $ = cheerio.load(html);
const title = $('.videoplayer h1').text().trim();
return [
{
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
url: playlistUrl,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
bytes: bytes.parse(sizeMatch[1] as string) as number,
countryCodes: [countryCode],
height: parseInt(heightMatch[1] as string),
...meta,
title,
...(size && { bytes: size }),
...(height && { height }),
},
requestHeaders: playlistHeaders,
},
];
};

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExternalUrl } from './ExternalUrl';
import { ExtractorRegistry } from './ExtractorRegistry';
@ -12,14 +11,10 @@ const ctx = createTestContext({ includeExternalUrls: 'on' });
describe('ExternalUrl', () => {
test('404 - not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/gjA1OQ4klyHxgJ'), CountryCode.fr)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/gjA1OQ4klyHxgJ'))).toMatchSnapshot();
});
test('netu.fanstream.us', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://netu.fanstream.us/e/0DFgfkcXOsDP'), CountryCode.fr)).toMatchSnapshot();
});
test('johnalwayssame.com with title', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://johnalwayssame.com/e/cqy9oue7sv0g'), CountryCode.fr, 'Black Mirror 4x2')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://netu.fanstream.us/e/0DFgfkcXOsDP'))).toMatchSnapshot();
});
});

View file

@ -1,6 +1,5 @@
import { BlockedError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher, showExternalUrls } from '../utils';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { showExternalUrls } from '../utils';
import { Extractor } from './Extractor';
export class ExternalUrl extends Extractor {
@ -8,43 +7,20 @@ export class ExternalUrl extends Extractor {
public readonly label = 'External';
public override readonly ttl = 3600000; // 1h
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public override readonly ttl = 21600000; // 6h
public supports(ctx: Context, url: URL): boolean {
return showExternalUrls(ctx.config) && null !== url.host.match(/.*/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title: string | undefined): Promise<UrlResult[]> {
try {
// Make sure the URL is accessible, but avoid causing noise and delays doing this
await this.fetcher.head(ctx, url, { noFlareSolverr: true, timeout: 1000, headers: { Referer: url.origin } });
} catch (error) {
/* istanbul ignore if */
if (!(error instanceof BlockedError)) {
return [];
}
}
protected async extractInternal(_ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
return [
{
url: url,
format: Format.unknown,
isExternal: true,
label: `${url.host}`,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
title,
},
meta,
},
];
};

View file

@ -1,5 +1,6 @@
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Context, Format, InternalUrlResult, Meta, UrlResult } from '../types';
import { Fetcher } from '../utils';
export abstract class Extractor {
public abstract readonly id: string;
@ -8,19 +9,34 @@ export abstract class Extractor {
public readonly ttl: number = 900000; // 15m
public readonly cacheVersion: number | undefined = undefined;
public readonly viaMediaFlowProxy: boolean = false;
protected readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
this.fetcher = fetcher;
}
public abstract supports(ctx: Context, url: URL): boolean;
public normalize(url: URL): URL {
return url;
};
protected abstract extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]>;
protected abstract extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]>;
public async extract(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]> {
public async extract(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
try {
return await this.extractInternal(ctx, url, countryCode, title);
return (await this.extractInternal(ctx, url, meta)).map(
urlResult => ({
...urlResult,
label: this.formatLabel(urlResult.label ?? this.label),
ttl: this.ttl,
}),
);
} catch (error) {
if (error instanceof NotFoundError) {
return [];
@ -32,14 +48,15 @@ export abstract class Extractor {
format: Format.unknown,
isExternal: true,
error,
label: this.label,
sourceId: `${this.id}`,
meta: {
countryCodes: [countryCode],
...(title && { title }),
},
label: this.formatLabel(this.label),
ttl: 0,
meta,
},
];
}
};
private formatLabel(label: string): string {
return this.viaMediaFlowProxy ? `${label} (MFP)` : label;
}
}

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { createExtractors } from './index';
@ -12,46 +11,53 @@ describe('ExtractorRegistry', () => {
const ctx = createTestContext();
test('returns error result from extractor', async () => {
const urlResult = await extractorRegistry.handle(ctx, new URL('https://some-url.test'), CountryCode.de);
const urlResult = await extractorRegistry.handle(ctx, new URL('https://some-url.test'));
expect(urlResult).toMatchSnapshot();
});
test('returns external URLs if enabled by config', async () => {
const urlResult = await extractorRegistry.handle({ ...ctx, config: { ...ctx.config, includeExternalUrls: 'on' } }, new URL('https://mixdrop.ag/e/3nzwveprim63or6'), CountryCode.de);
const urlResult = await extractorRegistry.handle({ ...ctx, config: { ...ctx.config, includeExternalUrls: 'on' } }, new URL('https://mixdrop.ag/e/3nzwveprim63or6'));
expect(urlResult).toMatchSnapshot();
});
test('does not return external URLs by default', async () => {
const urlResult = await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/l7v73zqrfdj19z'), CountryCode.de);
const urlResult = await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/l7v73zqrfdj19z'));
expect(urlResult).toStrictEqual([]);
});
test('returns from memory cache if possible', async () => {
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), CountryCode.de);
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), CountryCode.de);
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'));
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'));
expect(urlResults1).not.toStrictEqual([]);
expect(urlResults2).not.toStrictEqual([]);
});
test('ignores not found errors but caches them', async () => {
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), CountryCode.de);
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), CountryCode.de);
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'));
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'));
expect(urlResults1).toStrictEqual([]);
expect(urlResults2).toStrictEqual([]);
});
test('returns external url for error', async () => {
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/mocked-blocked.html'), CountryCode.de);
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/mocked-blocked.html'));
expect(urlResults).toMatchSnapshot();
});
test('returns external url for error with title', async () => {
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/mocked-blocked-2.html'), CountryCode.de, 'a title!');
test('empty results are cached', async () => {
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), { title: 'title' });
expect(urlResults).toMatchSnapshot();
});
test('stats returns something', async () => {
const stats = extractorRegistry.stats();
expect(stats).toHaveProperty('urlResultCache');
expect(stats.urlResultCache).toBeTruthy();
});
});

View file

@ -1,42 +1,112 @@
import TTLCache from '@isaacs/ttlcache';
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
import winston from 'winston';
import { Context, CountryCode, UrlResult } from '../types';
import { isExtractorDisabled } from '../utils';
import { Context, Format, Meta, UrlResult } from '../types';
import { createKeyvSqlite, envGet, isExtractorDisabled } from '../utils';
import { Extractor } from './Extractor';
export class ExtractorRegistry {
private readonly logger: winston.Logger;
private readonly extractors: Extractor[];
private readonly urlResultCache: TTLCache<string, UrlResult[]>;
private readonly urlResultCache: Cacheable;
private readonly lazyUrlResultCache: Cacheable;
public constructor(logger: winston.Logger, extractors: Extractor[]) {
this.logger = logger;
this.extractors = extractors;
this.urlResultCache = new TTLCache({ max: 4096 });
this.urlResultCache = new Cacheable({
nonBlocking: true,
primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }),
secondary: createKeyvSqlite('extractor-cache'),
stats: true,
});
this.lazyUrlResultCache = new Cacheable({
nonBlocking: true,
primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }),
secondary: createKeyvSqlite('extractor-lazy-cache'),
stats: true,
});
}
public async handle(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]> {
public stats() {
return {
urlResultCache: this.urlResultCache.stats,
lazyUrlResultCache: this.lazyUrlResultCache.stats,
};
};
public async handle(ctx: Context, url: URL, meta?: Meta, allowLazy?: boolean): Promise<UrlResult[]> {
const extractor = this.extractors.find(extractor => !isExtractorDisabled(ctx.config, extractor) && extractor.supports(ctx, url));
if (!extractor) {
return [];
}
const normalizedUrl = extractor.normalize(url);
const cacheKey = `${extractor.id}_${normalizedUrl}`;
const cacheKey = this.determineCacheKey(ctx, extractor, normalizedUrl);
let urlResults = this.urlResultCache.get(cacheKey) ?? [];
if (this.urlResultCache.has(cacheKey)) {
return urlResults.map(urlResult => ({ ...urlResult, ttl: this.urlResultCache.getRemainingTTL(cacheKey) }));
const storedDataRaw = await this.urlResultCache.getRaw<UrlResult[]>(cacheKey);
const expires = storedDataRaw?.expires;
if (storedDataRaw && expires) {
const ttl = expires - Date.now();
/* istanbul ignore if */
if (ttl > 0) {
return (storedDataRaw.value as UrlResult[]).map(urlResult => ({ ...urlResult, ttl, url: new URL(urlResult.url) }));
}
}
this.logger.info(`Extract stream URL using ${extractor.id} extractor from ${url}`, ctx);
const lazyUrlResults = await this.lazyUrlResultCache.get<UrlResult[]>(normalizedUrl.href) ?? [];
urlResults = await extractor.extract(ctx, normalizedUrl, countryCode, title);
/* istanbul ignore next */
if (
lazyUrlResults.length && allowLazy && !extractor.viaMediaFlowProxy
&& lazyUrlResults.every(urlResult => urlResult.format !== Format.hls) // related to Android issues, e.g. https://github.com/Stremio/stremio-bugs/issues/1574 or https://github.com/Stremio/stremio-bugs/issues/1579
) {
// generate lazy extract urls
return lazyUrlResults.map((urlResult, index) => {
const extractUrl = new URL(`${envGet('PROTOCOL')}:${envGet('HOST')}/extract/`);
if (!urlResults.some(urlResult => urlResult.error) && extractor.ttl) {
this.urlResultCache.set(cacheKey, urlResults, { ttl: extractor.ttl });
extractUrl.searchParams.set('index', `${index}`);
extractUrl.searchParams.set('url', url.href);
return { ...urlResult, url: extractUrl };
});
}
this.logger.info(`Extract ${url} using ${extractor.id} extractor`, ctx);
const mergedMeta: Meta = { ...meta, ...lazyUrlResults[0]?.meta };
const urlResults = await extractor.extract(ctx, normalizedUrl, { extractorId: extractor.id, ...mergedMeta });
if (!Object.keys(mergedMeta).length || urlResults.some(urlResult => urlResult.error)) {
await this.urlResultCache.delete(cacheKey);
await this.lazyUrlResultCache.delete(normalizedUrl.href);
return urlResults;
}
const ttl = urlResults.length ? extractor.ttl : 43200000; // 12h
await this.urlResultCache.set<UrlResult[]>(cacheKey, urlResults, ttl);
if (extractor.id !== 'external') {
await this.lazyUrlResultCache.set<UrlResult[]>(normalizedUrl.href, urlResults, 2629800000); // 1 month
}
return urlResults;
};
private determineCacheKey(ctx: Context, extractor: Extractor, url: URL): string {
let suffix = '';
if (extractor.viaMediaFlowProxy) {
suffix += `_${ctx.config.mediaFlowProxyUrl}`;
}
if (extractor.cacheVersion) {
suffix += `_${extractor.cacheVersion}`;
}
return `${extractor.id}_${url}${suffix}`;
}
}

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Fastream } from './Fastream';
@ -12,6 +11,10 @@ const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test
describe('Fastream', () => {
test('fastream.to embed', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-3aooif4ozt10.html'), CountryCode.es)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-3aooif4ozt10.html'))).toMatchSnapshot();
});
test('expired or deleted', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-a9rp1d1pqom7.html'))).toMatchSnapshot();
});
});

View file

@ -1,25 +1,16 @@
import bytes from 'bytes';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { buildMediaFlowProxyExtractorStreamUrl, Fetcher, supportsMediaFlowProxy } from '../utils';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class Fastream extends Extractor {
public readonly id = 'fastream';
public readonly label = 'Fastream (via MediaFlow Proxy)';
public override readonly ttl = 0;
public readonly label = 'Fastream';
public override viaMediaFlowProxy = true;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/fastream/) && supportsMediaFlowProxy(ctx);
}
@ -28,11 +19,17 @@ export class Fastream extends Extractor {
return new URL(url.href.replace('/e/', '/embed-').replace('/d/', '/embed-'));
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Fastream', url);
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const downloadUrl = new URL(url.href.replace('/embed-', '/d/'));
const html = await this.fetcher.text(ctx, downloadUrl);
const html = await this.fetcher.text(ctx, downloadUrl, { headers });
if (/No such file/.test(html)) {
throw new NotFoundError();
}
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Fastream', url, headers);
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/) as string[];
const titleMatch = html.match(/>Download (.*?)</) as string[];
@ -41,11 +38,8 @@ export class Fastream extends Extractor {
{
url: playlistUrl,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
height: parseInt(heightAndSizeMatch[1] as string),
title: titleMatch[1],

View file

@ -0,0 +1,28 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { FileLions } from './FileLions';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new FileLions(new FetcherMock(`${__dirname}/__fixtures__/FileLions`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('FileLions', () => {
test('filelions f', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://filelions.to/f/tyn45apubte2'))).toMatchSnapshot();
});
test('mivalyo v referer lock', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mivalyo.com/v/tah5znapz3e5'), { referer: 'https://kinoger.com' })).toMatchSnapshot();
});
test('file not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://filelions.to/v/ylcp2cu5qanb'))).toMatchSnapshot();
});
test('deleted by administration', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://callistanise.com/f/cy4t5nkerjrt'))).toMatchSnapshot();
});
});

View file

@ -0,0 +1,99 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy, unpackEval } from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/commits/master/script.module.resolveurl/lib/resolveurl/plugins/filelions.py */
export class FileLions extends Extractor {
public readonly id = 'filelions';
public readonly label = 'FileLions';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
const supportedDomain = null !== url.host.match(/.*lions?/)
|| [
'6sfkrspw4u.sbs',
'ajmidyadfihayh.sbs',
'alhayabambi.sbs',
'anime7u.com',
'azipcdn.com',
'bingezove.com',
'callistanise.com',
'coolciima.online',
'dhtpre.com',
'dingtezuni.com',
'dintezuvio.com',
'e4xb5c2xnz.sbs',
'egsyxutd.sbs',
'fdewsdc.sbs',
'gsfomqu.sbs',
'javplaya.com',
'katomen.online',
'lumiawatch.top',
'minochinos.com',
'mivalyo.com',
'moflix-stream.click',
'motvy55.store',
'movearnpre.com',
'peytonepre.com',
'ryderjet.com',
'smoothpre.com',
'taylorplayer.com',
'techradar.ink',
'videoland.sbs',
'vidhide.com',
'vidhide.fun',
'vidhidefast.com',
'vidhidehub.com',
'vidhideplus.com',
'vidhidepre.com',
'vidhidepro.com',
'vidhidevip.com',
].includes(url.host);
return supportedDomain && supportsMediaFlowProxy(ctx);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/v/', '/f/').replace('/download/', '/f/').replace('/file/', '/f/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (html.includes('This video can be watched as embed only')) {
return await this.extractInternal(ctx, new URL(url.href.replace('/f/', '/v/')), meta);
}
if (/File Not Found|deleted by administration/.test(html)) {
throw new NotFoundError();
}
const unpacked = unpackEval(html);
const heightMatch = unpacked.match(/(\d{3,})p/) as string[];
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/);
const $ = cheerio.load(html);
const title = $('meta[name="description"]').attr('content') as string;
return [
{
url: await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'FileLions', url, headers),
format: Format.hls,
meta: {
...meta,
height: parseInt(heightMatch[1] as string),
...(sizeMatch && { bytes: bytes.parse(sizeMatch[1] as string) as number }),
...(title && { title }),
},
},
];
};
}

View file

@ -0,0 +1,24 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { FileMoon } from './FileMoon';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new FileMoon(new FetcherMock(`${__dirname}/__fixtures__/FileMoon`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('FileMoon', () => {
test('z1ekv717 d', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://z1ekv717.fun/d/wkhcbggdxf1d'))).toMatchSnapshot();
});
test('page not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://filemoon.sx/e/n7i8zodwjqr9'))).toMatchSnapshot();
});
test('missing height', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://filemoon.to/d/ugj7f1kq94p1'))).toMatchSnapshot();
});
});

82
src/extractor/FileMoon.ts Normal file
View file

@ -0,0 +1,82 @@
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import {
buildMediaFlowProxyExtractorStreamUrl,
supportsMediaFlowProxy,
unpackEval,
} from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/filemoon.py */
export class FileMoon extends Extractor {
public readonly id = 'filemoon';
public readonly label = 'FileMoon';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
const supportedDomain = null !== url.host.match(/filemoon/)
|| [
'1azayf9w.xyz',
'222i8x.lol',
'81u6xl9d.xyz',
'8mhlloqo.fun',
'96ar.com',
'bf0skv.org',
'boosteradx.online',
'c1z39.com',
'cinegrab.com',
'f51rm.com',
'furher.in',
'kerapoxy.cc',
'l1afav.net',
'moonmov.pro',
'smdfs40r.skin',
'xcoic.com',
'z1ekv717.fun',
].includes(url.host);
return supportedDomain && supportsMediaFlowProxy(ctx);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/e/', '/d/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta, originalUrl?: URL): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (/Page not found/.test(html)) {
throw new NotFoundError();
}
const $ = cheerio.load(html);
const title = $('h3').text().trim();
const iframeUrlMatches = Array.from(html.matchAll(/iframe.*?src=["'](.*?)["']/g));
if (iframeUrlMatches.length) {
// Use last match because there can be fake adblock catcher urls before
return await this.extractInternal(ctx, new URL((iframeUrlMatches[iframeUrlMatches.length - 1] as RegExpExecArray)[1] as string), { title, ...meta }, url);
}
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'FileMoon', originalUrl as URL, headers);
const unpacked = unpackEval(html);
const heightMatch = unpacked.match(/(\d{3,})p/);
return [
{
url: playlistUrl,
format: Format.hls,
meta: {
...meta,
...(heightMatch && { height: parseInt(heightMatch[1] as string) }),
},
},
];
};
}

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Fsst } from './Fsst';
@ -11,15 +10,7 @@ const extractorRegistry = new ExtractorRegistry(logger, [new Fsst(new FetcherMoc
const ctx = createTestContext();
describe('Fsst', () => {
test('Blood & Sinners', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/900576/'), CountryCode.de)).toMatchSnapshot();
});
test('Dead City', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/901994/'), CountryCode.de)).toMatchSnapshot();
});
test('How to Train Your Dragon', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/902668/'), CountryCode.de)).toMatchSnapshot();
test('Wake up Dead Man', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/948429/'))).toMatchSnapshot();
});
});

View file

@ -1,6 +1,5 @@
import * as cheerio from 'cheerio';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Extractor } from './Extractor';
export class Fsst extends Extractor {
@ -8,44 +7,35 @@ export class Fsst extends Extractor {
public readonly label = 'Fsst';
public override readonly ttl = 0;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public override readonly ttl: number = 10800000; // 3h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/fsst/);
};
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers, noProxyHeaders: true });
const $ = cheerio.load(html);
const title = $('title').text().trim();
const filesMatch = html.match(/file:"(.*)"/) as string[];
return (filesMatch[1] as string).split(',').map((fileString, index) => {
const heightAndUrlMatch = fileString.match(/\[?([\d]*)p?]?(.*)/) as string[];
const fileHref = heightAndUrlMatch[2] as string;
const heightFromFileHrefMatch = fileHref.match(/([\d]+)p/) as string[];
const lastFile = (filesMatch[1] as string).split(',').pop() as string;
return {
url: new URL(fileHref),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}_${index}`,
meta: {
countryCodes: [countryCode],
height: parseInt(heightAndUrlMatch[1] || heightFromFileHrefMatch[1] as string),
title,
},
};
});
const heightAndUrlMatch = lastFile.match(/\[?([\d]*)p?]?(.*)/) as string[];
const fileHref = heightAndUrlMatch[2] as string;
return [{
url: await this.fetcher.getFinalRedirectUrl(ctx, new URL(fileHref), { headers, noProxyHeaders: true }, 1),
format: Format.mp4,
meta: {
...meta,
height: parseInt(heightAndUrlMatch[1] as string),
title,
},
}];
};
}

View file

@ -0,0 +1,28 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { HubCloud } from './HubCloud';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new HubCloud(new FetcherMock(`${__dirname}/__fixtures__/HubCloud`))]);
const ctx = createTestContext();
describe('HubCloud', () => {
test('handle dexter original sin 2024 s01e01', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubcloud.one/drive/idt1evqfuviqiei'))).toMatchSnapshot();
});
test('handle crayon shin-chan 1993', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubcloud.one/drive/bffzqlpqfllfcld'))).toMatchSnapshot();
});
test('handle dark 2017 s03e08', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubcloud.one/drive/nknlofk8snfnknh'))).toMatchSnapshot();
});
test('handle goat 2026', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubcloud.foo/drive/p94k4dccjwxjcx4'))).toMatchSnapshot();
});
});

102
src/extractor/HubCloud.ts Normal file
View file

@ -0,0 +1,102 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { findCountryCodes, findHeight } from '../utils';
import { Extractor } from './Extractor';
export class HubCloud extends Extractor {
public readonly id = 'hubcloud';
public readonly label = 'HubCloud';
public override readonly ttl: number = 43200000; // 12h
public override readonly cacheVersion = 1;
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/hubcloud/);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const redirectHtml = await this.fetcher.text(ctx, url, { headers });
const redirectUrlMatch = redirectHtml.match(/var url ?= ?'(.*?)'/) as string[];
const linksHtml = await this.fetcher.text(ctx, new URL(redirectUrlMatch[1] as string), { headers: { Referer: url.href } });
const $ = cheerio.load(linksHtml);
const title = $('title').text().trim();
const countryCodes = [...new Set([...meta.countryCodes ?? [], ...findCountryCodes(title)])];
const height = meta.height ?? findHeight(title);
return Promise.all([
...$('a')
.filter((_i, el) => {
const text = $(el).text();
return text.includes('FSL') && !text.includes('FSLv2');
})
.map((_i, el) => {
const url = new URL($(el).attr('href') as string);
return {
url,
format: Format.unknown,
label: `${this.label} (FSL)`,
meta: {
...meta,
bytes: bytes.parse($('#size').text()) as number,
extractorId: `${this.id}_fsl`,
countryCodes,
height,
title,
},
};
}).toArray(),
...$('a')
.filter((_i, el) => {
const text = $(el).text();
return text.includes('FSLv2');
})
.map((_i, el) => {
const url = new URL($(el).attr('href') as string);
return {
url,
format: Format.unknown,
label: `${this.label} (FSLv2)`,
meta: {
...meta,
bytes: bytes.parse($('#size').text()) as number,
extractorId: `${this.id}_fslv2`,
countryCodes,
height,
title,
},
};
}).toArray(),
...$('a')
.filter((_i, el) => $(el).text().includes('PixelServer'))
.map((_i, el) => {
const userUrl = new URL(($(el).attr('href') as string).replace('/api/file/', '/u/'));
const url = new URL(userUrl.href.replace('/u/', '/api/file/'));
url.searchParams.set('download', '');
return {
url,
format: Format.unknown,
label: `${this.label} (PixelServer)`,
meta: {
...meta,
bytes: bytes.parse($('#size').text()) as number,
extractorId: `${this.id}_pixelserver`,
countryCodes,
height,
title,
},
requestHeaders: { Referer: userUrl.href },
};
}).toArray(),
]);
};
}

View file

@ -0,0 +1,29 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { HubCloud } from './HubCloud';
import { HubDrive } from './HubDrive';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(
logger,
[
new HubDrive(
new FetcherMock(`${__dirname}/__fixtures__/HubDrive`),
new HubCloud(new FetcherMock(`${__dirname}/__fixtures__/HubDrive/HubCloud`)),
),
],
);
const ctx = createTestContext();
describe('HubDrive', () => {
test('handle avatar', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubdrive.space/file/7283903021'))).toMatchSnapshot();
});
test('handle missing hubcloud', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubdrive.space/file/2243124026'))).toMatchSnapshot();
});
});

40
src/extractor/HubDrive.ts Normal file
View file

@ -0,0 +1,40 @@
import * as cheerio from 'cheerio';
import { Context, InternalUrlResult, Meta } from '../types';
import { Fetcher } from '../utils';
import { Extractor } from './Extractor';
import { HubCloud } from './HubCloud';
export class HubDrive extends Extractor {
public readonly id = 'hubdrive';
public readonly label = 'HubDrive';
public override readonly ttl: number = 43200000; // 12h
public override readonly cacheVersion = 1;
private readonly hubCloud: HubCloud;
public constructor(fetcher: Fetcher, hubCloud: HubCloud) {
super(fetcher);
this.hubCloud = hubCloud;
}
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/hubdrive/);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
const $ = cheerio.load(html);
const hubCloudUrl = $('a:contains("HubCloud")')
.map((_i, el) => new URL($(el).attr('href') as string))
.get(0);
return hubCloudUrl ? this.hubCloud.extract(ctx, hubCloudUrl, meta) : [];
};
}

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { KinoGer } from './KinoGer';
@ -12,10 +11,18 @@ const ctx = createTestContext();
describe('KinoGer', () => {
test('Blood & Sinners', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#ge5fhb'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#ge5fhb'))).toMatchSnapshot();
});
test('Dead City', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#x6tsx9'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#x6tsx9'))).toMatchSnapshot();
});
test('filma365.strp2p.site', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://filma365.strp2p.site/#vih6bx'))).toMatchSnapshot();
});
test('kinoger.p2pplay.pro', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.p2pplay.pro/#k53k9'))).toMatchSnapshot();
});
});

View file

@ -1,6 +1,6 @@
import crypto from 'crypto';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher, guessHeightFromPlaylist } from '../utils';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/kinoger.py */
@ -9,28 +9,32 @@ export class KinoGer extends Extractor {
public readonly label = 'KinoGer';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public override readonly ttl: number = 21600000; // 6h
public supports(_ctx: Context, url: URL): boolean {
return [
'asianembed.cam',
'disneycdn.net',
'dzo.vidplayer.live',
'filedecrypt.link',
'filma365.strp2p.site',
'flimmer.rpmvip.com',
'flixfilmesonline.strp2p.site',
'kinoger.p2pplay.pro',
'kinoger.re',
'moflix.rpmplay.xyz',
'moflix.upns.xyz',
'player.upn.one',
'securecdn.shop',
'shiid4u.upn.one',
'srbe84.vidplayer.live',
'strp2p.site',
't1.p2pplay.pro',
'tuktuk.rpmvid.com',
'ultrastream.online',
'videoland.cfd',
'videoshar.uns.bio',
'w1tv.xyz',
'wasuytm.store',
].includes(url.host);
}
@ -39,34 +43,35 @@ export class KinoGer extends Extractor {
return new URL(`${url.origin}/api/v1/video?id=${url.hash.slice(1)}`);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const hexData = await this.fetcher.text(ctx, url, { headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' } });
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = {
'Origin': url.origin,
'Referer': url.origin + '/',
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
};
const encrypted = Buffer.from(hexData, 'hex');
const hexData = await this.fetcher.text(ctx, url, { headers });
const encrypted = Buffer.from(hexData.slice(0, -1), 'hex');
const key = Buffer.from('6b69656d7469656e6d75613931316361', 'hex');
const iv = Buffer.from('313233343536373839306f6975797472', 'hex');
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString();
const { cf, title } = JSON.parse(decrypted) as { cf: string; title: string };
const { source, title } = JSON.parse(decrypted) as { source: string; title: string };
const m3u8Url = new URL(cf);
const m3u8Url = new URL(source);
return [
{
url: new URL(cf),
url: m3u8Url,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: url.origin } }),
...meta,
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers }),
title,
},
requestHeaders: {
Referer: url.origin,
},
requestHeaders: headers,
},
];
};

View file

@ -0,0 +1,24 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { LuluStream } from './LuluStream';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new LuluStream(new FetcherMock(`${__dirname}/__fixtures__/LuluStream`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('LuluStream', () => {
test('streamhihi d', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamhihi.com/d/mk9m58lz8ts6'))).toMatchSnapshot();
});
test('streamhihi', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamhihi.com/mk9m58lz8ts6'))).toMatchSnapshot();
});
test('no such file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://lulustream.com/e/uthq0o0sljnx'))).toMatchSnapshot();
});
});

View file

@ -0,0 +1,67 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import {
buildMediaFlowProxyExtractorStreamUrl,
supportsMediaFlowProxy,
} from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/lulustream.py */
export class LuluStream extends Extractor {
public readonly id = 'lulustream';
public readonly label = 'LuluStream';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
const supportedDomain = null !== url.host.match(/lulu/)
|| [
'732eg54de642sa.sbs',
'cdn1.site',
'd00ds.site',
'streamhihi.com',
].includes(url.host);
return supportedDomain && supportsMediaFlowProxy(ctx);
}
public override normalize(url: URL): URL {
const videoId = url.pathname.replace(/\/+$/, '').split('/').at(-1) as string;
return new URL(`/e/${videoId}`, url);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const fileUrl = new URL(url.href.replace('/e/', '/d/'));
const html = await this.fetcher.text(ctx, fileUrl, { headers });
if (/No such file|File Not Found/.test(html)) {
throw new NotFoundError();
}
const $ = cheerio.load(html);
const title = $('h1').text().trim();
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/) as string[];
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'LuluStream', url, headers);
return [
{
url: playlistUrl,
format: Format.hls,
meta: {
...meta,
title,
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
height: parseInt(heightAndSizeMatch[1] as string),
},
},
];
};
}

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Mixdrop } from './Mixdrop';
@ -12,14 +11,10 @@ const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test
describe('Mixdrop', () => {
test('mixdrop.my /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.my/e/knq0kj8waq44l8'), CountryCode.de)).toMatchSnapshot();
});
test('mixdrop.my /f/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.my/f/knq0kj8waq44l8'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.my/e/knq0kj8waq44l8'))).toMatchSnapshot();
});
test('deleted or expired file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/123456789'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/123456789'))).toMatchSnapshot();
});
});

View file

@ -1,34 +1,24 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, Fetcher, supportsMediaFlowProxy } from '../utils';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class Mixdrop extends Extractor {
public readonly id = 'mixdrop';
public readonly label = 'Mixdrop (via MediaFlow Proxy)';
public override readonly ttl = 0;
public readonly label = 'Mixdrop';
public override viaMediaFlowProxy = true;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/mixdrop/) && supportsMediaFlowProxy(ctx);
return null !== url.host.match(/mixdrop|mixdrp|mixdroop|m1xdrop/) && supportsMediaFlowProxy(ctx);
}
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/f/', '/e/'));
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const fileUrl = new URL(url.href.replace('/e/', '/f/'));
const html = await this.fetcher.text(ctx, fileUrl);
@ -36,7 +26,7 @@ export class Mixdrop extends Extractor {
throw new NotFoundError();
}
const sizeMatch = html.match(/([\d.,]+ ?[GM]B)/);
const sizeMatch = html.match(/([\d.,]+ ?[GM]B)/) as string[];
const $ = cheerio.load(html);
const title = $('.title b').text().trim();
@ -45,15 +35,10 @@ export class Mixdrop extends Extractor {
{
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Mixdrop', url),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
bytes: bytes.parse((sizeMatch[1] as string).replace(',', '')) as number,
title,
...(sizeMatch && {
bytes: bytes.parse((sizeMatch[1] as string).replace(',', '')) as number,
}),
},
},
];

View file

@ -0,0 +1,24 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { RgShows } from './RgShows';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new RgShows(new FetcherMock(`${__dirname}/__fixtures__/RgShows`))]);
const ctx = createTestContext();
describe('RgShows', () => {
test('handle one battle after another', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/movie/1054867'))).toMatchSnapshot();
});
test('handle stranger things s05e01', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/tv/66732/5/1'))).toMatchSnapshot();
});
test('handle parasite 2019 (mp4)', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/movie/496243'))).toMatchSnapshot();
});
});

49
src/extractor/RgShows.ts Normal file
View file

@ -0,0 +1,49 @@
import { BlockedError } from '../error';
import { BlockedReason, Context, Format, InternalUrlResult, Meta } from '../types';
import { guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
interface RgShowsApiData {
stream: {
url: string;
};
}
export class RgShows extends Extractor {
public readonly id = 'rgshows';
public readonly label = 'RgShows';
public override readonly ttl: number = 10800000; // 3h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/rgshows/);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { 'Referer': 'https://www.rgshows.ru/', 'Origin': 'https://www.rgshows.ru', 'User-Agent': 'Mozilla' };
const data = await this.fetcher.json(ctx, url, { headers }) as RgShowsApiData;
const streamUrl = new URL(data.stream.url);
/* istanbul ignore if */
if (streamUrl.host.includes('vidzee')) {
throw new BlockedError(url, BlockedReason.unknown, {});
}
const isMp4 = streamUrl.href.includes('mp4');
const isHls = streamUrl.href.includes('m3u8') || streamUrl.href.includes('txt');
return [
{
url: streamUrl,
format: isMp4 ? Format.mp4 : (isHls ? Format.hls : /* istanbul ignore next */ Format.unknown),
meta: {
...meta,
...(isHls && { height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, streamUrl, { headers }) }),
},
requestHeaders: headers,
},
];
};
}

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { SaveFiles } from './SaveFiles';
@ -12,22 +11,26 @@ const ctx = createTestContext();
describe('SafeFiles', () => {
test('savefiles', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/ip0k0dj2g0i3'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/ip0k0dj2g0i3'))).toMatchSnapshot();
});
test('savefiles /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/e/edptfmhyjr39'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/e/edptfmhyjr39'))).toMatchSnapshot();
});
test('savefiles /d/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/d/s9g6zb5kjbqd'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/d/s9g6zb5kjbqd'))).toMatchSnapshot();
});
test('savefiles locked file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/omqq55i59nvv'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/omqq55i59nvv'))).toMatchSnapshot();
});
test('savefiles deleted file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/bejqk3vlsv02'))).toMatchSnapshot();
});
test('streamhls /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamhls.to/e/ip0k0dj2g0i3'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://streamhls.to/e/ip0k0dj2g0i3'))).toMatchSnapshot();
});
});

View file

@ -1,7 +1,6 @@
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Extractor } from './Extractor';
export class SaveFiles extends Extractor {
@ -9,13 +8,7 @@ export class SaveFiles extends Extractor {
public readonly label = 'SaveFiles';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public override readonly ttl: number = 21600000; // 6h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/savefiles|streamhls/);
@ -25,10 +18,12 @@ export class SaveFiles extends Extractor {
return new URL(url.href.replace('/e/', '/').replace('/d/', '/'));
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
if (/File was locked by administrator/.test(html)) {
const html = await this.fetcher.text(ctx, url, { headers });
if (/file was locked|file was deleted/i.test(html)) {
throw new NotFoundError();
}
@ -42,11 +37,8 @@ export class SaveFiles extends Extractor {
{
url: new URL(fileMatch[1] as string),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title,
height: parseInt(sizeMatch[1] as string),
},

View file

@ -1,25 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Soaper } from './Soaper';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Soaper(new FetcherMock(`${__dirname}/__fixtures__/Soaper`))]);
const ctx = createTestContext();
describe('Soaper', () => {
test('Full Metal Jacket', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/movie_d8kdeypDY9.html'), CountryCode.en, 'Full Metal Jacket (1987)')).toMatchSnapshot();
});
test('Black Mirror', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_5KDq78eGp1.html'), CountryCode.en, 'Black Mirror 4x2')).toMatchSnapshot();
});
test('last of us s2e3', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_rYg3vMEDL1.html'), CountryCode.de, 'The Last of Us 2x3')).toMatchSnapshot();
});
});

View file

@ -1,67 +0,0 @@
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
interface SoaperInfoResponsePartial {
val: string;
val_bak: string;
}
export class Soaper extends Extractor {
public readonly id = 'soaper';
public readonly label = 'Soaper';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/soaper/) && null !== url.pathname.match(/^\/(episode|movie)_/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title: string | undefined): Promise<UrlResult[]> {
const movieOrEpisodeId = (url.pathname.match(/\/\w+_(\w+)/) as string[])[1] as string;
const form = new URLSearchParams();
form.append('pass', movieOrEpisodeId);
form.append('param', '');
form.append('extra', '');
form.append('e2', '');
form.append('server', '0');
const response = await this.fetcher.textPost(
ctx,
new URL(url.pathname.includes('episode') ? '/home/index/GetEInfoAjax' : '/home/index/GetMInfoAjax', url.origin),
form.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': url.origin,
},
},
);
const jsonResponse = JSON.parse(response) as SoaperInfoResponsePartial;
const m3u8Url = new URL(jsonResponse['val'], url.origin);
return [
{
url: m3u8Url,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url),
title: `${title}`,
},
},
];
};
}

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { StreamEmbed } from './StreamEmbed';
@ -12,6 +11,10 @@ const ctx = createTestContext();
describe('StreamEmbed', () => {
test('watch.gxplayer.xyz', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://watch.gxplayer.xyz/watch?v=MEKI92PU'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://watch.gxplayer.xyz/watch?v=MEKI92PU'))).toMatchSnapshot();
});
test('video is not ready', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://watch.gxplayer.xyz/watch?v=PBO90WAS'))).toMatchSnapshot();
});
});

View file

@ -1,5 +1,5 @@
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Extractor } from './Extractor';
export class StreamEmbed extends Extractor {
@ -7,20 +7,20 @@ export class StreamEmbed extends Extractor {
public readonly label = 'StreamEmbed';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public override readonly ttl: number = 259200000; // 3d
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/bullstream|mp4player|watch\.gxplayer/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (/Video is not ready/.test(html)) {
throw new NotFoundError();
}
const video = JSON.parse((html.match(/video ?= ?(.*);/) as string[])[1] as string);
@ -28,11 +28,8 @@ export class StreamEmbed extends Extractor {
{
url: new URL(`/m3u8/${video.uid}/${video.md5}/master.txt?s=1&id=${video.id}&cache=${video.status}`, url.origin),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
height: parseInt(JSON.parse(video.quality)[0]),
title: decodeURIComponent(video.title),
},

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Streamtape } from './Streamtape';
@ -8,10 +7,10 @@ import { Streamtape } from './Streamtape';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Streamtape(new FetcherMock(`${__dirname}/__fixtures__/Streamtape`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg' });
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('Streamtape', () => {
test('streamtape.com /e', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/84Kb3mkoYAiokmW'), CountryCode.es)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/DjmZBG0K9gHkJ7z/WALL-E.2008.1080p.BrRip.x264.YIFY.mp4'))).toMatchSnapshot();
});
});

View file

@ -1,47 +1,68 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { Context, CountryCode, Format, UrlResult } from '../types';
import {
buildMediaFlowProxyExtractorRedirectUrl,
Fetcher,
supportsMediaFlowProxy,
} from '../utils';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class Streamtape extends Extractor {
public readonly id = 'streamtape';
public readonly label = 'Streamtape (via MediaFlow Proxy)';
public readonly label = 'Streamtape';
public override readonly ttl = 0;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/streamtape/) && supportsMediaFlowProxy(ctx);
const supportedDomain = null !== url.host.match(/streamtape/)
|| [
'strtape.cloud',
'streamta.pe',
'strcloud.link',
'strcloud.club',
'strtpe.link',
'scloud.online',
'stape.fun',
'streamadblockplus.com',
'shavetape.cash',
'streamta.site',
'streamadblocker.xyz',
'tapewithadblock.org',
'adblocktape.wiki',
'antiadtape.com',
'tapeblocker.com',
'streamnoads.com',
'tapeadvertisement.com',
'tapeadsenjoyer.com',
'watchadsontape.com',
].includes(url.host);
return supportedDomain && supportsMediaFlowProxy(ctx);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
public override normalize(url: URL): URL {
return new URL(url.href.replace('/e/', '/v/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
// Only needed to properly find non-existing files via 404 response
await this.fetcher.text(ctx, new URL(url.href.replace('/v/', '/e/')), { headers });
const html = await this.fetcher.text(ctx, url, { headers });
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/) as string[];
const $ = cheerio.load(html);
const title = $('meta[name="og:title"]').attr('content') as string;
return [
{
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Streamtape', url),
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Streamtape', url, headers),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title,
bytes: bytes.parse(sizeMatch[1] as string) as number,
},
},
];

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { SuperVideo } from './SuperVideo';
@ -12,22 +11,22 @@ const ctx = createTestContext();
describe('SuperVideo', () => {
test('supervideo.cc /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/q7i0sw1oytw3'), CountryCode.de)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/q7i0sw1oytw3'))).toMatchSnapshot();
});
test('supervideo.tv /embed-/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.tv/embed-1p0m1fi9mok8.html'), CountryCode.it)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.tv/embed-1p0m1fi9mok8.html'))).toMatchSnapshot();
});
test('embed only', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/bj6szat1pval'), CountryCode.it)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/bj6szat1pval'))).toMatchSnapshot();
});
test('deleted or expired file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/ndf5shmy9lpt'), CountryCode.it)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/ndf5shmy9lpt'))).toMatchSnapshot();
});
test('processing video', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/3h1qqoqtldo8'), CountryCode.it)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/3h1qqoqtldo8'))).toMatchSnapshot();
});
});

View file

@ -1,8 +1,8 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { extractUrlFromPacked, Fetcher } from '../utils';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
export class SuperVideo extends Extractor {
@ -10,53 +10,52 @@ export class SuperVideo extends Extractor {
public readonly label = 'SuperVideo';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public override readonly ttl: number = 10800000; // 3h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/supervideo/);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/e/', '/').replace('/embed-', '/'));
return new URL(url.href.replace('/e/', '/').replace('/k/', '/').replace('/embed-', '/'));
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (html.includes('This video can be watched as embed only')) {
return await this.extractInternal(ctx, new URL(`/e${url.pathname}`, url.origin), countryCode);
return await this.extractInternal(ctx, new URL(`/e${url.pathname}`, url.origin), meta);
}
if (/'The file was deleted|The file expired|Video is processing/.test(html)) {
throw new NotFoundError();
}
const playlistUrl = extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]);
const playlistHeaders = { Referer: 'https://supervideo.cc/' };
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/);
const size = heightAndSizeMatch ? bytes.parse(heightAndSizeMatch[2] as string) as number : undefined;
const height = heightAndSizeMatch
? parseInt(heightAndSizeMatch[1] as string)
: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers: playlistHeaders });
const $ = cheerio.load(html);
const title = $('.download__title').text().trim();
return [
{
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
url: playlistUrl,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title,
...(heightAndSizeMatch && {
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
height: parseInt(heightAndSizeMatch[1] as string),
}),
...(size && { bytes: size }),
...(height && { height }),
},
requestHeaders: playlistHeaders,
},
];
};

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Uqload } from './Uqload';
@ -8,14 +7,14 @@ import { Uqload } from './Uqload';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Uqload(new FetcherMock(`${__dirname}/__fixtures__/Uqload`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg' });
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('Uqload', () => {
test('uqload.net /embed-', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.net/embed-z0xbr87oz637.html'), CountryCode.fr)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.net/embed-z0xbr87oz637.html'))).toMatchSnapshot();
});
test('uqload.net /', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.net/z0xbr87oz637.html'), CountryCode.fr)).toMatchSnapshot();
test('file not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.cx/2ua47fdfkjgd.html'))).toMatchSnapshot();
});
});

View file

@ -1,25 +1,16 @@
import * as cheerio from 'cheerio';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, Fetcher, supportsMediaFlowProxy } from '../utils';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class Uqload extends Extractor {
public readonly id = 'uqload';
public readonly label = 'Uqload (via MediaFlow Proxy)';
public override readonly ttl = 0;
public readonly label = 'Uqload';
public override viaMediaFlowProxy = true;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/uqload/) && supportsMediaFlowProxy(ctx);
}
@ -28,9 +19,13 @@ export class Uqload extends Extractor {
return new URL(url.href.replace('/embed-', '/'));
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const html = await this.fetcher.text(ctx, url);
if (/File Not Found/.test(html)) {
throw new NotFoundError();
}
const heightMatch = html.match(/\d{3,}x(\d{3,})/);
const $ = cheerio.load(html);
@ -40,11 +35,8 @@ export class Uqload extends Extractor {
{
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Uqload', url),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title,
...(heightMatch && {
height: parseInt(heightMatch[1] as string),

View file

@ -1,42 +1,20 @@
import { MockAgent, setGlobalDispatcher } from 'undici';
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { Fetcher, FetcherMock } from '../utils';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { VidSrc } from './VidSrc';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new VidSrc(new FetcherMock(`${__dirname}/__fixtures__/VidSrc`), ['xyz'])]);
const extractorRegistry = new ExtractorRegistry(logger, [new VidSrc(new FetcherMock(`${__dirname}/__fixtures__/VidSrc`), ['vidsrc-embed.ru'])]);
const ctx = createTestContext();
describe('VidSrc', () => {
test('Full Metal Jacket', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt0093058'), CountryCode.en, 'Full Metal Jacket (1987)')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc-embed.ru/embed/movie/tt0093058'))).toMatchSnapshot();
});
test('Black Mirror', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/tv/tt2085059/4/2'), CountryCode.en, 'Black Mirror 4x2')).toMatchSnapshot();
});
test('not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt35628853'), CountryCode.en, 'Titan: The OceanGate Disaster')).toMatchSnapshot();
});
test('rate limit issues are retried and fail if no tlds are left', async () => {
const mockAgent = new MockAgent({ enableCallHistory: true });
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
mockAgent.get('https://vidsrc.xyz')
.intercept({ path: '/embed/movie/tt33043892/1/1' }).reply(429);
mockAgent.get('https://vidsrc.net')
.intercept({ path: '/embed/movie/tt33043892/1/1' }).reply(429);
const fetcher = new Fetcher(winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }));
const vidSrc = new VidSrc(fetcher, ['net', 'xyz']);
expect(await vidSrc.extract(ctx, new URL('https://vidsrc.xyz/embed/movie/tt33043892/1/1'), CountryCode.en, 'Dexter: Resurrection 1x1')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc-embed.ru/embed/tv/tt2085059/4-2'))).toMatchSnapshot();
});
});

View file

@ -1,7 +1,6 @@
import * as cheerio from 'cheerio';
import slugify from 'slugify';
import { NotFoundError, TooManyRequestsError } from '../error';
import { Context, CountryCode, Format, NonEmptyArray, UrlResult } from '../types';
import { BlockedError, TooManyRequestsError } from '../error';
import { Context, Format, InternalUrlResult, Meta, NonEmptyArray } from '../types';
import { Fetcher, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
@ -10,45 +9,49 @@ export class VidSrc extends Extractor {
public readonly label = 'VidSrc';
private readonly fetcher: Fetcher;
private readonly tlds: NonEmptyArray<string>;
public override readonly ttl: number = 10800000; // 3h
public constructor(fetcher: Fetcher, tlds: NonEmptyArray<string>) {
super();
private readonly domains: NonEmptyArray<string>;
this.fetcher = fetcher;
this.tlds = tlds;
public constructor(fetcher: Fetcher, domains: NonEmptyArray<string>) {
super(fetcher);
this.domains = domains;
}
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/vidsrc/);
return null !== url.host.match(/vidsrc|vsrc/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
return this.extractUsingRandomTld(ctx, url, countryCode, [...this.tlds]);
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
// While this is a crappy thing to do, they seem to be blocking overly strict IMO
const randomIp = `${Math.floor(Math.random() * 223) + 1}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`;
const newCtx = { ...ctx, ip: randomIp };
return this.extractUsingRandomDomain(newCtx, url, meta, [...this.domains]);
};
private async extractUsingRandomTld(ctx: Context, url: URL, countryCode: CountryCode, tlds: string[]): Promise<UrlResult[]> {
const tldIndex = Math.floor(Math.random() * tlds.length);
const [tld] = tlds.splice(tldIndex, 1) as [string];
private async extractUsingRandomDomain(ctx: Context, url: URL, meta: Meta, domains: string[]): Promise<InternalUrlResult[]> {
const domainIndex = Math.floor(Math.random() * domains.length);
const [domain] = domains.splice(domainIndex, 1) as [string];
const newUrl = new URL(url);
const hostnameParts = newUrl.hostname.split('.');
hostnameParts[hostnameParts.length - 1] = tld;
newUrl.hostname = hostnameParts.join('.');
newUrl.hostname = domain;
let html: string;
try {
html = await this.fetcher.text(ctx, newUrl);
html = await this.fetcher.text(ctx, newUrl, { queueLimit: 1 });
} catch (error) {
if (error instanceof TooManyRequestsError && tlds.length) {
return this.extractUsingRandomTld(ctx, url, countryCode, tlds);
/* istanbul ignore next */
if (domains.length && (error instanceof TooManyRequestsError || error instanceof BlockedError)) {
return this.extractUsingRandomDomain(ctx, url, meta, domains);
}
/* istanbul ignore next */
throw error;
}
const $ = cheerio.load(html);
const $ = cheerio.load(html.replace('<!--', '').replace('-->', '')); // server HTML is commented-out
const iframeUrl = new URL(($('#player_iframe').attr('src') as string).replace(/^\/\//, 'https://'));
const title = $('title').text().trim();
@ -59,26 +62,21 @@ export class VidSrc extends Extractor {
.toArray()
.filter(({ serverName }) => serverName === 'CloudStream Pro')
.map(async ({ serverName, dataHash }) => {
const iframeHtml = await this.fetcher.text(ctx, new URL(`/rcp/${dataHash}`, iframeUrl.origin), { headers: { Referer: iframeUrl.origin } });
const rcpUrl = new URL(`/rcp/${dataHash}`, iframeUrl.origin);
const iframeHtml = await this.fetcher.text(ctx, rcpUrl, { headers: { Referer: newUrl.origin } });
const srcMatch = iframeHtml.match(`src:\\s?'(.*)'`) as string[];
const playerHtml = await this.fetcher.text(ctx, new URL(srcMatch[1] as string, iframeUrl.origin), { headers: { Referer: iframeUrl.origin } });
const fileMatch = playerHtml.match(`file:\\s?'(.*)'`);
if (!fileMatch) {
throw new NotFoundError();
}
const m3u8Url = new URL(fileMatch[1] as string);
const playerHtml = await this.fetcher.text(ctx, new URL(srcMatch[1] as string, iframeUrl.origin), { headers: { Referer: rcpUrl.href } });
const fileMatch = playerHtml.match(`(https:\\/\\/.*?{v\\d}.*?) or`) as string[];
const m3u8Url = new URL((fileMatch[1] as string).replace(/{v\d}/, iframeUrl.host));
return {
url: m3u8Url,
format: Format.hls,
label: `${this.label} (${serverName})`,
sourceId: `${this.id}_${slugify(serverName)}_${countryCode}`,
ttl: this.ttl,
label: serverName,
meta: {
countryCodes: [countryCode],
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url),
...meta,
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: iframeUrl.href } }),
title,
},
};

View file

@ -0,0 +1,20 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Vidora } from './Vidora';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Vidora(new FetcherMock(`${__dirname}/__fixtures__/Vidora`))]);
const ctx = createTestContext();
describe('Vidora', () => {
test('vidora.stream', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidora.stream/5iz6c1elaqgq'))).toMatchSnapshot();
});
test('vidora.stream /embed/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidora.stream/embed/5iz6c1elaqgq'))).toMatchSnapshot();
});
});

43
src/extractor/Vidora.ts Normal file
View file

@ -0,0 +1,43 @@
import * as cheerio from 'cheerio';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
export class Vidora extends Extractor {
public readonly id = 'vidora';
public readonly label = 'Vidora';
public override readonly ttl: number = 43200000; // 12h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/vidora/);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/embed/', '/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const $ = cheerio.load(html);
const title = $('title').text().trim().replace(/^Watch /, '').trim();
const m3u8Url = extractUrlFromPacked(html, [/file: ?"(.*?)"/]);
const headers = { Origin: url.origin };
return [
{
url: m3u8Url,
format: Format.hls,
meta: {
...meta,
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers }),
title,
},
requestHeaders: headers,
},
];
};
}

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { VixSrc } from './VixSrc';
@ -8,20 +7,24 @@ import { VixSrc } from './VixSrc';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new VixSrc(new FetcherMock(`${__dirname}/__fixtures__/VixSrc`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg', de: 'on', en: 'on', it: 'on' });
const ctx = createTestContext();
describe('VixSrc', () => {
test('Full Metal Jacket', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/movie/600'), CountryCode.it, 'Full Metal Jacket (1987)')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/movie/600'))).toMatchSnapshot();
});
test('Black Mirror', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'), CountryCode.it, 'Black Mirror 4x2')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'))).toMatchSnapshot();
});
test('Rental Family', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/movie/1208348'))).toMatchSnapshot();
});
test('Black Mirror is excluded if no matching language was found', async () => {
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg', de: 'on' });
const ctx = createTestContext({ de: 'on' });
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'), CountryCode.it, 'Black Mirror 4x2')).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/3'))).toMatchSnapshot();
});
});

View file

@ -1,44 +1,34 @@
import bytes from 'bytes';
import { Context, CountryCode, Format, UrlResult } from '../types';
import {
buildMediaFlowProxyExtractorStreamUrl,
Fetcher,
hasMultiEnabled,
iso639FromCountryCode,
supportsMediaFlowProxy,
} from '../utils';
import { Context, CountryCode, Format, InternalUrlResult, Meta } from '../types';
import { CustomRequestConfig, guessHeightFromPlaylist, hasMultiEnabled, iso639FromCountryCode } from '../utils';
import { Extractor } from './Extractor';
export class VixSrc extends Extractor {
public readonly id = 'vixsrc';
public readonly label = 'VixSrc (via MediaFlow Proxy)';
public readonly label = 'VixSrc';
public override readonly ttl = 0;
public override readonly ttl: number = 21600000; // 6h
public override viaMediaFlowProxy = true;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/vixsrc/);
}
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/vixsrc/) && supportsMediaFlowProxy(ctx);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: url.href };
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const filenameMatch = html.match(/"filename":"(.*?)"/);
const sizeMatch = html.match(/"size":(\d+)/);
const qualityMatch = html.match(/"quality":(\d+)/);
const tokenMatch = html.match(/['"]token['"]: ?['"](.*?)['"]/) as string[];
const expiresMatch = html.match(/['"]expires['"]: ?['"](.*?)['"]/) as string[];
const urlMatch = html.match(/url: ?['"](.*?)['"]/) as string[];
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'VixCloud', url);
const countryCodes = await this.determineCountryCodesFromPlaylist(ctx, playlistUrl);
const baseUrl = new URL(`${urlMatch[1]}`);
const playlistUrl = new URL(`${baseUrl.origin}${baseUrl.pathname}.m3u8?${baseUrl.searchParams}`);
playlistUrl.searchParams.append('token', tokenMatch[1] as string);
playlistUrl.searchParams.append('expires', expiresMatch[1] as string);
playlistUrl.searchParams.append('h', '1');
const countryCodes = meta.countryCodes ?? [CountryCode.multi, ...(await this.determineCountryCodesFromPlaylist(ctx, playlistUrl, { headers }))];
if (!hasMultiEnabled(ctx.config) && !countryCodes.some(countryCode => countryCode in ctx.config)) {
return [];
@ -48,23 +38,19 @@ export class VixSrc extends Extractor {
{
url: playlistUrl,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
countryCodes,
...(filenameMatch && { title: filenameMatch[1] }),
...(sizeMatch && { bytes: bytes.parse(`${sizeMatch[1]} kb`) as number }),
...(qualityMatch && { height: parseInt(qualityMatch[1] as string) }),
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers }),
},
},
];
};
private async determineCountryCodesFromPlaylist(ctx: Context, playlistUrl: URL): Promise<CountryCode[]> {
const playlist = await this.fetcher.text(ctx, playlistUrl);
private async determineCountryCodesFromPlaylist(ctx: Context, playlistUrl: URL, init?: CustomRequestConfig): Promise<CountryCode[]> {
const playlist = await this.fetcher.text(ctx, playlistUrl, init);
const countryCodes: CountryCode[] = [CountryCode.it];
const countryCodes: CountryCode[] = [];
(Object.keys(CountryCode) as CountryCode[]).forEach((countryCode) => {
const iso639 = iso639FromCountryCode(countryCode);

28
src/extractor/Voe.test.ts Normal file
View file

@ -0,0 +1,28 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Voe } from './Voe';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Voe(new FetcherMock(`${__dirname}/__fixtures__/Voe`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('Voe', () => {
test('jilliandescribecompany', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://jilliandescribecompany.com/e/ea21l02gcygw'))).toMatchSnapshot();
});
test('premium only without resolution', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://jilliandescribecompany.com/qqfyi04w52mj'))).toMatchSnapshot();
});
test('encoding error', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://voe.sx/e/c2yxvit4f6bz'))).toMatchSnapshot();
});
test('embed only urls which otherwise lead to 404', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mikaylaarealike.com/e/gqlhm9hbobwu'))).toMatchSnapshot();
});
});

180
src/extractor/Voe.ts Normal file
View file

@ -0,0 +1,180 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import {
buildMediaFlowProxyExtractorStreamUrl, guessHeightFromPlaylist,
supportsMediaFlowProxy,
} from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/voesx.py */
export class Voe extends Extractor {
public readonly id = 'voe';
public readonly label = 'VOE';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
const supportedDomain = null !== url.host.match(/voe/)
|| [
'19turanosephantasia.com',
'20demidistance9elongations.com',
'30sensualizeexpression.com',
'321naturelikefurfuroid.com',
'35volitantplimsoles5.com',
'449unceremoniousnasoseptal.com',
'745mingiestblissfully.com',
'adrianmissionminute.com',
'alleneconomicmatter.com',
'antecoxalbobbing1010.com',
'apinchcaseation.com',
'audaciousdefaulthouse.com',
'availedsmallest.com',
'bigclatterhomesguideservice.com',
'boonlessbestselling244.com',
'bradleyviewdoctor.com',
'brittneystandardwestern.com',
'brucevotewithin.com',
'christopheruntilpoint.com',
'chromotypic.com',
'chuckle-tube.com',
'cindyeyefinal.com',
'counterclockwisejacky.com',
'crownmakermacaronicism.com',
'crystaltreatmenteast.com',
'cyamidpulverulence530.com',
'diananatureforeign.com',
'donaldlineelse.com',
'edwardarriveoften.com',
'erikcoldperson.com',
'figeterpiazine.com',
'fittingcentermondaysunday.com',
'fraudclatterflyingcar.com',
'gamoneinterrupted.com',
'generatesnitrosate.com',
'goofy-banana.com',
'graceaddresscommunity.com',
'greaseball6eventual20.com',
'guidon40hyporadius9.com',
'heatherdiscussionwhen.com',
'housecardsummerbutton.com',
'jamessoundcost.com',
'jamiesamewalk.com',
'jasminetesttry.com',
'jayservicestuff.com',
'jennifercertaindevelopment.com',
'jilliandescribecompany.com',
'johnalwayssame.com',
'jonathansociallike.com',
'josephseveralconcern.com',
'kathleenmemberhistory.com',
'kellywhatcould.com',
'kennethofficialitem.com',
'kinoger.ru',
'kristiesoundsimply.com',
'lancewhosedifficult.com',
'launchreliantcleaverriver.com',
'lauradaydo.com',
'lisatrialidea.com',
'loriwithinfamily.com',
'lukecomparetwo.com',
'lukesitturn.com',
'mariatheserepublican.com',
'matriculant401merited.com',
'maxfinishseveral.com',
'metagnathtuggers.com',
'michaelapplysome.com',
'mikaylaarealike.com',
'nathanfromsubject.com',
'nectareousoverelate.com',
'nonesnanking.com',
'paulkitchendark.com',
'realfinanceblogcenter.com',
'rebeccaneverbase.com',
'reputationsheriffkennethsand.com',
'richardsignfish.com',
'roberteachfinal.com',
'robertordercharacter.com',
'robertplacespace.com',
'sandratableother.com',
'sandrataxeight.com',
'scatch176duplicities.com',
'sethniceletter.com',
'shannonpersonalcost.com',
'simpulumlamerop.com',
'smoki.cc',
'stevenimaginelittle.com',
'strawberriesporail.com',
'telyn610zoanthropy.com',
'timberwoodanotia.com',
'toddpartneranimal.com',
'toxitabellaeatrebates306.com',
'uptodatefinishconferenceroom.com',
'v-o-e-unblock.com',
'valeronevijao.com',
'walterprettytheir.com',
'wolfdyslectic.com',
'yodelswartlike.com',
].includes(url.host);
return supportedDomain && supportsMediaFlowProxy(ctx);
}
public override normalize(url: URL): URL {
return new URL(`/${url.pathname.replace(/\/+$/, '').split('/').at(-1)}`, url);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
let html: string;
try {
html = await this.fetcher.text(ctx, url, { headers });
} catch (error) {
/* istanbul ignore next */
if (error instanceof NotFoundError && !url.href.includes('/e/')) {
return await this.extractInternal(ctx, new URL(`/e${url.pathname}`, url.origin), meta);
}
/* istanbul ignore next */
throw error;
}
const redirectMatch = html.match(/window\.location\.href\s*=\s*'([^']+)/);
if (redirectMatch && redirectMatch[1]) {
return await this.extractInternal(ctx, new URL(redirectMatch[1]), meta);
}
if (/An error occurred during encoding/.test(html)) {
throw new NotFoundError();
}
const $ = cheerio.load(html);
const title = $('meta[name="description"]').attr('content')?.trim().replace(/^Watch /, '').replace(/ at VOE$/, '').trim();
const sizeMatch = html.matchAll(/[\d.]+ ?[GM]B/g).toArray().at(-1);
const size = sizeMatch ? bytes.parse(sizeMatch[0] as string) as number : /* istanbul ignore next */null;
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Voe', url, headers);
const heightMatch = html.match(/<b>(\d{3,})p<\/b>/);
const height = heightMatch
? parseInt(heightMatch[1] as string)
: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl);
return [
{
url: playlistUrl,
format: Format.hls,
meta: {
...meta,
height,
title,
...(size && size > 16777216 && { bytes: size }),
},
},
];
};
}

View file

@ -1,29 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { XPrime } from './XPrime';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new XPrime(new FetcherMock(`${__dirname}/__fixtures__/XPrime`))]);
const ctx = createTestContext();
describe('XPrime', () => {
test('unknown backend is ignored', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/unknown?name=Superman&year=2025'), CountryCode.en, 'Superman (2025)')).toMatchSnapshot();
});
test('primebox Superman', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Superman&year=2025'), CountryCode.en, 'Superman (2025)')).toMatchSnapshot();
});
test('primebox Alien: Earth', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Alien%3A%20Earth&year=2025&season=1&episode=1'), CountryCode.en, 'Alien: Earth 1x1')).toMatchSnapshot();
});
test('primebox not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Jurassic%20World%20Rebirth&year=2025'), CountryCode.en, 'Jurassic World Rebirth (2025)')).toMatchSnapshot();
});
});

View file

@ -1,68 +0,0 @@
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Extractor } from './Extractor';
interface XPrimePrimeboxResponsePartial {
streams: Record<string, string>;
series_info?: {
episode: number;
season: number;
title: string;
year: string;
};
title?: string;
}
export class XPrime extends Extractor {
public readonly id = 'xprime';
public readonly label = 'XPrime';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/xprime/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const urlResults: UrlResult[] = [];
const referer = url.protocol + '//' + url.hostname.split('.').slice(-2).join('.'); // Strip subdomains
if (url.href.includes('primebox')) {
const jsonResponse = JSON.parse(await this.fetcher.text(ctx, url)) as XPrimePrimeboxResponsePartial;
for (const [resolution, stream] of Object.entries(jsonResponse.streams)) {
const url = new URL(stream);
const title = jsonResponse.series_info
? `${jsonResponse.series_info.title} ${jsonResponse.series_info.season}x${jsonResponse.series_info.episode} (${jsonResponse.series_info.year})`
: jsonResponse.title;
urlResults.push({
url,
format: Format.mp4,
label: `${this.label} Primebox`,
sourceId: `${this.id}_${countryCode}_primebox`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
bytes: parseInt((await this.fetcher.head(ctx, url, { headers: { Referer: referer }, minCacheTtl: this.ttl }))['content-length'] as string),
height: parseInt(resolution),
title,
},
requestHeaders: {
Referer: referer,
},
});
}
}
return urlResults;
};
}

View file

@ -1,6 +1,5 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { YouTube } from './YouTube';
@ -12,10 +11,10 @@ const ctx = createTestContext();
describe('YouTube', () => {
test('Solaris', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://www.youtube.com/watch?v=Z8ZhQPaw4rE'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://www.youtube.com/watch?v=Z8ZhQPaw4rE'))).toMatchSnapshot();
});
test('unsupported format', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://youtu.be/Z8ZhQPaw4rE?feature=shared'), CountryCode.en)).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://youtu.be/Z8ZhQPaw4rE?feature=shared'))).toMatchSnapshot();
});
});

View file

@ -1,5 +1,4 @@
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Extractor } from './Extractor';
export class YouTube extends Extractor {
@ -7,20 +6,16 @@ export class YouTube extends Extractor {
public readonly label = 'YouTube';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public override readonly ttl: number = 21600000; // 6h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/youtube/) && url.searchParams.has('v');
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
const titleMatch = html.match(/"title":{"runs":\[{"text":"(.*?)"/) as string[];
@ -29,11 +24,8 @@ export class YouTube extends Extractor {
url,
format: Format.unknown,
ytId: url.searchParams.get('v') as string,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
...meta,
title: titleMatch[1] as string,
},
},

View file

@ -1 +0,0 @@
{"server":"nginx","date":"Sun, 08 Jun 2025 19:20:59 GMT","content-type":"video/mp4","content-length":791702409,"last-modified":"Wed, 03 Apr 2024 09:53:31 GMT","connection":"keep-alive","etag":"660d271b-2f306b89","access-control-allow-origin":"*","accept-ranges":"bytes"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1,2 @@
<!doctype html><html lang="en"><head> <meta charset="utf-8"> <title>Video not found | DoodStream</title> <link rel="preconnect" href="//i.doodcdn.io"><link rel="preconnect" href="//cdnjs.cloudflare.com"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> <link rel="stylesheet" href="//i.doodcdn.io/theme_2/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="//i.doodcdn.io/theme_2/css/style.css" /></head><body> <style type="text/css"> .not_found img {height:250px !important;} @media (max-width: 768px){.not_found h1{font-size:1.8rem;}.not_found img {height:200px !important;margin: 0 auto 15px;}} @media (max-width: 480px){.not_found h1{font-size:1.4rem;}.not_found img {height:120px !important;margin: 0 auto 15px;}.not_found p{margin-top: -5px;}} </style> <div class="container pt-5 not_found"> <div class="row"> <div class="col-md-12 pt-2 text-center"> <img src="//i.doodcdn.io/img/no_video_3.svg"> <h1>Not Found</h1> <p>video you are looking for is not found.</p> </div> </div> </div><script data-cfasync="false" src="//d1f05vr3sjsuy7.cloudfront.net/?srvfd=908056"></script><script data-cfasync="false" async type="text/javascript" src="//faqirsgoliard.top/gHzOaAdOhbZ/71405"></script><script data-cfasync="false" async type="text/javascript" src="//faqirsgoliard.top/r67c0fc81985e5/70849"></script><script type="text/javascript" data-cfasync="false">/*<![CDATA[/* */(function(){var g=window,r="ca0967284e67bc53cb5b626985619403",w=[["siteId",317+185*82*507-3600486],["minBid",0],["popundersPerIP","0"],["delayBetween",300],["default","https://strettechoco.com/iYa7dlULceKw/27615"],["defaultPerDay",0],["topmostLayer","never"]],n=["d3d3LmJsb2NrYWRzbm90LmNvbS9iYWphLm1pbi5jc3M=","ZG5oZmk1bm4yZHQ2Ny5jbG91ZGZyb250Lm5ldC9RVHd0eC91YWxnZWJyYS5taW4uanM=","d3d3LmJibHZwbGd1b21hLmNvbS9mYWphLm1pbi5jc3M=","d3d3LndqZ3BzcGt4empiZGd1LmNvbS9NTmlIWS9rYWxnZWJyYS5taW4uanM="],f=-1,v,q,h=function(){clearTimeout(q);f++;if(n[f]&&!(1766620801000<(new Date).getTime()&&1<f)){v=g.document.createElement("script");v.type="text/javascript";v.async=!0;var o=g.document.getElementsByTagName("script")[0];v.src="https://"+atob(n[f]);v.crossOrigin="anonymous";v.onerror=h;v.onload=function(){clearTimeout(q);g[r.slice(0,16)+r.slice(0,16)]||h()};q=setTimeout(h,5E3);o.parentNode.insertBefore(v,o)}};if(!g[r]){try{Object.freeze(g[r]=w)}catch(e){}h()}})();/*]]>/* */</script></body></html>
<!doctype html><html lang="en"><head> <meta charset="utf-8"> <title>Video not found | DoodStream</title> <link rel="preconnect" href="//i.doodcdn.io"><link rel="preconnect" href="//cdnjs.cloudflare.com"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> <link rel="stylesheet" href="//i.doodcdn.io/theme_2/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="//i.doodcdn.io/theme_2/css/style.css" /></head><body> <style type="text/css"> .not_found img {height:250px !important;} @media (max-width: 768px){.not_found h1{font-size:1.8rem;}.not_found img {height:200px !important;margin: 0 auto 15px;}} @media (max-width: 480px){.not_found h1{font-size:1.4rem;}.not_found img {height:120px !important;margin: 0 auto 15px;}.not_found p{margin-top: -5px;}} </style> <div class="container pt-5 not_found"> <div class="row"> <div class="col-md-12 pt-2 text-center"> <img src="//i.doodcdn.io/img/no_video_3.svg"> <h1>Not Found</h1> <p>video you are looking for is not found.</p> </div> </div> </div><script data-cfasync="false" src="//d1f05vr3sjsuy7.cloudfront.net/?srvfd=908056"></script><script data-cfasync="false" async type="text/javascript" src="//faqirsgoliard.top/gHzOaAdOhbZ/71405"></script><script data-cfasync="false" async type="text/javascript" src="//faqirsgoliard.top/r67c0fc81985e5/70849"></script><script type="text/javascript" data-cfasync="false">/*<![CDATA[/* */(function(){var g=window,r="ca0967284e67bc53cb5b626985619403",w=[["siteId",317+185*82*507-3600486],["minBid",0],["popundersPerIP","0"],["delayBetween",300],["default","https://strettechoco.com/iYa7dlULceKw/27615"],["defaultPerDay",0],["topmostLayer","never"]],n=["d3d3LmJsb2NrYWRzbm90LmNvbS9iYWphLm1pbi5jc3M=","ZG5oZmk1bm4yZHQ2Ny5jbG91ZGZyb250Lm5ldC9RVHd0eC91YWxnZWJyYS5taW4uanM=","d3d3LmJibHZwbGd1b21hLmNvbS9mYWphLm1pbi5jc3M=","d3d3LndqZ3BzcGt4empiZGd1LmNvbS9NTmlIWS9rYWxnZWJyYS5taW4uanM="],f=-1,v,q,h=function(){clearTimeout(q);f++;if(n[f]&&!(1766620801000<(new Date).getTime()&&1<f)){v=g.document.createElement("script");v.type="text/javascript";v.async=!0;var o=g.document.getElementsByTagName("script")[0];v.src="https://"+atob(n[f]);v.crossOrigin="anonymous";v.onerror=h;v.onload=function(){clearTimeout(q);g[r.slice(0,16)+r.slice(0,16)]||h()};q=setTimeout(h,5E3);o.parentNode.insertBefore(v,o)}};if(!g[r]){try{Object.freeze(g[r]=w)}catch(e){}h()}})();/*]]>/* */</script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/vcd15cbe7772f49c399c6a5babf22c1241717689176015" integrity="sha512-ZpsOmlRQV6y907TI0dKBHq9Md29nnaEIPlkf84rnaERnq6zvWvPUqr2ft8M1aS28oN72PdrCzSjY4U6VaAw1EQ==" data-cf-beacon='{"version":"2024.11.0","token":"1c76230f8ab248eab0b0f2bf7c205c51","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
https://ijs155l.cloudatacdn.com/u5kj2rz5qtdlsdgge4hrgj2flz5cjodea6auatveaphxogr2vvukxfzmwzva/egprvdjvpr~

View file

@ -1 +0,0 @@
https://ee317r.cloudatacdn.com/u5kj63yvxddlsdgge7qremqqka27irwiupc4k7o5cikbhjliz6xwv2xr53uq/lco4d3heif~

View file

@ -1 +0,0 @@
https://wec6bnh7rx7rw11bv9e9vslw4rud8nam.5f1ebd98099ce35faeeddb30c1752191.r2.cloudflarestorage.com/icxxqqdsfdc4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ab085b515b950aae0a86cae59456cacd%2F20250714%2Fauto%2Fs3%2Faws4_request&X-Amz-Date=20250714T154400Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=845ebfa5feedd840027e431f31e013513dd4c4f74364b4e75a2c8ddb213bd97a

View file

@ -1 +0,0 @@
https://aa360cc.cloudatacdn.com/u5kjv4jxytd3sdgge5uogji5dg4huat2pxrm2qibdbm5rlyusvl46iz3u3ta/1f4isxg0tw~

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,5 @@
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3955353,RESOLUTION=1920x800,FRAME-RATE=23.974,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR
index-v1-a1.m3u8?t=jq0VCV2dOyLIIH07IGIpzruCXvbfdVqzGLWfMeo00AM&s=1759845990&e=14400&f=1571895&srv=ds3&i=0.0&sp=0&ii=2.184.0.222&p1=ds3&p2=ds3
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=68483,RESOLUTION=1920x800,CODECS="avc1.640028",URI="iframes-v1-a1.m3u8?t=jq0VCV2dOyLIIH07IGIpzruCXvbfdVqzGLWfMeo00AM&s=1759845990&e=14400&f=1571895&srv=ds3&i=0.0&sp=0&ii=2.184.0.222&p1=ds3&p2=ds3",VIDEO-RANGE=SDR

View file

@ -0,0 +1,230 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Fastream</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#FFF">
<link rel="apple-touch-icon" href="https://fastream.to/images2/favicon/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="https://fastream.to/images2/favicon/favicon-32x32.png" sizes="32x32">
<link rel="icon" href="https://fastream.to/images2/favicon/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="https://fastream.to/images2/favicon/site.webmanifest" crossorigin="use-credentials">
<link rel="icon" href="https://fastream.to/images2/favicon/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400..800&display=swap" rel="stylesheet">
<link href="https://fastream.to/css2/bootstrap.css?" rel="stylesheet">
<link href="https://fastream.to/css2/style.css?" rel="stylesheet">
<script src="https://fastream.to/js2/jquery.min.js?"></script>
<script src="https://fastream.to/js2/bootstrap.bundle.min.js?"></script>
<script src="https://fastream.to/js2/app.js?aс1"></script>
<script src="https://fastream.to/js2/xupload.js?aс1"></script>
</head>
<body class="">
<header class="header">
<div class="container">
<div class="row header-row align-items-center flex-nowrap">
<div class="col-auto flex-grow-1 flex-shrink-1">
<div class="row align-items-center">
<div class="col-auto d-lg-none">
<div class="dropdown">
<button class="btn icon-btn" data-bs-toggle="dropdown" data-bs-offset="0, 16">
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="22" height="16" viewBox="0 0 22 16">
<path d="M0 0h22v2H0V0zm0 7h22v2H0V7zm0 7h22v2H0v-2z"></path>
</svg>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<a href="/premium" class="dropdown-item">Premium</a>
<a href="/make_money" class="dropdown-item">Make Money</a>
<a href="/check_files" class="dropdown-item">Link Checker</a>
<a href="/api.html" class="dropdown-item">API</a>
<a href="/tos" class="dropdown-item">Terms of service</a>
<a href="/contact" class="dropdown-item">Contact Us</a>
<a href="/faq" class="dropdown-item">FAQ</a>
</ul>
</div>
</div>
<div class="col-auto me-lg-4">
<a href="/" class="logo">
<img src="https://fastream.to/images2/logo.svg?" alt="">
</a>
</div>
<div class="col-auto d-none d-lg-block">
<nav class="nav">
<a class="nav-link link-body-emphasis" href="/premium">Premium</a>
<a class="nav-link link-body-emphasis" href="/make_money">Make Money</a>
</nav>
</div>
</div>
</div>
<div class="col-auto">
<div class="dropdown header-lang">
<button type="button" class="btn icon-btn" data-bs-toggle="dropdown" data-bs-offset="0, 16">
<img src="https://fastream.to/images2/flags/english.svg" alt="">
</button>
<div class="dropdown-menu">
<a href="/?op=change_lang&lang=English" class="dropdown-item"><img src="https://fastream.to/images2/flags/english.svg" alt="">English</a>
<a href="/?op=change_lang&lang=Russian" class="dropdown-item"><img src="https://fastream.to/images2/flags/russian.svg" alt="">Russian</a>
</div>
</div>
</div>
<div class="col-auto d-none d-lg-block">
<a href="/login.html" class="btn me-2 login-btn link-body-emphasis">Login</a>
<a href="/?op=registration" class="btn btn-primary reg-btn">Sign Up</a>
</div>
<div class="col-auto d-lg-none">
<div class="dropdown">
<button class="btn icon-btn" data-bs-toggle="dropdown" data-bs-offset="0, 16">
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="22" height="22" viewBox="0 0 22 22">
<path d="M11 22C4.9 22 0 17.1 0 11S4.9 0 11 0s11 4.9 11 11-4.9 11-11 11zm0-20c-5 0-9 4-9 9s4 9 9 9 9-4 9-9-4-9-9-9z"></path>
<path d="M11 15.3c-3 0-5.4-2.4-5.4-5.4S8 4.4 11 4.4s5.4 2.4 5.4 5.4-2.4 5.5-5.4 5.5zm0-8.9c-1.9 0-3.4 1.5-3.4 3.4s1.5 3.4 3.4 3.4 3.4-1.5 3.4-3.4-1.5-3.4-3.4-3.4z"></path>
<path d="M4.9 19.8c-.2 0-.3 0-.5-.1-.5-.3-.6-.9-.4-1.4 1.5-2.5 3.9-3.8 7-3.8s5.5 1.3 7 3.8c.3.5.1 1.1-.3 1.4-.5.3-1.1.1-1.4-.3-1.1-1.9-2.9-2.8-5.2-2.8-2.4 0-4.1.9-5.3 2.8-.2.2-.6.4-.9.4z"></path>
</svg>
</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href="/login.html">Login</a>
<a class="dropdown-item" href="/?op=registration">Sign Up</a>
<a class="dropdown-item" href="/?op=forgot_pass">Forgot your password?</a>
</div>
</div>
</div>
</div>
</div>
</header>
<main>
<div class="container">
<div class="alert alert-danger" role="alert">
No such file=a9rp1d1pqom7
</div>
</div>
</main>
<footer class="footer">
<div class="container">
<div class="footer-main">
<div class="row justify-content-center justify-content-lg-start">
<div class="col-12 col-lg-4 col-xl-3 text-center text-lg-start">
<div class="mb-4">
<a href="https://fastream.to/" class="logo">
<img src="https://fastream.to/images2/logo-w.svg?" alt="">
</a>
</div>
<div class="mb-2 text-uppercase"><!--Share us: --></div>
<div class="mb-3">
<!--span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/fb.svg" alt=""></a>
</span>
<span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/red.svg" alt=""></a>
</span>
<span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/wha.svg" alt=""></a>
</span>
<span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/x.svg" alt=""></a>
</span>
<span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/msg.svg" alt=""></a>
</span>
<span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/in.svg" alt=""></a>
</span-->
</div>
</div>
<div class="col-lg-8 col-xl-7 d-none d-lg-block">
<div class="row">
<div class="col">
<div class="text-uppercase text-dark large">Info</div>
<div class="nav flex-column">
<a href="https://fastream.to/" class="nav-link">Home</a>
<a href="/api.html" class="nav-link">API</a>
<a href="/tos" class="nav-link">Terms of service</a>
</div>
</div>
<div class="col">
<div class="text-uppercase text-dark large">Features</div>
<div class="nav flex-column">
<a href="/premium" class="nav-link">Premium</a>
<a href="/make_money" class="nav-link">Make Money</a>
<a href="/check_files" class="nav-link">Link Checker</a>
</div>
</div>
<div class="col">
<div class="text-uppercase text-dark large">Support</div>
<div class="nav flex-column">
<a href="/contact" class="nav-link">Contact Us</a>
<a href="/faq" class="nav-link">FAQ</a>
</div>
</div>
</div>
</div>
<div class="col-xl-auto text-nowrap text-end d-none d-xl-block">
<a href="https://fastream.to/login.html" class="btn btn-light login-btn me-2">Login</a>
<a href="https://fastream.to/?op=registration" class="btn btn-primary reg-btn">Sign Up</a>
</div>
</div>
</div>
</div>
<div class="footer-bottom small">
<div class="container">
<div class="row align-items-center justify-content-between">
<div class="col-auto">
<div class="text-muted">&copy; 2024 Fastream</div>
</div>
<div class="col-auto">
</div>
</div>
</div>
</div>
</footer>
<div style ="display:none"><script id="_wau1xg">var _wau = _wau || []; _wau.push(["classic", "y7jk20u14x", "1xg"]);</script><script async src="//waust.at/c.js"></script></div>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'993a4e2d4ac5d2ab',t:'MTc2MTMxNzYyMw=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>

View file

@ -0,0 +1,145 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>
Watch 300938 570f5a24 c281 484d 93fa 734dabbec813 jkgg 2651705 vidhide mp4
</title>
<META NAME="description" CONTENT="">
<META NAME="keywords" CONTENT="">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#FFF">
<link rel="apple-touch-icon" href="/static/images/favicon/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="/static/images/favicon/favicon-32x32.png" sizes="32x32">
<link rel="icon" href="/static/images/favicon/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="/static/images/favicon/manifest.json">
<link rel="icon" href="/static/images/favicon/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="crossorigin">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;700&display=swap" rel="stylesheet">
<link href="/static/css/style.css?" rel="stylesheet">
<script src="/static/js/jquery-3.2.1.min.js?"></script>
<script src="/static/js/bootstrap.bundle.min.js?"></script>
<script src="/static/js/app.js?"></script>
<script src="/static/js/xupload.js?"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-48ZJD1VPGZ"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-48ZJD1VPGZ');
</script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(93381808, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/93381808" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</head>
<body>
<header class="header">
<div class="container">
<div class="row align-items-center flex-nowrap">
<div class="col-auto flex-grow-1 flex-shrink-1">
<div class="row align-items-center">
<div class="col-auto" style="margin: 0 auto;">
<a href="/" class="logo">
<img src="/static/images/logo.svg" alt="">
</a>
</div>
</div>
</div>
</div>
</div>
</header>
<main>
<section class="section">
<div class="container">
<div class="block">
<div class="mb-3">
The file was deleted by administration because it didn&#39;t comply with our Terms of Use:
</div>
<h4>
300938--570f5a24-c281-484d-93fa-734dabbec813--jkgg--2651705-vidhide.mp4
</h4>
<br>
Reason for deletion: <br>
<b class="text-danger">
DMCA request
</b>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<div class="row">
<div class="col-lg-4 col-xs-12">
<div class="mb-3 text-center">
<a href="#" class="logo">
<img src="/static/images/logo.svg" alt="">
</a>
</div>
</div>
<div class="col-lg-8 col-xs-12" style="position: relative">
<div class="mb-4 text-center">
<p class="text-muted ">
</p>
</div>
</div>
</div>
</div>
</footer>
<style>
@media (min-width:481px) {
.text-muted {}
}
@media (min-width:961px) {
.text-muted {
position: absolute;
right: 0;
top: 15px
}
}
</style>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>
Watch
</title>
<META NAME="description" CONTENT="">
<META NAME="keywords" CONTENT="">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#FFF">
<link rel="apple-touch-icon" href="/static/images/favicon/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="/static/images/favicon/favicon-32x32.png" sizes="32x32">
<link rel="icon" href="/static/images/favicon/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="/static/images/favicon/manifest.json">
<link rel="icon" href="/static/images/favicon/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="crossorigin">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;700&display=swap" rel="stylesheet">
<link href="/static/css/style.css?" rel="stylesheet">
<script src="/static/js/jquery-3.2.1.min.js?"></script>
<script src="/static/js/bootstrap.bundle.min.js?"></script>
<script src="/static/js/app.js?"></script>
<script src="/static/js/xupload.js?"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-48ZJD1VPGZ"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-48ZJD1VPGZ');
</script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(93381808, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/93381808" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</head>
<body>
<header class="header">
<div class="container">
<div class="row align-items-center flex-nowrap">
<div class="col-auto flex-grow-1 flex-shrink-1">
<div class="row align-items-center">
<div class="col-auto" style="margin: 0 auto;">
<a href="/" class="logo">
<img src="/static/images/logo.svg" alt="">
</a>
</div>
</div>
</div>
</div>
</div>
</header>
<main>
<section class="section">
<div class="container">
<div class="block">
<div class="text-center">
<div class="mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path fill-rule="evenodd" fill="#DC3545" d="M32 64C14.326 64 0 49.674 0 32S14.326 0 32 0c17.666.019 31.981 14.334 32 32 0 17.674-14.326 32-32 32Zm0-61C15.984 3 3 15.984 3 32s12.984 29 29 29c16.008-.019 28.981-12.992 29-29C61 15.984 48.016 3 32 3Zm14.065 43.125a1.504 1.504 0 0 1-2.125 0l-11.94-12L20.065 46.12A1.502 1.502 0 0 1 17.941 44L29.88 32 17.941 20a1.503 1.503 0 0 1 2.124-2.125l11.935 12 11.94-11.98a1.502 1.502 0 0 1 2.12 0A1.5 1.5 0 0 1 46.065 20L34.12 32l11.945 12a1.504 1.504 0 0 1 0 2.125Z"/></svg>
</div>
</div>
<h3 class="text-center mb-3">
File Not Found
</h3>
<div>
<p class="text-center">The file you were looking for could not be found, sorry for any inconvenience.</p>
</div>
</div>
<div class="text-muted">
<p class="text-center"><b>Possible causes of this error could be:</b></p>
<div class="row justify-content-center">
<div class="col-auto">
<ul>
<li>
The file expired
<li>
The file was deleted by its owner
<li>
The file was deleted by administration because it didn&#39;t comply with our Terms of Use
</ul>
</div>
</div>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<div class="row">
<div class="col-lg-4 col-xs-12">
<div class="mb-3 text-center">
<a href="#" class="logo">
<img src="/static/images/logo.svg" alt="">
</a>
</div>
</div>
<div class="col-lg-8 col-xs-12" style="position: relative">
<div class="mb-4 text-center">
<p class="text-muted ">
</p>
</div>
</div>
</div>
</div>
</footer>
<style>
@media (min-width:481px) {
.text-muted {}
}
@media (min-width:961px) {
.text-muted {
position: absolute;
right: 0;
top: 15px
}
}
</style>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show more