structables 0.2.1__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.1 → structables-0.3.0}/PKG-INFO +8 -2
- {structables-0.2.1 → structables-0.3.0}/README.md +7 -1
- {structables-0.2.1 → 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.2.1 → structables-0.3.0}/src/structables/static/css/style.css +31 -0
- structables-0.3.0/src/structables/templates/sitemap.html +27 -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.1/src/structables/main.py +0 -1282
- structables-0.2.1/src/structables/templates/sitemap.html +0 -23
- {structables-0.2.1 → structables-0.3.0}/.gitignore +0 -0
- {structables-0.2.1 → structables-0.3.0}/LICENSE +0 -0
- {structables-0.2.1 → structables-0.3.0}/cronjob.txt +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/__init__.py +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/static/dist/css/bootstrap.min.css +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/static/dist/css/bootstrap.min.css.map +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/static/dist/js/bootstrap.bundle.min.js +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/static/dist/js/bootstrap.bundle.min.js.map +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/static/img/logo.png +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/static/img/logo_lg.png +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/400.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/404.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/429.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/500.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/archives.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/article-review.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/article.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/base.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/category.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/collection.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/contest.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/contests.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/footer.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/header.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/iframe.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/index.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/member-instructables.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/member.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/privacypolicy.html +0 -0
- {structables-0.2.1 → structables-0.3.0}/src/structables/templates/projects.html +0 -0
- {structables-0.2.1 → 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
|
|
@@ -683,7 +683,7 @@ Description-Content-Type: text/markdown
|
|
|
683
683
|
An open source alternative front-end to Instructables. This is a fork of <a href="https://codeberg.org/indestructables/indestructables">snowcatridge10's Indestructables</a> to get rid of Selenium. Indestructables itself is a fork of <a href="https://git.vern.cc/cobra/Destructables">Cobra's Destructables</a>.
|
|
684
684
|
|
|
685
685
|
[](https://private.coffee)
|
|
686
|
-
[](https://matrix.
|
|
686
|
+
[](https://matrix.pcof.fi/#/#structables:private.coffee)
|
|
687
687
|
[](https://pypi.org/project/structables/)
|
|
688
688
|
[](https://pypi.org/project/structables/)
|
|
689
689
|
[](https://pypi.org/project/structables/)
|
|
@@ -697,6 +697,12 @@ An open source alternative front-end to Instructables. This is a fork of <a href
|
|
|
697
697
|
|
|
698
698
|
To add your own instance to this list, please open a pull request or issue.
|
|
699
699
|
|
|
700
|
+
## Opening Issues
|
|
701
|
+
|
|
702
|
+
If you're having problems using Structables, or if you have ideas or feedback for us, feel free to open an issue in the [Private.coffee Git](https://git.private.coffee/PrivateCoffee/structables/issues) or on [Github](https://github.com/PrivateCoffee/structables/issues).
|
|
703
|
+
|
|
704
|
+
Of course, you can also join our [Matrix room](https://matrix.pcof.fi/#/#structables:private.coffee) to discuss your ideas with us.
|
|
705
|
+
|
|
700
706
|
## Run your own instance
|
|
701
707
|
|
|
702
708
|
### Production
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
An open source alternative front-end to Instructables. This is a fork of <a href="https://codeberg.org/indestructables/indestructables">snowcatridge10's Indestructables</a> to get rid of Selenium. Indestructables itself is a fork of <a href="https://git.vern.cc/cobra/Destructables">Cobra's Destructables</a>.
|
|
6
6
|
|
|
7
7
|
[](https://private.coffee)
|
|
8
|
-
[](https://matrix.
|
|
8
|
+
[](https://matrix.pcof.fi/#/#structables:private.coffee)
|
|
9
9
|
[](https://pypi.org/project/structables/)
|
|
10
10
|
[](https://pypi.org/project/structables/)
|
|
11
11
|
[](https://pypi.org/project/structables/)
|
|
@@ -19,6 +19,12 @@ An open source alternative front-end to Instructables. This is a fork of <a href
|
|
|
19
19
|
|
|
20
20
|
To add your own instance to this list, please open a pull request or issue.
|
|
21
21
|
|
|
22
|
+
## Opening Issues
|
|
23
|
+
|
|
24
|
+
If you're having problems using Structables, or if you have ideas or feedback for us, feel free to open an issue in the [Private.coffee Git](https://git.private.coffee/PrivateCoffee/structables/issues) or on [Github](https://github.com/PrivateCoffee/structables/issues).
|
|
25
|
+
|
|
26
|
+
Of course, you can also join our [Matrix room](https://matrix.pcof.fi/#/#structables:private.coffee) to discuss your ideas with us.
|
|
27
|
+
|
|
22
28
|
## Run your own instance
|
|
23
29
|
|
|
24
30
|
### Production
|
|
@@ -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
|
+
)
|