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 +4 -4
- data/CHANGELOG.md +40 -0
- data/LICENSE +13 -0
- data/README.md +142 -31
- data/bin/pinterest-dl +107 -0
- data/lib/pinterest-dl.rb +15 -191
- data/lib/pinterest_dl/api.rb +84 -0
- data/lib/pinterest_dl/board.rb +101 -0
- data/lib/pinterest_dl/client.rb +111 -0
- data/lib/pinterest_dl/configuration.rb +46 -0
- data/lib/pinterest_dl/downloader.rb +57 -0
- data/lib/pinterest_dl/errors.rb +49 -0
- data/lib/pinterest_dl/extractors/image.rb +47 -0
- data/lib/pinterest_dl/extractors/video.rb +61 -0
- data/lib/pinterest_dl/progress_bar.rb +21 -0
- data/lib/pinterest_dl/search.rb +144 -0
- data/lib/pinterest_dl/version.rb +6 -0
- metadata +31 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b4d94f0a5bbac63a983308d541b5e50a6a13c1d5bee422c356e56395c686ebcd
|
|
4
|
+
data.tar.gz: 148473c309ec313a3e0d7ddb0864f0388d22a2bf9edee39e819e54ccaab10745
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
[](https://rubygems.org/gems/pinterest-dl)
|
|
6
|
+
[](https://rubygems.org/gems/pinterest-dl)
|
|
7
|
+
[](https://github.com/monji024/pinterest-dl/actions/workflows/ci.yml)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](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
|
-
|
|
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
|
-
|
|
19
|
+
## What it does
|
|
20
20
|
|
|
21
|
-
|
|
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
|
-
|
|
35
|
+
Or add it to your Gemfile:
|
|
30
36
|
|
|
31
37
|
```ruby
|
|
32
|
-
|
|
38
|
+
gem 'pinterest-dl'
|
|
33
39
|
```
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
## Quick start
|
|
36
42
|
|
|
37
43
|
```ruby
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
54
|
-
# id, pin_url, title, description, image_url, video_url
|
|
83
|
+
### Boards & sections
|
|
55
84
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
videos = PinterestDL.search_videos('tutorial', limit: 3)
|
|
62
|
-
# => ["https://v.pinimg.com/videos/...", ...]
|
|
92
|
+
### Downloading
|
|
63
93
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|