structables 0.4.4__tar.gz → 0.4.5__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.4.4 → structables-0.4.5}/.dockerignore +2 -0
- {structables-0.4.4 → structables-0.4.5}/.forgejo/workflows/docker.yml +8 -7
- {structables-0.4.4 → structables-0.4.5}/Dockerfile +4 -1
- {structables-0.4.4 → structables-0.4.5}/PKG-INFO +4 -3
- {structables-0.4.4 → structables-0.4.5}/README.md +0 -1
- {structables-0.4.4 → structables-0.4.5}/instances.json +0 -10
- {structables-0.4.4 → structables-0.4.5}/pyproject.toml +6 -1
- structables-0.4.5/scripts/create_release.py +153 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/routes/category.py +4 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/routes/contest.py +19 -37
- {structables-0.4.4 → structables-0.4.5}/src/structables/routes/main.py +48 -72
- structables-0.4.5/src/structables/routes/member.py +91 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/routes/proxy.py +28 -31
- {structables-0.4.4 → structables-0.4.5}/src/structables/static/css/style.css +163 -47
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/archives.html +1 -1
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/article.html +12 -8
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/category.html +2 -2
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/collection.html +2 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/contest.html +3 -3
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/contests.html +1 -1
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/index.html +1 -1
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/member-instructables.html +13 -4
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/member.html +8 -6
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/projects.html +1 -1
- structables-0.4.5/src/structables/templates/sitemap.html +19 -0
- structables-0.4.5/src/structables/utils/cache.py +23 -0
- structables-0.4.5/src/structables/utils/data.py +87 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/utils/helpers.py +62 -101
- structables-0.4.4/src/structables/routes/member.py +0 -166
- structables-0.4.4/src/structables/templates/sitemap.html +0 -27
- structables-0.4.4/src/structables/utils/data.py +0 -99
- {structables-0.4.4 → structables-0.4.5}/.env.example +0 -0
- {structables-0.4.4 → structables-0.4.5}/.forgejo/workflows/release.yml +0 -0
- {structables-0.4.4 → structables-0.4.5}/.gitignore +0 -0
- {structables-0.4.4 → structables-0.4.5}/LICENSE +0 -0
- {structables-0.4.4 → structables-0.4.5}/cronjob.txt +0 -0
- {structables-0.4.4 → structables-0.4.5}/docker-compose-example-dev.yml +0 -0
- {structables-0.4.4 → structables-0.4.5}/docker-compose-example.yml +0 -0
- {structables-0.4.4 → structables-0.4.5}/entrypoint.sh +0 -0
- {structables-0.4.4 → structables-0.4.5}/renovate.json +0 -0
- {structables-0.4.4 → structables-0.4.5}/requirements-dev.txt +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/__init__.py +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/config.py +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/main.py +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/routes/__init__.py +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/static/css/iframe.css +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/static/img/logo.png +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/static/img/logo_lg.png +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/400.html +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/404.html +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/429.html +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/500.html +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/article-review.html +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/base.html +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/footer.html +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/header.html +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/iframe.html +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/privacypolicy.html +0 -0
- {structables-0.4.4 → structables-0.4.5}/src/structables/templates/style.html +0 -0
|
@@ -8,7 +8,7 @@ on:
|
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
docker:
|
|
11
|
-
name: Docker Build and Push to
|
|
11
|
+
name: Docker Build and Push to git.private.coffee
|
|
12
12
|
runs-on: docker-privileged
|
|
13
13
|
container:
|
|
14
14
|
image: node:22-bookworm
|
|
@@ -23,15 +23,16 @@ jobs:
|
|
|
23
23
|
uses: docker/setup-qemu-action@v3
|
|
24
24
|
- name: Set up Docker Buildx
|
|
25
25
|
uses: docker/setup-buildx-action@v3
|
|
26
|
-
- name: Login to
|
|
26
|
+
- name: Login to Forgejo
|
|
27
27
|
uses: docker/login-action@v3
|
|
28
28
|
with:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
registry: git.private.coffee
|
|
30
|
+
username: ${{ secrets.FORGEJO_USER }}
|
|
31
|
+
password: ${{ secrets.FORGEJO_TOKEN }}
|
|
32
|
+
- name: Build and push to Forgejo
|
|
32
33
|
uses: docker/build-push-action@v6
|
|
33
34
|
with:
|
|
34
35
|
push: true
|
|
35
36
|
tags: |
|
|
36
|
-
privatecoffee/structables:latest
|
|
37
|
-
privatecoffee/structables:${{
|
|
37
|
+
git.private.coffee/privatecoffee/structables:latest
|
|
38
|
+
git.private.coffee/privatecoffee/structables:${{ env.GITHUB_REF_NAME }}
|
|
@@ -11,10 +11,13 @@ COPY . /app
|
|
|
11
11
|
|
|
12
12
|
RUN $APP_ENV/bin/pip install --no-cache-dir pip && \
|
|
13
13
|
$APP_ENV/bin/pip install /app && \
|
|
14
|
-
adduser -S -D -H structables
|
|
14
|
+
adduser -S -D -H structables && \
|
|
15
|
+
mkdir -p /tmp/structables_cache && chown structables /tmp/structables_cache
|
|
15
16
|
|
|
16
17
|
EXPOSE 8002
|
|
17
18
|
|
|
18
19
|
USER structables
|
|
19
20
|
|
|
21
|
+
STOPSIGNAL SIGQUIT
|
|
22
|
+
|
|
20
23
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: structables
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
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
|
|
@@ -675,6 +675,8 @@ Requires-Python: >=3.10
|
|
|
675
675
|
Requires-Dist: beautifulsoup4
|
|
676
676
|
Requires-Dist: flask
|
|
677
677
|
Requires-Dist: markdown2[all]
|
|
678
|
+
Provides-Extra: dev
|
|
679
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
678
680
|
Description-Content-Type: text/markdown
|
|
679
681
|
|
|
680
682
|
<img align="right" src="src/structables/static/img/logo.png">
|
|
@@ -699,7 +701,6 @@ An open source alternative front-end to Instructables. This is a fork of <a href
|
|
|
699
701
|
| [structables.private.coffee](https://structables.private.coffee) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
|
|
700
702
|
| [structables.bloat.cat](https://structables.bloat.cat) | [Bloat.cat](https://bloat.cat) | Germany 🇩🇪 🇪🇺 | |
|
|
701
703
|
| [structables.darkness.services](https://structables.darkness.services) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
|
|
702
|
-
| [structables.franklyflawless.org](https://structables.franklyflawless.org) | [FranklyFlawless](https://franklyflawless.org) | Germany 🇩🇪 🇪🇺 | |
|
|
703
704
|
|
|
704
705
|
<!-- END_INSTANCE_LIST -->
|
|
705
706
|
|
|
@@ -20,7 +20,6 @@ An open source alternative front-end to Instructables. This is a fork of <a href
|
|
|
20
20
|
| [structables.private.coffee](https://structables.private.coffee) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
|
|
21
21
|
| [structables.bloat.cat](https://structables.bloat.cat) | [Bloat.cat](https://bloat.cat) | Germany 🇩🇪 🇪🇺 | |
|
|
22
22
|
| [structables.darkness.services](https://structables.darkness.services) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
|
|
23
|
-
| [structables.franklyflawless.org](https://structables.franklyflawless.org) | [FranklyFlawless](https://franklyflawless.org) | Germany 🇩🇪 🇪🇺 | |
|
|
24
23
|
|
|
25
24
|
<!-- END_INSTANCE_LIST -->
|
|
26
25
|
|
|
@@ -50,15 +50,5 @@
|
|
|
50
50
|
"url": "https://darkness.services"
|
|
51
51
|
},
|
|
52
52
|
"location": "US"
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"name": "structables.franklyflawless.org",
|
|
56
|
-
"url": "https://structables.franklyflawless.org",
|
|
57
|
-
"type": "clearnet",
|
|
58
|
-
"provider": {
|
|
59
|
-
"name": "FranklyFlawless",
|
|
60
|
-
"url": "https://franklyflawless.org"
|
|
61
|
-
},
|
|
62
|
-
"location": "DE"
|
|
63
53
|
}
|
|
64
54
|
]
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "structables"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.5"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Private.coffee Team", email="support@private.coffee" },
|
|
10
10
|
]
|
|
@@ -23,6 +23,11 @@ dependencies = [
|
|
|
23
23
|
"markdown2[all]",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = [
|
|
28
|
+
"ruff",
|
|
29
|
+
]
|
|
30
|
+
|
|
26
31
|
[project.scripts]
|
|
27
32
|
structables = "structables.main:main"
|
|
28
33
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
VERSION_PATTERN = re.compile(
|
|
10
|
+
r'(?m)^(version\s*=\s*")(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(")$'
|
|
11
|
+
)
|
|
12
|
+
PRE_RELEASE_COMMANDS = (
|
|
13
|
+
("-m", "ruff", "check", "src", "scripts"),
|
|
14
|
+
("-m", "ruff", "format", "--check", "src", "scripts"),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_args() -> argparse.Namespace:
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
description=(
|
|
21
|
+
"Bump the project version in pyproject.toml and create a release commit "
|
|
22
|
+
"and tag."
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
26
|
+
group.add_argument(
|
|
27
|
+
"--major",
|
|
28
|
+
action="store_true",
|
|
29
|
+
help="Bump the major version and reset minor/patch.",
|
|
30
|
+
)
|
|
31
|
+
group.add_argument(
|
|
32
|
+
"--minor", action="store_true", help="Bump the minor version and reset patch."
|
|
33
|
+
)
|
|
34
|
+
group.add_argument("--patch", action="store_true", help="Bump the patch version.")
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--skip-checks",
|
|
37
|
+
"--skip-lint",
|
|
38
|
+
dest="skip_checks",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="Skip the pre-release checks.",
|
|
41
|
+
)
|
|
42
|
+
return parser.parse_args()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_git(*args: str, repo_root: Path) -> str:
|
|
46
|
+
result = subprocess.run(
|
|
47
|
+
["git", *args],
|
|
48
|
+
cwd=repo_root,
|
|
49
|
+
check=True,
|
|
50
|
+
text=True,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
)
|
|
53
|
+
return result.stdout.strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run_python(*args: str, repo_root: Path) -> None:
|
|
57
|
+
subprocess.run(
|
|
58
|
+
[sys.executable, *args],
|
|
59
|
+
cwd=repo_root,
|
|
60
|
+
check=True,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ensure_clean_worktree(repo_root: Path) -> None:
|
|
65
|
+
status = run_git("status", "--short", repo_root=repo_root)
|
|
66
|
+
if status:
|
|
67
|
+
raise SystemExit("Working tree is not clean. Commit or stash changes first.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_version(pyproject_path: Path) -> tuple[str, tuple[int, int, int]]:
|
|
71
|
+
content = pyproject_path.read_text(encoding="utf-8")
|
|
72
|
+
match = VERSION_PATTERN.search(content)
|
|
73
|
+
if not match:
|
|
74
|
+
raise SystemExit("Could not find [project] version in pyproject.toml.")
|
|
75
|
+
version = (
|
|
76
|
+
int(match.group("major")),
|
|
77
|
+
int(match.group("minor")),
|
|
78
|
+
int(match.group("patch")),
|
|
79
|
+
)
|
|
80
|
+
return content, version
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def bump_version(
|
|
84
|
+
version: tuple[int, int, int], args: argparse.Namespace
|
|
85
|
+
) -> tuple[int, int, int]:
|
|
86
|
+
major, minor, patch = version
|
|
87
|
+
if args.major:
|
|
88
|
+
return major + 1, 0, 0
|
|
89
|
+
if args.minor:
|
|
90
|
+
return major, minor + 1, 0
|
|
91
|
+
return major, minor, patch + 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def write_version(
|
|
95
|
+
pyproject_path: Path, content: str, new_version: tuple[int, int, int]
|
|
96
|
+
) -> str:
|
|
97
|
+
version_text = ".".join(str(part) for part in new_version)
|
|
98
|
+
updated = VERSION_PATTERN.sub(rf"\g<1>{version_text}\g<5>", content, count=1)
|
|
99
|
+
pyproject_path.write_text(updated, encoding="utf-8")
|
|
100
|
+
return version_text
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def ensure_tag_does_not_exist(repo_root: Path, tag_name: str) -> None:
|
|
104
|
+
if run_git("tag", "--list", tag_name, repo_root=repo_root):
|
|
105
|
+
raise SystemExit(f"Tag {tag_name} already exists.")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def ensure_pre_release_checks_pass(repo_root: Path) -> None:
|
|
109
|
+
for command in PRE_RELEASE_COMMANDS:
|
|
110
|
+
try:
|
|
111
|
+
run_python(*command, repo_root=repo_root)
|
|
112
|
+
except subprocess.CalledProcessError as exc:
|
|
113
|
+
raise SystemExit(
|
|
114
|
+
"Pre-release checks failed. Fix the errors or re-run with --skip-checks."
|
|
115
|
+
) from exc
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def create_release_commit(repo_root: Path, tag_name: str) -> None:
|
|
119
|
+
run_git("add", "pyproject.toml", repo_root=repo_root)
|
|
120
|
+
run_git("commit", "-m", f"Release {tag_name}", repo_root=repo_root)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def create_tag(repo_root: Path, tag_name: str) -> None:
|
|
124
|
+
run_git("tag", "-m", f"Release {tag_name}", tag_name, repo_root=repo_root)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main() -> int:
|
|
128
|
+
args = parse_args()
|
|
129
|
+
repo_root = Path(__file__).resolve().parent.parent
|
|
130
|
+
pyproject_path = repo_root / "pyproject.toml"
|
|
131
|
+
|
|
132
|
+
ensure_clean_worktree(repo_root)
|
|
133
|
+
if not args.skip_checks:
|
|
134
|
+
ensure_pre_release_checks_pass(repo_root)
|
|
135
|
+
content, current_version = load_version(pyproject_path)
|
|
136
|
+
new_version = bump_version(current_version, args)
|
|
137
|
+
version_text = ".".join(str(part) for part in new_version)
|
|
138
|
+
tag_name = f"v{version_text}"
|
|
139
|
+
ensure_tag_does_not_exist(repo_root, tag_name)
|
|
140
|
+
write_version(pyproject_path, content, new_version)
|
|
141
|
+
create_release_commit(repo_root, tag_name)
|
|
142
|
+
create_tag(repo_root, tag_name)
|
|
143
|
+
print(f"Released {tag_name}")
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
try:
|
|
149
|
+
raise SystemExit(main())
|
|
150
|
+
except subprocess.CalledProcessError as exc:
|
|
151
|
+
if exc.stderr:
|
|
152
|
+
print(exc.stderr.strip(), file=sys.stderr)
|
|
153
|
+
raise SystemExit(exc.returncode) from exc
|
|
@@ -58,6 +58,10 @@ def init_category_routes(app):
|
|
|
58
58
|
logger.debug("Rendering craft category page")
|
|
59
59
|
return category_page(app, "Craft")
|
|
60
60
|
|
|
61
|
+
@app.route("/design/")
|
|
62
|
+
def route_design():
|
|
63
|
+
return category_page(app, "Design")
|
|
64
|
+
|
|
61
65
|
@app.route("/cooking/")
|
|
62
66
|
def route_cooking():
|
|
63
67
|
logger.debug("Rendering cooking category page")
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from flask import render_template, request, abort, url_for
|
|
2
|
-
from urllib.request import urlopen, Request
|
|
3
2
|
from urllib.error import HTTPError
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
import
|
|
3
|
+
from urllib.parse import urlencode
|
|
4
|
+
from ..utils.cache import get_cached_json
|
|
5
|
+
from ..utils.helpers import proxy, thumbnail
|
|
7
6
|
import logging
|
|
8
7
|
|
|
9
8
|
logger = logging.getLogger(__name__)
|
|
@@ -20,11 +19,9 @@ def init_contest_routes(app):
|
|
|
20
19
|
logger.debug(f"Fetching contest archive page {page} with limit {limit}")
|
|
21
20
|
|
|
22
21
|
try:
|
|
23
|
-
# Fetch data using urlopen
|
|
24
22
|
url = f"https://www.instructables.com/json-api/getClosedContests?limit={limit}&offset={offset}"
|
|
25
23
|
logger.debug(f"Making request to {url}")
|
|
26
|
-
|
|
27
|
-
data = json.loads(response.read().decode())
|
|
24
|
+
data = get_cached_json(app, url)
|
|
28
25
|
logger.debug(
|
|
29
26
|
f"Received contest archive data with {len(data.get('contests', []))} contests"
|
|
30
27
|
)
|
|
@@ -85,9 +82,7 @@ def init_contest_routes(app):
|
|
|
85
82
|
try:
|
|
86
83
|
url = f"{base_url}?q=*&filter_by=contestPath:{contest}&sort_by=contestEntryDate:desc&per_page={per_page}&page={page}"
|
|
87
84
|
logger.debug(f"Making request to {url} (page {page})")
|
|
88
|
-
|
|
89
|
-
response = urlopen(request)
|
|
90
|
-
data = json.loads(response.read().decode())
|
|
85
|
+
data = get_cached_json(app, url, headers=headers)
|
|
91
86
|
except HTTPError as e:
|
|
92
87
|
logger.error(f"HTTP error fetching contest entries: {e.code}")
|
|
93
88
|
abort(e.code)
|
|
@@ -99,7 +94,7 @@ def init_contest_routes(app):
|
|
|
99
94
|
break
|
|
100
95
|
|
|
101
96
|
all_entries.extend(hits)
|
|
102
|
-
if len(
|
|
97
|
+
if len(all_entries) >= data["found"]:
|
|
103
98
|
break
|
|
104
99
|
page += 1
|
|
105
100
|
|
|
@@ -111,32 +106,20 @@ def init_contest_routes(app):
|
|
|
111
106
|
logger.debug(f"Fetching contest page for: {contest}")
|
|
112
107
|
|
|
113
108
|
try:
|
|
114
|
-
data =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
title = title_tag.get_text() if title_tag else "Contest"
|
|
109
|
+
data = get_cached_json(
|
|
110
|
+
app,
|
|
111
|
+
"https://www.instructables.com/json-api/contest?" + urlencode({"path": contest}),
|
|
112
|
+
)
|
|
113
|
+
title = data["title"]
|
|
120
114
|
logger.debug(f"Contest title: {title}")
|
|
121
115
|
|
|
122
|
-
|
|
123
|
-
img = img_tag.get("src") if img_tag else "default.jpg"
|
|
116
|
+
img = proxy(data["bannerUrl"])
|
|
124
117
|
|
|
125
118
|
logger.debug(f"Fetching entries for contest: {contest}")
|
|
126
119
|
entries = get_entries(contest)
|
|
127
|
-
entry_count =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
prizes_items = soup.select("article")
|
|
131
|
-
prizes = len(prizes_items) if prizes_items else 0
|
|
132
|
-
logger.debug(f"Found {prizes} prizes")
|
|
133
|
-
|
|
134
|
-
overview_section = soup.find("section", id="overview")
|
|
135
|
-
info = (
|
|
136
|
-
overview_section.decode_contents()
|
|
137
|
-
if overview_section
|
|
138
|
-
else "No Overview"
|
|
139
|
-
)
|
|
120
|
+
entry_count = data["numEntries"]
|
|
121
|
+
prizes = data["prizeCount"]
|
|
122
|
+
info = data.get("bodyFull", "")
|
|
140
123
|
|
|
141
124
|
except HTTPError as e:
|
|
142
125
|
logger.error(f"HTTP error fetching contest page: {e.code}")
|
|
@@ -147,11 +130,11 @@ def init_contest_routes(app):
|
|
|
147
130
|
doc = entry["document"]
|
|
148
131
|
entry_details = {
|
|
149
132
|
"link": url_for("route_article", article=doc["urlString"]),
|
|
150
|
-
"entry_img": doc["coverImageUrl"],
|
|
133
|
+
"entry_img": thumbnail(doc["coverImageUrl"]),
|
|
151
134
|
"entry_title": doc["title"],
|
|
152
135
|
"author": doc["screenName"],
|
|
153
136
|
"author_link": url_for("route_member", member=doc["screenName"]),
|
|
154
|
-
"channel": doc["channel"][0],
|
|
137
|
+
"channel": doc["channel"][0] if doc.get("channel") else "",
|
|
155
138
|
"channel_link": f"/{doc['primaryClassification']}",
|
|
156
139
|
"views": doc.get("views", 0),
|
|
157
140
|
}
|
|
@@ -173,10 +156,9 @@ def init_contest_routes(app):
|
|
|
173
156
|
|
|
174
157
|
try:
|
|
175
158
|
# Fetch current contests from the JSON API
|
|
176
|
-
|
|
177
|
-
"https://www.instructables.com/json-api/getCurrentContests?limit=50&offset=0"
|
|
159
|
+
data = get_cached_json(
|
|
160
|
+
app, "https://www.instructables.com/json-api/getCurrentContests?limit=50&offset=0"
|
|
178
161
|
)
|
|
179
|
-
data = json.loads(response.read().decode())
|
|
180
162
|
logger.debug("Received current contests data")
|
|
181
163
|
except HTTPError as e:
|
|
182
164
|
logger.error(f"HTTP error fetching current contests: {e.code}")
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from flask import render_template, abort, request
|
|
2
|
-
from urllib.request import urlopen
|
|
3
2
|
from urllib.error import HTTPError
|
|
4
3
|
from bs4 import BeautifulSoup
|
|
5
4
|
from urllib.parse import quote
|
|
@@ -8,11 +7,11 @@ from markdown2 import Markdown
|
|
|
8
7
|
from traceback import print_exc
|
|
9
8
|
|
|
10
9
|
import pathlib
|
|
11
|
-
import json
|
|
12
10
|
import logging
|
|
13
11
|
|
|
12
|
+
from ..utils.cache import get_cached_json
|
|
14
13
|
from ..utils.data import update_data
|
|
15
|
-
from ..utils.helpers import explore_lists, proxy
|
|
14
|
+
from ..utils.helpers import explore_lists, proxy, project_facets, projects_search, unslugify, thumbnail
|
|
16
15
|
from .category import project_list
|
|
17
16
|
from ..utils.data import maybe_update_data
|
|
18
17
|
|
|
@@ -26,21 +25,6 @@ def init_main_routes(app):
|
|
|
26
25
|
maybe_update_data(app)
|
|
27
26
|
|
|
28
27
|
try:
|
|
29
|
-
# Extract API endpoints from the JSON context data
|
|
30
|
-
logger.debug("Fetching data from instructables.com API")
|
|
31
|
-
data = urlopen("https://www.instructables.com/")
|
|
32
|
-
soup = BeautifulSoup(data.read().decode(), "html.parser")
|
|
33
|
-
|
|
34
|
-
# Try to extract JSON data from script tag
|
|
35
|
-
page_context = None
|
|
36
|
-
script_tag = soup.select("#js-page-context")
|
|
37
|
-
if script_tag:
|
|
38
|
-
try:
|
|
39
|
-
page_context = json.loads(script_tag[0].string)
|
|
40
|
-
logger.debug("Successfully extracted page context JSON")
|
|
41
|
-
except Exception as e:
|
|
42
|
-
logger.error(f"Error parsing page context JSON: {str(e)}")
|
|
43
|
-
|
|
44
28
|
# Use the API endpoints to fetch category data
|
|
45
29
|
categories = [
|
|
46
30
|
"circuits",
|
|
@@ -62,8 +46,7 @@ def init_main_routes(app):
|
|
|
62
46
|
f"Fetching data for category {category} from {api_url}"
|
|
63
47
|
)
|
|
64
48
|
|
|
65
|
-
|
|
66
|
-
category_json = json.loads(category_data.read().decode())
|
|
49
|
+
category_json = get_cached_json(app, api_url)
|
|
67
50
|
|
|
68
51
|
# Process category items
|
|
69
52
|
category_items = []
|
|
@@ -73,7 +56,7 @@ def init_main_routes(app):
|
|
|
73
56
|
"title": item["title"],
|
|
74
57
|
"link": item["url"],
|
|
75
58
|
"img": (
|
|
76
|
-
|
|
59
|
+
thumbnail(item["downloadUrl"])
|
|
77
60
|
if "downloadUrl" in item
|
|
78
61
|
else ""
|
|
79
62
|
),
|
|
@@ -132,50 +115,41 @@ def init_main_routes(app):
|
|
|
132
115
|
def route_sitemap(path=""):
|
|
133
116
|
logger.debug(f"Rendering sitemap for path: {path}")
|
|
134
117
|
|
|
118
|
+
parts = path.strip("/").split("/") if path.strip("/") else []
|
|
119
|
+
if len(parts) > 2:
|
|
120
|
+
abort(404)
|
|
121
|
+
|
|
135
122
|
try:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
123
|
+
categories = project_facets(app)["category"]
|
|
124
|
+
if parts:
|
|
125
|
+
categories = [name for name in categories if name.lower() == parts[0].lower()]
|
|
126
|
+
if not categories:
|
|
127
|
+
abort(404)
|
|
128
|
+
|
|
129
|
+
groups = []
|
|
130
|
+
for category in categories:
|
|
131
|
+
category_path = f"/{category.lower()}/"
|
|
132
|
+
if len(parts) == 2:
|
|
133
|
+
for channel in unslugify(parts[1]):
|
|
134
|
+
hits, _ = projects_search(app, category=category, channel=channel)
|
|
135
|
+
if hits:
|
|
136
|
+
break
|
|
137
|
+
channels = [
|
|
138
|
+
[hit["document"]["title"], f'/{hit["document"]["urlString"]}']
|
|
139
|
+
for hit in hits
|
|
140
|
+
]
|
|
141
|
+
else:
|
|
142
|
+
channels = [[f"All {category} Projects", category_path + "projects/"]]
|
|
143
|
+
for channel in project_facets(app, category)["channel"]:
|
|
144
|
+
slug = quote(channel.lower().replace(" ", "-"), safe="")
|
|
145
|
+
channels.append([channel, f"{category_path}{slug}/projects/"])
|
|
146
|
+
groups.append([category, category_path, channels])
|
|
147
|
+
if not parts:
|
|
148
|
+
groups.append(["Teachers", "/teachers/", [["All Teachers Projects", "/teachers/projects/"]]])
|
|
140
149
|
except HTTPError as e:
|
|
141
150
|
logger.error(f"HTTP error fetching sitemap: {e.code}")
|
|
142
151
|
abort(e.code)
|
|
143
152
|
|
|
144
|
-
soup = BeautifulSoup(data.read().decode(), "html.parser")
|
|
145
|
-
|
|
146
|
-
main = soup.select("div.sitemap-content")[0]
|
|
147
|
-
|
|
148
|
-
group_section = main.select("div.group-section")
|
|
149
|
-
|
|
150
|
-
if group_section:
|
|
151
|
-
logger.debug(f"Found {len(group_section)} group sections")
|
|
152
|
-
groups = []
|
|
153
|
-
for group in group_section:
|
|
154
|
-
category = group.select("h2 a")[0].text
|
|
155
|
-
category_link = group.select("h2 a")[0].get("href")
|
|
156
|
-
channels = []
|
|
157
|
-
for li in group.select("ul.sitemap-listing li"):
|
|
158
|
-
channel = li.a.text
|
|
159
|
-
channel_link = li.a["href"]
|
|
160
|
-
channels.append([channel, channel_link])
|
|
161
|
-
groups.append([category, category_link, channels])
|
|
162
|
-
logger.debug(f"Added group {category} with {len(channels)} channels")
|
|
163
|
-
|
|
164
|
-
else:
|
|
165
|
-
logger.debug("No group sections found, using flat list")
|
|
166
|
-
groups = []
|
|
167
|
-
channels = []
|
|
168
|
-
for li in main.select("ul.sitemap-listing li"):
|
|
169
|
-
channel = li.a.text
|
|
170
|
-
channel_link = li.a["href"]
|
|
171
|
-
|
|
172
|
-
if channel_link.startswith("https://"):
|
|
173
|
-
channel_link = f'/{"/".join(channel_link.split("/")[3:])}'
|
|
174
|
-
|
|
175
|
-
channels.append([channel, channel_link])
|
|
176
|
-
groups.append(["", "", channels])
|
|
177
|
-
logger.debug(f"Added flat list with {len(channels)} channels")
|
|
178
|
-
|
|
179
153
|
return render_template("sitemap.html", title="Sitemap", groups=groups)
|
|
180
154
|
|
|
181
155
|
@app.route("/<article>/")
|
|
@@ -185,10 +159,10 @@ def init_main_routes(app):
|
|
|
185
159
|
try:
|
|
186
160
|
logger.debug(f"Fetching article data from instructables.com for: {article}")
|
|
187
161
|
article_path = quote(article)
|
|
188
|
-
data =
|
|
189
|
-
|
|
162
|
+
data = get_cached_json(
|
|
163
|
+
app,
|
|
164
|
+
f"https://www.instructables.com/json-api/showInstructableModel?urlString={article_path}",
|
|
190
165
|
)
|
|
191
|
-
data = json.loads(data.read().decode())
|
|
192
166
|
logger.debug("Successfully fetched article data")
|
|
193
167
|
except HTTPError as e:
|
|
194
168
|
logger.error(f"HTTP error fetching article: {e.code}")
|
|
@@ -246,8 +220,10 @@ def init_main_routes(app):
|
|
|
246
220
|
if "embedType" not in "file":
|
|
247
221
|
step_imgs.append(
|
|
248
222
|
{
|
|
249
|
-
"src":
|
|
223
|
+
"src": thumbnail(file["downloadUrl"], 1024),
|
|
250
224
|
"alt": file["name"],
|
|
225
|
+
"width": file.get("width"),
|
|
226
|
+
"height": file.get("height"),
|
|
251
227
|
}
|
|
252
228
|
)
|
|
253
229
|
if file["embedType"] == "VIDEO":
|
|
@@ -393,17 +369,17 @@ def init_main_routes(app):
|
|
|
393
369
|
# Collections
|
|
394
370
|
logger.debug("Article is a collection")
|
|
395
371
|
thumbnails = []
|
|
396
|
-
for
|
|
397
|
-
text =
|
|
398
|
-
link =
|
|
399
|
-
img =
|
|
400
|
-
thumbnail_title =
|
|
401
|
-
thumbnail_author =
|
|
372
|
+
for item in data["instructables"]:
|
|
373
|
+
text = item["title"]
|
|
374
|
+
link = item["showUrl"]
|
|
375
|
+
img = thumbnail(item["downloadUrl"])
|
|
376
|
+
thumbnail_title = item["title"]
|
|
377
|
+
thumbnail_author = item["author"]["screenName"]
|
|
402
378
|
thumbnail_author_link = f"/member/{thumbnail_author}"
|
|
403
|
-
thumbnail_channel =
|
|
379
|
+
thumbnail_channel = item["classifications"][0]["channels"][0][
|
|
404
380
|
"title"
|
|
405
381
|
]
|
|
406
|
-
thumbnail_category =
|
|
382
|
+
thumbnail_category = item["classifications"][0]["title"]
|
|
407
383
|
thumbnail_channel_link = (
|
|
408
384
|
f"/{thumbnail_category}/{thumbnail_channel}"
|
|
409
385
|
)
|