2016-09-02 19:11:16 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2020-06-16 14:41:05 +02:00
|
|
|
# Copyright 2016-2020 Mike Fährmann
|
2016-09-02 19:11:16 +02:00
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License version 2 as
|
|
|
|
# published by the Free Software Foundation.
|
|
|
|
|
2020-06-16 14:41:05 +02:00
|
|
|
"""Extractors for https://www.pinterest.com/"""
|
2016-09-02 19:11:16 +02:00
|
|
|
|
|
|
|
from .common import Extractor, Message
|
2018-04-24 22:17:25 +02:00
|
|
|
from .. import text, exception
|
2020-06-16 14:41:05 +02:00
|
|
|
import itertools
|
2018-04-24 22:17:25 +02:00
|
|
|
import json
|
2016-09-02 19:11:16 +02:00
|
|
|
|
2017-02-01 00:53:19 +01:00
|
|
|
|
2018-08-15 21:28:27 +02:00
|
|
|
BASE_PATTERN = r"(?:https?://)?(?:\w+\.)?pinterest\.\w+"
|
|
|
|
|
|
|
|
|
2016-09-03 10:00:32 +02:00
|
|
|
class PinterestExtractor(Extractor):
|
|
|
|
"""Base class for pinterest extractors"""
|
2016-09-02 19:11:16 +02:00
|
|
|
category = "pinterest"
|
2018-04-24 22:17:25 +02:00
|
|
|
filename_fmt = "{category}_{id}.{extension}"
|
|
|
|
archive_fmt = "{id}"
|
2016-09-02 19:11:16 +02:00
|
|
|
|
2019-02-11 13:31:10 +01:00
|
|
|
def __init__(self, match):
|
|
|
|
Extractor.__init__(self, match)
|
2017-09-09 17:31:42 +02:00
|
|
|
self.api = PinterestAPI(self)
|
2016-09-02 19:11:16 +02:00
|
|
|
|
2018-08-15 21:28:27 +02:00
|
|
|
def items(self):
|
|
|
|
data = self.metadata()
|
|
|
|
yield Message.Version, 1
|
|
|
|
yield Message.Directory, data
|
|
|
|
|
|
|
|
for pin in self.pins():
|
2018-11-14 11:34:24 +01:00
|
|
|
if "images" in pin:
|
|
|
|
url, pin_data = self.data_from_pin(pin)
|
|
|
|
pin_data.update(data)
|
|
|
|
yield Message.Url, url, pin_data
|
2018-08-15 21:28:27 +02:00
|
|
|
|
2018-08-16 18:11:39 +02:00
|
|
|
def metadata(self):
|
|
|
|
"""Return general metadata"""
|
|
|
|
|
|
|
|
def pins(self):
|
|
|
|
"""Return all relevant pin-objects"""
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def data_from_pin(pin):
|
2016-09-03 10:00:32 +02:00
|
|
|
"""Get image url and metadata from a pin-object"""
|
2018-04-24 22:17:25 +02:00
|
|
|
img = pin["images"]["orig"]
|
2016-09-03 10:00:32 +02:00
|
|
|
url = img["url"]
|
2018-04-24 22:17:25 +02:00
|
|
|
pin["width"] = img["width"]
|
|
|
|
pin["height"] = img["height"]
|
|
|
|
return url, text.nameext_from_url(url, pin)
|
2016-09-03 10:00:32 +02:00
|
|
|
|
|
|
|
|
|
|
|
class PinterestPinExtractor(PinterestExtractor):
|
2016-09-12 10:20:57 +02:00
|
|
|
"""Extractor for images from a single pin from pinterest.com"""
|
2016-09-03 10:00:32 +02:00
|
|
|
subcategory = "pin"
|
2019-02-08 13:45:40 +01:00
|
|
|
pattern = BASE_PATTERN + r"/pin/([^/?#&]+)(?!.*#related$)"
|
|
|
|
test = (
|
2017-01-03 12:12:36 +01:00
|
|
|
("https://www.pinterest.com/pin/858146903966145189/", {
|
2018-03-06 14:28:21 +01:00
|
|
|
"url": "afb3c26719e3a530bb0e871c480882a801a4e8a5",
|
2020-01-18 21:06:44 +01:00
|
|
|
"content": ("4c435a66f6bb82bb681db2ecc888f76cf6c5f9ca",
|
|
|
|
"d3e24bc9f7af585e8c23b9136956bd45a4d9b947"),
|
2017-01-03 12:12:36 +01:00
|
|
|
}),
|
|
|
|
("https://www.pinterest.com/pin/858146903966145188/", {
|
2018-04-17 17:12:42 +02:00
|
|
|
"exception": exception.NotFoundError,
|
|
|
|
}),
|
2019-02-08 13:45:40 +01:00
|
|
|
)
|
2016-09-03 10:00:32 +02:00
|
|
|
|
|
|
|
def __init__(self, match):
|
2019-02-11 13:31:10 +01:00
|
|
|
PinterestExtractor.__init__(self, match)
|
2016-09-03 10:00:32 +02:00
|
|
|
self.pin_id = match.group(1)
|
2018-08-15 21:28:27 +02:00
|
|
|
self.pin = None
|
2016-09-03 10:00:32 +02:00
|
|
|
|
2018-08-15 21:28:27 +02:00
|
|
|
def metadata(self):
|
|
|
|
self.pin = self.api.pin(self.pin_id)
|
|
|
|
return self.data_from_pin(self.pin)[1]
|
|
|
|
|
|
|
|
def pins(self):
|
|
|
|
return (self.pin,)
|
2016-09-03 10:00:32 +02:00
|
|
|
|
|
|
|
|
|
|
|
class PinterestBoardExtractor(PinterestExtractor):
|
2016-09-12 10:20:57 +02:00
|
|
|
"""Extractor for images from a board from pinterest.com"""
|
2016-09-03 10:00:32 +02:00
|
|
|
subcategory = "board"
|
2019-02-08 13:45:40 +01:00
|
|
|
directory_fmt = ("{category}", "{board[owner][username]}", "{board[name]}")
|
2018-04-24 22:17:25 +02:00
|
|
|
archive_fmt = "{board[id]}_{id}"
|
2019-02-08 13:45:40 +01:00
|
|
|
pattern = BASE_PATTERN + r"/(?!pin/)([^/?#&]+)/([^/?#&]+)(?!.*#related$)"
|
|
|
|
test = (
|
2017-01-03 12:12:36 +01:00
|
|
|
("https://www.pinterest.com/g1952849/test-/", {
|
2018-12-02 19:37:50 +01:00
|
|
|
"pattern": r"https://i\.pinimg\.com/originals/",
|
|
|
|
"count": 2,
|
2017-01-03 12:12:36 +01:00
|
|
|
}),
|
2020-06-16 14:41:05 +02:00
|
|
|
# board with sections (#835)
|
|
|
|
("https://www.pinterest.de/g1952849/stuff/", {
|
|
|
|
"options": (("sections", True),),
|
|
|
|
"count": 5,
|
|
|
|
}),
|
2017-01-03 12:12:36 +01:00
|
|
|
("https://www.pinterest.com/g1952848/test/", {
|
2018-05-11 17:28:34 +02:00
|
|
|
"exception": exception.GalleryDLException,
|
2017-01-03 12:12:36 +01:00
|
|
|
}),
|
2019-02-08 13:45:40 +01:00
|
|
|
)
|
2016-09-03 10:00:32 +02:00
|
|
|
|
|
|
|
def __init__(self, match):
|
2019-02-11 13:31:10 +01:00
|
|
|
PinterestExtractor.__init__(self, match)
|
2018-04-26 16:36:42 +02:00
|
|
|
self.user = text.unquote(match.group(1))
|
2020-06-16 14:41:05 +02:00
|
|
|
self.board_name = text.unquote(match.group(2))
|
|
|
|
self.board = None
|
2016-09-03 10:00:32 +02:00
|
|
|
|
2018-08-15 21:28:27 +02:00
|
|
|
def metadata(self):
|
2020-06-16 14:41:05 +02:00
|
|
|
self.board = self.api.board(self.user, self.board_name)
|
|
|
|
return {"board": self.board}
|
2018-08-15 21:28:27 +02:00
|
|
|
|
|
|
|
def pins(self):
|
2020-06-16 14:41:05 +02:00
|
|
|
board = self.board
|
|
|
|
|
|
|
|
if board["section_count"] and self.config("sections", True):
|
|
|
|
pins = [self.api.board_pins(board["id"])]
|
|
|
|
for section in self.api.board_sections(board["id"]):
|
|
|
|
pins.append(self.api.board_section_pins(section["id"]))
|
|
|
|
return itertools.chain.from_iterable(pins)
|
|
|
|
else:
|
|
|
|
return self.api.board_pins(board["id"])
|
2018-08-15 21:28:27 +02:00
|
|
|
|
|
|
|
|
|
|
|
class PinterestRelatedPinExtractor(PinterestPinExtractor):
|
|
|
|
"""Extractor for related pins of another pin from pinterest.com"""
|
|
|
|
subcategory = "related-pin"
|
2019-02-08 13:45:40 +01:00
|
|
|
directory_fmt = ("{category}", "related {original_pin[id]}")
|
|
|
|
pattern = BASE_PATTERN + r"/pin/([^/?#&]+).*#related$"
|
|
|
|
test = ("https://www.pinterest.com/pin/858146903966145189/#related", {
|
2019-11-10 17:03:38 +01:00
|
|
|
"range": "31-70",
|
|
|
|
"count": 40,
|
|
|
|
"archive": False,
|
2019-02-08 13:45:40 +01:00
|
|
|
})
|
2018-08-15 21:28:27 +02:00
|
|
|
|
|
|
|
def metadata(self):
|
|
|
|
pin = self.api.pin(self.pin_id)
|
|
|
|
return {"original_pin": self.data_from_pin(pin)[1]}
|
|
|
|
|
|
|
|
def pins(self):
|
|
|
|
return self.api.pin_related(self.pin_id)
|
|
|
|
|
|
|
|
|
|
|
|
class PinterestRelatedBoardExtractor(PinterestBoardExtractor):
|
|
|
|
"""Extractor for related pins of a board from pinterest.com"""
|
|
|
|
subcategory = "related-board"
|
2019-02-08 13:45:40 +01:00
|
|
|
directory_fmt = ("{category}", "{board[owner][username]}",
|
|
|
|
"{board[name]}", "related")
|
|
|
|
pattern = BASE_PATTERN + r"/(?!pin/)([^/?#&]+)/([^/?#&]+).*#related$"
|
|
|
|
test = ("https://www.pinterest.com/g1952849/test-/#related", {
|
2019-11-10 17:03:38 +01:00
|
|
|
"range": "31-70",
|
|
|
|
"count": 40,
|
|
|
|
"archive": False,
|
2019-02-08 13:45:40 +01:00
|
|
|
})
|
2018-08-15 21:28:27 +02:00
|
|
|
|
|
|
|
def pins(self):
|
2020-06-16 14:41:05 +02:00
|
|
|
return self.api.board_related(self.board["id"])
|
2016-09-03 10:00:32 +02:00
|
|
|
|
2016-09-02 19:11:16 +02:00
|
|
|
|
2017-01-27 21:51:28 +01:00
|
|
|
class PinterestPinitExtractor(PinterestExtractor):
|
2017-05-08 15:08:39 +02:00
|
|
|
"""Extractor for images from a pin.it URL"""
|
2017-01-27 21:51:28 +01:00
|
|
|
subcategory = "pinit"
|
2019-02-08 13:45:40 +01:00
|
|
|
pattern = r"(?:https?://)?pin\.it/([^/?#&]+)"
|
2018-12-02 19:37:50 +01:00
|
|
|
|
2019-02-08 13:45:40 +01:00
|
|
|
test = (
|
2017-01-27 21:51:28 +01:00
|
|
|
("https://pin.it/Hvt8hgT", {
|
|
|
|
"url": "8daad8558382c68f0868bdbd17d05205184632fa",
|
|
|
|
}),
|
|
|
|
("https://pin.it/Hvt8hgS", {
|
|
|
|
"exception": exception.NotFoundError,
|
2017-05-08 15:08:39 +02:00
|
|
|
}),
|
2019-02-08 13:45:40 +01:00
|
|
|
)
|
2017-01-27 21:51:28 +01:00
|
|
|
|
|
|
|
def __init__(self, match):
|
2019-02-11 13:31:10 +01:00
|
|
|
PinterestExtractor.__init__(self, match)
|
2018-12-02 19:37:50 +01:00
|
|
|
self.shortened_id = match.group(1)
|
2017-01-27 21:51:28 +01:00
|
|
|
|
|
|
|
def items(self):
|
2018-12-02 19:37:50 +01:00
|
|
|
url = "https://api.pinterest.com/url_shortener/{}/redirect".format(
|
|
|
|
self.shortened_id)
|
|
|
|
response = self.request(url, method="HEAD", allow_redirects=False)
|
2017-01-27 21:51:28 +01:00
|
|
|
location = response.headers.get("Location")
|
2020-01-18 21:06:44 +01:00
|
|
|
if not location or not PinterestPinExtractor.pattern.match(location):
|
2017-01-27 21:51:28 +01:00
|
|
|
raise exception.NotFoundError("pin")
|
2019-12-29 23:37:34 +01:00
|
|
|
yield Message.Queue, location, {"_extractor": PinterestPinExtractor}
|
2017-01-27 21:51:28 +01:00
|
|
|
|
|
|
|
|
2016-09-02 19:11:16 +02:00
|
|
|
class PinterestAPI():
|
2018-04-24 22:17:25 +02:00
|
|
|
"""Minimal interface for the Pinterest Web API
|
|
|
|
|
|
|
|
For a better and more complete implementation in PHP, see
|
|
|
|
- https://github.com/seregazhuk/php-pinterest-bot
|
|
|
|
"""
|
|
|
|
|
2019-01-14 15:19:07 +01:00
|
|
|
BASE_URL = "https://www.pinterest.com"
|
2018-04-24 22:17:25 +02:00
|
|
|
HEADERS = {
|
|
|
|
"Accept" : "application/json, text/javascript, "
|
|
|
|
"*/*, q=0.01",
|
|
|
|
"Accept-Language" : "en-US,en;q=0.5",
|
|
|
|
"X-Pinterest-AppState": "active",
|
2020-06-16 14:41:05 +02:00
|
|
|
"X-APP-VERSION" : "b00dd49",
|
2018-04-24 22:17:25 +02:00
|
|
|
"X-Requested-With" : "XMLHttpRequest",
|
2020-06-16 14:41:05 +02:00
|
|
|
"Origin" : BASE_URL,
|
|
|
|
"Referer" : BASE_URL + "/",
|
2018-04-24 22:17:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, extractor):
|
2018-04-26 16:36:42 +02:00
|
|
|
self.extractor = extractor
|
2016-09-02 19:11:16 +02:00
|
|
|
|
2018-04-24 22:17:25 +02:00
|
|
|
def pin(self, pin_id):
|
2016-09-02 19:11:16 +02:00
|
|
|
"""Query information about a pin"""
|
2018-04-24 22:17:25 +02:00
|
|
|
options = {"id": pin_id, "field_set_key": "detailed"}
|
|
|
|
return self._call("Pin", options)["resource_response"]["data"]
|
2016-09-02 19:11:16 +02:00
|
|
|
|
2018-08-15 21:28:27 +02:00
|
|
|
def pin_related(self, pin_id):
|
|
|
|
"""Yield related pins of another pin"""
|
|
|
|
options = {"pin": pin_id, "add_vase": True, "pins_only": True}
|
|
|
|
return self._pagination("RelatedPinFeed", options)
|
|
|
|
|
2020-06-16 14:41:05 +02:00
|
|
|
def board(self, user, board_name):
|
2016-09-03 10:00:32 +02:00
|
|
|
"""Query information about a board"""
|
2020-06-16 14:41:05 +02:00
|
|
|
options = {"slug": board_name, "username": user,
|
2018-04-24 22:17:25 +02:00
|
|
|
"field_set_key": "detailed"}
|
|
|
|
return self._call("Board", options)["resource_response"]["data"]
|
2016-09-03 10:00:32 +02:00
|
|
|
|
2018-04-24 22:17:25 +02:00
|
|
|
def board_pins(self, board_id):
|
2016-09-02 19:11:16 +02:00
|
|
|
"""Yield all pins of a specific board"""
|
2018-04-24 22:17:25 +02:00
|
|
|
options = {"board_id": board_id}
|
|
|
|
return self._pagination("BoardFeed", options)
|
|
|
|
|
2020-06-16 14:41:05 +02:00
|
|
|
def board_sections(self, board_id):
|
|
|
|
"""Yield all sections of a specific board"""
|
|
|
|
options = {"board_id": board_id}
|
|
|
|
return self._pagination("BoardSections", options)
|
|
|
|
|
|
|
|
def board_section_pins(self, section_id):
|
|
|
|
"""Yield all pins from a board section"""
|
|
|
|
options = {"section_id": section_id}
|
|
|
|
return self._pagination("BoardSectionPins", options)
|
|
|
|
|
2018-08-15 21:28:27 +02:00
|
|
|
def board_related(self, board_id):
|
|
|
|
"""Yield related pins of a specific board"""
|
|
|
|
options = {"board_id": board_id, "add_vase": True}
|
|
|
|
return self._pagination("BoardRelatedPixieFeed", options)
|
|
|
|
|
2018-04-24 22:17:25 +02:00
|
|
|
def _call(self, resource, options):
|
2018-04-25 16:04:30 +02:00
|
|
|
url = "{}/resource/{}Resource/get/".format(self.BASE_URL, resource)
|
|
|
|
params = {"data": json.dumps({"options": options}), "source_url": ""}
|
2016-09-02 19:11:16 +02:00
|
|
|
|
2018-04-26 16:36:42 +02:00
|
|
|
response = self.extractor.request(
|
2019-07-04 23:45:26 +02:00
|
|
|
url, params=params, headers=self.HEADERS, fatal=False)
|
2018-05-11 17:28:34 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
data = response.json()
|
|
|
|
except ValueError:
|
|
|
|
data = {}
|
2018-04-17 17:12:42 +02:00
|
|
|
|
2019-07-04 23:45:26 +02:00
|
|
|
if response.status_code < 400 and not response.history:
|
2018-04-16 15:43:31 +02:00
|
|
|
return data
|
|
|
|
|
2018-04-26 16:36:42 +02:00
|
|
|
if response.status_code == 404 or response.history:
|
2018-08-15 21:28:27 +02:00
|
|
|
resource = self.extractor.subcategory.rpartition("-")[2]
|
|
|
|
raise exception.NotFoundError(resource)
|
2019-12-10 21:30:08 +01:00
|
|
|
self.extractor.log.debug("Server response: %s", response.text)
|
2019-10-28 16:06:36 +01:00
|
|
|
raise exception.StopExtraction("API request failed")
|
2018-04-17 17:12:42 +02:00
|
|
|
|
2018-04-25 16:04:30 +02:00
|
|
|
def _pagination(self, resource, options):
|
2018-04-17 17:12:42 +02:00
|
|
|
while True:
|
2018-04-24 22:17:25 +02:00
|
|
|
data = self._call(resource, options)
|
|
|
|
yield from data["resource_response"]["data"]
|
|
|
|
|
|
|
|
try:
|
|
|
|
bookmarks = data["resource"]["options"]["bookmarks"]
|
2018-08-16 18:11:39 +02:00
|
|
|
if (not bookmarks or bookmarks[0] == "-end-" or
|
|
|
|
bookmarks[0].startswith("Y2JOb25lO")):
|
2018-04-24 22:17:25 +02:00
|
|
|
return
|
2018-04-25 16:04:30 +02:00
|
|
|
options["bookmarks"] = bookmarks
|
2018-04-24 22:17:25 +02:00
|
|
|
except KeyError:
|
2018-04-17 17:12:42 +02:00
|
|
|
return
|