html2rss 0.27.2 → 0.28.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/lib/html2rss/auto_source/README.md +4 -1
- data/lib/html2rss/auto_source/scraper/native_feed.rb +90 -0
- data/lib/html2rss/auto_source/scraper.rb +4 -1
- data/lib/html2rss/auto_source.rb +7 -0
- data/lib/html2rss/config/auto_source_contract.rb +7 -0
- data/lib/html2rss/config.rb +10 -1
- data/lib/html2rss/feed_pipeline/README.md +2 -0
- data/lib/html2rss/feed_pipeline/auto_fallback.rb +53 -13
- data/lib/html2rss/feed_pipeline/runtime_policy.rb +1 -0
- data/lib/html2rss/feed_pipeline.rb +24 -8
- data/lib/html2rss/feed_resolution/README.md +52 -0
- data/lib/html2rss/feed_resolution/candidate_generator.rb +139 -0
- data/lib/html2rss/feed_resolution/diag.rb +32 -0
- data/lib/html2rss/feed_resolution/options.rb +31 -0
- data/lib/html2rss/feed_resolution/policy.rb +47 -0
- data/lib/html2rss/feed_resolution/probe.rb +59 -0
- data/lib/html2rss/feed_resolution/scorer.rb +49 -0
- data/lib/html2rss/feed_resolution.rb +274 -0
- data/lib/html2rss/mcp/inspect.rb +18 -121
- data/lib/html2rss/mcp/outcome.rb +4 -2
- data/lib/html2rss/page_recon.rb +243 -0
- data/lib/html2rss/request_service/botasaurus_contract.rb +8 -1
- data/lib/html2rss/request_service/botasaurus_strategy.rb +1 -1
- data/lib/html2rss/request_service/response.rb +26 -2
- data/lib/html2rss/request_service.rb +13 -1
- data/lib/html2rss/request_session.rb +5 -2
- data/lib/html2rss/scrape_target.rb +24 -0
- data/lib/html2rss/status.rb +49 -22
- data/lib/html2rss/surface_category.rb +63 -0
- data/lib/html2rss/syndication/README.md +21 -0
- data/lib/html2rss/syndication/candidate_catalog.rb +26 -0
- data/lib/html2rss/syndication/discovery.rb +217 -0
- data/lib/html2rss/syndication/parser.rb +137 -0
- data/lib/html2rss/syndication.rb +10 -0
- data/lib/html2rss/url.rb +22 -0
- data/lib/html2rss/version.rb +1 -1
- data/schema/html2rss-config.schema.json +38 -0
- metadata +19 -2
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Html2rss
|
|
4
|
+
module Syndication
|
|
5
|
+
##
|
|
6
|
+
# Finds a same-origin RSS/Atom URL for a page via head alternates and path guesses.
|
|
7
|
+
#
|
|
8
|
+
# Ports configs +probe_rss+ path/alternate logic onto {RequestSession#follow_up}.
|
|
9
|
+
module Discovery # rubocop:disable Metrics/ModuleLength -- discovery + path probes stay co-located
|
|
10
|
+
# Common feed path suffixes probed after head +rel=alternate+ hints.
|
|
11
|
+
DEFAULT_PATHS = CandidateCatalog::FEED_PATHS
|
|
12
|
+
|
|
13
|
+
LINK_TAG_RE = /<link\b[^>]*>/i
|
|
14
|
+
HREF_RE = /\bhref\s*=\s*["']([^"']+)["']/i
|
|
15
|
+
TYPE_RE = /\btype\s*=\s*["']([^"']+)["']/i
|
|
16
|
+
REL_RE = /\brel\s*=\s*["']([^"']+)["']/i
|
|
17
|
+
private_constant :LINK_TAG_RE, :HREF_RE, :TYPE_RE, :REL_RE
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# Probes candidates lazily and returns the first feedish URL.
|
|
23
|
+
#
|
|
24
|
+
# @param page_url [String, Html2rss::Url] page or origin URL
|
|
25
|
+
# @param request_session [Html2rss::RequestSession] shared session for follow-ups
|
|
26
|
+
# @param parsed_body [Nokogiri::HTML::Document, nil] parsed HTML when available
|
|
27
|
+
# @param html [String, nil] raw HTML when a document is unavailable
|
|
28
|
+
# @param extra_paths [Array<String>] additional path guesses
|
|
29
|
+
# @param max_probes [Integer, nil] max candidate GETs (nil = uncapped)
|
|
30
|
+
# @return [Html2rss::Url, nil]
|
|
31
|
+
# rubocop:disable Metrics/ParameterLists -- discovery kwargs stay co-located
|
|
32
|
+
def best_feed_url(page_url:, request_session:, parsed_body: nil, html: nil, extra_paths: [],
|
|
33
|
+
max_probes: nil)
|
|
34
|
+
best_feed_response(
|
|
35
|
+
page_url:, request_session:, parsed_body:, html:, extra_paths:, max_probes:
|
|
36
|
+
)&.url
|
|
37
|
+
end
|
|
38
|
+
# rubocop:enable Metrics/ParameterLists
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# Like {#best_feed_url} but returns the validated syndication response for parsing.
|
|
42
|
+
#
|
|
43
|
+
# @param page_url [String, Html2rss::Url]
|
|
44
|
+
# @param request_session [Html2rss::RequestSession]
|
|
45
|
+
# @param parsed_body [Nokogiri::HTML::Document, nil]
|
|
46
|
+
# @param html [String, nil]
|
|
47
|
+
# @param extra_paths [Array<String>]
|
|
48
|
+
# @param max_probes [Integer, nil] max candidate GETs (nil = uncapped)
|
|
49
|
+
# @return [Html2rss::RequestService::Response, nil]
|
|
50
|
+
# rubocop:disable Metrics/ParameterLists, Metrics/MethodLength -- discovery kwargs stay co-located
|
|
51
|
+
def best_feed_response(page_url:, request_session:, parsed_body: nil, html: nil, extra_paths: [],
|
|
52
|
+
max_probes: nil)
|
|
53
|
+
page = Html2rss::Url.from_absolute(page_url)
|
|
54
|
+
candidates = candidate_urls(page_url: page, parsed_body:, html:, extra_paths:)
|
|
55
|
+
Log.debug(
|
|
56
|
+
"Syndication::Discovery: host=#{page.host} candidate_count=#{candidates.size}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
first_feedish_response(
|
|
60
|
+
candidates, request_session:, origin_url: page, max_probes:
|
|
61
|
+
).tap do |response|
|
|
62
|
+
next unless response
|
|
63
|
+
|
|
64
|
+
Log.info("Syndication::Discovery: host=#{page.host} selected_feed_url=#{response.url}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
# rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# Ordered candidate feed URLs (head alternates first, then path guesses).
|
|
71
|
+
#
|
|
72
|
+
# @param page_url [String, Html2rss::Url]
|
|
73
|
+
# @param parsed_body [Nokogiri::HTML::Document, nil]
|
|
74
|
+
# @param html [String, nil]
|
|
75
|
+
# @param extra_paths [Array<String>]
|
|
76
|
+
# @return [Array<Html2rss::Url>]
|
|
77
|
+
def candidate_urls(page_url:, parsed_body: nil, html: nil, extra_paths: [])
|
|
78
|
+
page = Html2rss::Url.from_absolute(page_url)
|
|
79
|
+
(alternate_feed_urls(page, parsed_body:, html:) +
|
|
80
|
+
path_guess_urls(page, extra_paths:)).uniq
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# @param response [Html2rss::RequestService::Response, nil]
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def feedish?(response)
|
|
87
|
+
return false unless response
|
|
88
|
+
return false unless successful_status?(response)
|
|
89
|
+
|
|
90
|
+
response.feed_response?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def successful_status?(response)
|
|
94
|
+
status = response.status
|
|
95
|
+
status.nil? || status.between?(200, 299)
|
|
96
|
+
end
|
|
97
|
+
module_function :successful_status?
|
|
98
|
+
private_class_method :successful_status?
|
|
99
|
+
|
|
100
|
+
##
|
|
101
|
+
# Directory-relative feed path guesses for a page URL.
|
|
102
|
+
#
|
|
103
|
+
# @param page_url [String, Html2rss::Url]
|
|
104
|
+
# @return [Array<String>] relative paths
|
|
105
|
+
def page_dir_paths(page_url)
|
|
106
|
+
path = Html2rss::Url.from_absolute(page_url).path
|
|
107
|
+
return [] if path.nil? || path.empty? || path == '/'
|
|
108
|
+
|
|
109
|
+
dir = path.sub(%r{/[^/]*$}, '/')
|
|
110
|
+
["#{dir}feed", "#{dir}rss.xml", "#{dir}atom.xml"]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def first_feedish_response(urls, request_session:, origin_url:, max_probes: nil)
|
|
114
|
+
scoped = max_probes.nil? ? urls.lazy : urls.lazy.take(max_probes)
|
|
115
|
+
scoped.filter_map do |url|
|
|
116
|
+
response = probe(url, request_session:, origin_url:)
|
|
117
|
+
response if feedish?(response)
|
|
118
|
+
end.first
|
|
119
|
+
end
|
|
120
|
+
module_function :first_feedish_response
|
|
121
|
+
private_class_method :first_feedish_response
|
|
122
|
+
|
|
123
|
+
def first_feedish_url(urls, request_session:, origin_url:, max_probes: nil)
|
|
124
|
+
first_feedish_response(urls, request_session:, origin_url:, max_probes:)&.url
|
|
125
|
+
end
|
|
126
|
+
module_function :first_feedish_url
|
|
127
|
+
private_class_method :first_feedish_url
|
|
128
|
+
|
|
129
|
+
def probe(url, request_session:, origin_url:)
|
|
130
|
+
request_session.follow_up(url:, relation: :auto_source, origin_url:)
|
|
131
|
+
rescue Html2rss::Error => error
|
|
132
|
+
Log.debug(
|
|
133
|
+
"Syndication::Discovery: probe skipped host=#{origin_url.host} " \
|
|
134
|
+
"(#{error.class})"
|
|
135
|
+
)
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
module_function :probe
|
|
139
|
+
private_class_method :probe
|
|
140
|
+
|
|
141
|
+
def alternate_feed_urls(page, parsed_body:, html:)
|
|
142
|
+
from_document = feed_links_from_document(parsed_body, page)
|
|
143
|
+
return from_document if from_document.any?
|
|
144
|
+
|
|
145
|
+
alternate_feed_hrefs(html.to_s, page)
|
|
146
|
+
end
|
|
147
|
+
module_function :alternate_feed_urls
|
|
148
|
+
private_class_method :alternate_feed_urls
|
|
149
|
+
|
|
150
|
+
def feed_links_from_document(parsed_body, page)
|
|
151
|
+
return [] unless parsed_body.is_a?(Nokogiri::HTML::Document)
|
|
152
|
+
|
|
153
|
+
Html::FeedLink.from_document(parsed_body).filter_map do |link|
|
|
154
|
+
absolute_url(page, link.href)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
module_function :feed_links_from_document
|
|
158
|
+
private_class_method :feed_links_from_document
|
|
159
|
+
|
|
160
|
+
def path_guess_urls(page, extra_paths:)
|
|
161
|
+
paths = (page_dir_paths(page) + DEFAULT_PATHS + Array(extra_paths)).uniq
|
|
162
|
+
paths.filter_map { |path| absolute_url(page, path) }
|
|
163
|
+
end
|
|
164
|
+
module_function :path_guess_urls
|
|
165
|
+
private_class_method :path_guess_urls
|
|
166
|
+
|
|
167
|
+
def alternate_feed_hrefs(html, page)
|
|
168
|
+
return [] if html.nil? || html.empty?
|
|
169
|
+
|
|
170
|
+
html.scan(LINK_TAG_RE).filter_map { |tag| href_from_alternate_link(tag, page) }.uniq
|
|
171
|
+
end
|
|
172
|
+
module_function :alternate_feed_hrefs
|
|
173
|
+
private_class_method :alternate_feed_hrefs
|
|
174
|
+
|
|
175
|
+
def href_from_alternate_link(tag, page)
|
|
176
|
+
return unless alternate_rel?(tag)
|
|
177
|
+
|
|
178
|
+
href = tag[HREF_RE, 1]
|
|
179
|
+
return if href.nil? || href.empty?
|
|
180
|
+
return unless syndication_type?(tag[TYPE_RE, 1]) || feed_like_href?(href)
|
|
181
|
+
|
|
182
|
+
absolute_url(page, href.strip)
|
|
183
|
+
end
|
|
184
|
+
module_function :href_from_alternate_link
|
|
185
|
+
private_class_method :href_from_alternate_link
|
|
186
|
+
|
|
187
|
+
def alternate_rel?(tag)
|
|
188
|
+
tag[REL_RE, 1].to_s.downcase.split(/\s+/).include?('alternate')
|
|
189
|
+
end
|
|
190
|
+
module_function :alternate_rel?
|
|
191
|
+
private_class_method :alternate_rel?
|
|
192
|
+
|
|
193
|
+
def syndication_type?(type)
|
|
194
|
+
return false if type.nil? || type.empty?
|
|
195
|
+
|
|
196
|
+
t = type.downcase
|
|
197
|
+
t.include?('rss+xml') || t.include?('atom+xml') || t.include?('rss') || t.include?('atom')
|
|
198
|
+
end
|
|
199
|
+
module_function :syndication_type?
|
|
200
|
+
private_class_method :syndication_type?
|
|
201
|
+
|
|
202
|
+
def feed_like_href?(href)
|
|
203
|
+
href.match?(/rss|atom|feed|\.xml/i)
|
|
204
|
+
end
|
|
205
|
+
module_function :feed_like_href?
|
|
206
|
+
private_class_method :feed_like_href?
|
|
207
|
+
|
|
208
|
+
def absolute_url(base, path)
|
|
209
|
+
Html2rss::Url.from_relative(path, base)
|
|
210
|
+
rescue ArgumentError
|
|
211
|
+
nil
|
|
212
|
+
end
|
|
213
|
+
module_function :absolute_url
|
|
214
|
+
private_class_method :absolute_url
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rss'
|
|
4
|
+
|
|
5
|
+
module Html2rss
|
|
6
|
+
module Syndication
|
|
7
|
+
##
|
|
8
|
+
# Parses RSS 2.0 and Atom documents into AutoSource article hashes.
|
|
9
|
+
module Parser
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# @param response [Html2rss::RequestService::Response]
|
|
14
|
+
# @return [Array<Hash{Symbol => Object}>] article hashes (may be empty)
|
|
15
|
+
# @raise [ArgumentError] when the response is not a syndication feed
|
|
16
|
+
def parse_response(response)
|
|
17
|
+
raise ArgumentError, 'response is not a syndication feed' unless response.feed_response?
|
|
18
|
+
|
|
19
|
+
parse(response.body, base_url: response.url)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# @param body [String] raw RSS/Atom document
|
|
24
|
+
# @param base_url [String, Html2rss::Url, nil] fallback base for relative item links
|
|
25
|
+
# @return [Array<Hash{Symbol => Object}>]
|
|
26
|
+
def parse(body, base_url: nil)
|
|
27
|
+
feed = RSS::Parser.parse(body.to_s, false)
|
|
28
|
+
return [] unless feed
|
|
29
|
+
|
|
30
|
+
items_from(feed, base_url:).filter_map { |item| article_hash(item, base_url:) }
|
|
31
|
+
rescue RSS::Error => error
|
|
32
|
+
Log.warn("Syndication::Parser: failed to parse feed (#{error.class}: #{error.message})")
|
|
33
|
+
[]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def items_from(feed, base_url:) # rubocop:disable Lint/UnusedMethodArgument -- base reserved for callers
|
|
37
|
+
case feed
|
|
38
|
+
when RSS::Rss then Array(feed.items)
|
|
39
|
+
when RSS::Atom::Feed then Array(feed.entries)
|
|
40
|
+
else []
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
module_function :items_from
|
|
44
|
+
private_class_method :items_from
|
|
45
|
+
|
|
46
|
+
def article_hash(item, base_url:)
|
|
47
|
+
case item
|
|
48
|
+
when RSS::Rss::Channel::Item then rss_item_hash(item, base_url:)
|
|
49
|
+
when RSS::Atom::Entry, RSS::Atom::Feed::Entry then atom_entry_hash(item, base_url:)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
module_function :article_hash
|
|
53
|
+
private_class_method :article_hash
|
|
54
|
+
|
|
55
|
+
def rss_item_hash(item, base_url:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- RSS item mapping
|
|
56
|
+
url = absolute_url(item.link, base_url:)
|
|
57
|
+
return unless url
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
id: present(item.guid&.content) || url.to_s,
|
|
61
|
+
title: present(item.title),
|
|
62
|
+
description: present(item.description),
|
|
63
|
+
url:,
|
|
64
|
+
published_at: item.date || item.pubDate,
|
|
65
|
+
author: present(item.author),
|
|
66
|
+
categories: Array(item.categories).filter_map { |cat| present(cat.content) }
|
|
67
|
+
}.compact
|
|
68
|
+
end
|
|
69
|
+
module_function :rss_item_hash
|
|
70
|
+
private_class_method :rss_item_hash
|
|
71
|
+
|
|
72
|
+
def atom_entry_hash(entry, base_url:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity -- Atom entry mapping
|
|
73
|
+
url = atom_link(entry, base_url:)
|
|
74
|
+
return unless url
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
id: present(entry.id&.content) || url.to_s,
|
|
78
|
+
title: present(atom_text(entry.title)),
|
|
79
|
+
description: present(atom_text(entry.content) || atom_text(entry.summary)),
|
|
80
|
+
url:,
|
|
81
|
+
published_at: entry.published&.content || entry.updated&.content,
|
|
82
|
+
author: present(atom_author(entry)),
|
|
83
|
+
categories: Array(entry.categories).filter_map { |cat| present(cat.term) }
|
|
84
|
+
}.compact
|
|
85
|
+
end
|
|
86
|
+
module_function :atom_entry_hash
|
|
87
|
+
private_class_method :atom_entry_hash
|
|
88
|
+
|
|
89
|
+
def atom_link(entry, base_url:)
|
|
90
|
+
links = Array(entry.links)
|
|
91
|
+
link = links.find { |node| node.rel.nil? || node.rel == 'alternate' } || links.first
|
|
92
|
+
absolute_url(link&.href, base_url:)
|
|
93
|
+
end
|
|
94
|
+
module_function :atom_link
|
|
95
|
+
private_class_method :atom_link
|
|
96
|
+
|
|
97
|
+
def atom_text(node)
|
|
98
|
+
return if node.nil?
|
|
99
|
+
|
|
100
|
+
node.respond_to?(:content) ? node.content : node.to_s
|
|
101
|
+
end
|
|
102
|
+
module_function :atom_text
|
|
103
|
+
private_class_method :atom_text
|
|
104
|
+
|
|
105
|
+
def atom_author(entry)
|
|
106
|
+
author = Array(entry.author).first
|
|
107
|
+
return unless author
|
|
108
|
+
|
|
109
|
+
present(author.name&.content) || present(author.email&.content)
|
|
110
|
+
end
|
|
111
|
+
module_function :atom_author
|
|
112
|
+
private_class_method :atom_author
|
|
113
|
+
|
|
114
|
+
def absolute_url(raw, base_url:)
|
|
115
|
+
value = present(raw)
|
|
116
|
+
return unless value
|
|
117
|
+
|
|
118
|
+
if base_url
|
|
119
|
+
Html2rss::Url.from_relative(value, base_url)
|
|
120
|
+
else
|
|
121
|
+
Html2rss::Url.from_absolute(value)
|
|
122
|
+
end
|
|
123
|
+
rescue ArgumentError
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
module_function :absolute_url
|
|
127
|
+
private_class_method :absolute_url
|
|
128
|
+
|
|
129
|
+
def present(value)
|
|
130
|
+
text = value.to_s.strip
|
|
131
|
+
text unless text.empty?
|
|
132
|
+
end
|
|
133
|
+
module_function :present
|
|
134
|
+
private_class_method :present
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
data/lib/html2rss/url.rb
CHANGED
|
@@ -281,6 +281,18 @@ module Html2rss
|
|
|
281
281
|
def ==(other) = other.is_a?(Url) && to_s == other.to_s
|
|
282
282
|
alias eql? ==
|
|
283
283
|
|
|
284
|
+
##
|
|
285
|
+
# Whether +other+ names the same document (trailing slash and fragment ignored).
|
|
286
|
+
# Differs from {#==}: `/blog` and `/blog/` match; query strings must still match.
|
|
287
|
+
#
|
|
288
|
+
# @param other [Object]
|
|
289
|
+
# @return [Boolean]
|
|
290
|
+
def same_document?(other)
|
|
291
|
+
return false unless other.is_a?(Url)
|
|
292
|
+
|
|
293
|
+
document_identity == other.document_identity
|
|
294
|
+
end
|
|
295
|
+
|
|
284
296
|
##
|
|
285
297
|
# Returns the hash code for this URL.
|
|
286
298
|
#
|
|
@@ -293,6 +305,16 @@ module Html2rss
|
|
|
293
305
|
# @return [String] the debug representation
|
|
294
306
|
def inspect = "#<#{self.class}:#{object_id} @uri=#{@uri.inspect}>"
|
|
295
307
|
|
|
308
|
+
protected
|
|
309
|
+
|
|
310
|
+
# Scheme, authority, slash-stripped path, and query (fragment omitted).
|
|
311
|
+
#
|
|
312
|
+
# @return [String]
|
|
313
|
+
def document_identity
|
|
314
|
+
uri = @uri.omit(:fragment)
|
|
315
|
+
"#{uri.scheme}\0#{uri.authority}\0#{uri.path.to_s.chomp('/')}\0#{uri.query}"
|
|
316
|
+
end
|
|
317
|
+
|
|
296
318
|
private
|
|
297
319
|
|
|
298
320
|
def ip_address_host?
|
data/lib/html2rss/version.rb
CHANGED
|
@@ -157,9 +157,40 @@
|
|
|
157
157
|
},
|
|
158
158
|
"exclusiveMinimum": 0
|
|
159
159
|
},
|
|
160
|
+
"entry_resolution": {
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {
|
|
163
|
+
"enabled": {
|
|
164
|
+
"type": "boolean",
|
|
165
|
+
"not": {
|
|
166
|
+
"type": "null"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"max_probes": {
|
|
170
|
+
"type": "integer",
|
|
171
|
+
"not": {
|
|
172
|
+
"type": "null"
|
|
173
|
+
},
|
|
174
|
+
"exclusiveMinimum": 0
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"required": []
|
|
178
|
+
},
|
|
160
179
|
"scraper": {
|
|
161
180
|
"type": "object",
|
|
162
181
|
"properties": {
|
|
182
|
+
"native_feed": {
|
|
183
|
+
"type": "object",
|
|
184
|
+
"properties": {
|
|
185
|
+
"enabled": {
|
|
186
|
+
"type": "boolean",
|
|
187
|
+
"not": {
|
|
188
|
+
"type": "null"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"required": []
|
|
193
|
+
},
|
|
163
194
|
"wordpress_api": {
|
|
164
195
|
"type": "object",
|
|
165
196
|
"properties": {
|
|
@@ -338,7 +369,14 @@
|
|
|
338
369
|
"required": [],
|
|
339
370
|
"default": {
|
|
340
371
|
"limit": 25,
|
|
372
|
+
"entry_resolution": {
|
|
373
|
+
"enabled": true,
|
|
374
|
+
"max_probes": 5
|
|
375
|
+
},
|
|
341
376
|
"scraper": {
|
|
377
|
+
"native_feed": {
|
|
378
|
+
"enabled": true
|
|
379
|
+
},
|
|
342
380
|
"wordpress_api": {
|
|
343
381
|
"enabled": true
|
|
344
382
|
},
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: html2rss
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.28.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gil Desmarais
|
|
@@ -331,6 +331,7 @@ files:
|
|
|
331
331
|
- lib/html2rss/auto_source/scraper/meta_oembed.rb
|
|
332
332
|
- lib/html2rss/auto_source/scraper/microdata.rb
|
|
333
333
|
- lib/html2rss/auto_source/scraper/microformats2.rb
|
|
334
|
+
- lib/html2rss/auto_source/scraper/native_feed.rb
|
|
334
335
|
- lib/html2rss/auto_source/scraper/schema.rb
|
|
335
336
|
- lib/html2rss/auto_source/scraper/schema/category_extractor.rb
|
|
336
337
|
- lib/html2rss/auto_source/scraper/schema/item_list.rb
|
|
@@ -376,6 +377,14 @@ files:
|
|
|
376
377
|
- lib/html2rss/feed_pipeline/auto_fallback.rb
|
|
377
378
|
- lib/html2rss/feed_pipeline/runtime_policy.rb
|
|
378
379
|
- lib/html2rss/feed_pipeline/strategy_plan.rb
|
|
380
|
+
- lib/html2rss/feed_resolution.rb
|
|
381
|
+
- lib/html2rss/feed_resolution/README.md
|
|
382
|
+
- lib/html2rss/feed_resolution/candidate_generator.rb
|
|
383
|
+
- lib/html2rss/feed_resolution/diag.rb
|
|
384
|
+
- lib/html2rss/feed_resolution/options.rb
|
|
385
|
+
- lib/html2rss/feed_resolution/policy.rb
|
|
386
|
+
- lib/html2rss/feed_resolution/probe.rb
|
|
387
|
+
- lib/html2rss/feed_resolution/scorer.rb
|
|
379
388
|
- lib/html2rss/feed_result.rb
|
|
380
389
|
- lib/html2rss/hash_util.rb
|
|
381
390
|
- lib/html2rss/html.rb
|
|
@@ -416,6 +425,7 @@ files:
|
|
|
416
425
|
- lib/html2rss/mcp/inspect.rb
|
|
417
426
|
- lib/html2rss/mcp/outcome.rb
|
|
418
427
|
- lib/html2rss/mcp/server.rb
|
|
428
|
+
- lib/html2rss/page_recon.rb
|
|
419
429
|
- lib/html2rss/request_service.rb
|
|
420
430
|
- lib/html2rss/request_service/blocked_surface.rb
|
|
421
431
|
- lib/html2rss/request_service/botasaurus_contract.rb
|
|
@@ -446,6 +456,7 @@ files:
|
|
|
446
456
|
- lib/html2rss/scoring/observation.rb
|
|
447
457
|
- lib/html2rss/scoring/ranked_segment.rb
|
|
448
458
|
- lib/html2rss/scoring/score.rb
|
|
459
|
+
- lib/html2rss/scrape_target.rb
|
|
449
460
|
- lib/html2rss/selectors.rb
|
|
450
461
|
- lib/html2rss/selectors/extractors.rb
|
|
451
462
|
- lib/html2rss/selectors/extractors/attribute.rb
|
|
@@ -477,6 +488,12 @@ files:
|
|
|
477
488
|
- lib/html2rss/sst/tags.rb
|
|
478
489
|
- lib/html2rss/sst/text.rb
|
|
479
490
|
- lib/html2rss/status.rb
|
|
491
|
+
- lib/html2rss/surface_category.rb
|
|
492
|
+
- lib/html2rss/syndication.rb
|
|
493
|
+
- lib/html2rss/syndication/README.md
|
|
494
|
+
- lib/html2rss/syndication/candidate_catalog.rb
|
|
495
|
+
- lib/html2rss/syndication/discovery.rb
|
|
496
|
+
- lib/html2rss/syndication/parser.rb
|
|
480
497
|
- lib/html2rss/url.rb
|
|
481
498
|
- lib/html2rss/version.rb
|
|
482
499
|
- lib/tasks/config_schema.rake
|
|
@@ -486,7 +503,7 @@ licenses:
|
|
|
486
503
|
- MIT
|
|
487
504
|
metadata:
|
|
488
505
|
allowed_push_host: https://rubygems.org
|
|
489
|
-
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.
|
|
506
|
+
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.28.0
|
|
490
507
|
rubygems_mfa_required: 'true'
|
|
491
508
|
rdoc_options: []
|
|
492
509
|
require_paths:
|