fetch_util 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/README.md +3 -1
- data/Rakefile +0 -2
- data/lib/fetch_util/assets/extract.js +1 -1
- data/lib/fetch_util/browser/interaction_helpers/consent_helpers.rb +12 -17
- data/lib/fetch_util/browser/interaction_helpers/timing_helpers.rb +42 -15
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_commit_details_script.rb +48 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_product_state_script.rb +80 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_helpers_script.rb +53 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_state_script.rb +118 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_stabilization.rb +54 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_state.rb +27 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_product_state.rb +55 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_request_state.rb +100 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_state.rb +59 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_modules.rb +26 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_state.rb +93 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resources.rb +73 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_thread_state.rb +92 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_threads.rb +71 -0
- data/lib/fetch_util/browser/site_stabilization/community_and_marketplace.rb +9 -10
- data/lib/fetch_util/browser/site_stabilization/facebook_stabilization.rb +56 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_product_state_script.rb +97 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_request_state_script.rb +75 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_stabilization.rb +37 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_state.rb +39 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_fetch_state_script.rb +31 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_product_state_script.rb +76 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_request_state_script.rb +111 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_route_state_script.rb +71 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_stabilization.rb +37 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_state.rb +42 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_validation_state_script.rb +49 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_product_state.rb +88 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_state.rb +109 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resources.rb +30 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_stabilization.rb +60 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_product_state.rb +74 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_timeline_state.rb +69 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_threads.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_visibility_script.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/github_pull_resource_stabilization.rb +31 -0
- data/lib/fetch_util/browser/site_stabilization/github_pull_resources.rb +105 -0
- data/lib/fetch_util/browser/site_stabilization/github_threads.rb +82 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state.rb +17 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state_script.rb +98 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_visibility_script.rb +43 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resources.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_repo.rb +3 -11
- data/lib/fetch_util/browser/site_stabilization/gitlab_stabilization.rb +33 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_threads.rb +92 -0
- data/lib/fetch_util/browser/site_stabilization/social_platforms.rb +19 -38
- data/lib/fetch_util/browser/site_stabilization/travel_and_lodging.rb +3 -3
- data/lib/fetch_util/browser/site_stabilization.rb +89 -0
- data/lib/fetch_util/browser/stabilization/anubis.rb +4 -5
- data/lib/fetch_util/browser/stabilization/page_flow.rb +71 -27
- data/lib/fetch_util/browser/stabilization/spa_hydration.rb +7 -5
- data/lib/fetch_util/browser.rb +88 -17
- data/lib/fetch_util/cli.rb +69 -23
- data/lib/fetch_util/extractor.rb +8 -2
- data/lib/fetch_util/fetcher.rb +144 -43
- data/lib/fetch_util/http_redirect_client.rb +161 -0
- data/lib/fetch_util/parallel_fetcher.rb +125 -28
- data/lib/fetch_util/raw_docs_fallback.rb +158 -21
- data/lib/fetch_util/regulatory/cache_store.rb +47 -14
- data/lib/fetch_util/regulatory/fetch_records.rb +10 -7
- data/lib/fetch_util/regulatory/headers.rb +8 -1
- data/lib/fetch_util/regulatory/http_client.rb +2 -97
- data/lib/fetch_util/regulatory/orchestration.rb +10 -6
- data/lib/fetch_util/regulatory/page.rb +8 -11
- data/lib/fetch_util/regulatory/robots.rb +2 -10
- data/lib/fetch_util/regulatory/source_selection.rb +13 -3
- data/lib/fetch_util/regulatory/tdm_page.rb +9 -4
- data/lib/fetch_util/regulatory/tdm_policy.rb +16 -9
- data/lib/fetch_util/regulatory/tdm_rep.rb +4 -3
- data/lib/fetch_util/regulatory/tdm_support.rb +10 -6
- data/lib/fetch_util/regulatory.rb +5 -1
- data/lib/fetch_util/request_log.rb +24 -3
- data/lib/fetch_util/result.rb +37 -89
- data/lib/fetch_util/search_transport.rb +102 -35
- data/lib/fetch_util/searcher.rb +15 -13
- data/lib/fetch_util/version.rb +1 -1
- data/lib/fetch_util.rb +4 -1
- metadata +66 -2
|
@@ -3,13 +3,102 @@
|
|
|
3
3
|
module FetchUtil
|
|
4
4
|
class Browser
|
|
5
5
|
module SiteStabilization
|
|
6
|
+
autoload :AzureDevopsPrProductStateScript,
|
|
7
|
+
"fetch_util/browser/site_stabilization/azure_devops_pr_product_state_script"
|
|
8
|
+
autoload :AzureDevopsPrCommitDetailsScript,
|
|
9
|
+
"fetch_util/browser/site_stabilization/azure_devops_pr_commit_details_script"
|
|
10
|
+
autoload :AzureDevopsPrRequestHelpersScript,
|
|
11
|
+
"fetch_util/browser/site_stabilization/azure_devops_pr_request_helpers_script"
|
|
12
|
+
autoload :AzureDevopsPrRequestStateScript,
|
|
13
|
+
"fetch_util/browser/site_stabilization/azure_devops_pr_request_state_script"
|
|
14
|
+
autoload :AzureDevopsPrState, "fetch_util/browser/site_stabilization/azure_devops_pr_state"
|
|
15
|
+
autoload :AzureDevopsPrStabilization,
|
|
16
|
+
"fetch_util/browser/site_stabilization/azure_devops_pr_stabilization"
|
|
17
|
+
autoload :BitbucketCloudThreadState,
|
|
18
|
+
"fetch_util/browser/site_stabilization/bitbucket_cloud_thread_state"
|
|
19
|
+
autoload :BitbucketCloudThreads, "fetch_util/browser/site_stabilization/bitbucket_cloud_threads"
|
|
20
|
+
autoload :BitbucketCloudPullResourceModules,
|
|
21
|
+
"fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_modules"
|
|
6
22
|
autoload :CommunityAndMarketplace, "fetch_util/browser/site_stabilization/community_and_marketplace"
|
|
23
|
+
autoload :FacebookStabilization, "fetch_util/browser/site_stabilization/facebook_stabilization"
|
|
24
|
+
autoload :GerritChangeProductStateScript,
|
|
25
|
+
"fetch_util/browser/site_stabilization/gerrit_change_product_state_script"
|
|
26
|
+
autoload :GerritChangeRequestStateScript,
|
|
27
|
+
"fetch_util/browser/site_stabilization/gerrit_change_request_state_script"
|
|
28
|
+
autoload :GerritChangeState, "fetch_util/browser/site_stabilization/gerrit_change_state"
|
|
29
|
+
autoload :GerritChangeStabilization,
|
|
30
|
+
"fetch_util/browser/site_stabilization/gerrit_change_stabilization"
|
|
31
|
+
autoload :GerritFileResourceProductStateScript,
|
|
32
|
+
"fetch_util/browser/site_stabilization/gerrit_file_resource_product_state_script"
|
|
33
|
+
autoload :GerritFileResourceRouteStateScript,
|
|
34
|
+
"fetch_util/browser/site_stabilization/gerrit_file_resource_route_state_script"
|
|
35
|
+
autoload :GerritFileResourceFetchStateScript,
|
|
36
|
+
"fetch_util/browser/site_stabilization/gerrit_file_resource_fetch_state_script"
|
|
37
|
+
autoload :GerritFileResourceValidationStateScript,
|
|
38
|
+
"fetch_util/browser/site_stabilization/gerrit_file_resource_validation_state_script"
|
|
39
|
+
autoload :GerritFileResourceRequestStateScript,
|
|
40
|
+
"fetch_util/browser/site_stabilization/gerrit_file_resource_request_state_script"
|
|
41
|
+
autoload :GerritFileResourceState,
|
|
42
|
+
"fetch_util/browser/site_stabilization/gerrit_file_resource_state"
|
|
43
|
+
autoload :GerritFileResourceStabilization,
|
|
44
|
+
"fetch_util/browser/site_stabilization/gerrit_file_resource_stabilization"
|
|
45
|
+
autoload :GithubPullResources, "fetch_util/browser/site_stabilization/github_pull_resources"
|
|
46
|
+
autoload :GithubPullResourceStabilization,
|
|
47
|
+
"fetch_util/browser/site_stabilization/github_pull_resource_stabilization"
|
|
48
|
+
autoload :GithubThreads, "fetch_util/browser/site_stabilization/github_threads"
|
|
49
|
+
autoload :GiteaFamilyThreadProductState,
|
|
50
|
+
"fetch_util/browser/site_stabilization/gitea_family_thread_product_state"
|
|
51
|
+
autoload :GiteaFamilyThreadTimelineState,
|
|
52
|
+
"fetch_util/browser/site_stabilization/gitea_family_thread_timeline_state"
|
|
53
|
+
autoload :GiteaFamilyStabilization,
|
|
54
|
+
"fetch_util/browser/site_stabilization/gitea_family_stabilization"
|
|
55
|
+
autoload :GiteaFamilyVisibilityScript,
|
|
56
|
+
"fetch_util/browser/site_stabilization/gitea_family_visibility_script"
|
|
57
|
+
autoload :GiteaFamilyThreads, "fetch_util/browser/site_stabilization/gitea_family_threads"
|
|
58
|
+
autoload :GiteaFamilyPullResourceProductState,
|
|
59
|
+
"fetch_util/browser/site_stabilization/gitea_family_pull_resource_product_state"
|
|
60
|
+
autoload :GiteaFamilyPullResourceState,
|
|
61
|
+
"fetch_util/browser/site_stabilization/gitea_family_pull_resource_state"
|
|
62
|
+
autoload :GiteaFamilyPullResources, "fetch_util/browser/site_stabilization/gitea_family_pull_resources"
|
|
7
63
|
autoload :GitlabRepo, "fetch_util/browser/site_stabilization/gitlab_repo"
|
|
64
|
+
autoload :GitlabMergeRequestResourceVisibilityScript,
|
|
65
|
+
"fetch_util/browser/site_stabilization/gitlab_merge_request_resource_visibility_script"
|
|
66
|
+
autoload :GitlabMergeRequestResourceStateScript,
|
|
67
|
+
"fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state_script"
|
|
68
|
+
autoload :GitlabMergeRequestResourceState,
|
|
69
|
+
"fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state"
|
|
70
|
+
autoload :GitlabMergeRequestResources, "fetch_util/browser/site_stabilization/gitlab_merge_request_resources"
|
|
71
|
+
autoload :GitlabStabilization, "fetch_util/browser/site_stabilization/gitlab_stabilization"
|
|
72
|
+
autoload :GitlabThreads, "fetch_util/browser/site_stabilization/gitlab_threads"
|
|
8
73
|
autoload :SocialPlatforms, "fetch_util/browser/site_stabilization/social_platforms"
|
|
9
74
|
autoload :TravelAndLodging, "fetch_util/browser/site_stabilization/travel_and_lodging"
|
|
10
75
|
|
|
76
|
+
include AzureDevopsPrState
|
|
77
|
+
include AzureDevopsPrStabilization
|
|
78
|
+
include BitbucketCloudThreadState
|
|
79
|
+
include BitbucketCloudThreads
|
|
80
|
+
include BitbucketCloudPullResourceModules
|
|
11
81
|
include CommunityAndMarketplace
|
|
82
|
+
include FacebookStabilization
|
|
83
|
+
include GerritChangeState
|
|
84
|
+
include GerritChangeStabilization
|
|
85
|
+
include GerritFileResourceState
|
|
86
|
+
include GerritFileResourceStabilization
|
|
87
|
+
include GithubPullResources
|
|
88
|
+
include GithubPullResourceStabilization
|
|
89
|
+
include GithubThreads
|
|
90
|
+
include GiteaFamilyVisibilityScript
|
|
91
|
+
include GiteaFamilyThreadProductState
|
|
92
|
+
include GiteaFamilyThreadTimelineState
|
|
93
|
+
include GiteaFamilyStabilization
|
|
94
|
+
include GiteaFamilyThreads
|
|
95
|
+
include GiteaFamilyPullResourceProductState
|
|
96
|
+
include GiteaFamilyPullResourceState
|
|
97
|
+
include GiteaFamilyPullResources
|
|
12
98
|
include GitlabRepo
|
|
99
|
+
include GitlabStabilization
|
|
100
|
+
include GitlabMergeRequestResources
|
|
101
|
+
include GitlabThreads
|
|
13
102
|
include SocialPlatforms
|
|
14
103
|
include TravelAndLodging
|
|
15
104
|
end
|
|
@@ -8,18 +8,17 @@ module FetchUtil
|
|
|
8
8
|
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
-
def wait_for_anubis_challenge(page)
|
|
12
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
|
|
11
|
+
def wait_for_anubis_challenge(page, deadline: stabilization_deadline)
|
|
13
12
|
state = anubis_page_state(page)
|
|
14
|
-
if !valid_anubis_state?(state) &&
|
|
13
|
+
if !valid_anubis_state?(state) && monotonic_now < deadline
|
|
15
14
|
state = anubis_page_state(page)
|
|
16
15
|
end
|
|
17
16
|
return false unless valid_anubis_state?(state) && state["challenge"] == true
|
|
18
17
|
|
|
19
|
-
remaining = deadline
|
|
18
|
+
remaining = capped_timeout(@timeout, deadline: deadline)
|
|
20
19
|
return false unless remaining.positive?
|
|
21
20
|
|
|
22
|
-
retry_until_timeout(remaining, interval: ANUBIS_POLL) do
|
|
21
|
+
retry_until_timeout(remaining, interval: ANUBIS_POLL, deadline: deadline) do
|
|
23
22
|
state = anubis_page_state(page)
|
|
24
23
|
next false unless valid_anubis_state?(state)
|
|
25
24
|
next false unless state["challenge"] == false
|
|
@@ -10,6 +10,17 @@ module FetchUtil
|
|
|
10
10
|
SiteStabilization::SocialPlatforms::SOCIAL_PLATFORM_STABILIZATION_PROFILES[:stabilize_instagram],
|
|
11
11
|
SiteStabilization::SocialPlatforms::SOCIAL_PLATFORM_STABILIZATION_PROFILES[:stabilize_facebook],
|
|
12
12
|
SiteStabilization::CommunityAndMarketplace::COMMUNITY_MARKETPLACE_STABILIZATION_PROFILES[:stabilize_ebay_search],
|
|
13
|
+
SiteStabilization::GithubThreads::GITHUB_THREAD_STABILIZATION_PROFILE,
|
|
14
|
+
SiteStabilization::GithubPullResources::GITHUB_PULL_RESOURCE_STABILIZATION_PROFILE,
|
|
15
|
+
SiteStabilization::GitlabThreads::GITLAB_THREAD_STABILIZATION_PROFILE,
|
|
16
|
+
SiteStabilization::GitlabMergeRequestResources::GITLAB_MERGE_REQUEST_RESOURCE_STABILIZATION_PROFILE,
|
|
17
|
+
SiteStabilization::GiteaFamilyPullResources::GITEA_FAMILY_PULL_RESOURCE_STABILIZATION_PROFILE,
|
|
18
|
+
SiteStabilization::GiteaFamilyThreads::GITEA_FAMILY_THREAD_STABILIZATION_PROFILE,
|
|
19
|
+
SiteStabilization::BitbucketCloudPullResources::BITBUCKET_CLOUD_PULL_RESOURCE_STABILIZATION_PROFILE,
|
|
20
|
+
SiteStabilization::BitbucketCloudThreads::BITBUCKET_CLOUD_THREAD_STABILIZATION_PROFILE,
|
|
21
|
+
SiteStabilization::AzureDevopsPrStabilization::AZURE_DEVOPS_PR_STABILIZATION_PROFILE,
|
|
22
|
+
SiteStabilization::GerritFileResourceStabilization::GERRIT_FILE_RESOURCE_STABILIZATION_PROFILE,
|
|
23
|
+
SiteStabilization::GerritChangeStabilization::GERRIT_CHANGE_STABILIZATION_PROFILE,
|
|
13
24
|
SiteStabilization::TravelAndLodging::TRAVEL_LODGING_STABILIZATION_PROFILES[:stabilize_lodging_detail],
|
|
14
25
|
{ host: "t.me", path_query: ->(uri) { uri.path.match?(%r{\A/s/[^/]+/\d+/?\z}) },
|
|
15
26
|
strategy: :wait_for_telegram_message, notes: "Wait for the requested public Telegram preview message.",
|
|
@@ -38,31 +49,48 @@ module FetchUtil
|
|
|
38
49
|
private
|
|
39
50
|
|
|
40
51
|
def stabilize_page(page, url)
|
|
41
|
-
|
|
52
|
+
deadline = stabilization_deadline
|
|
53
|
+
wait_for_anubis_challenge(page, deadline: deadline)
|
|
54
|
+
return false unless stabilization_time_remaining?(deadline)
|
|
42
55
|
|
|
43
56
|
if (profile = matching_stabilization_profile(url, PAGE_FLOW_STABILIZATION_PROFILES))
|
|
44
|
-
|
|
57
|
+
handled = send(profile.fetch(:strategy), page, deadline: deadline)
|
|
58
|
+
return handled unless profile[:fallthrough] && !handled
|
|
59
|
+
return false unless stabilization_time_remaining?(deadline)
|
|
45
60
|
end
|
|
46
61
|
|
|
47
|
-
reached_idle = !@wait_for_idle || wait_for_idle_or_content(page)
|
|
62
|
+
reached_idle = !@wait_for_idle || wait_for_idle_or_content(page, deadline: deadline)
|
|
63
|
+
return false unless stabilization_time_remaining?(deadline)
|
|
64
|
+
|
|
48
65
|
preserve_consent = preserve_consent_wall?(page, url)
|
|
66
|
+
return false unless stabilization_time_remaining?(deadline)
|
|
67
|
+
|
|
49
68
|
accepted_cookies = preserve_consent ? false : dismiss_cookie_overlays(page)
|
|
69
|
+
return false unless stabilization_time_remaining?(deadline)
|
|
70
|
+
|
|
50
71
|
if @wait.positive? && (!@wait_for_idle || accepted_cookies)
|
|
51
|
-
|
|
72
|
+
sleep_before_deadline(@wait, deadline: deadline)
|
|
73
|
+
return false unless stabilization_time_remaining?(deadline)
|
|
74
|
+
|
|
52
75
|
accepted_cookies = dismiss_cookie_overlays(page) || accepted_cookies unless preserve_consent
|
|
76
|
+
return false unless stabilization_time_remaining?(deadline)
|
|
53
77
|
end
|
|
54
78
|
|
|
55
|
-
wait_for_spa_hydration(page) if @wait_for_idle && reached_idle
|
|
79
|
+
wait_for_spa_hydration(page, deadline: deadline) if @wait_for_idle && reached_idle
|
|
80
|
+
return false unless stabilization_time_remaining?(deadline)
|
|
81
|
+
|
|
56
82
|
if accepted_cookies
|
|
57
83
|
accepted_cookies = dismiss_cookie_overlays(page) || accepted_cookies
|
|
84
|
+
return false unless stabilization_time_remaining?(deadline)
|
|
58
85
|
end
|
|
59
86
|
if (profile = matching_stabilization_profile(url, POST_GENERIC_STABILIZATION_PROFILES))
|
|
60
|
-
send(profile.fetch(:strategy), page, url)
|
|
87
|
+
send(profile.fetch(:strategy), page, url, deadline: deadline)
|
|
88
|
+
return false unless stabilization_time_remaining?(deadline)
|
|
61
89
|
end
|
|
62
90
|
|
|
63
91
|
return unless accepted_cookies && @wait_for_idle && reached_idle
|
|
64
92
|
|
|
65
|
-
wait_for_network_idle(page)
|
|
93
|
+
wait_for_network_idle(page, deadline: deadline)
|
|
66
94
|
end
|
|
67
95
|
|
|
68
96
|
def matching_stabilization_profile(url, profiles)
|
|
@@ -75,13 +103,18 @@ module FetchUtil
|
|
|
75
103
|
|
|
76
104
|
def profile_match?(profile, uri, host) = stabilization_host_matches?(profile.fetch(:host), host) && profile.fetch(:path_query, ->(_) { true }).call(uri)
|
|
77
105
|
|
|
78
|
-
def stabilization_host_matches?(matcher, host)
|
|
106
|
+
def stabilization_host_matches?(matcher, host)
|
|
107
|
+
return true if matcher == true
|
|
79
108
|
|
|
80
|
-
|
|
109
|
+
Array(matcher).any? { |candidate| host == candidate || host.end_with?(".#{candidate}") }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def wait_for_idle_or_content(page, deadline: stabilization_deadline)
|
|
81
113
|
content_seen_at = nil
|
|
82
114
|
|
|
83
|
-
retry_until_timeout(@timeout,
|
|
84
|
-
|
|
115
|
+
retry_until_timeout(capped_timeout(@timeout, deadline: deadline),
|
|
116
|
+
interval: @idle_duration, deadline: deadline) do
|
|
117
|
+
now = monotonic_now
|
|
85
118
|
next true if page.network.idle?
|
|
86
119
|
if content_seen_at.nil? && page_has_content?(page)
|
|
87
120
|
content_seen_at = now
|
|
@@ -93,11 +126,19 @@ module FetchUtil
|
|
|
93
126
|
false
|
|
94
127
|
end
|
|
95
128
|
|
|
96
|
-
def wait_for_network_idle(page)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
129
|
+
def wait_for_network_idle(page, deadline: stabilization_deadline)
|
|
130
|
+
idle_since = nil
|
|
131
|
+
timeout = capped_timeout(POST_CONSENT_IDLE_TIMEOUT, deadline: deadline)
|
|
132
|
+
retry_until_timeout(timeout, interval: @idle_duration, deadline: deadline) do
|
|
133
|
+
now = monotonic_now
|
|
134
|
+
if page.network.idle?
|
|
135
|
+
idle_since ||= now
|
|
136
|
+
now >= idle_since + @idle_duration
|
|
137
|
+
else
|
|
138
|
+
idle_since = nil
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
end
|
|
101
142
|
rescue Ferrum::Error => e
|
|
102
143
|
raise if retryable_pending_connections_error?(e)
|
|
103
144
|
|
|
@@ -135,8 +176,8 @@ module FetchUtil
|
|
|
135
176
|
false
|
|
136
177
|
end
|
|
137
178
|
|
|
138
|
-
def wait_for_agora_article(page, _url)
|
|
139
|
-
retry_until_timeout(7.0, interval: 0.25) do
|
|
179
|
+
def wait_for_agora_article(page, _url, deadline: stabilization_deadline)
|
|
180
|
+
retry_until_timeout(capped_timeout(7.0, deadline: deadline), interval: 0.25, deadline: deadline) do
|
|
140
181
|
page.evaluate(<<~JS)
|
|
141
182
|
(() => {
|
|
142
183
|
const selectors = ['.mrf-article-body', '.article_body', 'div.articleBody', '.art_content', '.article-inner', 'section.article', '[itemprop=articleBody]'];
|
|
@@ -150,8 +191,8 @@ module FetchUtil
|
|
|
150
191
|
false
|
|
151
192
|
end
|
|
152
193
|
|
|
153
|
-
def wait_for_france24_article(page, _url)
|
|
154
|
-
retry_until_timeout(15.0, interval: 0.25) do
|
|
194
|
+
def wait_for_france24_article(page, _url, deadline: stabilization_deadline)
|
|
195
|
+
retry_until_timeout(capped_timeout(15.0, deadline: deadline), interval: 0.25, deadline: deadline) do
|
|
155
196
|
page.evaluate(<<~JS)
|
|
156
197
|
(() => {
|
|
157
198
|
const body = document.querySelector('.t-content__body') || document.querySelector('.t-content--article');
|
|
@@ -165,18 +206,21 @@ module FetchUtil
|
|
|
165
206
|
false
|
|
166
207
|
end
|
|
167
208
|
|
|
168
|
-
def wait_for_onet_homepage(page)
|
|
169
|
-
wait_for_structural_readiness(page, "section[class*='Feed_']", "article[class*='Card_']"
|
|
209
|
+
def wait_for_onet_homepage(page, deadline: stabilization_deadline)
|
|
210
|
+
wait_for_structural_readiness(page, "section[class*='Feed_']", "article[class*='Card_']",
|
|
211
|
+
deadline: deadline)
|
|
170
212
|
end
|
|
171
213
|
|
|
172
|
-
def wait_for_wp_homepage(page)
|
|
173
|
-
wait_for_structural_readiness(page, ".wp-section-grid", ".wp-teaser-tile, .wp-teaser-regular"
|
|
214
|
+
def wait_for_wp_homepage(page, deadline: stabilization_deadline)
|
|
215
|
+
wait_for_structural_readiness(page, ".wp-section-grid", ".wp-teaser-tile, .wp-teaser-regular",
|
|
216
|
+
deadline: deadline)
|
|
174
217
|
end
|
|
175
218
|
|
|
176
|
-
def wait_for_telegram_message(page)
|
|
219
|
+
def wait_for_telegram_message(page, deadline: stabilization_deadline)
|
|
177
220
|
target = URI.parse(page.current_url).path.delete_prefix("/s/")
|
|
178
221
|
|
|
179
|
-
ready = retry_until_timeout(capped_timeout(5.0
|
|
222
|
+
ready = retry_until_timeout(capped_timeout(5.0, deadline: deadline),
|
|
223
|
+
interval: 0.25, deadline: deadline) do
|
|
180
224
|
page.evaluate(<<~JS)
|
|
181
225
|
(() => {
|
|
182
226
|
const card = document.querySelector('.tgme_widget_message[data-post="#{target}"]');
|
|
@@ -185,7 +229,7 @@ module FetchUtil
|
|
|
185
229
|
})()
|
|
186
230
|
JS
|
|
187
231
|
end
|
|
188
|
-
|
|
232
|
+
sleep_before_deadline(PRE_EXTRACTION_SETTLE_WAIT, deadline: deadline) if ready
|
|
189
233
|
ready
|
|
190
234
|
rescue URI::InvalidURIError, Ferrum::JavaScriptError, Ferrum::TimeoutError
|
|
191
235
|
false
|
|
@@ -7,21 +7,23 @@ module FetchUtil
|
|
|
7
7
|
module SpaHydration
|
|
8
8
|
private
|
|
9
9
|
|
|
10
|
-
def wait_for_spa_hydration(page)
|
|
10
|
+
def wait_for_spa_hydration(page, deadline: stabilization_deadline)
|
|
11
11
|
framework = detect_spa_framework(page)
|
|
12
12
|
return unless framework
|
|
13
13
|
|
|
14
|
-
retry_until_timeout(SPA_HYDRATION_TIMEOUT,
|
|
14
|
+
retry_until_timeout(capped_timeout(SPA_HYDRATION_TIMEOUT, deadline: deadline),
|
|
15
|
+
interval: SPA_HYDRATION_POLL, deadline: deadline) do
|
|
15
16
|
spa_hydration_complete?(page, framework)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
sleep_before_deadline(SPA_HYDRATION_POLL, deadline: deadline)
|
|
19
20
|
rescue Ferrum::JavaScriptError, Ferrum::TimeoutError
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
def wait_for_structural_readiness(page, region_selector, card_selector)
|
|
23
|
+
def wait_for_structural_readiness(page, region_selector, card_selector, deadline: stabilization_deadline)
|
|
23
24
|
previous_count = nil
|
|
24
|
-
retry_until_timeout(SPA_HYDRATION_TIMEOUT,
|
|
25
|
+
retry_until_timeout(capped_timeout(SPA_HYDRATION_TIMEOUT, deadline: deadline),
|
|
26
|
+
interval: SPA_HYDRATION_POLL, deadline: deadline) do
|
|
25
27
|
counts = page.evaluate(<<~JS)
|
|
26
28
|
(() => ({
|
|
27
29
|
regions: document.querySelectorAll(#{region_selector.to_json}).length,
|
data/lib/fetch_util/browser.rb
CHANGED
|
@@ -45,18 +45,19 @@ module FetchUtil
|
|
|
45
45
|
viewport: DEFAULT_VIEWPORT, user_agent: DEFAULT_USER_AGENT,
|
|
46
46
|
accept_language: DEFAULT_ACCEPT_LANGUAGE, browser_path: nil,
|
|
47
47
|
browser_options: nil)
|
|
48
|
-
@timeout = timeout
|
|
49
|
-
@wait = wait
|
|
48
|
+
@timeout = positive_timeout(timeout)
|
|
49
|
+
@wait = nonnegative_duration(wait, "wait")
|
|
50
50
|
@wait_for_idle = wait_for_idle
|
|
51
|
-
@idle_duration = idle_duration
|
|
51
|
+
@idle_duration = nonnegative_duration(idle_duration, "idle_duration")
|
|
52
52
|
@viewport = DEFAULT_VIEWPORT.merge(symbolize_hash(viewport || {}))
|
|
53
|
-
@user_agent = user_agent
|
|
54
|
-
@accept_language = accept_language
|
|
53
|
+
@user_agent = user_agent.to_s.dup.freeze
|
|
54
|
+
@accept_language = accept_language.to_s.dup.freeze
|
|
55
55
|
@default_headers = {
|
|
56
56
|
"User-Agent" => @user_agent,
|
|
57
57
|
"Accept-Language" => @accept_language
|
|
58
58
|
}.freeze
|
|
59
|
-
|
|
59
|
+
selected_browser_path = browser_path || ENV["BROWSER_PATH"] || BROWSER_CANDIDATES.find { |path| File.executable?(path) }
|
|
60
|
+
@browser_path = selected_browser_path&.dup&.freeze
|
|
60
61
|
@full_browser = @browser_path && !@browser_path.include?("headless_shell")
|
|
61
62
|
default_opts = { "no-sandbox": nil }
|
|
62
63
|
# Use newer headless mode with the full browser binary for closer runtime
|
|
@@ -67,9 +68,10 @@ module FetchUtil
|
|
|
67
68
|
default_opts["headless"] = "new"
|
|
68
69
|
default_opts["enable-automation"] = false # override Ferrum default
|
|
69
70
|
end
|
|
70
|
-
@browser_options = default_opts.merge(browser_options || {})
|
|
71
|
+
@browser_options = immutable_browser_option(default_opts.merge(browser_options || {}))
|
|
71
72
|
@navigator_patch = build_navigator_patch
|
|
72
73
|
@ferrum = nil
|
|
74
|
+
@pending_shutdown = nil
|
|
73
75
|
@mutex = Mutex.new
|
|
74
76
|
end
|
|
75
77
|
|
|
@@ -82,12 +84,12 @@ module FetchUtil
|
|
|
82
84
|
|
|
83
85
|
normalized_url = FetchUtil.normalize_url(url)
|
|
84
86
|
page = load_page_with_retry(ensure_browser, normalized_url)
|
|
85
|
-
sleep PRE_EXTRACTION_SETTLE_WAIT if heavy_script_page?(page, normalized_url)
|
|
87
|
+
sleep PRE_EXTRACTION_SETTLE_WAIT if heavy_script_page?(page, loaded_page_url(page, normalized_url))
|
|
86
88
|
yield page
|
|
87
89
|
rescue Ferrum::Error => e
|
|
88
90
|
raise BrowserError, e.message
|
|
89
91
|
ensure
|
|
90
|
-
page
|
|
92
|
+
close_page(page)
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
# Shut down the underlying Chromium process. Safe to call multiple times or
|
|
@@ -95,33 +97,92 @@ module FetchUtil
|
|
|
95
97
|
# +with_page+ call will transparently launch a new process.
|
|
96
98
|
def quit
|
|
97
99
|
@mutex.synchronize do
|
|
98
|
-
@ferrum
|
|
100
|
+
browser = @ferrum || @pending_shutdown
|
|
99
101
|
@ferrum = nil
|
|
102
|
+
@pending_shutdown = browser
|
|
103
|
+
shutdown_pending_browser
|
|
100
104
|
end
|
|
101
105
|
end
|
|
102
106
|
|
|
103
107
|
private
|
|
104
108
|
|
|
109
|
+
def positive_timeout(value)
|
|
110
|
+
timeout = Float(value)
|
|
111
|
+
return timeout if timeout.positive? && timeout.finite?
|
|
112
|
+
|
|
113
|
+
raise ArgumentError
|
|
114
|
+
rescue ArgumentError, TypeError
|
|
115
|
+
raise InputError, "timeout must be positive"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def nonnegative_duration(value, name)
|
|
119
|
+
duration = Float(value)
|
|
120
|
+
return duration if duration >= 0 && duration.finite?
|
|
121
|
+
|
|
122
|
+
raise ArgumentError
|
|
123
|
+
rescue ArgumentError, TypeError
|
|
124
|
+
raise InputError, "#{name} must be nonnegative"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def immutable_browser_option(value, memo = {}.compare_by_identity)
|
|
128
|
+
return memo[value] if memo.key?(value)
|
|
129
|
+
|
|
130
|
+
case value
|
|
131
|
+
when String
|
|
132
|
+
memo[value] = value.dup.freeze
|
|
133
|
+
when Array
|
|
134
|
+
owned = []
|
|
135
|
+
memo[value] = owned
|
|
136
|
+
value.each { |item| owned << immutable_browser_option(item, memo) }
|
|
137
|
+
owned.freeze
|
|
138
|
+
when Hash
|
|
139
|
+
owned = {}
|
|
140
|
+
memo[value] = owned
|
|
141
|
+
value.each do |key, item|
|
|
142
|
+
owned[immutable_browser_option(key, memo)] = immutable_browser_option(item, memo)
|
|
143
|
+
end
|
|
144
|
+
owned.freeze
|
|
145
|
+
else
|
|
146
|
+
value
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
105
150
|
# Lazily start the shared Chromium process on first use. The
|
|
106
151
|
# +evaluate_on_new_document+ call registers the navigator patch once;
|
|
107
152
|
# Ferrum automatically applies it to every new page/context created
|
|
108
153
|
# afterwards.
|
|
109
154
|
def ensure_browser
|
|
110
155
|
@mutex.synchronize do
|
|
156
|
+
shutdown_pending_browser
|
|
111
157
|
return @ferrum if @ferrum
|
|
112
158
|
|
|
113
|
-
|
|
159
|
+
browser = Ferrum::Browser.new(
|
|
114
160
|
headless: true,
|
|
115
161
|
browser_path: @browser_path,
|
|
116
162
|
timeout: @timeout,
|
|
117
163
|
window_size: [@viewport.fetch(:width), @viewport.fetch(:height)],
|
|
118
164
|
browser_options: @browser_options
|
|
119
165
|
)
|
|
120
|
-
|
|
121
|
-
@ferrum
|
|
166
|
+
browser.evaluate_on_new_document(navigator_patch)
|
|
167
|
+
@ferrum = browser
|
|
168
|
+
rescue Ferrum::Error
|
|
169
|
+
@pending_shutdown = browser
|
|
170
|
+
begin
|
|
171
|
+
shutdown_pending_browser
|
|
172
|
+
rescue Ferrum::Error
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
raise
|
|
122
176
|
end
|
|
123
177
|
end
|
|
124
178
|
|
|
179
|
+
def shutdown_pending_browser
|
|
180
|
+
return unless @pending_shutdown
|
|
181
|
+
|
|
182
|
+
@pending_shutdown.quit
|
|
183
|
+
@pending_shutdown = nil
|
|
184
|
+
end
|
|
185
|
+
|
|
125
186
|
def host_matches?(url, host)
|
|
126
187
|
normalized_host = FetchUtil.strip_www_host(url)
|
|
127
188
|
normalized_host == host || normalized_host.end_with?(".#{host}")
|
|
@@ -132,8 +193,7 @@ module FetchUtil
|
|
|
132
193
|
end
|
|
133
194
|
|
|
134
195
|
def retryable_navigation_error?(error)
|
|
135
|
-
error.is_a?(Ferrum::
|
|
136
|
-
error.message.to_s.match?(/pending connections/i)
|
|
196
|
+
error.is_a?(Ferrum::TimeoutError) || retryable_pending_connections_error?(error)
|
|
137
197
|
end
|
|
138
198
|
|
|
139
199
|
def heavy_script_page?(page, url)
|
|
@@ -146,6 +206,17 @@ module FetchUtil
|
|
|
146
206
|
false
|
|
147
207
|
end
|
|
148
208
|
|
|
209
|
+
def close_page(page)
|
|
210
|
+
page&.close
|
|
211
|
+
rescue Ferrum::Error
|
|
212
|
+
nil
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def loaded_page_url(page, fallback)
|
|
216
|
+
current_url = page.current_url
|
|
217
|
+
current_url.to_s.empty? ? fallback : current_url
|
|
218
|
+
end
|
|
219
|
+
|
|
149
220
|
def load_page_with_retry(ferrum, url)
|
|
150
221
|
retries = 0
|
|
151
222
|
|
|
@@ -158,10 +229,10 @@ module FetchUtil
|
|
|
158
229
|
rescue Ferrum::TimeoutError
|
|
159
230
|
raise unless page_loaded_enough?(page)
|
|
160
231
|
end
|
|
161
|
-
stabilize_page(page, url)
|
|
232
|
+
stabilize_page(page, loaded_page_url(page, url))
|
|
162
233
|
page
|
|
163
234
|
rescue Ferrum::PendingConnectionsError, Ferrum::TimeoutError, Ferrum::Error => e
|
|
164
|
-
page
|
|
235
|
+
close_page(page)
|
|
165
236
|
raise unless retryable_navigation_error?(e)
|
|
166
237
|
raise if retries >= NAVIGATION_MAX_RETRIES
|
|
167
238
|
|
data/lib/fetch_util/cli.rb
CHANGED
|
@@ -6,6 +6,31 @@ require "thor"
|
|
|
6
6
|
|
|
7
7
|
module FetchUtil
|
|
8
8
|
class CLI < Thor
|
|
9
|
+
class << self
|
|
10
|
+
def start(given_args = ARGV, config = {})
|
|
11
|
+
command = given_args.first
|
|
12
|
+
arguments = given_args.drop(1)
|
|
13
|
+
help_command = command_for_local_help(command, arguments)
|
|
14
|
+
return super(["help", help_command], config) if help_command
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def exit_on_failure? = true
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def command_for_local_help(command, arguments)
|
|
24
|
+
normalized_command = normalize_command_name(command)
|
|
25
|
+
return unless all_commands.key?(normalized_command)
|
|
26
|
+
|
|
27
|
+
help_requested = arguments.take_while { |argument| argument != "--" }.any? do |argument|
|
|
28
|
+
Thor::HELP_MAPPINGS.include?(argument)
|
|
29
|
+
end
|
|
30
|
+
normalized_command if help_requested
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
9
34
|
DEFAULT_FETCH_FIELDS = %i[
|
|
10
35
|
title
|
|
11
36
|
byline
|
|
@@ -55,13 +80,15 @@ module FetchUtil
|
|
|
55
80
|
|
|
56
81
|
desc "fetch URL [URL...]", "Fetch one or more URLs"
|
|
57
82
|
def fetch(*urls)
|
|
58
|
-
raise
|
|
83
|
+
raise Thor::Error, "at least one URL is required" if urls.empty?
|
|
59
84
|
|
|
60
|
-
results =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
85
|
+
results = translate_input_errors do
|
|
86
|
+
if urls.length == 1
|
|
87
|
+
[FetchUtil.fetch(urls.first, **fetch_options, request_log: request_log)]
|
|
88
|
+
else
|
|
89
|
+
FetchUtil.fetch_many(urls, **fetch_options, request_log: request_log, concurrency: options[:concurrency])
|
|
90
|
+
end
|
|
91
|
+
end
|
|
65
92
|
|
|
66
93
|
if options[:format] == "markdown"
|
|
67
94
|
puts results.map { |result| front_matter_document(result) }.join("\n\n")
|
|
@@ -76,15 +103,18 @@ module FetchUtil
|
|
|
76
103
|
option :verbose_search, type: :boolean, default: false, desc: "Include per-result search provenance"
|
|
77
104
|
def search(*terms)
|
|
78
105
|
query = terms.join(" ").strip
|
|
79
|
-
raise
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
raise Thor::Error, "query is required" if query.empty?
|
|
107
|
+
|
|
108
|
+
searcher = translate_input_errors do
|
|
109
|
+
Searcher.new(
|
|
110
|
+
request_log: request_log,
|
|
111
|
+
sources: options[:source],
|
|
112
|
+
limit: options[:limit],
|
|
113
|
+
verbose: options[:verbose_search],
|
|
114
|
+
timeout: options[:timeout]
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
payload = translate_input_errors { searcher.search(query) }
|
|
88
118
|
|
|
89
119
|
emit(payload)
|
|
90
120
|
end
|
|
@@ -93,22 +123,38 @@ module FetchUtil
|
|
|
93
123
|
option :sources, type: :string, default: "machine", desc: "Comma-separated source selectors, e.g. machine,-robotstxt or human,machine,-human"
|
|
94
124
|
option :cache_path, type: :string, desc: "Structured regulatory cache directory"
|
|
95
125
|
def regulatory(url)
|
|
96
|
-
raise
|
|
126
|
+
raise Thor::Error, "url is required" if url.to_s.strip.empty?
|
|
97
127
|
|
|
98
128
|
request_log.append("regulatory://#{url}?sources=#{options[:sources]}")
|
|
99
|
-
payload =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
payload = translate_input_errors do
|
|
130
|
+
FetchUtil.regulatory(
|
|
131
|
+
url,
|
|
132
|
+
cache_path: options[:cache_path],
|
|
133
|
+
sources: options[:sources],
|
|
134
|
+
timeout: options[:timeout]
|
|
135
|
+
)
|
|
136
|
+
end
|
|
105
137
|
|
|
106
138
|
emit(payload)
|
|
107
139
|
end
|
|
108
140
|
|
|
109
141
|
no_commands do
|
|
110
142
|
def request_log
|
|
111
|
-
@request_log ||= RequestLog.new(path: options[:log_path]
|
|
143
|
+
@request_log ||= options[:log_path] ? RequestLog.new(path: options[:log_path]) : RequestLog.new
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def translate_input_errors
|
|
147
|
+
yield
|
|
148
|
+
rescue ParallelFetcher::ParallelFetchError => e
|
|
149
|
+
raise unless e.errors.all? { |error| input_error?(error) }
|
|
150
|
+
|
|
151
|
+
raise Thor::Error, e.message
|
|
152
|
+
rescue URI::InvalidURIError, InputError => e
|
|
153
|
+
raise Thor::Error, e.message
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def input_error?(error)
|
|
157
|
+
error.is_a?(URI::InvalidURIError) || error.is_a?(InputError)
|
|
112
158
|
end
|
|
113
159
|
|
|
114
160
|
def fetch_options
|