pinterest-dl 2.0.5 → 2.1.5
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 +10 -2
- data/bin/pinterest-dl +0 -2
- data/lib/pinterest_dl/api.rb +44 -9
- data/lib/pinterest_dl/board.rb +28 -3
- data/lib/pinterest_dl/extractors/pin_data.rb +23 -6
- data/lib/pinterest_dl/search.rb +0 -3
- data/lib/pinterest_dl/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51b7122dcca9ae653e0f63a70665fa8b5dfddd87a9d7e17eeef3462843ed90c1
|
|
4
|
+
data.tar.gz: e82c0d139c5bf84dfd5e13fa96488d8013636f72ce7e2bdc5bc9a8efc49151cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 911490799bd9db4405781306091bbe06271413e25b834190d46394ad7b288be5ce3153ba25d9261122dbb4efe0be08188c49583751999d08e87be223a017300e
|
|
7
|
+
data.tar.gz: f18ffdcc24defdbf6bb392f738ed6b6dc62047ccecacfb90eb90fb52a879b18dd84c7677288d538f8105ecae7e611faec6470a0dfeaedb07f8767d9e822c0e20
|
data/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
-
## [2.
|
|
6
|
+
## [2.1.5]
|
|
7
7
|
|
|
8
8
|
### Fixed
|
|
9
9
|
- Pin URLs on country domains (e.g. `de.pinterest.com/pin/...`) didn't download
|
|
@@ -17,6 +17,14 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
17
17
|
- Video pins returned "No video found". The best `.mp4` is picked now (not just
|
|
18
18
|
the first format, which could be an `.m3u8`), story pins are supported, and
|
|
19
19
|
`og:video` meta tags are read in any attribute order.
|
|
20
|
+
- `video` worked only some of the time: Pinterest sometimes serves a lighter
|
|
21
|
+
page without the video data. Now every `__PWS_*__` data block of the page is
|
|
22
|
+
read, then the pin is asked from the `PinResource` API (with the
|
|
23
|
+
`X-Pinterest-PWS-Handler` header Pinterest needs, otherwise it answers 403),
|
|
24
|
+
and the page is fetched fresh up to 3 times before giving up. The "this is an
|
|
25
|
+
image" hint only shows when Pinterest really returned an image pin.
|
|
26
|
+
- `board` reads the board id from the page first and only asks the API when it
|
|
27
|
+
isn't there. Story modules ("Related interests") are no longer treated as pins.
|
|
20
28
|
- `board` never worked: Pinterest needs the real board id, we sent an empty
|
|
21
29
|
one. The id is now looked up first, the session is created once instead of
|
|
22
30
|
once per page, links with `?invite_code=...` are parsed correctly, and
|
|
@@ -89,4 +97,4 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
89
97
|
## [1.0.3]
|
|
90
98
|
|
|
91
99
|
- Initial public release: `get_image_url`, `get_video_url`, `search`,
|
|
92
|
-
`search_json`, `search_images`, `search_videos`.
|
|
100
|
+
`search_json`, `search_images`, `search_videos`.
|
data/bin/pinterest-dl
CHANGED
|
@@ -19,7 +19,6 @@ def usage
|
|
|
19
19
|
-t, --type TYPE What to collect in search/board: all, image, video, pin (default all)
|
|
20
20
|
-q, --quality Q Image quality: originals, 736x, 474x, 236x (default originals)
|
|
21
21
|
-c, --cookies STR Cookie string for authenticated requests
|
|
22
|
-
-v, --verbose Print debug logging
|
|
23
22
|
-h, --help Show this help
|
|
24
23
|
USAGE
|
|
25
24
|
end
|
|
@@ -31,7 +30,6 @@ parser = OptionParser.new do |opts|
|
|
|
31
30
|
opts.on('-t T', '--type T') { |v| options[:type] = v.to_sym }
|
|
32
31
|
opts.on('-q Q', '--quality Q') { |v| options[:quality] = v.to_sym }
|
|
33
32
|
opts.on('-c STR', '--cookies STR') { |v| options[:cookies] = v }
|
|
34
|
-
opts.on('-v', '--verbose') { options[:verbose] = true }
|
|
35
33
|
opts.on('-h', '--help') do
|
|
36
34
|
puts usage
|
|
37
35
|
exit 0
|
data/lib/pinterest_dl/api.rb
CHANGED
|
@@ -21,20 +21,25 @@ module PinterestDL
|
|
|
21
21
|
def get_image_url(pin_url, quality: config.quality, client: Client.new(config: config))
|
|
22
22
|
html, pin_id = load_pin(pin_url, client)
|
|
23
23
|
url = extract_pin_image(html, pin_id, quality: quality)
|
|
24
|
+
url ||= api_image_url(pin_id, client, quality)
|
|
24
25
|
raise NotFoundError, "No image found at #{pin_url}" unless url
|
|
25
26
|
|
|
26
27
|
url
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def get_video_url(pin_url, client: Client.new(config: config))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
pin = nil
|
|
32
|
+
3.times do |attempt|
|
|
33
|
+
html, pin_id = load_pin(pin_url, client, fresh: attempt.positive?)
|
|
34
|
+
url = extract_pin_video(html, pin_id)
|
|
35
|
+
return url if url
|
|
36
|
+
|
|
37
|
+
pin = pin_from_api(pin_id, client)
|
|
38
|
+
return pin[:video_url] if pin && pin[:video_url]
|
|
35
39
|
end
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
hint = pin && pin[:image_url] ? ' (it looks like an image pin, try the image command)' : ''
|
|
42
|
+
raise NotFoundError, "No video found at #{pin_url}#{hint}"
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
def get_media(pin_url, quality: config.quality, client: Client.new(config: config))
|
|
@@ -79,10 +84,40 @@ module PinterestDL
|
|
|
79
84
|
|
|
80
85
|
private
|
|
81
86
|
|
|
82
|
-
def load_pin(pin_url, client)
|
|
87
|
+
def load_pin(pin_url, client, fresh: false)
|
|
83
88
|
validate_pin_url!(pin_url)
|
|
84
|
-
html = client.get(normalize_pin_url(pin_url))
|
|
85
|
-
|
|
89
|
+
html = client.get(normalize_pin_url(pin_url), use_cache: !fresh)
|
|
90
|
+
id = pin_id_from(client.last_url) || pin_id_from(pin_url) || html.to_s[%r{pinterest\.[a-z.]+/pin/(\d+)}, 1]
|
|
91
|
+
[html, id]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def pin_from_api(pin_id, client)
|
|
95
|
+
return nil unless pin_id
|
|
96
|
+
|
|
97
|
+
data = { options: { id: pin_id, field_set_key: 'detailed' }, context: {} }.to_json
|
|
98
|
+
uri = URI('https://www.pinterest.com/resource/PinResource/get/')
|
|
99
|
+
uri.query = URI.encode_www_form('source_url' => "/pin/#{pin_id}/", 'data' => data)
|
|
100
|
+
headers = {
|
|
101
|
+
'Accept' => 'application/json, text/javascript, */*, q=0.01',
|
|
102
|
+
'X-Requested-With' => 'XMLHttpRequest',
|
|
103
|
+
'X-Pinterest-PWS-Handler' => 'www/pin/[id].js',
|
|
104
|
+
'X-Pinterest-Source-Url' => "/pin/#{pin_id}/",
|
|
105
|
+
'X-CSRFToken' => client.cookie('csrftoken') || 'undefined',
|
|
106
|
+
'Referer' => "https://www.pinterest.com/pin/#{pin_id}/"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pin = JSON.parse(client.get(uri.to_s, headers: headers, use_cache: false)).dig('resource_response', 'data')
|
|
110
|
+
return nil unless pin.is_a?(Hash)
|
|
111
|
+
|
|
112
|
+
{ image_url: pin.dig('images', 'orig', 'url'), video_url: Extractors::PinData.first_video_url(pin) }
|
|
113
|
+
rescue PinterestDL::Error, JSON::ParserError => e
|
|
114
|
+
logger.debug("PinResource failed for pin #{pin_id}: #{e.message}")
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def api_image_url(pin_id, client, quality)
|
|
119
|
+
url = pin_from_api(pin_id, client)&.dig(:image_url)
|
|
120
|
+
url && Extractors::Image.apply_quality(url, quality)
|
|
86
121
|
end
|
|
87
122
|
|
|
88
123
|
def normalize_pin_url(pin_url)
|
data/lib/pinterest_dl/board.rb
CHANGED
|
@@ -14,8 +14,8 @@ module PinterestDL
|
|
|
14
14
|
|
|
15
15
|
def call(board_url, limit: 50, pages: 10, type: :all)
|
|
16
16
|
username, slug, section = parse_url(board_url)
|
|
17
|
-
start_session(username, slug)
|
|
18
|
-
board_id = find_board_id(username, slug)
|
|
17
|
+
html = start_session(username, slug)
|
|
18
|
+
board_id = board_id_from_page(html, slug) || find_board_id(username, slug)
|
|
19
19
|
resource, options = feed_request(board_id, section)
|
|
20
20
|
|
|
21
21
|
results = []
|
|
@@ -57,10 +57,34 @@ module PinterestDL
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def start_session(username, slug)
|
|
60
|
-
@client.raw_get("https://www.pinterest.com/#{username}/#{slug}/")
|
|
60
|
+
response = @client.raw_get("https://www.pinterest.com/#{username}/#{slug}/")
|
|
61
61
|
@page_url = @client.last_url
|
|
62
62
|
@host = URI(@page_url).host
|
|
63
63
|
@source_path = URI(@page_url).path
|
|
64
|
+
response.body
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def board_id_from_page(html, slug)
|
|
68
|
+
html.to_s.scan(PinterestDL::Extractors::PinData::SCRIPT_PATTERN).each do |(raw)|
|
|
69
|
+
data = PinterestDL::Extractors::PinData.safe_parse(raw)
|
|
70
|
+
stack = [data]
|
|
71
|
+
until stack.empty?
|
|
72
|
+
node = stack.pop
|
|
73
|
+
case node
|
|
74
|
+
when Hash
|
|
75
|
+
return node['id'].to_s if board_node?(node, slug)
|
|
76
|
+
|
|
77
|
+
stack.concat(node.values)
|
|
78
|
+
when Array
|
|
79
|
+
stack.concat(node)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def board_node?(node, slug)
|
|
87
|
+
node['type'] == 'board' && node['id'].to_s.match?(/\A\d+\z/) && node['url'].to_s.include?("/#{slug}/")
|
|
64
88
|
end
|
|
65
89
|
|
|
66
90
|
def find_board_id(username, slug)
|
|
@@ -130,6 +154,7 @@ module PinterestDL
|
|
|
130
154
|
|
|
131
155
|
def build_pin(raw)
|
|
132
156
|
return nil unless raw.is_a?(Hash)
|
|
157
|
+
return nil if raw['type'] && raw['type'] != 'pin'
|
|
133
158
|
|
|
134
159
|
id = raw['id']
|
|
135
160
|
return nil unless id
|
|
@@ -5,17 +5,26 @@ require 'json'
|
|
|
5
5
|
module PinterestDL
|
|
6
6
|
module Extractors
|
|
7
7
|
module PinData
|
|
8
|
-
SCRIPT_PATTERN = %r{<script[^>]*id=["']
|
|
8
|
+
SCRIPT_PATTERN = %r{<script[^>]*id=["']__PWS_[A-Z_]+__["'][^>]*>(.*?)</script>}m.freeze
|
|
9
9
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
12
|
def extract(html, pin_id = nil)
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
found = nil
|
|
14
|
+
html.to_s.scan(SCRIPT_PATTERN).each do |(raw)|
|
|
15
|
+
data = safe_parse(raw)
|
|
16
|
+
pin = data && find_pin_node(data, pin_id)
|
|
17
|
+
next unless pin
|
|
18
|
+
|
|
19
|
+
result = build_result(pin)
|
|
20
|
+
return result if result[:video_url]
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
found ||= result
|
|
23
|
+
end
|
|
24
|
+
found
|
|
25
|
+
end
|
|
18
26
|
|
|
27
|
+
def build_result(pin)
|
|
19
28
|
{
|
|
20
29
|
image_url: pin.dig('images', 'orig', 'url'),
|
|
21
30
|
video_url: first_video_url(pin),
|
|
@@ -76,7 +85,15 @@ module PinterestDL
|
|
|
76
85
|
return best['url'] if best
|
|
77
86
|
|
|
78
87
|
hls = formats.find { |v| v['url'].include?('.m3u8') }
|
|
79
|
-
hls && hls['url']
|
|
88
|
+
hls && hls_to_mp4(hls['url'])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def hls_to_mp4(url)
|
|
92
|
+
if url.include?('/iht/hls/')
|
|
93
|
+
url.sub('/hls/', '/expMp4/').sub(/\.m3u8.*\z/, '_720w.mp4')
|
|
94
|
+
else
|
|
95
|
+
url.sub('/hls/', '/720p/').sub(/\.m3u8.*\z/, '.mp4')
|
|
96
|
+
end
|
|
80
97
|
end
|
|
81
98
|
|
|
82
99
|
def deep_find(node, key)
|
data/lib/pinterest_dl/search.rb
CHANGED
|
@@ -49,9 +49,6 @@ module PinterestDL
|
|
|
49
49
|
|
|
50
50
|
parse_search_response(json)
|
|
51
51
|
end
|
|
52
|
-
|
|
53
|
-
# Pinterest's search resource requires a session cookie + CSRF token
|
|
54
|
-
# obtained by first loading the human-facing search page.
|
|
55
52
|
def establish_session(query)
|
|
56
53
|
source_path = "/search/pins/?q=#{URI.encode_www_form_component(query)}"
|
|
57
54
|
home_uri = URI("https://www.pinterest.com#{source_path}")
|
data/lib/pinterest_dl/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pinterest-dl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Monji
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A Ruby toolkit for getting direct image and video URLs from Pinterest
|
|
14
14
|
pins, searching pins, fetching board contents, and downloading media to disk — as
|