structables 0.3.15__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {structables-0.3.15 → structables-0.4.0}/.forgejo/workflows/docker.yml +2 -2
  2. {structables-0.3.15 → structables-0.4.0}/.forgejo/workflows/release.yml +1 -1
  3. {structables-0.3.15 → structables-0.4.0}/.gitignore +1 -0
  4. {structables-0.3.15 → structables-0.4.0}/Dockerfile +1 -1
  5. {structables-0.3.15 → structables-0.4.0}/PKG-INFO +42 -7
  6. {structables-0.3.15 → structables-0.4.0}/README.md +40 -5
  7. structables-0.4.0/instances.json +54 -0
  8. {structables-0.3.15 → structables-0.4.0}/pyproject.toml +1 -1
  9. structables-0.4.0/renovate.json +6 -0
  10. structables-0.4.0/requirements-dev.txt +6 -0
  11. structables-0.4.0/src/structables/config.py +44 -0
  12. structables-0.4.0/src/structables/main.py +65 -0
  13. {structables-0.3.15 → structables-0.4.0}/src/structables/routes/category.py +21 -0
  14. {structables-0.3.15 → structables-0.4.0}/src/structables/routes/contest.py +39 -4
  15. {structables-0.3.15 → structables-0.4.0}/src/structables/routes/main.py +125 -49
  16. {structables-0.3.15 → structables-0.4.0}/src/structables/routes/member.py +22 -3
  17. structables-0.4.0/src/structables/routes/proxy.py +358 -0
  18. structables-0.4.0/src/structables/static/css/iframe.css +114 -0
  19. structables-0.4.0/src/structables/static/css/style.css +1190 -0
  20. structables-0.4.0/src/structables/static/img/magnifying-glass-solid.svg +2 -0
  21. structables-0.4.0/src/structables/templates/base.html +58 -0
  22. structables-0.4.0/src/structables/templates/category.html +35 -0
  23. structables-0.4.0/src/structables/templates/footer.html +17 -0
  24. structables-0.4.0/src/structables/templates/header.html +49 -0
  25. structables-0.4.0/src/structables/templates/iframe.html +23 -0
  26. structables-0.4.0/src/structables/templates/index.html +37 -0
  27. structables-0.4.0/src/structables/templates/projects.html +49 -0
  28. structables-0.4.0/src/structables/utils/data.py +76 -0
  29. structables-0.4.0/src/structables/utils/helpers.py +551 -0
  30. structables-0.3.15/src/structables/config.py +0 -13
  31. structables-0.3.15/src/structables/main.py +0 -43
  32. structables-0.3.15/src/structables/routes/proxy.py +0 -60
  33. structables-0.3.15/src/structables/static/css/style.css +0 -282
  34. structables-0.3.15/src/structables/static/dist/css/bootstrap.min.css +0 -6
  35. structables-0.3.15/src/structables/static/dist/css/bootstrap.min.css.map +0 -1
  36. structables-0.3.15/src/structables/static/img/magnifying-glass-solid.svg +0 -1
  37. structables-0.3.15/src/structables/templates/base.html +0 -27
  38. structables-0.3.15/src/structables/templates/category.html +0 -68
  39. structables-0.3.15/src/structables/templates/footer.html +0 -21
  40. structables-0.3.15/src/structables/templates/header.html +0 -39
  41. structables-0.3.15/src/structables/templates/iframe.html +0 -12
  42. structables-0.3.15/src/structables/templates/index.html +0 -45
  43. structables-0.3.15/src/structables/templates/projects.html +0 -56
  44. structables-0.3.15/src/structables/utils/data.py +0 -56
  45. structables-0.3.15/src/structables/utils/helpers.py +0 -462
  46. {structables-0.3.15 → structables-0.4.0}/.env.example +0 -0
  47. {structables-0.3.15 → structables-0.4.0}/LICENSE +0 -0
  48. {structables-0.3.15 → structables-0.4.0}/cronjob.txt +0 -0
  49. {structables-0.3.15 → structables-0.4.0}/docker-compose-example-dev.yml +0 -0
  50. {structables-0.3.15 → structables-0.4.0}/docker-compose-example.yml +0 -0
  51. {structables-0.3.15 → structables-0.4.0}/entrypoint.sh +0 -0
  52. {structables-0.3.15 → structables-0.4.0}/src/structables/__init__.py +0 -0
  53. {structables-0.3.15 → structables-0.4.0}/src/structables/routes/__init__.py +0 -0
  54. {structables-0.3.15 → structables-0.4.0}/src/structables/static/img/logo.png +0 -0
  55. {structables-0.3.15 → structables-0.4.0}/src/structables/static/img/logo_lg.png +0 -0
  56. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/400.html +0 -0
  57. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/404.html +0 -0
  58. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/429.html +0 -0
  59. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/500.html +0 -0
  60. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/archives.html +0 -0
  61. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/article-review.html +0 -0
  62. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/article.html +0 -0
  63. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/collection.html +0 -0
  64. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/contest.html +0 -0
  65. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/contests.html +0 -0
  66. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/member-instructables.html +0 -0
  67. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/member.html +0 -0
  68. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/privacypolicy.html +0 -0
  69. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/sitemap.html +0 -0
  70. {structables-0.3.15 → structables-0.4.0}/src/structables/templates/style.html +0 -0
@@ -9,7 +9,7 @@ jobs:
9
9
  docker:
10
10
  name: Docker Build and Push to Docker Hub
11
11
  container:
12
- image: node:20-bookworm
12
+ image: node:22-bookworm
13
13
  steps:
14
14
  - name: Install dependencies
15
15
  run: |
@@ -25,7 +25,7 @@ jobs:
25
25
  username: ${{ secrets.DOCKERHUB_USERNAME }}
26
26
  password: ${{ secrets.DOCKERHUB_TOKEN }}
27
27
  - name: Build and push to Docker Hub
28
- uses: docker/build-push-action@v5
28
+ uses: docker/build-push-action@v6
29
29
  with:
30
30
  push: true
31
31
  tags: |
@@ -10,7 +10,7 @@ jobs:
10
10
  publish:
11
11
  name: Publish to PyPI
12
12
  container:
13
- image: node:20-bookworm
13
+ image: node:22-bookworm
14
14
 
15
15
  steps:
16
16
  - name: Check out code
@@ -6,6 +6,7 @@ privacy.md
6
6
  privacy.txt
7
7
  *.pyc
8
8
  venv/
9
+ /output
9
10
 
10
11
  # Special rules needed for building the Docker image
11
12
  /dist/*
@@ -1,4 +1,4 @@
1
- FROM alpine:3.20
1
+ FROM alpine:3.21
2
2
 
3
3
  ENV APP_ENV=/opt/venv
4
4
  ENV PATH="${APP_ENV}/bin:$PATH"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: structables
3
- Version: 0.3.15
3
+ Version: 0.4.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
@@ -692,12 +692,30 @@ An open source alternative front-end to Instructables. This is a fork of <a href
692
692
 
693
693
  ## Instances
694
694
 
695
- | URL | Provided by | Country | Comments |
696
- | ----------------------------------------------------------------- | ----------------------------------------- | ------- | -------- |
697
- | [structables.private.coffee](https://structables.private.coffee/) | [Private.coffee](https://private.coffee/) | Austria | |
698
- | [structables.bloat.cat](https://structables.bloat.cat/) | [Bloat.cat](https://bloat.cat) | Germany | |
695
+ <!-- START_INSTANCE_LIST type:eq=clearnet -->
699
696
 
700
- To add your own instance to this list, please open a pull request or issue.
697
+ | URL | Provided by | Country | Notes |
698
+ | ---------------------------------------------------------------------- | ---------------------------------------------- | ---------------- | ------------- |
699
+ | [structables.private.coffee](https://structables.private.coffee) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
700
+ | [structables.bloat.cat](https://structables.bloat.cat) | [Bloat.cat](https://bloat.cat) | Germany 🇩🇪 🇪🇺 | |
701
+ | [structables.darkness.services](https://structables.darkness.services) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
702
+
703
+ <!-- END_INSTANCE_LIST -->
704
+
705
+ ### Tor Hidden Services
706
+
707
+ <!-- START_INSTANCE_LIST type:eq=onion -->
708
+
709
+ | URL | Provided by | Country | Notes |
710
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ---------------- | ------------- |
711
+ | [structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion](http://structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
712
+ | [structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion](http://structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
713
+
714
+ <!-- END_INSTANCE_LIST -->
715
+
716
+ ### Adding Your Instance
717
+
718
+ To add your own instance to this list, please open a pull request or issue, see below.
701
719
 
702
720
  ## Opening Issues
703
721
 
@@ -740,6 +758,23 @@ Of course, you can also join our [Matrix room](https://matrix.pcof.fi/#/#structa
740
758
  5. Run `structables`
741
759
  6. Connect to http://localhost:8002
742
760
 
761
+ ### Environment Variables
762
+
763
+ Structables supports the use of the following environment variables for configuration:
764
+
765
+ - `STRUCTABLES_PORT`: The port to listen on (default: 8002)
766
+ - `STRUCTABLES_LISTEN_HOST`: The host/IP address to listen on (default: 127.0.0.1)
767
+ - `STRUCTABLES_INVIDIOUS`: The hostname of an Invidious instance to use for embedded YouTube videos (currently not recommended due to YouTube blocks)
768
+ - `STRUCTABLES_UNSAFE`: If set, allow embedding untrusted iframes (if unset, display a warning and allow loading the content manually)
769
+ - `STRUCTABLES_PRIVACY_FILE`: The path to a text file or Markdown file (with .md suffix) to use for the Privacy Policy page (if unset, try `privacy.txt` or `privacy.md` in the working directory, or fall back to a generic message)
770
+ - `STRUCTABLES_DEBUG`: If set, log additional debug information to stdout
771
+ - `STRUCTABLES_THEME`: Allows selecting a theme for the frontend. Currently, only `dark` and `light` are supported. If not set, it will be automatically detected based on the user's system settings, and a toggle will be provided in the header.
772
+ - `STRUCTABLES_CACHE_ENABLED`: Whether to enable caching of proxied content (default: true). Set to "false" or "0" to disable caching.
773
+ - `STRUCTABLES_CACHE_DIR`: The directory to use for caching proxied content (default: `structables_cache` within the temporary directory as returned by `tempfile.gettempdir()`)
774
+ - `STRUCTABLES_CACHE_MAX_AGE`: The maximum age of cached content in seconds before it's considered stale (default: 604800 seconds, or 1 week)
775
+ - `STRUCTABLES_CACHE_MAX_SIZE`: The maximum size of the cache directory in bytes (default: 1073741824 bytes, or 1GB)
776
+ - `STRUCTABLES_CACHE_CLEANUP_INTERVAL`: How often to run the cache cleanup process in seconds (default: 3600 seconds, or 1 hour)
777
+
743
778
  ## License
744
779
 
745
780
  This project, as well as the two projects it is based on, are licensed under the GNU Affero General Public License v3. See the [LICENSE](LICENSE) file for more information.
@@ -13,12 +13,30 @@ An open source alternative front-end to Instructables. This is a fork of <a href
13
13
 
14
14
  ## Instances
15
15
 
16
- | URL | Provided by | Country | Comments |
17
- | ----------------------------------------------------------------- | ----------------------------------------- | ------- | -------- |
18
- | [structables.private.coffee](https://structables.private.coffee/) | [Private.coffee](https://private.coffee/) | Austria | |
19
- | [structables.bloat.cat](https://structables.bloat.cat/) | [Bloat.cat](https://bloat.cat) | Germany | |
16
+ <!-- START_INSTANCE_LIST type:eq=clearnet -->
20
17
 
21
- To add your own instance to this list, please open a pull request or issue.
18
+ | URL | Provided by | Country | Notes |
19
+ | ---------------------------------------------------------------------- | ---------------------------------------------- | ---------------- | ------------- |
20
+ | [structables.private.coffee](https://structables.private.coffee) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
21
+ | [structables.bloat.cat](https://structables.bloat.cat) | [Bloat.cat](https://bloat.cat) | Germany 🇩🇪 🇪🇺 | |
22
+ | [structables.darkness.services](https://structables.darkness.services) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
23
+
24
+ <!-- END_INSTANCE_LIST -->
25
+
26
+ ### Tor Hidden Services
27
+
28
+ <!-- START_INSTANCE_LIST type:eq=onion -->
29
+
30
+ | URL | Provided by | Country | Notes |
31
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ---------------- | ------------- |
32
+ | [structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion](http://structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion) | [Private.coffee](https://private.coffee) | Austria 🇦🇹 🇪🇺 | Main instance |
33
+ | [structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion](http://structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion) | [Darkness.services](https://darkness.services) | United States 🇺🇸 | |
34
+
35
+ <!-- END_INSTANCE_LIST -->
36
+
37
+ ### Adding Your Instance
38
+
39
+ To add your own instance to this list, please open a pull request or issue, see below.
22
40
 
23
41
  ## Opening Issues
24
42
 
@@ -61,6 +79,23 @@ Of course, you can also join our [Matrix room](https://matrix.pcof.fi/#/#structa
61
79
  5. Run `structables`
62
80
  6. Connect to http://localhost:8002
63
81
 
82
+ ### Environment Variables
83
+
84
+ Structables supports the use of the following environment variables for configuration:
85
+
86
+ - `STRUCTABLES_PORT`: The port to listen on (default: 8002)
87
+ - `STRUCTABLES_LISTEN_HOST`: The host/IP address to listen on (default: 127.0.0.1)
88
+ - `STRUCTABLES_INVIDIOUS`: The hostname of an Invidious instance to use for embedded YouTube videos (currently not recommended due to YouTube blocks)
89
+ - `STRUCTABLES_UNSAFE`: If set, allow embedding untrusted iframes (if unset, display a warning and allow loading the content manually)
90
+ - `STRUCTABLES_PRIVACY_FILE`: The path to a text file or Markdown file (with .md suffix) to use for the Privacy Policy page (if unset, try `privacy.txt` or `privacy.md` in the working directory, or fall back to a generic message)
91
+ - `STRUCTABLES_DEBUG`: If set, log additional debug information to stdout
92
+ - `STRUCTABLES_THEME`: Allows selecting a theme for the frontend. Currently, only `dark` and `light` are supported. If not set, it will be automatically detected based on the user's system settings, and a toggle will be provided in the header.
93
+ - `STRUCTABLES_CACHE_ENABLED`: Whether to enable caching of proxied content (default: true). Set to "false" or "0" to disable caching.
94
+ - `STRUCTABLES_CACHE_DIR`: The directory to use for caching proxied content (default: `structables_cache` within the temporary directory as returned by `tempfile.gettempdir()`)
95
+ - `STRUCTABLES_CACHE_MAX_AGE`: The maximum age of cached content in seconds before it's considered stale (default: 604800 seconds, or 1 week)
96
+ - `STRUCTABLES_CACHE_MAX_SIZE`: The maximum size of the cache directory in bytes (default: 1073741824 bytes, or 1GB)
97
+ - `STRUCTABLES_CACHE_CLEANUP_INTERVAL`: How often to run the cache cleanup process in seconds (default: 3600 seconds, or 1 hour)
98
+
64
99
  ## License
65
100
 
66
101
  This project, as well as the two projects it is based on, are licensed under the GNU Affero General Public License v3. See the [LICENSE](LICENSE) file for more information.
@@ -0,0 +1,54 @@
1
+ [
2
+ {
3
+ "name": "structables.private.coffee",
4
+ "url": "https://structables.private.coffee",
5
+ "type": "clearnet",
6
+ "provider": {
7
+ "name": "Private.coffee",
8
+ "url": "https://private.coffee"
9
+ },
10
+ "location": "AT",
11
+ "notes": "Main instance"
12
+ },
13
+ {
14
+ "name": "structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion",
15
+ "url": "http://structables.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion",
16
+ "type": "onion",
17
+ "provider": {
18
+ "name": "Private.coffee",
19
+ "url": "https://private.coffee"
20
+ },
21
+ "location": "AT",
22
+ "notes": "Main instance"
23
+ },
24
+ {
25
+ "name": "structables.bloat.cat",
26
+ "url": "https://structables.bloat.cat",
27
+ "type": "clearnet",
28
+ "provider": {
29
+ "name": "Bloat.cat",
30
+ "url": "https://bloat.cat"
31
+ },
32
+ "location": "DE"
33
+ },
34
+ {
35
+ "name": "structables.darkness.services",
36
+ "url": "https://structables.darkness.services",
37
+ "type": "clearnet",
38
+ "provider": {
39
+ "name": "Darkness.services",
40
+ "url": "https://darkness.services"
41
+ },
42
+ "location": "US"
43
+ },
44
+ {
45
+ "name": "structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion",
46
+ "url": "http://structables.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion",
47
+ "type": "onion",
48
+ "provider": {
49
+ "name": "Darkness.services",
50
+ "url": "https://darkness.services"
51
+ },
52
+ "location": "US"
53
+ }
54
+ ]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "structables"
7
- version = "0.3.15"
7
+ version = "0.4.0"
8
8
  authors = [
9
9
  { name="Private.coffee Team", email="support@private.coffee" },
10
10
  ]
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:recommended"
5
+ ]
6
+ }
@@ -0,0 +1,6 @@
1
+ ruff
2
+ black
3
+ isort
4
+ mypy
5
+ types-beautifulsoup4
6
+ types-colorama
@@ -0,0 +1,44 @@
1
+ import os
2
+ import tempfile
3
+
4
+ from .utils.helpers import get_typesense_api_key
5
+
6
+
7
+ class Config:
8
+ DEBUG = os.environ.get("FLASK_DEBUG", os.environ.get("STRUCTABLES_DEBUG", False))
9
+ PORT = int(os.environ.get("STRUCTABLES_PORT", 8002))
10
+ LISTEN_HOST = os.environ.get("STRUCTABLES_LISTEN_HOST", "127.0.0.1")
11
+ INVIDIOUS = os.environ.get("STRUCTABLES_INVIDIOUS")
12
+ UNSAFE = os.environ.get("STRUCTABLES_UNSAFE", False)
13
+ PRIVACY_FILE = os.environ.get("STRUCTABLES_PRIVACY_FILE")
14
+ THEME = os.environ.get("STRUCTABLES_THEME", "auto")
15
+ TYPESENSE_API_KEY = get_typesense_api_key()
16
+
17
+ # Cache settings
18
+ CACHE_ENABLED = os.environ.get("STRUCTABLES_CACHE_ENABLED", "true").lower() not in (
19
+ "false",
20
+ "0",
21
+ "no",
22
+ "off",
23
+ "n",
24
+ )
25
+ CACHE_DIR = os.environ.get("STRUCTABLES_CACHE_DIR")
26
+
27
+ if CACHE_DIR is None:
28
+ CACHE_DIR = os.path.join(
29
+ tempfile.gettempdir(), "structables_cache"
30
+ )
31
+
32
+ CACHE_MAX_AGE = int(
33
+ os.environ.get("STRUCTABLES_CACHE_MAX_AGE", 60 * 60 * 24 * 7)
34
+ ) # 1 week default
35
+ CACHE_MAX_SIZE = int(
36
+ os.environ.get("STRUCTABLES_CACHE_MAX_SIZE", 1024 * 1024 * 1024)
37
+ ) # 1GB default
38
+ CACHE_CLEANUP_INTERVAL = int(
39
+ os.environ.get("STRUCTABLES_CACHE_CLEANUP_INTERVAL", 60 * 60)
40
+ ) # 1 hour default
41
+
42
+ @staticmethod
43
+ def init_app(app):
44
+ pass
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python
2
+
3
+ from flask import Flask
4
+ import threading
5
+ import time
6
+ import logging
7
+
8
+ from .config import Config
9
+ from .routes import init_routes
10
+ from .utils.data import update_data
11
+ from .routes.proxy import start_cache_cleanup_thread
12
+
13
+ # Configure logging
14
+ logger = logging.getLogger(__name__)
15
+
16
+ app = Flask(__name__, template_folder="templates", static_folder="static")
17
+ app.config.from_object(Config)
18
+
19
+ logger.debug("Initializing routes")
20
+ init_routes(app)
21
+ logger.debug("Performing initial data update")
22
+ update_data(app)
23
+
24
+ def background_update_data(app):
25
+ """Runs the update_data function every 5 minutes.
26
+
27
+ This replaces the need for a cron job to update the data.
28
+
29
+ Args:
30
+ app (Flask): The Flask app instance.
31
+ """
32
+ logger.debug("Starting background update thread")
33
+ while True:
34
+ logger.debug("Running scheduled data update")
35
+ update_data(app)
36
+ logger.debug("Data update complete, sleeping for 5 minutes")
37
+ time.sleep(300)
38
+
39
+ def main():
40
+ if app.config["DEBUG"]:
41
+ logging.basicConfig(
42
+ level=logging.DEBUG,
43
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
44
+ )
45
+ else:
46
+ logging.basicConfig(
47
+ level=logging.INFO,
48
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
49
+ )
50
+
51
+ logger.debug("Starting background update thread")
52
+ threading.Thread(target=background_update_data, args=(app,), daemon=True).start()
53
+
54
+ # Start the cache cleanup thread
55
+ start_cache_cleanup_thread(app)
56
+
57
+ logger.info(f"Starting Structables on {app.config['LISTEN_HOST']}:{app.config['PORT']}")
58
+ app.run(
59
+ port=app.config["PORT"],
60
+ host=app.config["LISTEN_HOST"],
61
+ debug=app.config["DEBUG"],
62
+ )
63
+
64
+ if __name__ == "__main__":
65
+ main()
@@ -1,15 +1,22 @@
1
1
  from flask import redirect
2
2
  from werkzeug.exceptions import NotFound
3
3
  from ..utils.helpers import project_list, category_page
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
4
7
 
5
8
 
6
9
  def init_category_routes(app):
7
10
  @app.route("/<category>/<channel>/projects/")
8
11
  def route_channel_projects(category, channel):
12
+ logger.debug(f"Rendering channel projects for {category}/{channel}")
9
13
  return project_list(app, channel.title())
10
14
 
11
15
  @app.route("/<category>/<channel>/projects/<sort>/")
12
16
  def route_channel_projects_sort(category, channel, sort):
17
+ logger.debug(
18
+ f"Rendering channel projects for {category}/{channel} sorted by {sort}"
19
+ )
13
20
  return project_list(
14
21
  app,
15
22
  channel.title(),
@@ -18,50 +25,62 @@ def init_category_routes(app):
18
25
 
19
26
  @app.route("/<category>/projects/")
20
27
  def route_category_projects(category):
28
+ logger.debug(f"Rendering category projects for {category}")
21
29
  return project_list(app, category.title())
22
30
 
23
31
  @app.route("/<category>/projects/<sort>/")
24
32
  def route_category_projects_sort(category, sort):
33
+ logger.debug(f"Rendering category projects for {category} sorted by {sort}")
25
34
  return project_list(app, category.title(), " Sorted by " + sort.title())
26
35
 
27
36
  @app.route("/projects/")
28
37
  def route_projects():
38
+ logger.debug("Rendering all projects")
29
39
  return project_list(app, "")
30
40
 
31
41
  @app.route("/projects/<sort>/")
32
42
  def route_projects_sort(sort):
43
+ logger.debug(f"Rendering all projects sorted by {sort}")
33
44
  return project_list(app, "", " Sorted by " + sort.title())
34
45
 
35
46
  @app.route("/circuits/")
36
47
  def route_circuits():
48
+ logger.debug("Rendering circuits category page")
37
49
  return category_page(app, "Circuits")
38
50
 
39
51
  @app.route("/workshop/")
40
52
  def route_workshop():
53
+ logger.debug("Rendering workshop category page")
41
54
  return category_page(app, "Workshop")
42
55
 
43
56
  @app.route("/craft/")
44
57
  def route_craft():
58
+ logger.debug("Rendering craft category page")
45
59
  return category_page(app, "Craft")
46
60
 
47
61
  @app.route("/cooking/")
48
62
  def route_cooking():
63
+ logger.debug("Rendering cooking category page")
49
64
  return category_page(app, "Cooking")
50
65
 
51
66
  @app.route("/living/")
52
67
  def route_living():
68
+ logger.debug("Rendering living category page")
53
69
  return category_page(app, "Living")
54
70
 
55
71
  @app.route("/outside/")
56
72
  def route_outside():
73
+ logger.debug("Rendering outside category page")
57
74
  return category_page(app, "Outside")
58
75
 
59
76
  @app.route("/teachers/")
60
77
  def route_teachers():
78
+ logger.debug("Rendering teachers category page")
61
79
  return category_page(app, "Teachers", True)
62
80
 
63
81
  @app.route("/<category>/<channel>/")
64
82
  def route_channel_redirect(category, channel):
83
+ logger.debug(f"Channel redirect for {category}/{channel}")
65
84
  if (
66
85
  category == "circuits"
67
86
  or category == "workshop"
@@ -71,6 +90,8 @@ def init_category_routes(app):
71
90
  or category == "outside"
72
91
  or category == "teachers"
73
92
  ):
93
+ logger.debug(f"Redirecting to /{category}/{channel}/projects/")
74
94
  return redirect(f"/{category}/{channel}/projects/", 307)
75
95
  else:
96
+ logger.warning(f"Invalid category: {category}")
76
97
  raise NotFound()
@@ -4,6 +4,9 @@ from urllib.error import HTTPError
4
4
  from ..utils.helpers import proxy
5
5
  from bs4 import BeautifulSoup
6
6
  import json
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
7
10
 
8
11
 
9
12
  def init_contest_routes(app):
@@ -14,18 +17,27 @@ def init_contest_routes(app):
14
17
  page = request.args.get("page", default=1, type=int)
15
18
  offset = (page - 1) * limit
16
19
 
20
+ logger.debug(f"Fetching contest archive page {page} with limit {limit}")
21
+
17
22
  try:
18
23
  # Fetch data using urlopen
19
24
  url = f"https://www.instructables.com/json-api/getClosedContests?limit={limit}&offset={offset}"
25
+ logger.debug(f"Making request to {url}")
20
26
  response = urlopen(url)
21
27
  data = json.loads(response.read().decode())
28
+ logger.debug(
29
+ f"Received contest archive data with {len(data.get('contests', []))} contests"
30
+ )
22
31
  except HTTPError as e:
32
+ logger.error(f"HTTP error fetching contest archive: {e.code}")
23
33
  abort(e.code)
24
34
  except Exception as e:
35
+ logger.error(f"Error fetching contest archive: {str(e)}")
25
36
  abort(500) # Handle other exceptions like JSON decode errors
26
37
 
27
38
  contests = data.get("contests", [])
28
39
  full_list_size = data.get("fullListSize", 0)
40
+ logger.debug(f"Total contests in archive: {full_list_size}")
29
41
 
30
42
  contest_list = []
31
43
  for contest in contests:
@@ -42,6 +54,7 @@ def init_contest_routes(app):
42
54
 
43
55
  # Calculate total pages
44
56
  total_pages = (full_list_size + limit - 1) // limit
57
+ logger.debug(f"Pagination: page {page}/{total_pages}")
45
58
 
46
59
  # Create pagination
47
60
  pagination = {
@@ -61,21 +74,27 @@ def init_contest_routes(app):
61
74
  )
62
75
 
63
76
  def get_entries(contest):
64
- base_url = f"https://www.instructables.com/api_proxy/search/collections/projects/documents/search"
65
- headers = {"x-typesense-api-key": app.typesense_api_key}
77
+ base_url = "https://www.instructables.com/api_proxy/search/collections/projects/documents/search"
78
+ headers = {"x-typesense-api-key": app.config["TYPESENSE_API_KEY"]}
66
79
  page, per_page = 1, 100
67
80
  all_entries = []
68
81
 
82
+ logger.debug(f"Fetching entries for contest: {contest}")
83
+
69
84
  while True:
70
85
  try:
71
86
  url = f"{base_url}?q=*&filter_by=contestPath:{contest}&sort_by=contestEntryDate:desc&per_page={per_page}&page={page}"
87
+ logger.debug(f"Making request to {url} (page {page})")
72
88
  request = Request(url, headers=headers)
73
89
  response = urlopen(request)
74
90
  data = json.loads(response.read().decode())
75
91
  except HTTPError as e:
92
+ logger.error(f"HTTP error fetching contest entries: {e.code}")
76
93
  abort(e.code)
77
94
 
78
95
  hits = data.get("hits", [])
96
+ logger.debug(f"Received {len(hits)} entries on page {page}")
97
+
79
98
  if not hits:
80
99
  break
81
100
 
@@ -84,10 +103,13 @@ def init_contest_routes(app):
84
103
  break
85
104
  page += 1
86
105
 
106
+ logger.debug(f"Total entries fetched: {len(all_entries)}")
87
107
  return all_entries
88
108
 
89
109
  @app.route("/contest/<contest>/")
90
110
  def route_contest(contest):
111
+ logger.debug(f"Fetching contest page for: {contest}")
112
+
91
113
  try:
92
114
  data = urlopen(f"https://www.instructables.com/contest/{contest}/")
93
115
  html = data.read().decode()
@@ -95,13 +117,19 @@ def init_contest_routes(app):
95
117
 
96
118
  title_tag = soup.find("h1")
97
119
  title = title_tag.get_text() if title_tag else "Contest"
120
+ logger.debug(f"Contest title: {title}")
98
121
 
99
122
  img_tag = soup.find("img", alt=lambda x: x and "Banner" in x)
100
123
  img = img_tag.get("src") if img_tag else "default.jpg"
101
124
 
102
- entry_count = len(get_entries(contest))
125
+ logger.debug(f"Fetching entries for contest: {contest}")
126
+ entries = get_entries(contest)
127
+ entry_count = len(entries)
128
+ logger.debug(f"Found {entry_count} entries")
129
+
103
130
  prizes_items = soup.select("article")
104
131
  prizes = len(prizes_items) if prizes_items else 0
132
+ logger.debug(f"Found {prizes} prizes")
105
133
 
106
134
  overview_section = soup.find("section", id="overview")
107
135
  info = (
@@ -111,10 +139,10 @@ def init_contest_routes(app):
111
139
  )
112
140
 
113
141
  except HTTPError as e:
142
+ logger.error(f"HTTP error fetching contest page: {e.code}")
114
143
  abort(e.code)
115
144
 
116
145
  entry_list = []
117
- entries = get_entries(contest)
118
146
  for entry in entries:
119
147
  doc = entry["document"]
120
148
  entry_details = {
@@ -141,18 +169,25 @@ def init_contest_routes(app):
141
169
 
142
170
  @app.route("/contest/")
143
171
  def route_contests():
172
+ logger.debug("Fetching current contests")
173
+
144
174
  try:
145
175
  # Fetch current contests from the JSON API
146
176
  response = urlopen(
147
177
  "https://www.instructables.com/json-api/getCurrentContests?limit=50&offset=0"
148
178
  )
149
179
  data = json.loads(response.read().decode())
180
+ logger.debug("Received current contests data")
150
181
  except HTTPError as e:
182
+ logger.error(f"HTTP error fetching current contests: {e.code}")
151
183
  abort(e.code)
152
184
  except Exception as e:
185
+ logger.error(f"Error fetching current contests: {str(e)}")
153
186
  abort(500) # Handle other exceptions such as JSON decode errors
154
187
 
155
188
  contests = data.get("contests", [])
189
+ logger.debug(f"Found {len(contests)} current contests")
190
+
156
191
  contest_list = []
157
192
  for contest in contests:
158
193
  contest_details = {