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.
Files changed (60) hide show
  1. {structables-0.4.4 → structables-0.4.5}/.dockerignore +2 -0
  2. {structables-0.4.4 → structables-0.4.5}/.forgejo/workflows/docker.yml +8 -7
  3. {structables-0.4.4 → structables-0.4.5}/Dockerfile +4 -1
  4. {structables-0.4.4 → structables-0.4.5}/PKG-INFO +4 -3
  5. {structables-0.4.4 → structables-0.4.5}/README.md +0 -1
  6. {structables-0.4.4 → structables-0.4.5}/instances.json +0 -10
  7. {structables-0.4.4 → structables-0.4.5}/pyproject.toml +6 -1
  8. structables-0.4.5/scripts/create_release.py +153 -0
  9. {structables-0.4.4 → structables-0.4.5}/src/structables/routes/category.py +4 -0
  10. {structables-0.4.4 → structables-0.4.5}/src/structables/routes/contest.py +19 -37
  11. {structables-0.4.4 → structables-0.4.5}/src/structables/routes/main.py +48 -72
  12. structables-0.4.5/src/structables/routes/member.py +91 -0
  13. {structables-0.4.4 → structables-0.4.5}/src/structables/routes/proxy.py +28 -31
  14. {structables-0.4.4 → structables-0.4.5}/src/structables/static/css/style.css +163 -47
  15. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/archives.html +1 -1
  16. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/article.html +12 -8
  17. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/category.html +2 -2
  18. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/collection.html +2 -0
  19. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/contest.html +3 -3
  20. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/contests.html +1 -1
  21. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/index.html +1 -1
  22. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/member-instructables.html +13 -4
  23. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/member.html +8 -6
  24. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/projects.html +1 -1
  25. structables-0.4.5/src/structables/templates/sitemap.html +19 -0
  26. structables-0.4.5/src/structables/utils/cache.py +23 -0
  27. structables-0.4.5/src/structables/utils/data.py +87 -0
  28. {structables-0.4.4 → structables-0.4.5}/src/structables/utils/helpers.py +62 -101
  29. structables-0.4.4/src/structables/routes/member.py +0 -166
  30. structables-0.4.4/src/structables/templates/sitemap.html +0 -27
  31. structables-0.4.4/src/structables/utils/data.py +0 -99
  32. {structables-0.4.4 → structables-0.4.5}/.env.example +0 -0
  33. {structables-0.4.4 → structables-0.4.5}/.forgejo/workflows/release.yml +0 -0
  34. {structables-0.4.4 → structables-0.4.5}/.gitignore +0 -0
  35. {structables-0.4.4 → structables-0.4.5}/LICENSE +0 -0
  36. {structables-0.4.4 → structables-0.4.5}/cronjob.txt +0 -0
  37. {structables-0.4.4 → structables-0.4.5}/docker-compose-example-dev.yml +0 -0
  38. {structables-0.4.4 → structables-0.4.5}/docker-compose-example.yml +0 -0
  39. {structables-0.4.4 → structables-0.4.5}/entrypoint.sh +0 -0
  40. {structables-0.4.4 → structables-0.4.5}/renovate.json +0 -0
  41. {structables-0.4.4 → structables-0.4.5}/requirements-dev.txt +0 -0
  42. {structables-0.4.4 → structables-0.4.5}/src/structables/__init__.py +0 -0
  43. {structables-0.4.4 → structables-0.4.5}/src/structables/config.py +0 -0
  44. {structables-0.4.4 → structables-0.4.5}/src/structables/main.py +0 -0
  45. {structables-0.4.4 → structables-0.4.5}/src/structables/routes/__init__.py +0 -0
  46. {structables-0.4.4 → structables-0.4.5}/src/structables/static/css/iframe.css +0 -0
  47. {structables-0.4.4 → structables-0.4.5}/src/structables/static/img/logo.png +0 -0
  48. {structables-0.4.4 → structables-0.4.5}/src/structables/static/img/logo_lg.png +0 -0
  49. {structables-0.4.4 → structables-0.4.5}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
  50. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/400.html +0 -0
  51. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/404.html +0 -0
  52. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/429.html +0 -0
  53. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/500.html +0 -0
  54. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/article-review.html +0 -0
  55. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/base.html +0 -0
  56. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/footer.html +0 -0
  57. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/header.html +0 -0
  58. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/iframe.html +0 -0
  59. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/privacypolicy.html +0 -0
  60. {structables-0.4.4 → structables-0.4.5}/src/structables/templates/style.html +0 -0
@@ -1,7 +1,9 @@
1
1
  .env
2
+ .git
2
3
  .vscode
3
4
  __pycache__/
4
5
  docker-compose.yml
6
+ compose.yaml
5
7
  privacy.md
6
8
  privacy.txt
7
9
  *.pyc
@@ -8,7 +8,7 @@ on:
8
8
 
9
9
  jobs:
10
10
  docker:
11
- name: Docker Build and Push to Docker Hub
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 Docker Hub
26
+ - name: Login to Forgejo
27
27
  uses: docker/login-action@v3
28
28
  with:
29
- username: ${{ secrets.DOCKERHUB_USERNAME }}
30
- password: ${{ secrets.DOCKERHUB_TOKEN }}
31
- - name: Build and push to Docker Hub
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:${{ github.ref_name }}
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.4
1
+ Metadata-Version: 2.5
2
2
  Name: structables
3
- Version: 0.4.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.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 ..utils.helpers import proxy
5
- from bs4 import BeautifulSoup
6
- import json
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
- response = urlopen(url)
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
- request = Request(url, headers=headers)
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(hits) < per_page:
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 = urlopen(f"https://www.instructables.com/contest/{contest}/")
115
- html = data.read().decode()
116
- soup = BeautifulSoup(html, "html.parser")
117
-
118
- title_tag = soup.find("h1")
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
- img_tag = soup.find("img", alt=lambda x: x and "Banner" in x)
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 = len(entries)
128
- logger.debug(f"Found {entry_count} entries")
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
- response = urlopen(
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
- category_data = urlopen(api_url)
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
- proxy(item["downloadUrl"])
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
- logger.debug(
137
- f"Fetching sitemap data from instructables.com for path: {path}"
138
- )
139
- data = urlopen("https://www.instructables.com/sitemap/" + path)
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 = urlopen(
189
- f"https://www.instructables.com/json-api/showInstructableModel?urlString={article_path}"
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": proxy(file["downloadUrl"], file["name"]),
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 thumbnail in data["instructables"]:
397
- text = thumbnail["title"]
398
- link = thumbnail["showUrl"]
399
- img = proxy(thumbnail["downloadUrl"])
400
- thumbnail_title = thumbnail["title"]
401
- thumbnail_author = thumbnail["author"]["screenName"]
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 = thumbnail["classifications"][0]["channels"][0][
379
+ thumbnail_channel = item["classifications"][0]["channels"][0][
404
380
  "title"
405
381
  ]
406
- thumbnail_category = thumbnail["classifications"][0]["title"]
382
+ thumbnail_category = item["classifications"][0]["title"]
407
383
  thumbnail_channel_link = (
408
384
  f"/{thumbnail_category}/{thumbnail_channel}"
409
385
  )