11/// Scrapers for ext.to torrent site — 5 catalog variants.
22///
3- /// ext.to is behind Cloudflare protection. We use the byparr (FlareSolverr)
4- /// endpoint to get past the challenge when configured.
3+ /// ext.to is behind Cloudflare protection. We use TRAWL (FlareSolverr-compatible )
4+ /// to get past the challenge when configured.
55///
66/// Scraping flow:
77/// 1. Browse the site (profile pages or search queries) via the listing URL.
2323/// otherwise `magnet`), `timestamp`, `hmac` (SHA256 of
2424/// `torrent_id|timestamp|pageToken`, despite the field's name it's a
2525/// plain hash, not an HMAC) and `sessid` (the csrfToken value).
26- /// - This endpoint re-validates the CF clearance cookie against the
27- /// User-Agent that earned it, so a bare `reqwest` POST always gets
28- /// re-challenged even with the right cookies. We replay the cookie
29- /// jar + UA harvested from byparr's detail-page fetch through a
30- /// real browserless Chrome instance (`BROWSERLESS_URL`), which
31- /// passes because it's an actual browser, not a bare HTTP client.
26+ /// - This endpoint re-validates the CF clearance cookie, so a bare
27+ /// `reqwest` POST gets re-challenged. TRAWL's browser session cache
28+ /// handles the same-origin AJAX POST instead.
3229/// - Fallback: legacy inline `magnet:?...` in HTML.
3330/// 3. Build `ScrapedStream` and write via `stream_convert::write_back_torrents`.
3431use std:: time:: { SystemTime , UNIX_EPOCH } ;
@@ -46,8 +43,8 @@ use crate::{
4643 } ,
4744 parser,
4845 scrapers:: {
49- ScrapedStream , SearchMeta , StreamFile , browser ,
50- fetcher:: { fetch_byparr , fetch_plain } ,
46+ ScrapedStream , SearchMeta , StreamFile ,
47+ fetcher:: { fetch_plain , fetch_trawl , fetch_trawl_bytes , fetch_trawl_post } ,
5148 media_resolve, stream_convert, torrent_metadata,
5249 } ,
5350 util:: { rate_limit, retry} ,
@@ -206,15 +203,15 @@ async fn fetch_html(
206203 label : & str ,
207204 url : & str ,
208205 client : & reqwest:: Client ,
209- byparr_url : & Option < String > ,
206+ trawl_url : & Option < String > ,
210207) -> Option < String > {
211208 retry:: with_retry ( label, || {
212209 let url = url. to_string ( ) ;
213210 let client = client. clone ( ) ;
214- let bp = byparr_url . clone ( ) ;
211+ let bp = trawl_url . clone ( ) ;
215212 async move {
216213 if let Some ( bp_url) = & bp
217- && let Some ( r) = fetch_byparr ( & client, bp_url, & url) . await
214+ && let Some ( r) = fetch_trawl ( & client, bp_url, & url) . await
218215 {
219216 return Ok ( r. html ) ;
220217 }
@@ -477,23 +474,19 @@ fn detail_has_torrent_download(html: &str) -> bool {
477474async fn post_ext_to_ajax (
478475 label : & str ,
479476 client : & reqwest:: Client ,
480- browserless_url : & str ,
477+ trawl_url : & str ,
481478 detail_url : & str ,
482479 ajax_url : & str ,
483480 tid : u64 ,
484481 page_token : & str ,
485482 csrf : & str ,
486483 download_type : & str ,
487- detail_cookies : & [ ( String , String ) ] ,
488- detail_user_agent : & str ,
489484) -> Option < String > {
490485 retry:: with_retry ( label, || {
491486 let client = client. clone ( ) ;
492487 let ajax_url = ajax_url. to_string ( ) ;
493488 let referer = detail_url. to_string ( ) ;
494- let bl_url = browserless_url. to_string ( ) ;
495- let detail_cookies = detail_cookies. to_vec ( ) ;
496- let detail_user_agent = detail_user_agent. to_string ( ) ;
489+ let trawl_url = trawl_url. to_string ( ) ;
497490 let pt = page_token. to_string ( ) ;
498491 let csrf = csrf. to_string ( ) ;
499492 let download_type = download_type. to_string ( ) ;
@@ -507,17 +500,19 @@ async fn post_ext_to_ajax(
507500 urlencoding:: encode( & csrf) ,
508501 ) ;
509502
510- browser :: post_with_cookies_via_browser (
503+ fetch_trawl_post (
511504 & client,
512- & bl_url,
513- & referer,
505+ & trawl_url,
514506 & ajax_url,
515507 & form_data,
516- & detail_cookies,
517- & detail_user_agent,
508+ & [
509+ ( "Content-Type" , "application/x-www-form-urlencoded" ) ,
510+ ( "X-Requested-With" , "XMLHttpRequest" ) ,
511+ ( "Referer" , & referer) ,
512+ ] ,
518513 )
519514 . await
520- . ok_or_else ( || "browserless request failed" . to_string ( ) )
515+ . ok_or_else ( || "trawl POST request failed" . to_string ( ) )
521516 }
522517 } )
523518 . await
@@ -543,9 +538,8 @@ fn magnet_from_ajax_response(raw: &str) -> Option<String> {
543538
544539async fn torrent_bytes_from_ajax_response (
545540 client : & reqwest:: Client ,
546- browserless_url : & str ,
541+ trawl_url : & str ,
547542 base_url : & str ,
548- detail_url : & str ,
549543 raw : & str ,
550544) -> Option < Vec < u8 > > {
551545 if raw. as_bytes ( ) . first ( ) == Some ( & b'd' ) {
@@ -569,14 +563,14 @@ async fn torrent_bytes_from_ajax_response(
569563 format ! ( "{base_url}/{}" , url. trim_start_matches( '/' ) )
570564 } ;
571565
572- browser:: fetch_torrent_via_browser ( client, browserless_url, detail_url, & absolute)
573- . await
574- . or ( torrent_metadata:: download_torrent_bytes (
566+ fetch_trawl_bytes ( client, trawl_url, & absolute) . await . or (
567+ torrent_metadata:: download_torrent_bytes (
575568 client,
576569 & absolute,
577570 std:: time:: Duration :: from_secs ( 30 ) ,
578571 )
579- . await )
572+ . await ,
573+ )
580574}
581575
582576/// Fetch a detail page and extract magnet / torrent file plus HTML file rows.
@@ -585,16 +579,15 @@ async fn fetch_detail_download(
585579 base_url : & str ,
586580 detail_url : & str ,
587581 client : & reqwest:: Client ,
588- byparr_url : & Option < String > ,
589- browserless_url : & Option < String > ,
582+ trawl_url : & Option < String > ,
590583) -> Option < DetailDownload > {
591584 let detail_result = retry:: with_retry ( label, || {
592585 let url = detail_url. to_string ( ) ;
593586 let client = client. clone ( ) ;
594- let bp = byparr_url . clone ( ) ;
587+ let bp = trawl_url . clone ( ) ;
595588 async move {
596589 if let Some ( bp_url) = & bp
597- && let Some ( r) = fetch_byparr ( & client, bp_url, & url) . await
590+ && let Some ( r) = fetch_trawl ( & client, bp_url, & url) . await
598591 {
599592 return Ok ( r) ;
600593 }
@@ -605,8 +598,6 @@ async fn fetch_detail_download(
605598 } )
606599 . await
607600 . ok ( ) ?;
608- let detail_cookies = detail_result. cookies ;
609- let detail_user_agent = detail_result. user_agent ;
610601 let detail_html = detail_result. html ;
611602 let detail_uploader = extract_uploader_from_detail_html ( & detail_html) ;
612603 let html_files = parse_html_file_list ( & detail_html) ;
@@ -636,56 +627,34 @@ async fn fetch_detail_download(
636627
637628 if let Some ( tid) = numeric_id {
638629 let ajax_url = format ! ( "{base_url}/ajax/getTorrentMagnet.php" ) ;
639- if browserless_url . is_none ( ) {
630+ if trawl_url . is_none ( ) {
640631 warn ! (
641- "{label}: BROWSERLESS_URL not configured — ext.to AJAX downloads \
642- require replaying a CF-cleared cookie through a real browser ."
632+ "{label}: TRAWL_URL not configured — ext.to AJAX downloads \
633+ require TRAWL's browser session cache ."
643634 ) ;
644635 }
645636
646- if let Some ( bl_url ) = browserless_url . as_deref ( ) {
637+ if let Some ( trawl ) = trawl_url . as_deref ( ) {
647638 let prefer_torrent =
648639 detail_has_torrent_download ( & detail_html) || !html_files. is_empty ( ) ;
649640
650641 if prefer_torrent
651642 && let Some ( raw) = post_ext_to_ajax (
652- label,
653- client,
654- bl_url,
655- detail_url,
656- & ajax_url,
657- tid,
658- & pt,
659- & csrf,
660- "torrent" ,
661- & detail_cookies,
662- & detail_user_agent,
643+ label, client, trawl, detail_url, & ajax_url, tid, & pt, & csrf, "torrent" ,
663644 )
664645 . await
665646 {
666647 debug ! ( label, ajax_response = %& raw[ ..raw. len( ) . min( 500 ) ] , "AJAX torrent response" ) ;
667- torrent_bytes = torrent_bytes_from_ajax_response (
668- client, bl_url, base_url, detail_url, & raw ,
669- )
670- . await ;
648+ torrent_bytes =
649+ torrent_bytes_from_ajax_response ( client, trawl, base_url, & raw ) . await ;
671650 if torrent_bytes. is_some ( ) {
672651 debug ! ( label, "downloaded .torrent via AJAX" ) ;
673652 }
674653 }
675654
676655 if magnet. is_none ( )
677656 && let Some ( raw) = post_ext_to_ajax (
678- label,
679- client,
680- bl_url,
681- detail_url,
682- & ajax_url,
683- tid,
684- & pt,
685- & csrf,
686- "magnet" ,
687- & detail_cookies,
688- & detail_user_agent,
657+ label, client, trawl, detail_url, & ajax_url, tid, & pt, & csrf, "magnet" ,
689658 )
690659 . await
691660 {
@@ -716,8 +685,7 @@ pub(crate) async fn scrape_ext_catalog(
716685 let domain = ext_to_domain ( & root) ;
717686 let base_url = format ! ( "https://{domain}" ) ;
718687 let client = & ctx. state . http ;
719- let byparr_url = ctx. state . config . byparr_url . clone ( ) ;
720- let browserless_url = ctx. state . config . browserless_url . clone ( ) ;
688+ let trawl_url = ctx. state . config . trawl_url . clone ( ) ;
721689 let pool = & ctx. state . pool ;
722690 let rate_key = domain. clone ( ) ;
723691
@@ -754,8 +722,7 @@ pub(crate) async fn scrape_ext_catalog(
754722 return Err ( JobError :: Cancelled ) ;
755723 }
756724 rate_limit:: wait ( & rate_key, 1 ) . await ;
757- let Some ( html) = fetch_html ( spec. source , & current_url, client, & byparr_url) . await
758- else {
725+ let Some ( html) = fetch_html ( spec. source , & current_url, client, & trawl_url) . await else {
759726 break ;
760727 } ;
761728 let next_url = find_next_page_url ( & html, & base_url, & current_url, is_profile) ;
@@ -778,7 +745,7 @@ pub(crate) async fn scrape_ext_catalog(
778745
779746 rate_limit:: wait ( & rate_key, 1 ) . await ;
780747
781- let html = match fetch_html ( spec. source , & current_url, client, & byparr_url ) . await {
748+ let html = match fetch_html ( spec. source , & current_url, client, & trawl_url ) . await {
782749 Some ( h) => h,
783750 None => {
784751 warn ! ( "{}: failed to fetch listing {current_url}" , spec. source) ;
@@ -808,15 +775,9 @@ pub(crate) async fn scrape_ext_catalog(
808775
809776 info ! ( "{}: scraping \" {}\" — {detail_url}" , spec. source, title) ;
810777
811- let download = fetch_detail_download (
812- spec. source ,
813- & base_url,
814- & detail_url,
815- client,
816- & byparr_url,
817- & browserless_url,
818- )
819- . await ;
778+ let download =
779+ fetch_detail_download ( spec. source , & base_url, & detail_url, client, & trawl_url)
780+ . await ;
820781
821782 let Some ( download) = download else {
822783 warn ! (
0 commit comments