pinterest-dl 1.0.3 → 2.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73cae48cd87360325168ee188b8a67b25944402f33857a61dee4ea3a13457f91
4
- data.tar.gz: 313a17fd00772aa8980ee8389dc5bd934b8217225f4e4b11f130ad4882e629c0
3
+ metadata.gz: b4d94f0a5bbac63a983308d541b5e50a6a13c1d5bee422c356e56395c686ebcd
4
+ data.tar.gz: 148473c309ec313a3e0d7ddb0864f0388d22a2bf9edee39e819e54ccaab10745
5
5
  SHA512:
6
- metadata.gz: e08fc0e08bec2a47a91659d10b1ba3a87e6d2c70e81fc27134366768108ebf3e1a5f10bfea2dc3391c63fa038eb9daba58594ffd8c5997338b240dd21fcaa558
7
- data.tar.gz: 7bcfe71875c82f31ddaf9e27a0f648b646bb6381045830d03d6ee0a8fb561283e8a0eb4e85036915a6d71e6f5865e05f0aa4a5e0658f601b593f8e5a6f42f714
6
+ metadata.gz: 4e26a226009b81da00d41f46f9ccfcdca2f058aa7ff93210b488e1f2082c27f927103f5a0329dcea921ea34a62c49c3ad70c77d411d4a1b569b4b97fbba8c4cf
7
+ data.tar.gz: 66b3ddf6d10555f9e018198a3de376578b22fc504dfe386d8a49f06bdcc213519e0b273c5f123d34bcb33eb3217da51af7609712a2f1a0a3b67004be6858eb9b
data/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ This project follows [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [2.0.0]
7
+
8
+ ### Added
9
+ - `PinterestDL.download` / `PinterestDL.download_batch` for real file downloads,
10
+ the latter with per-file progress callbacks and a `PinterestDL::ProgressBar` helper.
11
+ - `PinterestDL.board` for fetching pins from a board or board section, with pagination.
12
+ - `PinterestDL.configure` for global settings: cookies/session, user agent,
13
+ timeouts, retry count, request rate limiting, and default image quality.
14
+ - `PinterestDL.get_media` to fetch image + video URLs for a pin in one request.
15
+ - Pagination support in `PinterestDL.search` (`pages:` option).
16
+ - `creator` field on pin metadata returned by `search` and `board`.
17
+ - Meaningful exception hierarchy under `PinterestDL::Error` (`NetworkError`,
18
+ `NotFoundError`, `RateLimitError`, `AuthenticationError`, `InvalidURLError`,
19
+ `DownloadError`) replacing silent `nil` returns and `puts`-based error reporting.
20
+ - Real CLI: `pinterest-dl image|video|search|board ...`.
21
+ - Configurable timeouts and retry/backoff on all HTTP requests.
22
+ - Small in-memory response cache and optional request rate limiting.
23
+ - Test suite (Minitest) and GitHub Actions CI matrix (Ruby 3.0–3.3).
24
+ - RuboCop configuration.
25
+
26
+ ### Changed
27
+ - User-Agent updated to a current Chrome UA string.
28
+ - All `puts` debug/status output removed from library code in favor of a
29
+ configurable `Logger` (`PinterestDL.config.logger`).
30
+ - Internals reorganized into `lib/pinterest_dl/{client,configuration,errors,
31
+ extractors,search,board,downloader,progress_bar,api}.rb`; `PinterestDL::VERSION`
32
+ is now the single source of truth for the gem version.
33
+
34
+ ### Fixed
35
+ - The gem no longer prints anythign on `require`.
36
+
37
+ ## [1.0.3]
38
+
39
+ - Initial public release: `get_image_url`, `get_video_url`, `search`,
40
+ `search_json`, `search_images`, `search_videos`.
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
+ Version 2, December 2004
3
+
4
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
data/README.md CHANGED
@@ -1,24 +1,30 @@
1
- <div align="center">
2
1
 
3
2
  # pinterest-dl
3
+ **Get direct image/video URLs from Pinterest, search pins, fetch board contents, and download media — as a Ruby library or a CLI.**
4
4
 
5
- ---
6
-
7
- [![Gem Downloads](https://img.shields.io/gem/dt/pinterest-dl)](https://rubygems.org/gems/pinterest-dl)
8
- [![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)
9
- [![Ruby](https://img.shields.io/badge/Ruby-2.5+-red)](https://www.ruby-lang.org/)
10
-
5
+ [![Gem Version](https://img.shields.io/gem/v/pinterest-dl)](https://rubygems.org/gems/pinterest-dl)
6
+ [![Gem Downloads](https://img.shields.io/gem/dt/pinterest-dl)](https://rubygems.org/gems/pinterest-dl)
7
+ [![CI](https://github.com/monji024/pinterest-dl/actions/workflows/ci.yml/badge.svg)](https://github.com/monji024/pinterest-dl/actions/workflows/ci.yml)
8
+ [![License](https://img.shields.io/badge/License-WTFPL-blue)](LICENSE)
9
+ [![Ruby](https://img.shields.io/badge/Ruby-2.7+-red)](https://www.ruby-lang.org/)
11
10
  <img src="https://img.icons8.com/color/96/000000/pinterest--v1.png" width="80">
12
11
 
13
- </div>
14
-
15
12
  ---
16
13
 
17
- Get direct image and video URLs from Pinterest.
14
+ > **Note:** Users in some regions (e.g. Iran) may need a fu*king VPN to reach Pinterest.
15
+ > (Yeah, because even in 2026 Pinterest still gives you the middle finger on clean APIs.)
16
+
17
+ Pinterest doesn't give you a clean, official API for getting a direct media URL out of a pin, walking a board, or downloading a batch of images. `pinterest-dl` does that: point it at a pin, a search query, or a board, and get back real `i.pinimg.com` / `v.pinimg.com` URLs — or the fucking files themselves, already saved to disk.
18
18
 
19
- ![Demo](https://github.com/monji024/pinterest-dl/raw/main/test.gif)
19
+ ## What it does
20
20
 
21
- > **Note:** Users in Iran may need a VPN to access Pinterest.
21
+ - Pulls the **direct image or video URL** out of a single pin page.
22
+ - **Searches** Pinterest and returns pin metadata (id, url, title, description, creator, image/video URL), with pagination.
23
+ - Fetches all pins from a **board** (or a specific board section).
24
+ - **Downloads** a URL — or a whole batch of them — straight to disk, with progress reporting and per-file error handling.
25
+ - Supports **authenticated requests** via cookies, for boards/pins that require a logged-in session.
26
+ - Raises **specific, catchable exceptions** (`NotFoundError`, `RateLimitError`, `NetworkError`, ...) instead of returning `nil` or printing to stdout.
27
+ - Ships a **CLI** (`pinterest-dl`) for one-off use from the shell.
22
28
 
23
29
  ## Installation
24
30
 
@@ -26,51 +32,156 @@ Get direct image and video URLs from Pinterest.
26
32
  gem install pinterest-dl
27
33
  ```
28
34
 
29
- ## Usage
35
+ Or add it to your Gemfile:
30
36
 
31
37
  ```ruby
32
- require 'pinterest-dl'
38
+ gem 'pinterest-dl'
33
39
  ```
34
40
 
35
- ### Get Image URL
41
+ ## Quick start
36
42
 
37
43
  ```ruby
38
- url = PinterestDL.get_image_url('https://www.pinterest.com/pin/example/')
44
+ require 'pinterest-dl'
45
+ # Direct image URL
46
+ PinterestDL.get_image_url('https://www.pinterest.com/pin/1234567890123456789/') # => "https://i.pinimg.com/originals/aa/bb/cc/aabbcc...jpg"
47
+ # Direct video URL (raises PinterestDL::NotFoundError if the pin has no video)
48
+ PinterestDL.get_video_url('https://www.pinterest.com/pin/1234567890123456789/') # => "https://v.pinimg.com/videos/mc/720p/aa/bb/cc/aabbcc....mp4"
49
+ # Download it
50
+ PinterestDL.download(
51
+ PinterestDL.get_image_url('https://www.pinterest.com/pin/1234567890123456789/'),
52
+ path: 'downloads/pin.jpg'
53
+ )
39
54
  ```
40
55
 
41
- ### Get Video URL
56
+ ## Usage
57
+
58
+ ### Get image / video URLs
42
59
 
43
60
  ```ruby
61
+ url = PinterestDL.get_image_url('https://www.pinterest.com/pin/example/') # Pick a specific size bucket
62
+ url = PinterestDL.get_image_url('https://www.pinterest.com/pin/example/', quality: :"474x") # quality can be :originals, :"736x", :"474x", :"236x"
44
63
  url = PinterestDL.get_video_url('https://www.pinterest.com/pin/example/')
64
+ # Both at once
65
+ media = PinterestDL.get_media('https://www.pinterest.com/pin/example/') # => { image_url: "https://i.pinimg.com/...", video_url: nil }
45
66
  ```
46
67
 
47
- ### Search Pins
68
+ ### Search
48
69
 
49
70
  ```ruby
50
71
  # Search for pins with a specific query
51
- results = PinterestDL.search('nature landscape', limit: 10)
72
+ results = PinterestDL.search('nature landscape', limit: 10) # => [{ id: "...", pin_url: "...", title: "...", description: "...", creator: "...", image_url: "...", video_url: nil }, ...]
73
+ # Fetch more than one page of results (25 pins/page from Pinterest)
74
+ results = PinterestDL.search('nature landscape', limit: 100, pages: 10)
75
+ # Get only image URLs from search results
76
+ images = PinterestDL.search_images('sunset', limit: 5) # => ["https://i.pinimg.com/originals/...", ...]
77
+ # Get only video URLs from search results
78
+ videos = PinterestDL.search_videos('tutorial', limit: 3) # => ["https://v.pinimg.com/videos/...", ...]
79
+ # Get search results as a JSON string
80
+ json_results = PinterestDL.search_json('anime art', limit: 20)
81
+ ```
52
82
 
53
- # Returns an array of pin objects with:
54
- # id, pin_url, title, description, image_url, video_url
83
+ ### Boards & sections
55
84
 
56
- # Get only image URLs from search results
57
- images = PinterestDL.search_images('sunset', limit: 5)
58
- # => ["https://i.pinimg.com/originals/...", ...]
85
+ ```ruby
86
+ # All pins on a board
87
+ pins = PinterestDL.board('https://www.pinterest.com/username/board-name/', limit: 50)
88
+ # A specific section within a board
89
+ pins = PinterestDL.board('https://www.pinterest.com/username/board-name/section-slug/', limit: 50)
90
+ ```
59
91
 
60
- # Get only video URLs from search results
61
- videos = PinterestDL.search_videos('tutorial', limit: 3)
62
- # => ["https://v.pinimg.com/videos/...", ...]
92
+ ### Downloading
63
93
 
64
- # Get search results as JSON
65
- json_results = PinterestDL.search_json('anime art', limit: 20)
66
- # => Returns formatted JSON string
94
+ ```ruby
95
+ # Single file
96
+ PinterestDL.download('https://i.pinimg.com/originals/aa/bb/cc/img.jpg', path: 'out/img.jpg')
97
+ # Batch download (returns { succeeded: [...], failed: [{ url:, error: }, ...] })
98
+ urls = PinterestDL.search_images('mountains', limit: 20)
99
+ result = PinterestDL.download_batch(urls, dir: './downloads')
100
+ # With progress reporting
101
+ bar = PinterestDL::ProgressBar.new(total: urls.size)
102
+ PinterestDL.download_batch(urls, dir: './downloads') do |done, total, url|
103
+ bar.update(done)
104
+ end
105
+ ```
106
+
107
+ ### Cookies / authenticated sessions
108
+
109
+ Some boards and pins require a logged-in session to view. Pass your browser's Pinterest cookies through global configuration:
110
+
111
+ ```ruby
112
+ PinterestDL.configure do |c|
113
+ c.cookies = 'sessionid=...; csrftoken=...' # yeah, steal your own fucking session if you have to
114
+ c.user_agent = 'Mozilla/5.0 ...' # optional override
115
+ end
116
+ ```
117
+
118
+ ### Other configuration
119
+
120
+ ```ruby
121
+ PinterestDL.configure do |c|
122
+ c.open_timeout = 10 # seconds, connection open timeout
123
+ c.read_timeout = 20 # seconds, response read timeout
124
+ c.max_retries = 3 # retries on transient network failures
125
+ c.retry_wait = 1.0 # base seconds between retries (backs off linearly)
126
+ c.rate_limit_interval = 0.5 # minimum seconds between outgoing requests
127
+ c.quality = :originals # default image quality for get_image_url
128
+ c.logger.level = Logger::INFO # library uses this Logger instead of puts
129
+ end
130
+ ```
131
+
132
+ ### Error handling
133
+
134
+ Every method raises a subclass of `PinterestDL::Error` instead of returning `nil` or printing to the console:
135
+
136
+ ```ruby
137
+ begin
138
+ PinterestDL.get_image_url('https://www.pinterest.com/pin/does-not-exist/')
139
+ rescue PinterestDL::NotFoundError
140
+ # pin/board/search had nothing to extract
141
+ rescue PinterestDL::RateLimitError => e
142
+ # Pinterest throttled us; e.retry_after may hold a hint
143
+ rescue PinterestDL::AuthenticationError
144
+ # the resource needs c.cookies to be set
145
+ rescue PinterestDL::NetworkError => e
146
+ # DNS/timeout/connection-level failure; e.cause_error has the original exception
147
+ rescue PinterestDL::InvalidURLError
148
+ # the given string isn't a Pinterest pin/board URL
149
+ rescue PinterestDL::Error
150
+ # catch-all for anything above
151
+ end
152
+ ```
153
+
154
+ ### CLI
155
+
156
+ ```bash
157
+ pinterest-dl image https://www.pinterest.com/pin/example/
158
+ pinterest-dl image https://www.pinterest.com/pin/example/ -o pin.jpg
159
+ pinterest-dl video https://www.pinterest.com/pin/example/ -o pin.mp4
160
+ pinterest-dl search "cyberpunk city" -n 20
161
+ pinterest-dl search "cyberpunk city" -n 20 -o ./downloads
162
+ pinterest-dl board https://www.pinterest.com/username/board-name/ -n 50 -o ./downloads
163
+ pinterest-dl --help
164
+ ```
165
+
166
+ Flags: `-o/--output`, `-n/--limit`, `-q/--quality`, `-c/--cookies`, `-v/--verbose`.
167
+
168
+
169
+ ## Development
170
+
171
+ ```bash
172
+ bundle install
173
+ bundle exec rake test # run the test suite
174
+ bundle exec rake rubocop # lint
175
+ bundle exec rake # both (default task)
67
176
  ```
68
177
 
69
178
  ## Links
70
179
 
71
180
  - [RubyGems](https://rubygems.org/gems/pinterest-dl)
72
181
  - [GitHub](https://github.com/monji024/pinterest-dl)
182
+ - [Changelog](CHANGELOG.md)
183
+ - [Issue tracker](https://github.com/monji024/pinterest-dl/issues)
73
184
 
74
185
  ## License
75
186
 
76
- MIT © [monji024](https://github.com/monji024)
187
+ WTFPL © [monji024](https://github.com/monji024)
data/bin/pinterest-dl ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env ruby
2
+ # Monji024
3
+ require_relative '../lib/pinterest-dl'
4
+ require 'optparse'
5
+
6
+ def usage
7
+ <<~USAGE
8
+ pinterest-dl v#{PinterestDL::VERSION}
9
+
10
+ Usage:
11
+ pinterest-dl image URL [-o FILE]
12
+ pinterest-dl video URL [-o FILE]
13
+ pinterest-dl search QUERY [-n LIMIT] [-o DIR]
14
+ pinterest-dl board URL [-n LIMIT] [-o DIR]
15
+
16
+ Options:
17
+ -o, --output PATH File (image/video) or directory (search/board) to save to
18
+ -n, --limit N Max number of results (search/board), default 25
19
+ -q, --quality Q Image quality: originals, 736x, 474x, 236x (default originals)
20
+ -c, --cookies STR Cookie string for authenticated requests
21
+ -v, --verbose Print debug logging
22
+ -h, --help Show this help
23
+ USAGE
24
+ end
25
+
26
+ options = { limit: 25, quality: :originals }
27
+ parser = OptionParser.new do |opts|
28
+ opts.on('-o PATH', '--output PATH') { |v| options[:output] = v }
29
+ opts.on('-n N', '--limit N', Integer) { |v| options[:limit] = v }
30
+ opts.on('-q Q', '--quality Q') { |v| options[:quality] = v.to_sym }
31
+ opts.on('-c STR', '--cookies STR') { |v| options[:cookies] = v }
32
+ opts.on('-v', '--verbose') { options[:verbose] = true }
33
+ opts.on('-h', '--help') do
34
+ puts usage
35
+ exit 0
36
+ end
37
+ end
38
+
39
+ begin
40
+ parser.parse!(ARGV)
41
+ rescue OptionParser::ParseError => e
42
+ warn "pinterest-dl: #{e.message}"
43
+ exit 1
44
+ end
45
+
46
+ command = ARGV.shift
47
+ target = ARGV.shift
48
+
49
+ if command.nil? || target.nil?
50
+ puts usage
51
+ exit(command.nil? ? 0 : 1)
52
+ end
53
+
54
+ PinterestDL.configure do |c|
55
+ c.cookies = options[:cookies] if options[:cookies]
56
+ c.logger.level = Logger::DEBUG if options[:verbose]
57
+ end
58
+
59
+ def die(message)
60
+ warn "pinterest-dl: #{message}"
61
+ exit 1
62
+ end
63
+
64
+ begin
65
+ case command
66
+ when 'image'
67
+ url = PinterestDL.get_image_url(target, quality: options[:quality])
68
+ if options[:output]
69
+ PinterestDL.download(url, path: options[:output])
70
+ puts "Saved to #{options[:output]}"
71
+ else
72
+ puts url
73
+ end
74
+ when 'video'
75
+ url = PinterestDL.get_video_url(target)
76
+ if options[:output]
77
+ PinterestDL.download(url, path: options[:output])
78
+ puts "Saved to #{options[:output]}"
79
+ else
80
+ puts url
81
+ end
82
+ when 'search'
83
+ pins = PinterestDL.search(target, limit: options[:limit])
84
+ if options[:output]
85
+ urls = pins.filter_map { |p| p[:image_url] }
86
+ bar = PinterestDL::ProgressBar.new(total: urls.size)
87
+ result = PinterestDL.download_batch(urls, dir: options[:output]) { |done, total, _| bar.update(done) }
88
+ puts "Downloaded #{result[:succeeded].size}/#{urls.size} images to #{options[:output]}"
89
+ else
90
+ puts JSON.pretty_generate(pins)
91
+ end
92
+ when 'board'
93
+ pins = PinterestDL.board(target, limit: options[:limit])
94
+ if options[:output]
95
+ urls = pins.filter_map { |p| p[:image_url] }
96
+ bar = PinterestDL::ProgressBar.new(total: urls.size)
97
+ result = PinterestDL.download_batch(urls, dir: options[:output]) { |done, total, _| bar.update(done) }
98
+ puts "Downloaded #{result[:succeeded].size}/#{urls.size} images to #{options[:output]}"
99
+ else
100
+ puts JSON.pretty_generate(pins)
101
+ end
102
+ else
103
+ die "unknown command '#{command}'\n\n#{usage}"
104
+ end
105
+ rescue PinterestDL::Error => e
106
+ die e.message
107
+ end
data/lib/pinterest-dl.rb CHANGED
@@ -1,193 +1,17 @@
1
- #!/usr/bin/env ruby
2
- # Pinterest downloader
3
- # creator: Monji
4
- # ver: 1.0.3
5
- # github: https://github.com/monji024/pinterest-dl
6
-
7
- require 'net/http'
8
- require 'uri'
9
- require 'json'
10
- require 'openssl'
11
-
1
+ # frozen_string_literal: true
2
+
3
+ # Monji024
4
+ require_relative 'pinterest_dl/version'
5
+ require_relative 'pinterest_dl/errors'
6
+ require_relative 'pinterest_dl/configuration'
7
+ require_relative 'pinterest_dl/client'
8
+ require_relative 'pinterest_dl/extractors/image'
9
+ require_relative 'pinterest_dl/extractors/video'
10
+ require_relative 'pinterest_dl/search'
11
+ require_relative 'pinterest_dl/board'
12
+ require_relative 'pinterest_dl/downloader'
13
+ require_relative 'pinterest_dl/progress_bar'
14
+ require_relative 'pinterest_dl/api'
12
15
 
13
16
  module PinterestDL
14
- ver = '1.0.3'.freeze
15
- github = 'https://github.com/monji024/pinterest-dl'.freeze
16
-
17
- puts "pinterest-dl v#{ver} (#{github})\n\n"
18
-
19
- USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'.freeze
20
-
21
- class << self
22
- def get_image_url(pin_url)
23
- html = fetch_html(pin_url)
24
- return nil unless html
25
-
26
- # 1 -> json-ld
27
- url = extract_json_ld(html, 'ImageObject')
28
- return url if url
29
- # 2 -> open graph
30
- match = html.match(%r{<meta[^>]*property="og:image"[^>]*content="([^"]+)"})
31
- if match
32
- url = match[1]
33
- puts "image url: #{url}"
34
- return url
35
- end
36
- # 3 -> direct i.pinimg.com patterns
37
- match = html.match(%r{https://i\.pinimg\.com/(?:originals|474x|236x|736x)/[^"\s]+})
38
- if match
39
- url = match[0]
40
- reset = "\e[0m"
41
- green = "\e[32m"
42
- puts "#{green}image url :#{reset} #{url}"
43
- return url
44
- end
45
- puts 'No found:)'
46
- nil
47
- end
48
-
49
-
50
- def get_video_url(pin_url)
51
- html = fetch_html(pin_url)
52
- return nil unless html
53
-
54
- # 1 -> json-ld
55
- url = extract_json_ld(html, 'VideoObject')
56
- if url
57
- url = url.gsub('\\u002F', '/').gsub('\\', '')
58
- puts "ok : #{url}"
59
- return url
60
- end
61
- # 2 -> Open Graph video
62
- match = html.match(%r{<meta[^>]*property="og:video"[^>]*content="([^"]+)"})
63
- if match
64
- url = match[1].gsub('\\u002F', '/').gsub('\\', '')
65
- puts "video url: #{url}"
66
- return url
67
- end
68
- # 3 -> contentUrl in page source
69
- match = html.match(/"contentUrl"\s*:\s*"([^"]+\.mp4[^"]*)"/)
70
- if match
71
- url = match[1].gsub('\\u002F', '/').gsub('\\', '')
72
- green = "\e[32m"
73
- reset = "\e[0m"
74
- puts "#{green}video url :#{reset} #{url}"
75
- return url
76
- end
77
-
78
- puts 'No found'
79
- nil
80
- end
81
- def search(query, limit: 25)
82
- source_path = "/search/pins/?q=#{URI.encode_www_form_component(query)}"
83
- home_uri = URI("https://www.pinterest.com#{source_path}")
84
- http = Net::HTTP.new(home_uri.host, home_uri.port)
85
- http.use_ssl = true
86
- http.open_timeout = 10
87
- http.read_timeout = 20
88
- home_req = Net::HTTP::Get.new(home_uri, 'User-Agent' => USER_AGENT)
89
- home_resp = http.request(home_req)
90
- cookies = (home_resp.get_fields('set-cookie') || []).map { |c| c.split(';').first }
91
- cookie_header = cookies.join('; ')
92
- csrf = cookies.find { |c| c.start_with?('csrftoken=') }&.split('=', 2)&.last || 'undefined'
93
- data = {
94
- options: { query: query, scope: 'pins', page_size: limit },
95
- context: {}}.to_json
96
- res_uri = URI('https://www.pinterest.com/resource/BaseSearchResource/get/')
97
- res_uri.query = URI.encode_www_form('source_url' => source_path, 'data' => data)
98
- req = Net::HTTP::Get.new(res_uri)
99
- req['User-Agent'] = USER_AGENT
100
- req['Accept'] = 'application/json, text/javascript, */*, q=0.01'
101
- req['X-Requested-With'] = 'XMLHttpRequest'
102
- req['X-Pinterest-PWS-Handler'] = 'www/search/[scope].js'
103
- req['X-CSRFToken'] = csrf
104
- req['Referer'] = home_uri.to_s
105
- req['Cookie'] = cookie_header
106
- resp = http.request(req)
107
- unless resp.is_a?(Net::HTTPSuccess)
108
- puts "\e[31msearch http err!:\e[0m #{resp.code}"
109
- return []
110
- end
111
- json = begin
112
- JSON.parse(resp.body)
113
- rescue JSON::ParserError
114
- nil
115
- end
116
- return [] unless json
117
- raw_results = (json.dig('resource_response', 'data', 'results') || []).first(limit)
118
- pins = raw_results.filter_map do |r|
119
- id = r['id']
120
- next nil unless id
121
- orig_image = r.dig('images', 'orig', 'url')
122
- fallback_image = r['images']&.values&.map { |v| v.is_a?(Hash) ? v['url'] : nil }&.compact&.last
123
- image_url = orig_image || fallback_image
124
- video_url = r.dig('videos', 'video_list')&.values&.first&.dig('url')
125
- {
126
- id: id,
127
- pin_url: "https://www.pinterest.com/pin/#{id}/",
128
- title: r['title'] || r['grid_title'],
129
- description: r['description'],
130
- image_url: image_url,
131
- video_url: video_url
132
- }
133
- end
134
- green = "\e[32m"
135
- reset = "\e[0m"
136
- puts "#{green}found #{pins.size} pin(s) for \"#{query}\"#{reset}"
137
- pins
138
- rescue StandardError => e
139
- puts "\e[31msearch err!:\e[0m #{e.message}"
140
- []
141
- end
142
- def search_json(query, limit: 25)
143
- JSON.pretty_generate(search(query, limit: limit))
144
- end
145
- def search_images(query, limit: 25)
146
- search(query, limit: limit).filter_map { |p| p[:image_url] }
147
- end
148
- def search_videos(query, limit: 25)
149
- search(query, limit: limit).filter_map { |p| p[:video_url] }
150
- end
151
- private
152
-
153
- def fetch_html(url)
154
- uri = URI(url)
155
- http = Net::HTTP.new(uri.host, uri.port)
156
- http.use_ssl = (uri.scheme == 'https')
157
- http.max_retries = 2
158
- http.open_timeout = 10
159
- http.read_timeout = 20
160
- resp = http.request(Net::HTTP::Head.new(uri, 'User-Agent' => USER_AGENT))
161
- 5.times do
162
- break unless resp.is_a?(Net::HTTPRedirection)
163
- location = resp['location']
164
- location = URI.join(uri.to_s, location) if location.start_with?('/')
165
- uri = URI(location)
166
- http = Net::HTTP.new(uri.host, uri.port)
167
- http.use_ssl = (uri.scheme == 'https')
168
- resp = http.request(Net::HTTP::Head.new(uri, 'User-Agent' => USER_AGENT))
169
- end
170
- # get final page
171
- req = Net::HTTP::Get.new(uri, 'User-Agent' => USER_AGENT)
172
- resp = http.request(req)
173
- return resp.body if resp.is_a?(Net::HTTPSuccess)
174
- nil
175
- rescue StandardError => e
176
- red = "\e[31m"
177
- reset = "\e[0m"
178
- puts "#{red}http err!:#{reset} #{e.message}"
179
- nil
180
- end
181
- def extract_json_ld(html, type)
182
- match = html.match(%r{<script[^>]*type="application/ld\+json"[^>]*>(.*?)</script>}m)
183
- return nil unless match
184
- begin
185
- data = JSON.parse(match[1])
186
- data = data[0] if data.is_a?(Array) && !data.empty?
187
- return data['contentUrl'] if data['@type'] == type && data['contentUrl']
188
- rescue JSON::ParserError
189
- end
190
- nil
191
- end
192
- end
193
- end
17
+ end