html2rss 0.26.0 → 0.27.1

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -18
  3. data/html2rss.gemspec +1 -1
  4. data/lib/html2rss/auto_source/README.md +57 -0
  5. data/lib/html2rss/auto_source/cleanup.rb +121 -56
  6. data/lib/html2rss/auto_source/scraper.rb +13 -0
  7. data/lib/html2rss/auto_source.rb +34 -8
  8. data/lib/html2rss/capture/README.md +61 -0
  9. data/lib/html2rss/capture.rb +120 -117
  10. data/lib/html2rss/cli.rb +35 -17
  11. data/lib/html2rss/config/schema.rb +12 -0
  12. data/lib/html2rss/config/validator.rb +33 -8
  13. data/lib/html2rss/config.rb +28 -0
  14. data/lib/html2rss/error.rb +24 -6
  15. data/lib/html2rss/feed_pipeline/README.md +42 -0
  16. data/lib/html2rss/feed_pipeline/auto_fallback.rb +22 -9
  17. data/lib/html2rss/feed_pipeline/strategy_plan.rb +11 -0
  18. data/lib/html2rss/feed_pipeline.rb +30 -12
  19. data/lib/html2rss/html/article_extractor/category_extractor.rb +36 -22
  20. data/lib/html2rss/html/article_extractor/date_extractor.rb +5 -3
  21. data/lib/html2rss/html/article_extractor.rb +95 -17
  22. data/lib/html2rss/html/article_rules/category.rb +28 -11
  23. data/lib/html2rss/html/article_rules/date.rb +60 -6
  24. data/lib/html2rss/html/article_rules/description.rb +122 -0
  25. data/lib/html2rss/html/card_walk.rb +42 -0
  26. data/lib/html2rss/html/feed_link.rb +34 -0
  27. data/lib/html2rss/html/navigator.rb +18 -0
  28. data/lib/html2rss/html/sst_article_extractor.rb +119 -32
  29. data/lib/html2rss/link_destination/path_classifier.rb +49 -35
  30. data/lib/html2rss/mcp/config_argument.rb +42 -0
  31. data/lib/html2rss/mcp/contract.rb +173 -0
  32. data/lib/html2rss/mcp/inspect.rb +241 -0
  33. data/lib/html2rss/mcp/outcome.rb +188 -0
  34. data/lib/html2rss/mcp/server.rb +253 -409
  35. data/lib/html2rss/request_service/botasaurus_contract.rb +193 -102
  36. data/lib/html2rss/request_service/botasaurus_strategy.rb +15 -8
  37. data/lib/html2rss/request_service/compressed_body.rb +109 -0
  38. data/lib/html2rss/request_service/faraday_strategy.rb +3 -1
  39. data/lib/html2rss/request_service/policy.rb +1 -1
  40. data/lib/html2rss/request_service/response.rb +57 -6
  41. data/lib/html2rss/request_service.rb +6 -1
  42. data/lib/html2rss/selectors.rb +2 -1
  43. data/lib/html2rss/status.rb +27 -11
  44. data/lib/html2rss/url.rb +20 -0
  45. data/lib/html2rss/version.rb +1 -1
  46. data/lib/html2rss.rb +30 -6
  47. data/schema/html2rss-config.schema.json +40 -16
  48. metadata +15 -4
@@ -33,8 +33,13 @@ module Html2rss
33
33
  class BlockedSurfaceDetected < Html2rss::Error; end
34
34
  # Raised when a request times out.
35
35
  class RequestTimedOut < Html2rss::Error; end
36
+
36
37
  # Raised when Botasaurus configuration is missing or invalid.
37
- class BotasaurusConfigurationError < Html2rss::Error; end
38
+ class BotasaurusConfigurationError < Html2rss::Error
39
+ # Short empty-feed guidance owned once (composed by {Html2rss::NoFeedItemsExtracted}).
40
+ EMPTY_FEED_HINT = 'Configure BOTASAURUS_SCRAPER_URL to enable the Botasaurus request strategy.'
41
+ end
42
+
38
43
  # Raised when the Botasaurus service cannot be reached (network / DNS / SSL).
39
44
  class BotasaurusConnectionFailed < Html2rss::Error; end
40
45
  # Raised when Botasaurus responds but the scrape fails (upstream error, bad payload).
@@ -119,7 +119,8 @@ module Html2rss
119
119
  article_tag,
120
120
  base_url:,
121
121
  selected_anchor:,
122
- fallback_anchorless: true
122
+ fallback_anchorless: true,
123
+ time_zone: @time_zone
123
124
  )
124
125
  return article_hash unless extracted
125
126
 
@@ -8,7 +8,8 @@ module Html2rss
8
8
  # Stable telemetry payload for cross-repo consumers (e.g. html2rss-web observability).
9
9
  # Tallies and counters are validated and frozen at construction (including Marshal load).
10
10
  Status = Data.define(
11
- :version, :scraper_tallies, :dedup_dropped, :selected_strategy, :attempt_count, :strategy_attempts
11
+ :version, :scraper_tallies, :dedup_dropped, :selected_strategy, :attempt_count,
12
+ :strategy_attempts, :admission_drops
12
13
  ) do
13
14
  class << self
14
15
  ##
@@ -19,8 +20,11 @@ module Html2rss
19
20
  # @param selected_strategy [Symbol, nil] concrete strategy that succeeded under +:auto+ (else +nil+)
20
21
  # @param attempt_count [Integer] auto-fallback attempt count (0 when not under +:auto+)
21
22
  # @param strategy_attempts [Array<Hash>] auto-fallback attempt hashes (empty outside +:auto+)
23
+ # @param admission_drops [Hash{String => Integer}] Cleanup reason → count (empty when unused)
22
24
  # @return [Html2rss::Status]
23
- def build(articles:, dedup_dropped: 0, selected_strategy: nil, attempt_count: 0, strategy_attempts: [])
25
+ # rubocop:disable Metrics/ParameterLists -- Status kwargs stay co-located
26
+ def build(articles:, dedup_dropped: 0, selected_strategy: nil, attempt_count: 0,
27
+ strategy_attempts: [], admission_drops: {})
24
28
  tallies = articles.filter_map(&:scraper).tally.transform_keys { |klass| scraper_name(klass) }
25
29
  new(
26
30
  version: Html2rss::VERSION,
@@ -28,9 +32,11 @@ module Html2rss
28
32
  dedup_dropped:,
29
33
  selected_strategy:,
30
34
  attempt_count:,
31
- strategy_attempts:
35
+ strategy_attempts:,
36
+ admission_drops:
32
37
  )
33
38
  end
39
+ # rubocop:enable Metrics/ParameterLists
34
40
 
35
41
  ##
36
42
  # @param klass [Class, #to_s] scraper class
@@ -47,9 +53,11 @@ module Html2rss
47
53
  # @param selected_strategy [Symbol, nil]
48
54
  # @param attempt_count [Integer]
49
55
  # @param strategy_attempts [Array<Hash>]
56
+ # @param admission_drops [Hash{String => Integer}]
50
57
  # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength -- Status Data.define members
51
58
  def initialize(
52
- version:, scraper_tallies:, dedup_dropped:, selected_strategy: nil, attempt_count: 0, strategy_attempts: []
59
+ version:, scraper_tallies:, dedup_dropped:, selected_strategy: nil, attempt_count: 0,
60
+ strategy_attempts: [], admission_drops: {}
53
61
  )
54
62
  dedup = Integer(dedup_dropped)
55
63
  attempts = Integer(attempt_count)
@@ -61,7 +69,8 @@ module Html2rss
61
69
  dedup_dropped: dedup,
62
70
  selected_strategy:,
63
71
  attempt_count: attempts,
64
- strategy_attempts: freeze_attempts(strategy_attempts)
72
+ strategy_attempts: freeze_attempts(strategy_attempts),
73
+ admission_drops: freeze_tallies(admission_drops)
65
74
  )
66
75
  end
67
76
  # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
@@ -69,11 +78,13 @@ module Html2rss
69
78
  ##
70
79
  # Observability hash for web (+scraper_status+). Omits empty/absent optional keys:
71
80
  # +:scraper_tallies+ when empty, +:selected_strategy+ when +nil+, +:attempt_count+ when zero,
72
- # +:strategy_attempts+ when empty.
81
+ # +:strategy_attempts+ / +:admission_drops+ when empty.
73
82
  # Data members remain available via readers even when omitted here.
74
83
  #
75
84
  # @return [Hash{Symbol => Object}] always +:version+ (String), +:dedup_dropped+ (Integer);
76
- # optionally +:scraper_tallies+, +:selected_strategy+, +:attempt_count+, +:strategy_attempts+
85
+ # optionally +:scraper_tallies+, +:selected_strategy+, +:attempt_count+,
86
+ # +:strategy_attempts+, +:admission_drops+
87
+ # rubocop:disable Metrics/AbcSize -- omit-empty optional keys stay explicit
77
88
  def to_h
78
89
  {
79
90
  version:,
@@ -81,9 +92,11 @@ module Html2rss
81
92
  **(scraper_tallies.any? ? { scraper_tallies: } : {}),
82
93
  **(selected_strategy.nil? ? {} : { selected_strategy: }),
83
94
  **(attempt_count.positive? ? { attempt_count: } : {}),
84
- **(strategy_attempts.any? ? { strategy_attempts: } : {})
95
+ **(strategy_attempts.any? ? { strategy_attempts: } : {}),
96
+ **(admission_drops.any? ? { admission_drops: } : {})
85
97
  }
86
98
  end
99
+ # rubocop:enable Metrics/AbcSize
87
100
 
88
101
  ##
89
102
  # Formats the RSS +generator+ string and JSON Feed +user_comment+.
@@ -101,11 +114,14 @@ module Html2rss
101
114
  private
102
115
 
103
116
  def marshal_dump
104
- [version, scraper_tallies, dedup_dropped, selected_strategy, attempt_count, strategy_attempts]
117
+ [version, scraper_tallies, dedup_dropped, selected_strategy, attempt_count, strategy_attempts,
118
+ admission_drops]
105
119
  end
106
120
 
107
- def marshal_load((version, scraper_tallies, dedup_dropped, selected_strategy, attempt_count, strategy_attempts))
108
- initialize(version:, scraper_tallies:, dedup_dropped:, selected_strategy:, attempt_count:, strategy_attempts:)
121
+ def marshal_load((version, scraper_tallies, dedup_dropped, selected_strategy, attempt_count,
122
+ strategy_attempts, admission_drops))
123
+ initialize(version:, scraper_tallies:, dedup_dropped:, selected_strategy:, attempt_count:,
124
+ strategy_attempts:, admission_drops: admission_drops || {})
109
125
  end
110
126
 
111
127
  def freeze_tallies(tallies)
data/lib/html2rss/url.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'addressable/uri'
4
4
  require 'cgi'
5
+ require 'ipaddr'
5
6
  require 'nokogiri'
6
7
 
7
8
  module Html2rss
@@ -152,6 +153,16 @@ module Html2rss
152
153
  # @return [String, nil] URI host component
153
154
  def host = @uri.host
154
155
 
156
+ # Registrable domain (eTLD+1). Same-site hosts like www.wp.pl and wiadomosci.wp.pl
157
+ # share `wp.pl`. IP literals stay the host so 1.2.3.4 is not treated as `3.4`.
158
+ #
159
+ # @return [String, nil]
160
+ def domain
161
+ return host if ip_address_host?
162
+
163
+ @uri.domain || host
164
+ end
165
+
155
166
  # @return [Integer, nil] URI port component
156
167
  def port = @uri.port
157
168
 
@@ -281,6 +292,15 @@ module Html2rss
281
292
  #
282
293
  # @return [String] the debug representation
283
294
  def inspect = "#<#{self.class}:#{object_id} @uri=#{@uri.inspect}>"
295
+
296
+ private
297
+
298
+ def ip_address_host?
299
+ IPAddr.new(host.to_s)
300
+ true
301
+ rescue IPAddr::Error
302
+ false
303
+ end
284
304
  end
285
305
  # rubocop:enable Metrics/ClassLength
286
306
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Html2rss
4
4
  # Current application version.
5
- VERSION = '0.26.0'
5
+ VERSION = '0.27.1'
6
6
  public_constant :VERSION
7
7
  end
data/lib/html2rss.rb CHANGED
@@ -70,11 +70,35 @@ module Html2rss
70
70
 
71
71
  # rubocop:disable Metrics/ParameterLists
72
72
 
73
+ ##
74
+ # Scrapes the provided URL without hand-written selectors and returns {FeedResult}.
75
+ #
76
+ # Prefer this when callers need {FeedResult#status} (MCP envelope payload, CLI +--explain+).
77
+ #
78
+ # @param url [String] source page URL
79
+ # @param strategy [Symbol] request strategy to use
80
+ # @param items_selector [String, nil] optional selector hint for item extraction
81
+ # @param max_redirects [Integer, nil] optional redirect limit override
82
+ # @param max_requests [Integer] optional request budget override (default: 4 for sitemap sub-fetches)
83
+ # @param local_file_path [String, nil] optional local HTML file path
84
+ # @param limit [Integer, nil] max articles to keep (default: {AutoSource::DEFAULT_LIMIT})
85
+ # @return [Html2rss::FeedResult]
86
+ def self.auto_feed_result(url,
87
+ strategy: :auto,
88
+ items_selector: nil,
89
+ max_redirects: nil,
90
+ max_requests: 4,
91
+ local_file_path: nil,
92
+ limit: nil)
93
+ feed_result(build_auto_source_config(url:, strategy:, items_selector:, max_redirects:, max_requests:,
94
+ local_file_path:, limit:))
95
+ end
96
+
73
97
  ##
74
98
  # Scrapes the provided URL without hand-written selectors and returns an RSS object.
75
99
  #
76
100
  # Builds an auto_source config, then FeedPipeline runs structured scrapers and
77
- # (when needed) the SST heuristic path — see docs/auto_source.md.
101
+ # (when needed) the SST heuristic path — see lib/html2rss/auto_source/README.md.
78
102
  #
79
103
  # @param url [String] source page URL
80
104
  # @param strategy [Symbol] request strategy to use
@@ -91,14 +115,14 @@ module Html2rss
91
115
  max_requests: 4,
92
116
  local_file_path: nil,
93
117
  limit: nil)
94
- feed(build_auto_source_config(url:, strategy:, items_selector:, max_redirects:, max_requests:,
95
- local_file_path:, limit:))
118
+ auto_feed_result(url, strategy:, items_selector:, max_redirects:, max_requests:,
119
+ local_file_path:, limit:).to_rss
96
120
  end
97
121
 
98
122
  ##
99
123
  # Scrapes the provided URL without hand-written selectors and returns a JSONFeed 1.1 hash.
100
124
  #
101
- # Same auto_source pipeline as {.auto_source}; see docs/auto_source.md.
125
+ # Same auto_source pipeline as {.auto_source}; see lib/html2rss/auto_source/README.md.
102
126
  #
103
127
  # @param url [String] source page URL
104
128
  # @param strategy [Symbol] request strategy to use
@@ -115,8 +139,8 @@ module Html2rss
115
139
  max_requests: 4,
116
140
  local_file_path: nil,
117
141
  limit: nil)
118
- json_feed(build_auto_source_config(url:, strategy:, items_selector:, max_redirects:, max_requests:,
119
- local_file_path:, limit:))
142
+ auto_feed_result(url, strategy:, items_selector:, max_redirects:, max_requests:,
143
+ local_file_path:, limit:).to_json_feed
120
144
  end
121
145
 
122
146
  ##
@@ -66,6 +66,17 @@
66
66
  "directory": {
67
67
  "type": "object",
68
68
  "properties": {
69
+ "title": {
70
+ "type": "string",
71
+ "minLength": 1
72
+ },
73
+ "summary": {
74
+ "type": [
75
+ "null",
76
+ "string"
77
+ ],
78
+ "maxLength": 160
79
+ },
69
80
  "topics": {
70
81
  "type": "array",
71
82
  "items": {
@@ -92,7 +103,9 @@
92
103
  "minItems": 1
93
104
  }
94
105
  },
95
- "required": []
106
+ "required": [
107
+ "title"
108
+ ]
96
109
  },
97
110
  "headers": {
98
111
  "type": "object",
@@ -684,7 +697,8 @@
684
697
  "not": {
685
698
  "type": "null"
686
699
  },
687
- "exclusiveMinimum": 0
700
+ "minimum": 1,
701
+ "maximum": 20
688
702
  },
689
703
  "scroll": {
690
704
  "type": "boolean",
@@ -692,12 +706,6 @@
692
706
  "type": "null"
693
707
  }
694
708
  },
695
- "scroll_to_bottom": {
696
- "type": "boolean",
697
- "not": {
698
- "type": "null"
699
- }
700
- },
701
709
  "block_images": {
702
710
  "type": "boolean",
703
711
  "not": {
@@ -737,13 +745,28 @@
737
745
  "minLength": 1
738
746
  },
739
747
  "window_size": {
740
- "type": "array",
741
- "items": {
742
- "minLength": 2,
743
- "maxLength": 2,
744
- "type": "integer",
745
- "exclusiveMinimum": 0
746
- }
748
+ "type": "object",
749
+ "properties": {
750
+ "width": {
751
+ "type": "integer",
752
+ "not": {
753
+ "type": "null"
754
+ },
755
+ "exclusiveMinimum": 0
756
+ },
757
+ "height": {
758
+ "type": "integer",
759
+ "not": {
760
+ "type": "null"
761
+ },
762
+ "exclusiveMinimum": 0
763
+ }
764
+ },
765
+ "required": [
766
+ "width",
767
+ "height"
768
+ ],
769
+ "additionalProperties": false
747
770
  },
748
771
  "lang": {
749
772
  "type": "string",
@@ -756,7 +779,8 @@
756
779
  "type": "object"
757
780
  }
758
781
  },
759
- "required": []
782
+ "required": [],
783
+ "additionalProperties": false
760
784
  },
761
785
  "local_file_path": {
762
786
  "type": "string",
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.26.0
4
+ version: 0.27.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gil Desmarais
@@ -119,14 +119,14 @@ dependencies:
119
119
  requirements:
120
120
  - - "~>"
121
121
  - !ruby/object:Gem::Version
122
- version: '1.0'
122
+ version: '1.2'
123
123
  type: :runtime
124
124
  prerelease: false
125
125
  version_requirements: !ruby/object:Gem::Requirement
126
126
  requirements:
127
127
  - - "~>"
128
128
  - !ruby/object:Gem::Version
129
- version: '1.0'
129
+ version: '1.2'
130
130
  - !ruby/object:Gem::Dependency
131
131
  name: mime-types
132
132
  requirement: !ruby/object:Gem::Requirement
@@ -319,6 +319,7 @@ files:
319
319
  - lib/html2rss/article/deduplicator.rb
320
320
  - lib/html2rss/article/enclosure.rb
321
321
  - lib/html2rss/auto_source.rb
322
+ - lib/html2rss/auto_source/README.md
322
323
  - lib/html2rss/auto_source/cleanup.rb
323
324
  - lib/html2rss/auto_source/scraper.rb
324
325
  - lib/html2rss/auto_source/scraper/html.rb
@@ -350,6 +351,7 @@ files:
350
351
  - lib/html2rss/auto_source/segmenter/primary_link.rb
351
352
  - lib/html2rss/auto_source/segmenter/semantic.rb
352
353
  - lib/html2rss/capture.rb
354
+ - lib/html2rss/capture/README.md
353
355
  - lib/html2rss/channel.rb
354
356
  - lib/html2rss/cli.rb
355
357
  - lib/html2rss/config.rb
@@ -370,6 +372,7 @@ files:
370
372
  - lib/html2rss/feed_builder/rss.rb
371
373
  - lib/html2rss/feed_builder/rss/stylesheet.rb
372
374
  - lib/html2rss/feed_pipeline.rb
375
+ - lib/html2rss/feed_pipeline/README.md
373
376
  - lib/html2rss/feed_pipeline/auto_fallback.rb
374
377
  - lib/html2rss/feed_pipeline/runtime_policy.rb
375
378
  - lib/html2rss/feed_pipeline/strategy_plan.rb
@@ -386,8 +389,11 @@ files:
386
389
  - lib/html2rss/html/article_rules.rb
387
390
  - lib/html2rss/html/article_rules/category.rb
388
391
  - lib/html2rss/html/article_rules/date.rb
392
+ - lib/html2rss/html/article_rules/description.rb
389
393
  - lib/html2rss/html/article_rules/enclosure.rb
390
394
  - lib/html2rss/html/article_rules/image.rb
395
+ - lib/html2rss/html/card_walk.rb
396
+ - lib/html2rss/html/feed_link.rb
391
397
  - lib/html2rss/html/navigator.rb
392
398
  - lib/html2rss/html/navigator/text_extractor.rb
393
399
  - lib/html2rss/html/rendering.rb
@@ -405,12 +411,17 @@ files:
405
411
  - lib/html2rss/link_destination/path_classifier.rb
406
412
  - lib/html2rss/link_destination/text_classifier.rb
407
413
  - lib/html2rss/mcp.rb
414
+ - lib/html2rss/mcp/config_argument.rb
415
+ - lib/html2rss/mcp/contract.rb
416
+ - lib/html2rss/mcp/inspect.rb
417
+ - lib/html2rss/mcp/outcome.rb
408
418
  - lib/html2rss/mcp/server.rb
409
419
  - lib/html2rss/request_service.rb
410
420
  - lib/html2rss/request_service/blocked_surface.rb
411
421
  - lib/html2rss/request_service/botasaurus_contract.rb
412
422
  - lib/html2rss/request_service/botasaurus_strategy.rb
413
423
  - lib/html2rss/request_service/budget.rb
424
+ - lib/html2rss/request_service/compressed_body.rb
414
425
  - lib/html2rss/request_service/context.rb
415
426
  - lib/html2rss/request_service/faraday_strategy.rb
416
427
  - lib/html2rss/request_service/local_file_strategy.rb
@@ -475,7 +486,7 @@ licenses:
475
486
  - MIT
476
487
  metadata:
477
488
  allowed_push_host: https://rubygems.org
478
- changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.26.0
489
+ changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.27.1
479
490
  rubygems_mfa_required: 'true'
480
491
  rdoc_options: []
481
492
  require_paths: