You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
310 lines
8.5 KiB
310 lines
8.5 KiB
import os
|
|
import sys
|
|
import uuid
|
|
import argparse
|
|
import tempfile
|
|
import requests
|
|
import configparser
|
|
import urllib.parse
|
|
from taiga import TaigaAPI
|
|
from bottle import run, put, request, response
|
|
|
|
try:
|
|
scriptname = os.path.basename(__file__)
|
|
except:
|
|
scriptname = "importer.py"
|
|
parser = argparse.ArgumentParser(
|
|
prog = scriptname,
|
|
description = "Import printables.com links into your taiga instance",
|
|
epilog = "The source of this program can be found at: https://git.geekify.de/sqozz/taiga-printable-importer")
|
|
parser.add_argument("-u", "--url", help="printables.com model url to import into taiga")
|
|
parser.add_argument("-w", "--enable-webserver", help="enable a webserver to receive URLs continuously", action=argparse.BooleanOptionalAction)
|
|
args = parser.parse_args()
|
|
|
|
# Config parsing
|
|
CONFIG=configparser.ConfigParser()
|
|
CONFIG.read("config.ini")
|
|
|
|
# Static variables for scrape requests to printables.com
|
|
UUID=str(uuid.uuid4())
|
|
ENDPOINT_URL="https://api.printables.com/graphql/"
|
|
HEADERS = {
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0",
|
|
"Accept": "application/json, text/plain, */*",
|
|
"Accept-Language": "en",
|
|
"Content-Type": "application/json",
|
|
"Referer": "https://www.printables.com/",
|
|
"operation": "PrintProfile",
|
|
"apollographql-client-version": "v2.46.2",
|
|
"Client-Uid": UUID,
|
|
"Origin": "https://www.printables.com",
|
|
"Sec-Fetch-Dest": "empty",
|
|
"Sec-Fetch-Mode": "cors",
|
|
"Sec-Fetch-Site": "same-site",
|
|
"Authorization": "",
|
|
"Connection": "keep-alive"
|
|
}
|
|
|
|
def get_printables_print_data(model_id):
|
|
PARAMS = '{"operationName":"PrintProfile","variables":{"id":"' + str(model_id) + '''"},"query":"query PrintProfile($id: ID!) {
|
|
print(id: $id) {
|
|
...PrintDetailFragment
|
|
__typename
|
|
}
|
|
}
|
|
|
|
fragment PrintDetailFragment on PrintType {
|
|
id
|
|
slug
|
|
name
|
|
authorship
|
|
description
|
|
hasModel
|
|
summary
|
|
printDuration
|
|
numPieces
|
|
weight
|
|
nozzleDiameters
|
|
usedMaterial
|
|
layerHeights
|
|
materials {
|
|
id
|
|
name
|
|
__typename
|
|
}
|
|
filesCount
|
|
pdfFilePath
|
|
userGcodeCount
|
|
printer {
|
|
id
|
|
name
|
|
__typename
|
|
}
|
|
images {
|
|
...ImageSimpleFragment
|
|
__typename
|
|
}
|
|
tags {
|
|
name
|
|
id
|
|
__typename
|
|
}
|
|
thingiverseLink
|
|
filesType
|
|
foundInUserGcodes
|
|
remixParents {
|
|
...remixParentDetail
|
|
__typename
|
|
}
|
|
gcodes {
|
|
id
|
|
name
|
|
filePath
|
|
fileSize
|
|
filePreviewPath
|
|
__typename
|
|
}
|
|
stls {
|
|
id
|
|
name
|
|
filePath
|
|
fileSize
|
|
filePreviewPath
|
|
__typename
|
|
}
|
|
slas {
|
|
id
|
|
name
|
|
filePath
|
|
fileSize
|
|
filePreviewPath
|
|
__typename
|
|
}
|
|
...LatestCompetitionResult
|
|
competitions {
|
|
id
|
|
name
|
|
slug
|
|
description
|
|
isOpened
|
|
__typename
|
|
}
|
|
competitionResults {
|
|
placement
|
|
competition {
|
|
id
|
|
name
|
|
slug
|
|
printsCount
|
|
openedFrom
|
|
openedTo
|
|
__typename
|
|
}
|
|
__typename
|
|
}
|
|
__typename
|
|
}
|
|
|
|
fragment ImageSimpleFragment on PrintImageType {
|
|
id
|
|
filePath
|
|
rotation
|
|
__typename
|
|
}
|
|
|
|
fragment remixParentDetail on PrintRemixType {
|
|
id
|
|
parentPrintId
|
|
parentPrintName
|
|
parentPrintAuthor {
|
|
id
|
|
slug
|
|
publicUsername
|
|
company
|
|
verified
|
|
handle
|
|
__typename
|
|
}
|
|
parentPrint {
|
|
id
|
|
name
|
|
slug
|
|
datePublished
|
|
images {
|
|
...ImageSimpleFragment
|
|
__typename
|
|
}
|
|
license {
|
|
id
|
|
name
|
|
disallowRemixing
|
|
__typename
|
|
}
|
|
eduProject {
|
|
id
|
|
__typename
|
|
}
|
|
__typename
|
|
}
|
|
url
|
|
urlAuthor
|
|
urlImage
|
|
urlTitle
|
|
__typename
|
|
}
|
|
|
|
fragment LatestCompetitionResult on PrintType {
|
|
latestCompetitionResult {
|
|
placement
|
|
competitionId
|
|
__typename
|
|
}
|
|
__typename
|
|
}"}'''
|
|
PARAMS=PARAMS.replace("\n", "\\n")
|
|
|
|
req = requests.post(ENDPOINT_URL, headers=HEADERS, data=PARAMS)
|
|
print_data = req.json()["data"]["print"]
|
|
return print_data
|
|
|
|
def get_modelid_from_url(url):
|
|
parsed_url = urllib.parse.urlparse(url)
|
|
path = parsed_url.path
|
|
path_components = path.split("/")
|
|
path_components = list(filter(lambda x: x != "", path_components)) #filter empty elements
|
|
if path_components[0] != "model":
|
|
raise Exception("Only direct links to models are supported")
|
|
model_slug = path_components[1]
|
|
model_id = model_slug.split("-")[0]
|
|
return model_id
|
|
|
|
@put('/import')
|
|
def http_import():
|
|
data = request.body.read()
|
|
url = data.decode("utf-8")
|
|
us_url = generate_taiga_userstory(url)
|
|
response.set_header('Content-Location', us_url)
|
|
response.status = 201
|
|
|
|
def generate_from_cmdline():
|
|
link = get_url_interactively()
|
|
us_url = generate_taiga_userstory(link)
|
|
return us_url
|
|
|
|
def get_url_interactively():
|
|
url = input("Please paste link: ")
|
|
return url
|
|
|
|
def generate_taiga_userstory(link):
|
|
model_id = get_modelid_from_url(link)
|
|
print_data = get_printables_print_data(model_id)
|
|
api = TaigaAPI(host=CONFIG["taiga"]["url"])
|
|
api.auth(
|
|
username=CONFIG["taiga"]["username"],
|
|
password=CONFIG["taiga"]["password"]
|
|
)
|
|
global proj
|
|
proj = api.projects.get_by_slug(CONFIG["taiga"]["project_slug"])
|
|
|
|
# Create userstory for printable
|
|
story = proj.add_user_story(
|
|
print_data["name"],
|
|
description = print_data["description"],
|
|
tags = list(map(lambda x: x.get("name"), print_data["tags"])))
|
|
|
|
# Set custom field for platform link if enabled and configured
|
|
if bool(CONFIG["taiga"]["userstory_use_custom_field"]):
|
|
us_attributes = list(map(lambda x: {"id": x.id, "name": x.name, "project": x.project}, api.user_story_attributes.list()))
|
|
attribute_id = list(filter(lambda x: CONFIG["taiga"]["userstory_custom_field_name"] in x["name"] and x["project"] == proj.id, us_attributes))[0]["id"]
|
|
story.set_attribute(attribute_id, link)
|
|
|
|
# Find id for desired status of newly tasks
|
|
task_statuses = list(map(lambda x: {"id": x.id, "name": x.name, "project": x.project}, api.task_statuses.list()))
|
|
task_status_id = list(filter(lambda x: CONFIG["taiga"]["initial_task_status"] == x["name"] and x["project"] == proj.id, task_statuses))[0]["id"]
|
|
|
|
tmpdir = tempfile.TemporaryDirectory() #workdir for downloads
|
|
|
|
imagepath = print_data["images"][0]["filePath"]
|
|
imageurl = urllib.parse.urljoin("https://media.printables.com", imagepath)
|
|
local_file = os.path.join(tmpdir.name, os.path.basename(imagepath))
|
|
print("Downloading first image of printable to {}…".format(local_file), end="")
|
|
with requests.get(imageurl, stream=True) as r:
|
|
r.raise_for_status()
|
|
with open(local_file, "wb") as f:
|
|
for chunk in r.iter_content(chunk_size=8192):
|
|
f.write(chunk)
|
|
print(" done.")
|
|
|
|
story.attach(local_file)
|
|
|
|
stls = list(filter(lambda x: x["filePath"].endswith(".stl"), print_data["stls"]))
|
|
stl_files = list(map(lambda x: {"name": x["name"], "filePath": x["filePath"]},stls))
|
|
for stl_file in stl_files:
|
|
stlpath = stl_file["filePath"]
|
|
filename = os.path.basename(stlpath)
|
|
print("Creating task for file {}…".format(filename), end="")
|
|
|
|
task = story.add_task(filename, task_status_id)
|
|
|
|
stlurl = urllib.parse.urljoin("https://files.printables.com", stlpath)
|
|
local_file = os.path.join(tmpdir.name, filename)
|
|
with requests.get(stlurl, stream=True) as r:
|
|
r.raise_for_status()
|
|
with open(local_file, "wb") as f:
|
|
for chunk in r.iter_content(chunk_size=8192):
|
|
f.write(chunk)
|
|
task.attach(local_file)
|
|
print(" done.")
|
|
|
|
newstory_url= urllib.parse.urljoin(CONFIG["taiga"]["url"], os.path.join("project", CONFIG["taiga"]["project_slug"], "us", str(story.ref)))
|
|
return newstory_url
|
|
|
|
if __name__ == "__main__":
|
|
if bool(args.enable_webserver) == True:
|
|
run(host=CONFIG["webserver"]["host"], port=CONFIG["webserver"]["port"])
|
|
sys.exit(0)
|
|
elif args.url != None:
|
|
us_url = generate_taiga_userstory(args.url)
|
|
else:
|
|
us_url = generate_from_cmdline()
|
|
print("New story created at: {}".format(us_url))
|
|
|