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