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.
- {structables-0.2.2 → structables-0.3.0}/PKG-INFO +1 -1
- {structables-0.2.2 → structables-0.3.0}/pyproject.toml +1 -1
- structables-0.3.0/src/structables/config.py +13 -0
- structables-0.3.0/src/structables/main.py +20 -0
- structables-0.3.0/src/structables/routes/__init__.py +12 -0
- structables-0.3.0/src/structables/routes/category.py +76 -0
- structables-0.3.0/src/structables/routes/contest.py +177 -0
- structables-0.3.0/src/structables/routes/main.py +362 -0
- structables-0.3.0/src/structables/routes/member.py +124 -0
- structables-0.3.0/src/structables/routes/proxy.py +50 -0
- structables-0.3.0/src/structables/utils/data.py +56 -0
- structables-0.3.0/src/structables/utils/helpers.py +451 -0
- structables-0.2.2/src/structables/main.py +0 -1313
- {structables-0.2.2 → structables-0.3.0}/.gitignore +0 -0
- {structables-0.2.2 → structables-0.3.0}/LICENSE +0 -0
- {structables-0.2.2 → structables-0.3.0}/README.md +0 -0
- {structables-0.2.2 → structables-0.3.0}/cronjob.txt +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/__init__.py +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/static/css/style.css +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/static/dist/css/bootstrap.min.css +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/static/dist/css/bootstrap.min.css.map +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/static/dist/js/bootstrap.bundle.min.js +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/static/dist/js/bootstrap.bundle.min.js.map +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/static/img/logo.png +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/static/img/logo_lg.png +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/400.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/404.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/429.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/500.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/archives.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/article-review.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/article.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/base.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/category.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/collection.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/contest.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/contests.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/footer.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/header.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/iframe.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/index.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/member-instructables.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/member.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/privacypolicy.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/projects.html +0 -0
- {structables-0.2.2 → structables-0.3.0}/src/structables/templates/sitemap.html +0 -0
- {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.
|
|
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
|
|
@@ -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
|