structables 0.2.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {structables-0.2.2 → structables-0.3.0}/PKG-INFO +1 -1
  2. {structables-0.2.2 → structables-0.3.0}/pyproject.toml +1 -1
  3. structables-0.3.0/src/structables/config.py +13 -0
  4. structables-0.3.0/src/structables/main.py +20 -0
  5. structables-0.3.0/src/structables/routes/__init__.py +12 -0
  6. structables-0.3.0/src/structables/routes/category.py +76 -0
  7. structables-0.3.0/src/structables/routes/contest.py +177 -0
  8. structables-0.3.0/src/structables/routes/main.py +362 -0
  9. structables-0.3.0/src/structables/routes/member.py +124 -0
  10. structables-0.3.0/src/structables/routes/proxy.py +50 -0
  11. structables-0.3.0/src/structables/utils/data.py +56 -0
  12. structables-0.3.0/src/structables/utils/helpers.py +451 -0
  13. structables-0.2.2/src/structables/main.py +0 -1313
  14. {structables-0.2.2 → structables-0.3.0}/.gitignore +0 -0
  15. {structables-0.2.2 → structables-0.3.0}/LICENSE +0 -0
  16. {structables-0.2.2 → structables-0.3.0}/README.md +0 -0
  17. {structables-0.2.2 → structables-0.3.0}/cronjob.txt +0 -0
  18. {structables-0.2.2 → structables-0.3.0}/src/structables/__init__.py +0 -0
  19. {structables-0.2.2 → structables-0.3.0}/src/structables/static/css/style.css +0 -0
  20. {structables-0.2.2 → structables-0.3.0}/src/structables/static/dist/css/bootstrap.min.css +0 -0
  21. {structables-0.2.2 → structables-0.3.0}/src/structables/static/dist/css/bootstrap.min.css.map +0 -0
  22. {structables-0.2.2 → structables-0.3.0}/src/structables/static/dist/js/bootstrap.bundle.min.js +0 -0
  23. {structables-0.2.2 → structables-0.3.0}/src/structables/static/dist/js/bootstrap.bundle.min.js.map +0 -0
  24. {structables-0.2.2 → structables-0.3.0}/src/structables/static/img/logo.png +0 -0
  25. {structables-0.2.2 → structables-0.3.0}/src/structables/static/img/logo_lg.png +0 -0
  26. {structables-0.2.2 → structables-0.3.0}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
  27. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/400.html +0 -0
  28. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/404.html +0 -0
  29. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/429.html +0 -0
  30. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/500.html +0 -0
  31. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/archives.html +0 -0
  32. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/article-review.html +0 -0
  33. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/article.html +0 -0
  34. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/base.html +0 -0
  35. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/category.html +0 -0
  36. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/collection.html +0 -0
  37. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/contest.html +0 -0
  38. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/contests.html +0 -0
  39. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/footer.html +0 -0
  40. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/header.html +0 -0
  41. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/iframe.html +0 -0
  42. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/index.html +0 -0
  43. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/member-instructables.html +0 -0
  44. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/member.html +0 -0
  45. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/privacypolicy.html +0 -0
  46. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/projects.html +0 -0
  47. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/sitemap.html +0 -0
  48. {structables-0.2.2 → structables-0.3.0}/src/structables/templates/style.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: structables
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: A simple frontend for Instructables
5
5
  Project-URL: Homepage, https://git.private.coffee/privatecoffee/structables
6
6
  Project-URL: Bug Tracker, https://git.private.coffee/privatecoffee/structables/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "structables"
7
- version = "0.2.2"
7
+ version = "0.3.0"
8
8
  authors = [
9
9
  { name="Private.coffee Team", email="support@private.coffee" },
10
10
  ]
@@ -0,0 +1,13 @@
1
+ import os
2
+
3
+ class Config:
4
+ DEBUG = os.environ.get("FLASK_DEBUG", os.environ.get("STRUCTABLES_DEBUG", False))
5
+ PORT = int(os.environ.get("STRUCTABLES_PORT", 8002))
6
+ LISTEN_HOST = os.environ.get("STRUCTABLES_LISTEN_HOST", "127.0.0.1")
7
+ INVIDIOUS = os.environ.get("STRUCTABLES_INVIDIOUS")
8
+ UNSAFE = os.environ.get("STRUCTABLES_UNSAFE", False)
9
+ PRIVACY_FILE = os.environ.get("STRUCTABLES_PRIVACY_FILE", "privacy.txt")
10
+
11
+ @staticmethod
12
+ def init_app(app):
13
+ pass
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python
2
+
3
+ from flask import Flask
4
+
5
+ from .config import Config
6
+ from .routes import init_routes
7
+ from .utils.data import update_data
8
+ from .utils.helpers import get_typesense_api_key
9
+
10
+ app = Flask(__name__, template_folder="templates", static_folder="static")
11
+ app.config.from_object(Config)
12
+ app.typesense_api_key = get_typesense_api_key()
13
+
14
+ init_routes(app)
15
+
16
+ if __name__ == "__main__":
17
+ app.run(port=app.config['PORT'], host=app.config['LISTEN_HOST'], debug=app.config['DEBUG'])
18
+
19
+ # Initialize data when the server starts
20
+ update_data(app)
@@ -0,0 +1,12 @@
1
+ from .main import init_main_routes
2
+ from .category import init_category_routes
3
+ from .member import init_member_routes
4
+ from .proxy import init_proxy_routes
5
+ from .contest import init_contest_routes
6
+
7
+ def init_routes(app):
8
+ init_main_routes(app)
9
+ init_category_routes(app)
10
+ init_member_routes(app)
11
+ init_proxy_routes(app)
12
+ init_contest_routes(app)
@@ -0,0 +1,76 @@
1
+ from flask import redirect
2
+ from werkzeug.exceptions import NotFound
3
+ from ..utils.helpers import project_list, category_page
4
+
5
+
6
+ def init_category_routes(app):
7
+ @app.route("/<category>/<channel>/projects/")
8
+ def route_channel_projects(category, channel):
9
+ return project_list(app, channel.title())
10
+
11
+ @app.route("/<category>/<channel>/projects/<sort>/")
12
+ def route_channel_projects_sort(category, channel, sort):
13
+ return project_list(
14
+ app,
15
+ channel.title(),
16
+ " Sorted by " + sort.title(),
17
+ )
18
+
19
+ @app.route("/<category>/projects/")
20
+ def route_category_projects(category):
21
+ return project_list(app, category.title())
22
+
23
+ @app.route("/<category>/projects/<sort>/")
24
+ def route_category_projects_sort(category, sort):
25
+ return project_list(app, category.title(), " Sorted by " + sort.title())
26
+
27
+ @app.route("/projects/")
28
+ def route_projects():
29
+ return project_list(app, "")
30
+
31
+ @app.route("/projects/<sort>/")
32
+ def route_projects_sort(sort):
33
+ return project_list(app, "", " Sorted by " + sort.title())
34
+
35
+ @app.route("/circuits/")
36
+ def route_circuits():
37
+ return category_page(app, "Circuits")
38
+
39
+ @app.route("/workshop/")
40
+ def route_workshop():
41
+ return category_page(app, "Workshop")
42
+
43
+ @app.route("/craft/")
44
+ def route_craft():
45
+ return category_page(app, "Craft")
46
+
47
+ @app.route("/cooking/")
48
+ def route_cooking():
49
+ return category_page(app, "Cooking")
50
+
51
+ @app.route("/living/")
52
+ def route_living():
53
+ return category_page(app, "Living")
54
+
55
+ @app.route("/outside/")
56
+ def route_outside():
57
+ return category_page(app, "Outside")
58
+
59
+ @app.route("/teachers/")
60
+ def route_teachers():
61
+ return category_page(app, "Teachers", True)
62
+
63
+ @app.route("/<category>/<channel>/")
64
+ def route_channel_redirect(category, channel):
65
+ if (
66
+ category == "circuits"
67
+ or category == "workshop"
68
+ or category == "craft"
69
+ or category == "cooking"
70
+ or category == "living"
71
+ or category == "outside"
72
+ or category == "teachers"
73
+ ):
74
+ return redirect(f"/{category}/{channel}/projects/", 307)
75
+ else:
76
+ raise NotFound()
@@ -0,0 +1,177 @@
1
+ from flask import render_template, request, abort
2
+ from urllib.request import urlopen
3
+ from urllib.error import HTTPError
4
+ from ..utils.helpers import proxy
5
+ from bs4 import BeautifulSoup
6
+
7
+ def init_contest_routes(app):
8
+ @app.route("/contest/archive/")
9
+ def route_contest_archive():
10
+ page = 1
11
+ if request.args.get("page") is not None:
12
+ page = request.args.get("page")
13
+
14
+ try:
15
+ data = urlopen(f"https://www.instructables.com/contest/archive/?page={page}")
16
+ except HTTPError as e:
17
+ abort(e.code)
18
+
19
+ soup = BeautifulSoup(data.read().decode(), "html.parser")
20
+
21
+ main = soup.select("div#contest-archive-wrapper")[0]
22
+
23
+ contest_count = main.select("p.contest-count")[0].text
24
+
25
+ contest_list = []
26
+ for index, year in enumerate(main.select("div.contest-archive-list h2")):
27
+ year_list = main.select(
28
+ "div.contest-archive-list div.contest-archive-list-year"
29
+ )[index]
30
+ year_name = year.text
31
+ month_list = []
32
+ for month in year_list.select("div.contest-archive-list-month"):
33
+ month_name = month.select("h3")[0].text
34
+ month_contest_list = []
35
+ for p in month.select("p"):
36
+ date = p.select("span")[0].text
37
+ link = p.select("a")[0].get("href")
38
+ title = p.select("a")[0].text
39
+ month_contest_list.append([date, link, title])
40
+ month_list.append([month_name, month_contest_list])
41
+ contest_list.append([year_name, month_list])
42
+
43
+ pagination = main.select("nav.pagination ul.pagination")[0]
44
+
45
+ return render_template(
46
+ "archives.html",
47
+ title=f"Contest Archives (Page {page})",
48
+ page=page,
49
+ contest_count=contest_count,
50
+ pagination=pagination,
51
+ contest_list=contest_list,
52
+ )
53
+
54
+ @app.route("/contest/<contest>/")
55
+ def route_contest(contest):
56
+ try:
57
+ data = urlopen(f"https://www.instructables.com/contest/{contest}/")
58
+ except HTTPError as e:
59
+ abort(e.code)
60
+
61
+ soup = BeautifulSoup(data.read().decode(), "html.parser")
62
+
63
+ title = soup.select('meta[property="og:title"]')[0].get("content")
64
+
65
+ body = soup.select("div#contest-wrapper")[0]
66
+
67
+ img = proxy(body.select("div#contest-masthead img")[0].get("src"))
68
+
69
+ entry_count = body.select("li.entries-nav-btn")[0].text.split(" ")[0]
70
+ prizes = body.select("li.prizes-nav-btn")[0].text.split(" ")[0]
71
+
72
+ info = body.select("div.contest-body-column-left")[0]
73
+ info.select("div#site-announcements-page")[0].decompose()
74
+ info.select("h3")[0].decompose()
75
+ info.select("div#contest-body-nav")[0].decompose()
76
+ info = str(info).replace("https://www.instructables.com", "/")
77
+
78
+ body.select("span.contest-entity-count")[0].text
79
+
80
+ entry_list = []
81
+ for entry in body.select("div.contest-entries-list div.contest-entries-list-ible"):
82
+ link = entry.a["href"]
83
+ entry_img = proxy(entry.select("a noscript img")[0].get("src"))
84
+ entry_title = entry.select("a.ible-title")[0].text
85
+ author = entry.select("div span.ible-author a")[0].text
86
+ author_link = entry.select("div span.ible-author a")[0].get("href")
87
+ channel = entry.select("div span.ible-channel a")[0].text
88
+ channel_link = entry.select("div span.ible-channel a")[0].get("href")
89
+ views = entry.select(".ible-views")[0].text
90
+
91
+ entry_list.append(
92
+ {
93
+ "link": link,
94
+ "entry_img": entry_img,
95
+ "entry_title": entry_title,
96
+ "author": author,
97
+ "author_link": author_link,
98
+ "channel": channel,
99
+ "channel_link": channel_link,
100
+ "views": views,
101
+ }
102
+ )
103
+
104
+ return render_template(
105
+ "contest.html",
106
+ title=title,
107
+ img=img,
108
+ entry_count=entry_count,
109
+ prizes=prizes,
110
+ info=info,
111
+ entry_list=entry_list,
112
+ )
113
+
114
+ @app.route("/contest/")
115
+ def route_contests():
116
+ try:
117
+ data = urlopen("https://www.instructables.com/contest/")
118
+ except HTTPError as e:
119
+ abort(e.code)
120
+
121
+ soup = BeautifulSoup(data.read().decode(), "html.parser")
122
+
123
+ contest_count = str(soup.select("p.contest-count")[0])
124
+
125
+ contests = []
126
+ for contest in soup.select("div#cur-contests div.row-fluid div.contest-banner"):
127
+ link = contest.select("div.contest-banner-inner a")[0].get("href")
128
+ img = proxy(contest.select("div.contest-banner-inner a img")[0].get("src"))
129
+ alt = contest.select("div.contest-banner-inner a img")[0].get("alt")
130
+ deadline = contest.select("span.contest-meta-deadline")[0].get("data-deadline")
131
+ prizes = contest.select("span.contest-meta-count")[0].text
132
+ entries = contest.select("span.contest-meta-count")[1].text
133
+
134
+ contests.append(
135
+ {
136
+ "link": link,
137
+ "img": img,
138
+ "alt": alt,
139
+ "deadline": deadline,
140
+ "prizes": prizes,
141
+ "entries": entries,
142
+ }
143
+ )
144
+
145
+ closed = []
146
+ for display in soup.select("div.contest-winner-display"):
147
+ link = display.select("div.contest-banner-inner a")[0].get("href")
148
+ img = proxy(display.select("div.contest-banner-inner a img")[0].get("src"))
149
+ alt = display.select("div.contest-banner-inner a img")[0].get("alt")
150
+ featured_items = []
151
+ for featured_item in display.select("ul.featured-items li"):
152
+ item_link = featured_item.select("div.ible-thumb a")[0].get("href")
153
+ item_img = proxy(featured_item.select("div.ible-thumb a img")[0].get("src"))
154
+ item_title = featured_item.select("a.title")[0].text
155
+ item_author = featured_item.select("a.author")[0].text
156
+ item_author_link = featured_item.select("a.author")[0].get("href")
157
+
158
+ featured_items.append(
159
+ {
160
+ "link": item_link,
161
+ "img": item_img,
162
+ "title": item_title,
163
+ "author": item_author,
164
+ "author_link": item_author_link,
165
+ }
166
+ )
167
+ closed.append(
168
+ {"link": link, "img": img, "alt": alt, "featured_items": featured_items}
169
+ )
170
+
171
+ return render_template(
172
+ "contests.html",
173
+ title="Contests",
174
+ contest_count=contest_count,
175
+ contests=contests,
176
+ closed=closed,
177
+ )
@@ -0,0 +1,362 @@
1
+ from flask import render_template, abort
2
+ from urllib.request import urlopen
3
+ from urllib.error import HTTPError
4
+ from bs4 import BeautifulSoup
5
+ from urllib.parse import quote
6
+ from werkzeug.exceptions import InternalServerError
7
+ from traceback import print_exc
8
+ import pathlib
9
+ import json
10
+
11
+ from ..utils.data import update_data
12
+ from ..utils.helpers import explore_lists, proxy
13
+ from .category import project_list
14
+
15
+
16
+ def init_main_routes(app):
17
+ @app.route("/")
18
+ def route_explore():
19
+ try:
20
+ data = urlopen("https://www.instructables.com/")
21
+ except HTTPError as e:
22
+ abort(e.code)
23
+
24
+ soup = BeautifulSoup(data.read().decode(), "html.parser")
25
+
26
+ explore = soup.select(".home-content-explore-wrap")[0]
27
+
28
+ title = explore.select("h2")[0].text
29
+
30
+ circuits = explore_lists(
31
+ explore.select(".home-content-explore-category-circuits")[0]
32
+ )
33
+ workshop = explore_lists(
34
+ explore.select(".home-content-explore-category-workshop")[0]
35
+ )
36
+ craft = explore_lists(explore.select(".home-content-explore-category-craft")[0])
37
+ cooking = explore_lists(
38
+ explore.select(".home-content-explore-category-cooking")[0]
39
+ )
40
+ living = explore_lists(
41
+ explore.select(".home-content-explore-category-living")[0]
42
+ )
43
+ outside = explore_lists(
44
+ explore.select(".home-content-explore-category-outside")[0]
45
+ )
46
+ teachers = explore_lists(
47
+ explore.select(".home-content-explore-category-teachers")[0]
48
+ )
49
+
50
+ return render_template(
51
+ "index.html",
52
+ title=title,
53
+ sections=[
54
+ ("Circuits", "/circuits", circuits),
55
+ ("Workshop", "/workshop", workshop),
56
+ ("Craft", "/craft", craft),
57
+ ("Cooking", "/cooking", cooking),
58
+ ("Living", "/living", living),
59
+ ("Outside", "/outside", outside),
60
+ ("Teachers", "/teachers", teachers),
61
+ ],
62
+ )
63
+
64
+ @app.route("/sitemap/")
65
+ @app.route("/sitemap/<path:path>")
66
+ def route_sitemap(path=""):
67
+ try:
68
+ data = urlopen("https://www.instructables.com/sitemap/" + path)
69
+ except HTTPError as e:
70
+ abort(e.code)
71
+
72
+ soup = BeautifulSoup(data.read().decode(), "html.parser")
73
+
74
+ main = soup.select("div.sitemap-content")[0]
75
+
76
+ group_section = main.select("div.group-section")
77
+
78
+ if group_section:
79
+ groups = []
80
+ for group in group_section:
81
+ category = group.select("h2 a")[0].text
82
+ category_link = group.select("h2 a")[0].get("href")
83
+ channels = []
84
+ for li in group.select("ul.sitemap-listing li"):
85
+ channel = li.a.text
86
+ channel_link = li.a["href"]
87
+ channels.append([channel, channel_link])
88
+ groups.append([category, category_link, channels])
89
+
90
+ else:
91
+ groups = []
92
+ channels = []
93
+ for li in main.select("ul.sitemap-listing li"):
94
+ channel = li.a.text
95
+ channel_link = li.a["href"]
96
+
97
+ if channel_link.startswith("https://"):
98
+ channel_link = f'/{"/".join(channel_link.split("/")[3:])}'
99
+
100
+ channels.append([channel, channel_link])
101
+ groups.append(["", "", channels])
102
+
103
+ return render_template("sitemap.html", title="Sitemap", groups=groups)
104
+
105
+ @app.route("/<article>/")
106
+ def route_article(article):
107
+ try:
108
+ data = urlopen(
109
+ f"https://www.instructables.com/json-api/showInstructableModel?urlString={article}"
110
+ )
111
+ data = json.loads(data.read().decode())
112
+ except HTTPError as e:
113
+ abort(e.code)
114
+
115
+ try:
116
+ title = data["title"]
117
+ author = data["author"]["screenName"]
118
+ author_link = f"/member/{author}"
119
+ category = data["classifications"][0]["title"]
120
+ category_slug = data["classifications"][0]["name"]
121
+ category_link = f"/{category_slug}/"
122
+ channel = data["classifications"][0]["channels"][0]["title"]
123
+ channel_slug = data["classifications"][0]["channels"][0]["name"]
124
+ channel_link = f"/{category_slug}/{channel_slug}/"
125
+
126
+ views = data["views"]
127
+ favorites = data["favorites"]
128
+
129
+ if "steps" in data:
130
+ steps = []
131
+
132
+ if "supplies" in data:
133
+ supplies = data["supplies"]
134
+
135
+ supplies_files = []
136
+
137
+ if "suppliesFiles" in data:
138
+ supplies_files = data["suppliesFiles"]
139
+
140
+ data["steps"].insert(
141
+ 1,
142
+ {
143
+ "title": "Supplies",
144
+ "body": supplies,
145
+ "files": supplies_files,
146
+ },
147
+ )
148
+
149
+ for step in data["steps"]:
150
+ step_title = step["title"]
151
+
152
+ step_imgs = []
153
+ step_videos = [] # TODO: Check if this is still required
154
+ step_iframes = []
155
+ step_downloads = []
156
+
157
+ for file in step["files"]:
158
+ if file["image"] and "embedType" not in "file":
159
+ step_imgs.append(
160
+ {"src": proxy(file["downloadUrl"]), "alt": file["name"]}
161
+ )
162
+
163
+ elif not file["image"]:
164
+ step_downloads.append(
165
+ {
166
+ "src": proxy(file["downloadUrl"]),
167
+ "name": file["name"],
168
+ }
169
+ )
170
+
171
+ else: # Leaves us with embeds
172
+ embed_code = file["embedHtmlCode"]
173
+ soup = BeautifulSoup(embed_code, "html.parser")
174
+
175
+ iframe = soup.select("iframe")[0]
176
+
177
+ src = iframe.get("src")
178
+
179
+ if src.startswith("https://content.instructables.com"):
180
+ src = src.replace(
181
+ "https://content.instructables.com",
182
+ f"/proxy/?url={src}",
183
+ )
184
+
185
+ elif app.config["INVIDIOUS"] and src.startswith(
186
+ "https://www.youtube.com"
187
+ ):
188
+ src = src.replace(
189
+ "https://www.youtube.com", app.config["INVIDIOUS"]
190
+ )
191
+
192
+ elif not app.config["UNSAFE"]:
193
+ src = "/iframe/?url=" + quote(src)
194
+
195
+ step_iframes.append(
196
+ {
197
+ "src": src,
198
+ "width": file.get("width"),
199
+ "height": file.get("height"),
200
+ }
201
+ )
202
+
203
+ step_text = step["body"]
204
+ step_text = step_text.replace(
205
+ "https://content.instructables.com",
206
+ "/proxy/?url=https://content.instructables.com",
207
+ )
208
+ steps.append(
209
+ {
210
+ "title": step_title,
211
+ "imgs": step_imgs,
212
+ "text": step_text,
213
+ "videos": step_videos,
214
+ "iframes": step_iframes,
215
+ "downloads": step_downloads,
216
+ }
217
+ )
218
+
219
+ comments_list = []
220
+ comment_count = 0
221
+
222
+ # TODO: Fix comments
223
+
224
+ # comments = body.select("section.discussion")[0]
225
+
226
+ # comment_count = comments.select("h2")[0].text
227
+ # comment_list = comments.select("div.posts")
228
+
229
+ # if comment_list != []:
230
+ # comment_list = comment_list[0]
231
+ # comments_list = []
232
+ # replies_used = 0
233
+ # for comment in comment_list.select(".post.js-comment:not(.reply)"):
234
+ # comment_votes = comment.select(".votes")[0].text
235
+ # comment_author_img_src = proxy(comment.select(".avatar a noscript img")[0].get("src"))
236
+ # comment_author_img_alt = comment.select(".avatar a noscript img")[0].get("alt")
237
+ # comment_author = comment.select(".posted-by a")[0].text
238
+ # comment_author_link = comment.select(".posted-by a")[0].get("href")
239
+ # comment_date = comment.select(".posted-by p.posted-date")[0].text
240
+ # comment_text = comment.select("div.text p")[0]
241
+ # comment_reply_count = comment.select("button.js-show-replies")
242
+ # if comment_reply_count != []:
243
+ # comment_reply_count = comment_reply_count[0].get("data-num-hidden")
244
+ # else:
245
+ # comment_reply_count = 0
246
+ # reply_list = []
247
+ # for index, reply in enumerate(comment_list.select(".post.js-comment:not(.reply) ~ .post.js-comment.reply.hide:has(~.post.js-comment:not(.reply))")[replies_used:int(comment_reply_count) + replies_used]):
248
+ # reply_votes = reply.select(".votes")[0].text
249
+ # reply_author_img_src = proxy(reply.select(".avatar a noscript img")[0].get("src"))
250
+ # reply_author_img_alt = reply.select(".avatar a noscript img")[0].get("alt")
251
+ # reply_author = reply.select(".posted-by a")[0].text
252
+ # reply_author_link = reply.select(".posted-by a")[0].get("href")
253
+ # reply_date = reply.select(".posted-by p.posted-date")[0].text
254
+ # reply_text = reply.select("div.text p")[0]
255
+
256
+ # reply_list.append([reply_votes, reply_author_img_src, reply_author_img_alt, reply_author, reply_author_link, reply_date, reply_text])
257
+ # replies_used += 1
258
+
259
+ # comments_list.append([comment_votes, comment_author_img_src, comment_author_img_alt, comment_author, comment_author_link, comment_date, comment_text, comment_reply_count, reply_list])
260
+ return render_template(
261
+ "article.html",
262
+ title=title,
263
+ author=author,
264
+ author_link=author_link,
265
+ category=category,
266
+ category_link=category_link,
267
+ channel=channel,
268
+ channel_link=channel_link,
269
+ views=views,
270
+ favorites=favorites,
271
+ steps=steps,
272
+ comment_count=comment_count,
273
+ comments_list=comments_list,
274
+ enumerate=enumerate,
275
+ )
276
+ else:
277
+ ## Collections
278
+ thumbnails = []
279
+ for thumbnail in data["instructables"]:
280
+ text = thumbnail["title"]
281
+ link = thumbnail["showUrl"]
282
+ img = proxy(thumbnail["downloadUrl"])
283
+ thumbnail_title = thumbnail["title"]
284
+ thumbnail_author = thumbnail["author"]["screenName"]
285
+ thumbnail_author_link = f"/member/{thumbnail_author}"
286
+ thumbnail_channel = thumbnail["classifications"][0]["channels"][0][
287
+ "title"
288
+ ]
289
+ thumbnail_category = thumbnail["classifications"][0]["title"]
290
+ thumbnail_channel_link = (
291
+ f"/{thumbnail_category}/{thumbnail_channel}"
292
+ )
293
+
294
+ thumbnails.append(
295
+ {
296
+ "text": text,
297
+ "link": link,
298
+ "img": img,
299
+ "title": thumbnail_title,
300
+ "author": thumbnail_author,
301
+ "author_link": thumbnail_author_link,
302
+ "channel": thumbnail_channel,
303
+ "channel_link": thumbnail_channel_link,
304
+ }
305
+ )
306
+
307
+ return render_template(
308
+ "collection.html",
309
+ title=title,
310
+ author=author,
311
+ author_link=author_link,
312
+ category=category,
313
+ category_link=category_link,
314
+ channel=channel,
315
+ channel_link=channel_link,
316
+ views=views,
317
+ favorites=favorites,
318
+ thumbnails=thumbnails,
319
+ )
320
+
321
+ except Exception:
322
+ print_exc()
323
+ raise InternalServerError()
324
+
325
+ @app.route("/search", methods=["POST", "GET"])
326
+ def route_search():
327
+ return project_list(app, "Search")
328
+
329
+ @app.route("/cron/")
330
+ def cron():
331
+ update_data(app)
332
+ return "OK"
333
+
334
+ @app.route("/privacypolicy/")
335
+ def privacypolicy():
336
+ content = "No privacy policy found."
337
+
338
+ try:
339
+ with (pathlib.Path(__file__).parent / "privacy.txt").open() as f:
340
+ content = f.read()
341
+ except OSError:
342
+ pass
343
+
344
+ return render_template(
345
+ "privacypolicy.html", title="Privacy Policy", content=content
346
+ )
347
+
348
+ @app.errorhandler(404)
349
+ def not_found(e):
350
+ return render_template("404.html"), 404
351
+
352
+ @app.errorhandler(400)
353
+ def bad_request(e):
354
+ return render_template("400.html"), 400
355
+
356
+ @app.errorhandler(429)
357
+ def too_many_requests(e):
358
+ return render_template("429.html"), 429
359
+
360
+ @app.errorhandler(500)
361
+ def internal_server_error(e):
362
+ return render_template("500.html"), 500