taiga-printable-importer/importer.py

311 lines
8.5 KiB
Python

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))