mirror of
https://github.com/manyfold3d/manyfold.git
synced 2026-10-22 03:32:18 -05:00
parse model metadata from thingiverse
This commit is contained in:
4 files changed
+211
-1
No files matched your search
@@ -0,0 +1,36 @@
|
||||
class Integrations::Thingiverse::ModelDeserializer < Integrations::Thingiverse::BaseDeserializer
|
||||
attr_reader :object_id
|
||||
|
||||
def deserialize
|
||||
return {} unless valid?
|
||||
# Fetch from MMF API
|
||||
connection = Faraday.new do |builder|
|
||||
builder.response :json
|
||||
end
|
||||
r = connection.get("https://api.thingiverse.com/things/#{CGI.escapeURIComponent(@object_id)}",
|
||||
{},
|
||||
{
|
||||
Authorization: "Bearer #{@api_key}",
|
||||
Accept: "application/json"
|
||||
})
|
||||
{
|
||||
name: r.body["name"],
|
||||
notes: ReverseMarkdown.convert(r.body["description"]),
|
||||
tag_list: r.body["tags"]&.pluck("tag"),
|
||||
sensitive: r.body["is_nsfw"]
|
||||
# image_urls: r.body["images"].map { |it| it.dig("original", "url") }
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def target_class
|
||||
Model
|
||||
end
|
||||
|
||||
def valid_path?(path)
|
||||
match = /\A\/thing:([[:digit:]]+)\Z/.match(path)
|
||||
@object_id = match[1] if match.present?
|
||||
match.present?
|
||||
end
|
||||
end
|
||||
+2
-1
@@ -41,7 +41,8 @@ class Link < ApplicationRecord
|
||||
Integrations::MyMiniFactory::CollectionDeserializer,
|
||||
Integrations::MyMiniFactory::ModelDeserializer,
|
||||
Integrations::Thingiverse::CreatorDeserializer,
|
||||
Integrations::Thingiverse::CollectionDeserializer
|
||||
Integrations::Thingiverse::CollectionDeserializer,
|
||||
Integrations::Thingiverse::ModelDeserializer
|
||||
].map do |klass|
|
||||
klass.new(uri: url)
|
||||
end.find { |it| it.valid?(for_class: linkable.class) }
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.thingiverse.com/things/4049220
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
User-Agent:
|
||||
- Manyfold/unknown
|
||||
Authorization:
|
||||
- Bearer <THINGIVERSE_API_KEY>
|
||||
Accept:
|
||||
- application/json
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
date:
|
||||
- Tue, 08 Jul 2025 14:42:47 GMT
|
||||
content-type:
|
||||
- application/json; charset=utf-8
|
||||
transfer-encoding:
|
||||
- chunked
|
||||
connection:
|
||||
- keep-alive
|
||||
vary:
|
||||
- Accept-Encoding, Authorization,Cookie
|
||||
expires:
|
||||
- Tue, 08 Jul 2025 15:12:47 +0000
|
||||
cache-control:
|
||||
- max-age=1800
|
||||
content-security-policy:
|
||||
- frame-ancestors *.makerbot.com *.thingiverse.com *.mbot.me *.onshape.com *.makerbot.local
|
||||
*.thingiverse.local;
|
||||
access-control-max-age:
|
||||
- '604800'
|
||||
access-control-allow-headers:
|
||||
- Authorization, Referer, Content-Type, User-Agent, Accept
|
||||
access-control-allow-credentials:
|
||||
- 'true'
|
||||
access-control-allow-methods:
|
||||
- GET, POST, PATCH, DELETE
|
||||
access-control-allow-origin:
|
||||
- "*"
|
||||
content-encoding:
|
||||
- gzip
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
set-cookie:
|
||||
- __cf_bm=bIIxzxXf1p_kauVCXjGF9xZm0Jc86sGjIFAQF1aDVqA-1751985767-1.0.1.1-OkpdVlmSByV42Z6hVfhB7Ff3BnbQfCl5LefHHccPLgRuGu5SMSLNBNlqlT1zlyTyWl6U8D4ao7Kx3j7AthVK25d6BAPEgkRaYoYxYJYKSd8;
|
||||
path=/; expires=Tue, 08-Jul-25 15:12:47 GMT; domain=.thingiverse.com; HttpOnly;
|
||||
Secure; SameSite=None, _cfuvid=vSX1evorn7tBsegzOZw3tmAH9kyg71ylNuxR1TuL1uM-1751985767556-0.0.1.1-604800000;
|
||||
path=/; domain=.thingiverse.com; HttpOnly; Secure; SameSite=None
|
||||
speculation-rules:
|
||||
- '"/cdn-cgi/speculation"'
|
||||
server:
|
||||
- cloudflare
|
||||
cf-ray:
|
||||
- 95c059a5eca94197-LHR
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"id":4049220,"name":"Slatwall support","thumbnail":"https:\/\/cdn.thingiverse.com\/assets\/e3\/bc\/80\/9f\/87\/large_display_Slatwall_support.stl","url":"https:\/\/api.thingiverse.com\/things\/4049220","public_url":"https:\/\/www.thingiverse.com\/thing:4049220","creator":{"id":3154799,"name":"floppy_uk","first_name":"James","last_name":"Smith","url":"https:\/\/api.thingiverse.com\/users\/floppy_uk","public_url":"https:\/\/www.thingiverse.com\/floppy_uk","thumbnail":"https:\/\/cdn.thingiverse.com\/assets\/04\/b8\/80\/49\/bd\/sketch_head.PNG","count_of_followers":32,"count_of_following":20,"count_of_designs":13,"collection_count":18,"make_count":7,"accepts_tips":false,"is_following":false,"location":"UK","cover":"https:\/\/cdn.thingiverse.com\/site\/img\/default\/cover\/cover-4_preview_large.jpg","is_admin":false,"is_moderator":false,"is_featured":false,"is_verified":false},"added":"2019-12-14T20:50:10+00:00","modified":"2019-12-14T20:50:10+00:00","is_published":1,"is_wip":0,"is_featured":false,"is_nsfw":false,"is_ai":false,"is_winner":false,"is_edu_approved":false,"is_printable":false,"like_count":2,"is_liked":false,"collect_count":10,"is_collected":false,"comment_count":0,"is_watched":false,"default_image":{"id":12250747,"url":"https:\/\/cdn.thingiverse.com\/assets\/62\/09\/06\/48\/fa\/Slatwall_support.stl","name":"Slatwall_support.png","sizes":[{"type":"thumb","size":"large","url":"https:\/\/cdn.thingiverse.com\/assets\/48\/33\/76\/10\/2a\/large_thumb_Slatwall_support.stl"},{"type":"thumb","size":"medium","url":"https:\/\/cdn.thingiverse.com\/assets\/36\/44\/9e\/83\/e2\/medium_thumb_Slatwall_support.stl"},{"type":"thumb","size":"small","url":"https:\/\/cdn.thingiverse.com\/assets\/bf\/38\/c4\/75\/7c\/small_thumb_Slatwall_support.stl"},{"type":"thumb","size":"tiny","url":"https:\/\/cdn.thingiverse.com\/assets\/91\/3d\/55\/06\/aa\/tiny_thumb_Slatwall_support.stl"},{"type":"preview","size":"featured","url":"https:\/\/cdn.thingiverse.com\/assets\/3d\/e1\/cb\/53\/2e\/featured_preview_Slatwall_support.stl"},{"type":"preview","size":"card","url":"https:\/\/cdn.thingiverse.com\/assets\/20\/16\/75\/fb\/61\/card_preview_Slatwall_support.stl"},{"type":"preview","size":"large","url":"https:\/\/cdn.thingiverse.com\/assets\/ab\/61\/0d\/3f\/de\/large_preview_Slatwall_support.stl"},{"type":"preview","size":"medium","url":"https:\/\/cdn.thingiverse.com\/assets\/82\/32\/01\/85\/fa\/medium_preview_Slatwall_support.stl"},{"type":"preview","size":"small","url":"https:\/\/cdn.thingiverse.com\/assets\/61\/a2\/21\/1d\/58\/small_preview_Slatwall_support.stl"},{"type":"preview","size":"birdwing","url":"https:\/\/cdn.thingiverse.com\/assets\/5b\/36\/d3\/64\/7e\/birdwing_preview_Slatwall_support.stl"},{"type":"preview","size":"tiny","url":"https:\/\/cdn.thingiverse.com\/assets\/47\/af\/cf\/ca\/04\/tiny_preview_Slatwall_support.stl"},{"type":"preview","size":"tinycard","url":"https:\/\/cdn.thingiverse.com\/assets\/14\/5f\/84\/39\/ea\/tinycard_preview_Slatwall_support.stl"},{"type":"display","size":"large","url":"https:\/\/cdn.thingiverse.com\/assets\/e3\/bc\/80\/9f\/87\/large_display_Slatwall_support.stl"},{"type":"display","size":"medium","url":"https:\/\/cdn.thingiverse.com\/assets\/92\/3c\/75\/1d\/91\/medium_display_Slatwall_support.stl"},{"type":"display","size":"small","url":"https:\/\/cdn.thingiverse.com\/assets\/b2\/77\/e7\/8f\/17\/small_display_Slatwall_support.stl"}],"added":"2019-12-14T20:49:52+00:00"},"description":"This
|
||||
is a support that nicely fits into Slatwall plastic inserts. It''s not intended
|
||||
for printing directly, rather as something to combine with other objects to
|
||||
attach them to the Slatwall. The Fusion360 file is included so you can remix
|
||||
the shape with whatever else you want.","instructions":"","description_html":"<div><p>This
|
||||
is a support that nicely fits into Slatwall plastic inserts. It''s not intended
|
||||
for printing directly, rather as something to combine with other objects to
|
||||
attach them to the Slatwall. The Fusion360 file is included so you can remix
|
||||
the shape with whatever else you want.<\/p><\/div>","instructions_html":"<div><\/div>","details":"<div><h1
|
||||
class=\"thing-component-header summary summary\">Summary<\/h1><div><p>This
|
||||
is a support that nicely fits into Slatwall plastic inserts. It''s not intended
|
||||
for printing directly, rather as something to combine with other objects to
|
||||
attach them to the Slatwall. The Fusion360 file is included so you can remix
|
||||
the shape with whatever else you want.<\/p><\/div><\/div>","details_parts":[{"type":"summary","name":"Summary","required":"required","data":[{"content":"<div><p>This
|
||||
is a support that nicely fits into Slatwall plastic inserts. It''s not intended
|
||||
for printing directly, rather as something to combine with other objects to
|
||||
attach them to the Slatwall. The Fusion360 file is included so you can remix
|
||||
the shape with whatever else you want.<\/p><\/div>"}]},{"type":"settings","name":"Print
|
||||
Settings"},{"type":"tips","name":"Post-Printing"},{"type":"design","name":"How
|
||||
I Designed This"},{"type":"custom","name":"Custom Section"}],"edu_details":"","edu_details_parts":[{"type":"grades","name":"Grade","required":"recommended","save_as_component":false,"template":"taglist_select","fieldname":"grades","default":"Choose
|
||||
a range, help teachers see if this is a fit for their class","opts":{"15":"Kindergarten","16":"1st
|
||||
Grade","17":"2nd Grade","18":"3rd Grade","19":"4th Grade","20":"5th Grade","21":"6th
|
||||
Grade","22":"7th Grade","23":"8th Grade","24":"9th Grade","25":"10th Grade","26":"11th
|
||||
Grade","27":"12th Grade","28":"Higher Education"}},{"type":"subjects","name":"Subject","required":"recommended","save_as_component":false,"template":"taglist_select","fieldname":"subjects","default":"Choose
|
||||
a Subject, Make your project more findable","opts":{"11":"Science","12":"Math","13":"Engineering","14":"History","15":"Geography","16":"Art","17":"Languages","18":"Music","19":"Technology","20":"Special
|
||||
Ed"}},{"type":"standards","name":"Standards","required":"recommended","save_as_component":false,"template":"taglist_select","fieldname":"standards","default":"Choose
|
||||
a Requirement this lesson meets","opts":{"3":"NGSS","4":"CCSS"}},{"type":"overview","name":"Overview
|
||||
and Background","required":"required"},{"type":"plan","name":"Lesson Plan
|
||||
and Activity","required":"required"},{"type":"materials","name":"Materials
|
||||
Needed","required":"recommended"},{"type":"skills","name":"Skills Learned","label":"Enter
|
||||
each skill and press enter to add it as a tagged skill","required":false,"template":"taglist","fieldname":"skills","default":"Choose
|
||||
Skills","opts":[]},{"type":"duration","name":"Duration of Lesson","template":"textfield"},{"type":"prep","name":"Preparation"},{"type":"assets","name":"Handouts
|
||||
and Assets","save_as_component":false,"label":"Upload any files, docs, etc.
|
||||
They will be found under the Files tab on the Thing Page.","template":"upload"},{"type":"assessment","name":"Rubric
|
||||
and Assessment"},{"type":"references","name":"References"}],"license":"Creative
|
||||
Commons - Attribution - Share Alike","allows_derivatives":true,"files_url":"https:\/\/api.thingiverse.com\/things\/4049220\/files","images_url":"https:\/\/api.thingiverse.com\/things\/4049220\/images","likes_url":"https:\/\/api.thingiverse.com\/things\/4049220\/likes","ancestors_url":"https:\/\/api.thingiverse.com\/things\/4049220\/ancestors","derivatives_url":"https:\/\/api.thingiverse.com\/things\/4049220\/derivatives","tags_url":"https:\/\/api.thingiverse.com\/things\/4049220\/tags","tags":[{"name":"slatwall","tag":"slatwall","url":"https:\/\/api.thingiverse.com\/tags\/slatwall","count":114,"things_url":"https:\/\/api.thingiverse.com\/tags\/slatwall\/things","absolute_url":"\/tag:slatwall"}],"categories_url":"https:\/\/api.thingiverse.com\/things\/4049220\/categories","file_count":2,"is_private":0,"is_purchased":0,"app_id":null,"download_count":327,"view_count":203,"education":{"grades":[],"subjects":[]},"remix_count":0,"make_count":0,"app_count":4,"root_comment_count":0,"moderation":"","is_derivative":false,"ancestors":[],"type_name":"Thing","is_banned":false,"is_comments_disabled":false,"needs_moderation":0,"is_decoy":0,"zip_data":{"files":[{"name":"Slatwall_support.stl","url":"https:\/\/cdn.thingiverse.com\/assets\/62\/09\/06\/48\/fa\/Slatwall_support.stl"},{"name":"Slatwall_support.f3d","url":"https:\/\/cdn.thingiverse.com\/assets\/a1\/c0\/aa\/da\/ce\/Slatwall_support.f3d"}],"images":[{"name":"Slatwall_support.png","url":"https:\/\/cdn.thingiverse.com\/assets\/81\/fd\/d4\/4e\/ec\/Slatwall_support.png"}]}}'
|
||||
recorded_at: Tue, 08 Jul 2025 14:42:47 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
@@ -0,0 +1,69 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Integrations::Thingiverse::ModelDeserializer, :thingiverse_api_key do
|
||||
context "when creating from URI" do
|
||||
it "accepts object URIs" do
|
||||
deserializer = described_class.new(uri: "https://www.thingiverse.com/thing:4049220")
|
||||
expect(deserializer).to be_valid
|
||||
end
|
||||
|
||||
it "rejects non-object URIs" do
|
||||
deserializer = described_class.new(uri: "https://www.thingiverse.com/floppy_uk")
|
||||
expect(deserializer).not_to be_valid
|
||||
end
|
||||
|
||||
it "extracts object ID" do
|
||||
deserializer = described_class.new(uri: "https://www.thingiverse.com/thing:4049220")
|
||||
expect(deserializer.object_id).to eq "4049220"
|
||||
end
|
||||
end
|
||||
|
||||
context "when pulling data from MMF API", vcr: {cassette_name: "Integrations_Thingiverse_ModelDeserializer/success"} do
|
||||
subject(:deserializer) { described_class.new(uri: uri) }
|
||||
|
||||
let(:uri) { "https://www.thingiverse.com/thing:4049220" }
|
||||
|
||||
it "extracts name" do
|
||||
expect(deserializer.deserialize[:name]).to eq "Slatwall support"
|
||||
end
|
||||
|
||||
it "extracts description" do
|
||||
expect(deserializer.deserialize[:notes]).to include "Slatwall plastic inserts"
|
||||
end
|
||||
|
||||
it "extracts tags" do
|
||||
expect(deserializer.deserialize[:tag_list]).to include "slatwall"
|
||||
end
|
||||
|
||||
it "extracts sensitive tag" do
|
||||
expect(deserializer.deserialize[:sensitive]).to be false
|
||||
end
|
||||
|
||||
it "extracts image URLs"
|
||||
it "extracts creator"
|
||||
end
|
||||
|
||||
context "with a valid URI" do
|
||||
subject(:deserializer) { described_class.new(uri: uri) }
|
||||
|
||||
let(:uri) { "https://www.thingiverse.com/thing:4049220" }
|
||||
|
||||
it "deserializes to a Model" do
|
||||
expect(deserializer.send(:target_class)).to eq Model
|
||||
end
|
||||
|
||||
it "is valid for deserialization to Model" do
|
||||
expect(deserializer.valid?(for_class: Model)).to be true
|
||||
end
|
||||
|
||||
it "is not valid for deserialization to Creator" do
|
||||
expect(deserializer.valid?(for_class: Creator)).to be false
|
||||
end
|
||||
|
||||
it "is created for this URI by a link object" do # rubocop:disable RSpec/MultipleExpectations
|
||||
des = create(:link, url: uri, linkable: create(:model)).deserializer
|
||||
expect(des).to be_a(described_class)
|
||||
expect(des).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user