html2rss 0.30.0 → 0.31.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/README.md +2 -2
- data/lib/html2rss/article/deduplicator.rb +2 -4
- data/lib/html2rss/article.rb +16 -39
- data/lib/html2rss/auto_source/README.md +1 -1
- data/lib/html2rss/auto_source/cleanup.rb +15 -6
- data/lib/html2rss/auto_source/scraper/html.rb +5 -0
- data/lib/html2rss/auto_source/scraper/json_state.rb +5 -0
- data/lib/html2rss/auto_source/scraper/meta_oembed.rb +8 -1
- data/lib/html2rss/auto_source/scraper/microdata.rb +8 -0
- data/lib/html2rss/auto_source/scraper/microformats2.rb +8 -0
- data/lib/html2rss/auto_source/scraper/native_feed.rb +3 -0
- data/lib/html2rss/auto_source/scraper/schema/thing.rb +4 -5
- data/lib/html2rss/auto_source/scraper/schema.rb +8 -0
- data/lib/html2rss/auto_source/scraper/semantic_html.rb +5 -0
- data/lib/html2rss/auto_source/scraper/sitemap.rb +3 -0
- data/lib/html2rss/auto_source/scraper/wordpress_api.rb +3 -0
- data/lib/html2rss/auto_source/scraper.rb +4 -14
- data/lib/html2rss/auto_source/segment.rb +7 -4
- data/lib/html2rss/auto_source/segmenter/cluster.rb +22 -23
- data/lib/html2rss/auto_source/segmenter/list.rb +54 -31
- data/lib/html2rss/auto_source/segmenter.rb +5 -5
- data/lib/html2rss/auto_source.rb +2 -2
- data/lib/html2rss/batch.rb +3 -1
- data/lib/html2rss/capture/README.md +6 -4
- data/lib/html2rss/capture.rb +67 -35
- data/lib/html2rss/channel.rb +3 -3
- data/lib/html2rss/cli/probe_view.rb +5 -3
- data/lib/html2rss/cli/render.rb +6 -5
- data/lib/html2rss/cli/validate.rb +28 -27
- data/lib/html2rss/cli.rb +1 -1
- data/lib/html2rss/config/issue_mapper.rb +108 -0
- data/lib/html2rss/config/request_headers.rb +2 -3
- data/lib/html2rss/config/schema.rb +16 -1
- data/lib/html2rss/config/selectors_validator.rb +92 -70
- data/lib/html2rss/config/validation_issue.rb +60 -0
- data/lib/html2rss/config/validation_report.rb +55 -0
- data/lib/html2rss/config/validator.rb +10 -6
- data/lib/html2rss/config.rb +35 -113
- data/lib/html2rss/defaults.rb +42 -30
- data/lib/html2rss/doctor/botasaurus.rb +6 -4
- data/lib/html2rss/doctor.rb +8 -0
- data/lib/html2rss/error.rb +1 -1
- data/lib/html2rss/feed_builder/rss.rb +9 -14
- data/lib/html2rss/feed_pipeline/runtime_policy.rb +14 -10
- data/lib/html2rss/feed_pipeline/strategy_plan.rb +6 -2
- data/lib/html2rss/feed_resolution/candidate_generator.rb +1 -4
- data/lib/html2rss/feed_resolution/diag.rb +3 -1
- data/lib/html2rss/feed_resolution/options.rb +3 -1
- data/lib/html2rss/feed_resolution/policy.rb +2 -2
- data/lib/html2rss/html/article_extractor/date_extractor.rb +1 -1
- data/lib/html2rss/html/article_extractor.rb +2 -4
- data/lib/html2rss/html/feed_link.rb +3 -1
- data/lib/html2rss/html/navigator/text_extractor.rb +1 -1
- data/lib/html2rss/html/navigator.rb +10 -10
- data/lib/html2rss/html/rendering/description_builder.rb +1 -1
- data/lib/html2rss/html/sst_article_extractor.rb +8 -5
- data/lib/html2rss/link_destination/destination_facts.rb +4 -2
- data/lib/html2rss/link_destination/noise_policy.rb +10 -1
- data/lib/html2rss/link_destination/path_classifier.rb +45 -38
- data/lib/html2rss/mcp/README.md +2 -0
- data/lib/html2rss/mcp/config_argument.rb +3 -1
- data/lib/html2rss/mcp/contract.rb +6 -20
- data/lib/html2rss/mcp/outcome/playbook.rb +2 -2
- data/lib/html2rss/mcp/outcome.rb +36 -43
- data/lib/html2rss/mcp/server/tools.rb +111 -131
- data/lib/html2rss/mcp/server.rb +26 -18
- data/lib/html2rss/page_recon/README.md +5 -5
- data/lib/html2rss/page_recon/diagnostics.rb +71 -81
- data/lib/html2rss/page_recon.rb +9 -5
- data/lib/html2rss/recon.rb +19 -30
- data/lib/html2rss/request_service/botasaurus_contract.rb +11 -12
- data/lib/html2rss/request_service/botasaurus_strategy.rb +1 -1
- data/lib/html2rss/request_service/compressed_body.rb +4 -1
- data/lib/html2rss/request_service/httpx_strategy.rb +39 -28
- data/lib/html2rss/request_service/network_guard.rb +7 -5
- data/lib/html2rss/request_service/response.rb +1 -2
- data/lib/html2rss/request_service/strategy.rb +3 -8
- data/lib/html2rss/scoring/container_assessor.rb +3 -1
- data/lib/html2rss/scoring/engine.rb +8 -8
- data/lib/html2rss/scoring/observation.rb +5 -3
- data/lib/html2rss/scoring/ranked_segment.rb +3 -1
- data/lib/html2rss/scrape_target.rb +3 -1
- data/lib/html2rss/selector_candidates.rb +234 -0
- data/lib/html2rss/selectors/README.md +64 -0
- data/lib/html2rss/selectors/extractors/attribute.rb +13 -15
- data/lib/html2rss/selectors/extractors/href.rb +15 -16
- data/lib/html2rss/selectors/extractors/html.rb +8 -9
- data/lib/html2rss/selectors/extractors/static.rb +12 -14
- data/lib/html2rss/selectors/extractors/text.rb +8 -9
- data/lib/html2rss/selectors/extractors.rb +33 -20
- data/lib/html2rss/selectors/item_env.rb +31 -0
- data/lib/html2rss/selectors/{object_to_xml_converter.rb → json_xml.rb} +11 -12
- data/lib/html2rss/selectors/option_spec.rb +140 -0
- data/lib/html2rss/selectors/post_processors/base.rb +79 -31
- data/lib/html2rss/selectors/post_processors/gsub.rb +103 -39
- data/lib/html2rss/selectors/post_processors/html_transformers/transform_urls_to_absolute_ones.rb +4 -4
- data/lib/html2rss/selectors/post_processors/markdown_to_html.rb +10 -11
- data/lib/html2rss/selectors/post_processors/parse_time.rb +50 -26
- data/lib/html2rss/selectors/post_processors/parse_uri.rb +11 -7
- data/lib/html2rss/selectors/post_processors/sanitize_html.rb +46 -32
- data/lib/html2rss/selectors/post_processors/substring.rb +14 -25
- data/lib/html2rss/selectors/post_processors/template.rb +23 -19
- data/lib/html2rss/selectors/post_processors.rb +6 -5
- data/lib/html2rss/selectors/{schema_doc.rb → schema_export.rb} +12 -31
- data/lib/html2rss/selectors/step_env.rb +24 -0
- data/lib/html2rss/selectors.rb +231 -142
- data/lib/html2rss/sst/attrs.rb +5 -3
- data/lib/html2rss/sst/document.rb +3 -1
- data/lib/html2rss/sst/index.rb +3 -1
- data/lib/html2rss/sst/node.rb +29 -10
- data/lib/html2rss/sst/normalizer.rb +12 -16
- data/lib/html2rss/sst/tags.rb +7 -7
- data/lib/html2rss/status.rb +13 -8
- data/lib/html2rss/surface_category.rb +14 -32
- data/lib/html2rss/syndication/parser.rb +2 -2
- data/lib/html2rss/test/enhance_audit.rb +1 -3
- data/lib/html2rss/test/policy.rb +106 -0
- data/lib/html2rss/test.rb +126 -173
- data/lib/html2rss/url.rb +25 -4
- data/lib/html2rss/version.rb +1 -1
- data/lib/html2rss.rb +21 -13
- data/lib/tasks/config_schema.rake +2 -2
- data/schema/html2rss-config.schema.json +11 -8
- metadata +15 -6
- data/lib/html2rss/selectors/item_scope.rb +0 -30
|
@@ -35,6 +35,7 @@ module Html2rss
|
|
|
35
35
|
style |
|
|
36
36
|
datetime |
|
|
37
37
|
itemprop |
|
|
38
|
+
role |
|
|
38
39
|
type |
|
|
39
40
|
category |
|
|
40
41
|
categories |
|
|
@@ -54,7 +55,7 @@ module Html2rss
|
|
|
54
55
|
/xi
|
|
55
56
|
|
|
56
57
|
# Typed Attrs fields excluded from the leftover raw hash.
|
|
57
|
-
TYPED_ATTR_NAMES =
|
|
58
|
+
TYPED_ATTR_NAMES = Set['href', 'src', 'id', 'class', 'datetime', 'itemprop', 'style', 'srcset', 'type'].freeze
|
|
58
59
|
|
|
59
60
|
# String form of Tags::IGNORED_CONTAINER_NAMES for chrome checks without to_sym.
|
|
60
61
|
IGNORED_CONTAINER_TAGS = Tags::IGNORED_CONTAINER_NAMES.to_set(&:to_s).freeze
|
|
@@ -113,23 +114,19 @@ module Html2rss
|
|
|
113
114
|
def resolve_root_nk
|
|
114
115
|
parsed = @parsed_body
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return html if html
|
|
117
|
+
html = parsed.at_css('html')
|
|
118
|
+
return html if html
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
end
|
|
120
|
+
body = parsed.at_css('body')
|
|
121
|
+
if body
|
|
122
|
+
Html2rss::Log.warn('sst.normalizer root fallback: body')
|
|
123
|
+
return body
|
|
125
124
|
end
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return first
|
|
132
|
-
end
|
|
126
|
+
first = parsed.element_children.find { |child| element_root?(child) }
|
|
127
|
+
if first
|
|
128
|
+
Html2rss::Log.warn('sst.normalizer root fallback: first element child')
|
|
129
|
+
return first
|
|
133
130
|
end
|
|
134
131
|
|
|
135
132
|
Html2rss::Log.warn('sst.normalizer root fallback: self')
|
|
@@ -142,7 +139,6 @@ module Html2rss
|
|
|
142
139
|
|
|
143
140
|
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
144
141
|
def normalize_element(nk_node, parent:, depth:, path:, chrome:)
|
|
145
|
-
return unless nk_node.respond_to?(:name)
|
|
146
142
|
return unless nk_node.element?
|
|
147
143
|
|
|
148
144
|
tag = Html2rss::Html::Probe.tag(nk_node)
|
data/lib/html2rss/sst/tags.rb
CHANGED
|
@@ -6,21 +6,21 @@ module Html2rss
|
|
|
6
6
|
# Shared tag-name and href constants for SST nodes.
|
|
7
7
|
module Tags
|
|
8
8
|
# Heading element names.
|
|
9
|
-
HEADING_NAMES =
|
|
9
|
+
HEADING_NAMES = Set[:h1, :h2, :h3, :h4, :h5, :h6].freeze
|
|
10
10
|
# Block-level names that contribute structural newlines in text.
|
|
11
|
-
BLOCK_NAMES =
|
|
11
|
+
BLOCK_NAMES = Set[:p, :div, :li, :ul, :ol, :h1, :h2, :h3, :h4, :h5, :h6, :tr, :br].freeze
|
|
12
12
|
# Names skipped when collecting visible text.
|
|
13
|
-
INVISIBLE_NAMES =
|
|
13
|
+
INVISIBLE_NAMES = Set[:svg, :script, :noscript, :style, :template].freeze
|
|
14
14
|
# Landmark names treated as utility chrome.
|
|
15
|
-
UTILITY_LANDMARK_NAMES =
|
|
15
|
+
UTILITY_LANDMARK_NAMES = Set[:nav, :aside, :footer, :menu].freeze
|
|
16
16
|
# Container names ignored as article roots.
|
|
17
|
-
IGNORED_CONTAINER_NAMES =
|
|
17
|
+
IGNORED_CONTAINER_NAMES = Set[:nav, :footer, :header, :svg, :script, :style].freeze
|
|
18
18
|
# Names excluded from class/structure clustering.
|
|
19
|
-
CLUSTER_EXCLUDED_NAMES =
|
|
19
|
+
CLUSTER_EXCLUDED_NAMES = Set[:html, :body, :nav, :footer, :header, :svg, :script, :style].freeze
|
|
20
20
|
# Href prefixes that never count as content destinations.
|
|
21
21
|
SKIP_HREF_PREFIXES = ['#', 'javascript:', 'mailto:', 'tel:', 'file://', 'sms:', 'data:'].freeze
|
|
22
22
|
# Layout-ish names used when resolving nested wrapper groups.
|
|
23
|
-
LAYOUT_NAMES =
|
|
23
|
+
LAYOUT_NAMES = Set[:div, :section, :article, :li, :ul, :ol].freeze
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
end
|
data/lib/html2rss/status.rb
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module Html2rss
|
|
3
|
+
module Html2rss
|
|
4
|
+
Status = Data.define(
|
|
5
|
+
:version, :scraper_tallies, :dedup_dropped, :selected_strategy, :attempt_count,
|
|
6
|
+
:strategy_attempts, :admission_drops, :entry_url, :scrape_url, :entry_resolution
|
|
7
|
+
)
|
|
8
|
+
|
|
4
9
|
##
|
|
5
10
|
# Shared RSS +generator+ / JSON Feed +user_comment+ formatter string.
|
|
6
11
|
#
|
|
7
12
|
# Exposed publicly via {FeedResult#status}. Safe to log without reading articles.
|
|
8
13
|
# Stable telemetry payload for cross-repo consumers (e.g. html2rss-web observability).
|
|
9
14
|
# Tallies and counters are validated and frozen at construction (including Marshal load).
|
|
10
|
-
Status
|
|
11
|
-
:version, :scraper_tallies, :dedup_dropped, :selected_strategy, :attempt_count,
|
|
12
|
-
:strategy_attempts, :admission_drops, :entry_url, :scrape_url, :entry_resolution
|
|
13
|
-
) do
|
|
15
|
+
class Status
|
|
14
16
|
class << self
|
|
15
17
|
##
|
|
16
18
|
# Builds status from extracted articles and scrape telemetry.
|
|
@@ -63,7 +65,7 @@ module Html2rss # rubocop:disable Metrics/ModuleLength -- Status Data.define + m
|
|
|
63
65
|
# @param admission_drops [Hash{String => Integer}]
|
|
64
66
|
# @param entry_url [String, nil]
|
|
65
67
|
# @param scrape_url [String, nil]
|
|
66
|
-
# @param entry_resolution [Hash, nil]
|
|
68
|
+
# @param entry_resolution [Html2rss::FeedResolution::Diag, Hash, nil]
|
|
67
69
|
# rubocop:disable-next Metrics/ParameterLists, Metrics/MethodLength -- Status Data.define members
|
|
68
70
|
def initialize(
|
|
69
71
|
version:, scraper_tallies:, dedup_dropped:, selected_strategy: nil, attempt_count: 0,
|
|
@@ -152,8 +154,11 @@ module Html2rss # rubocop:disable Metrics/ModuleLength -- Status Data.define + m
|
|
|
152
154
|
def freeze_entry_resolution(value)
|
|
153
155
|
return if value.nil?
|
|
154
156
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
case value
|
|
158
|
+
when Hash, FeedResolution::Diag then value.to_h
|
|
159
|
+
else
|
|
160
|
+
raise ArgumentError, "entry_resolution must be a Hash or FeedResolution::Diag, got #{value.class}"
|
|
161
|
+
end.transform_keys(&:to_sym).freeze
|
|
157
162
|
end
|
|
158
163
|
|
|
159
164
|
def validate_counters!(dedup:, attempts:, selected_strategy:)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Html2rss
|
|
4
|
+
SurfaceCategory = Data.define(:name)
|
|
5
|
+
|
|
4
6
|
##
|
|
5
7
|
# Closed surface class for no-scraper / page-assessment gates.
|
|
6
8
|
#
|
|
@@ -10,27 +12,15 @@ module Html2rss
|
|
|
10
12
|
# Surfaces that warrant listing/feed resolution (hubs / shells — not blocked).
|
|
11
13
|
WEAK = Set[:high_entropy_surface, :app_shell, :unsupported_surface].freeze
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def coerce(value)
|
|
18
|
-
return value if value.is_a?(self)
|
|
19
|
-
return new(name: nil) if value.nil?
|
|
20
|
-
|
|
21
|
-
new(name: value.to_sym)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
##
|
|
26
|
-
# @return [Symbol, nil]
|
|
27
|
-
attr_reader :name
|
|
15
|
+
# Frozen name → instance table.
|
|
16
|
+
ALL = %i[listing blocked_surface app_shell high_entropy_surface unsupported_surface]
|
|
17
|
+
.to_h { |name| [name, new(name:)] }.freeze
|
|
18
|
+
private_class_method :new
|
|
28
19
|
|
|
29
20
|
##
|
|
30
|
-
# @param name [Symbol,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
21
|
+
# @param name [Symbol, String]
|
|
22
|
+
# @return [SurfaceCategory]
|
|
23
|
+
def self.[](name) = ALL.fetch(name.to_sym) { raise ArgumentError, "unknown surface category: #{name.inspect}" }
|
|
34
24
|
|
|
35
25
|
##
|
|
36
26
|
# @return [Boolean]
|
|
@@ -41,23 +31,15 @@ module Html2rss
|
|
|
41
31
|
def blocked? = name == :blocked_surface
|
|
42
32
|
|
|
43
33
|
##
|
|
44
|
-
# @return [Boolean]
|
|
45
|
-
def listing_bonus? =
|
|
34
|
+
# @return [Boolean] listing surface eligible for listing bonus
|
|
35
|
+
def listing_bonus? = name == :listing
|
|
46
36
|
|
|
47
37
|
##
|
|
48
|
-
# @return [Symbol
|
|
38
|
+
# @return [Symbol]
|
|
49
39
|
def to_sym = name
|
|
50
40
|
|
|
51
41
|
##
|
|
52
|
-
# @
|
|
53
|
-
|
|
54
|
-
def ==(other)
|
|
55
|
-
other.is_a?(self.class) && name == other.name
|
|
56
|
-
end
|
|
57
|
-
alias eql? ==
|
|
58
|
-
|
|
59
|
-
##
|
|
60
|
-
# @return [Integer]
|
|
61
|
-
def hash = [self.class, name].hash
|
|
42
|
+
# @return [String]
|
|
43
|
+
def to_s = name.to_s
|
|
62
44
|
end
|
|
63
45
|
end
|
|
@@ -95,9 +95,9 @@ module Html2rss
|
|
|
95
95
|
private_class_method :atom_link
|
|
96
96
|
|
|
97
97
|
def atom_text(node)
|
|
98
|
-
return
|
|
98
|
+
return unless node.is_a?(RSS::Atom::TextConstruct) || node.is_a?(RSS::Atom::Feed::Entry::Content)
|
|
99
99
|
|
|
100
|
-
node.
|
|
100
|
+
node.content
|
|
101
101
|
end
|
|
102
102
|
module_function :atom_text
|
|
103
103
|
private_class_method :atom_text
|
|
@@ -40,9 +40,7 @@ module Html2rss
|
|
|
40
40
|
}
|
|
41
41
|
warnings = []
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
baseline = selector_engine.extract_article(item, response)
|
|
45
|
-
enhanced = selector_engine.enhance_article_hash(baseline.dup, item, response.url)
|
|
43
|
+
selector_engine.each_enhance_pair do |baseline, enhanced, item|
|
|
46
44
|
item_keys = keys_added(baseline, enhanced)
|
|
47
45
|
merge_keys_added!(gains, item_keys)
|
|
48
46
|
append_item_warnings!(warnings, item:, enhanced:, item_keys:)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Html2rss
|
|
4
|
+
module Test
|
|
5
|
+
##
|
|
6
|
+
# Pass/fail classification for a configuration test after live extraction.
|
|
7
|
+
# Owns min_items vs quality thresholds; {Test.call} orchestrates only.
|
|
8
|
+
module Policy
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
Outcome = Data.define(:passed, :failure_kind, :error_message)
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# Outcome of evaluating test thresholds.
|
|
15
|
+
class Outcome
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def success? = passed
|
|
18
|
+
|
|
19
|
+
# @return [Boolean]
|
|
20
|
+
def failure? = !passed
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# Evaluates whether the test passed thresholds and determines any failure kind and error message.
|
|
25
|
+
#
|
|
26
|
+
# @param item_count [Integer]
|
|
27
|
+
# @param min_items [Integer]
|
|
28
|
+
# @param strict_quality [Boolean]
|
|
29
|
+
# @param quality_report [QualityReport]
|
|
30
|
+
# @return [Outcome]
|
|
31
|
+
def evaluate(item_count:, min_items:, strict_quality:, quality_report:)
|
|
32
|
+
min_items_passed = item_count >= min_items
|
|
33
|
+
quality_failed = strict_quality && min_items_passed && quality_failure?(quality_report)
|
|
34
|
+
passed = min_items_passed && !quality_failed
|
|
35
|
+
failure_kind, error_message = outcome_failure(
|
|
36
|
+
min_items_passed:, quality_failed:, item_count:, min_items:, quality_report:
|
|
37
|
+
)
|
|
38
|
+
log_strict_quality_failure(failure_kind, item_count) if quality_failed
|
|
39
|
+
Outcome.new(passed:, failure_kind:, error_message:)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def log_strict_quality_failure(failure_kind, item_count)
|
|
43
|
+
Log.info("Test strict quality: failure_kind=#{failure_kind.to_sym} item_count=#{item_count}")
|
|
44
|
+
end
|
|
45
|
+
private_class_method :log_strict_quality_failure
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# @param quality_report [QualityReport]
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def quality_failure?(quality_report)
|
|
51
|
+
metrics = quality_report.metrics
|
|
52
|
+
item_count = metrics[:item_count]
|
|
53
|
+
return false if item_count.zero?
|
|
54
|
+
|
|
55
|
+
duplicate_urls_failure?(metrics) || junk_title_ratio_failure?(metrics) || short_title_failure?(metrics)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# @param min_items_passed [Boolean]
|
|
60
|
+
# @param quality_failed [Boolean]
|
|
61
|
+
# @param item_count [Integer]
|
|
62
|
+
# @param min_items [Integer]
|
|
63
|
+
# @param quality_report [QualityReport]
|
|
64
|
+
# @return [Array(FailureKind, nil), Array(String, nil)]
|
|
65
|
+
def outcome_failure(min_items_passed:, quality_failed:, item_count:, min_items:, quality_report:)
|
|
66
|
+
return [nil, nil] if min_items_passed && !quality_failed
|
|
67
|
+
unless min_items_passed
|
|
68
|
+
return [FailureKind[:min_items],
|
|
69
|
+
"Extracted #{item_count} items (minimum required: #{min_items})"]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
[FailureKind[:quality], quality_failure_message(quality_report)]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def quality_failure_message(quality_report)
|
|
76
|
+
reasons = quality_failure_reasons(quality_report.metrics)
|
|
77
|
+
"Feed quality check failed (#{reasons.join(', ')})"
|
|
78
|
+
end
|
|
79
|
+
private_class_method :quality_failure_message
|
|
80
|
+
|
|
81
|
+
def quality_failure_reasons(metrics)
|
|
82
|
+
reasons = []
|
|
83
|
+
reasons << 'duplicate_urls' if duplicate_urls_failure?(metrics)
|
|
84
|
+
reasons << 'generic_titles' if junk_title_ratio_failure?(metrics)
|
|
85
|
+
reasons << 'short_titles' if short_title_failure?(metrics)
|
|
86
|
+
reasons
|
|
87
|
+
end
|
|
88
|
+
private_class_method :quality_failure_reasons
|
|
89
|
+
|
|
90
|
+
def duplicate_urls_failure?(metrics)
|
|
91
|
+
metrics[:item_count] >= 2 && metrics[:unique_url_count] < 2
|
|
92
|
+
end
|
|
93
|
+
private_class_method :duplicate_urls_failure?
|
|
94
|
+
|
|
95
|
+
def junk_title_ratio_failure?(metrics)
|
|
96
|
+
metrics[:junk_title_count] > (metrics[:item_count] / 2.0)
|
|
97
|
+
end
|
|
98
|
+
private_class_method :junk_title_ratio_failure?
|
|
99
|
+
|
|
100
|
+
def short_title_failure?(metrics)
|
|
101
|
+
metrics[:short_title_count].positive?
|
|
102
|
+
end
|
|
103
|
+
private_class_method :short_title_failure?
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|