structables 0.4.0__tar.gz → 0.4.3__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 (56) hide show
  1. structables-0.4.3/.dockerignore +15 -0
  2. {structables-0.4.0 → structables-0.4.3}/.forgejo/workflows/docker.yml +4 -6
  3. {structables-0.4.0 → structables-0.4.3}/.forgejo/workflows/release.yml +1 -0
  4. {structables-0.4.0 → structables-0.4.3}/.gitignore +1 -0
  5. {structables-0.4.0 → structables-0.4.3}/Dockerfile +2 -3
  6. {structables-0.4.0 → structables-0.4.3}/PKG-INFO +10 -7
  7. {structables-0.4.0 → structables-0.4.3}/README.md +8 -5
  8. {structables-0.4.0 → structables-0.4.3}/docker-compose-example-dev.yml +2 -1
  9. {structables-0.4.0 → structables-0.4.3}/docker-compose-example.yml +3 -2
  10. {structables-0.4.0 → structables-0.4.3}/instances.json +10 -0
  11. {structables-0.4.0 → structables-0.4.3}/pyproject.toml +2 -2
  12. {structables-0.4.0 → structables-0.4.3}/src/structables/config.py +19 -4
  13. structables-0.4.3/src/structables/main.py +46 -0
  14. {structables-0.4.0 → structables-0.4.3}/src/structables/routes/main.py +7 -1
  15. {structables-0.4.0 → structables-0.4.3}/src/structables/routes/proxy.py +54 -44
  16. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/base.html +24 -4
  17. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/header.html +0 -8
  18. {structables-0.4.0 → structables-0.4.3}/src/structables/utils/data.py +24 -1
  19. structables-0.4.0/src/structables/main.py +0 -65
  20. {structables-0.4.0 → structables-0.4.3}/.env.example +0 -0
  21. {structables-0.4.0 → structables-0.4.3}/LICENSE +0 -0
  22. {structables-0.4.0 → structables-0.4.3}/cronjob.txt +0 -0
  23. {structables-0.4.0 → structables-0.4.3}/entrypoint.sh +0 -0
  24. {structables-0.4.0 → structables-0.4.3}/renovate.json +0 -0
  25. {structables-0.4.0 → structables-0.4.3}/requirements-dev.txt +0 -0
  26. {structables-0.4.0 → structables-0.4.3}/src/structables/__init__.py +0 -0
  27. {structables-0.4.0 → structables-0.4.3}/src/structables/routes/__init__.py +0 -0
  28. {structables-0.4.0 → structables-0.4.3}/src/structables/routes/category.py +0 -0
  29. {structables-0.4.0 → structables-0.4.3}/src/structables/routes/contest.py +0 -0
  30. {structables-0.4.0 → structables-0.4.3}/src/structables/routes/member.py +0 -0
  31. {structables-0.4.0 → structables-0.4.3}/src/structables/static/css/iframe.css +0 -0
  32. {structables-0.4.0 → structables-0.4.3}/src/structables/static/css/style.css +0 -0
  33. {structables-0.4.0 → structables-0.4.3}/src/structables/static/img/logo.png +0 -0
  34. {structables-0.4.0 → structables-0.4.3}/src/structables/static/img/logo_lg.png +0 -0
  35. {structables-0.4.0 → structables-0.4.3}/src/structables/static/img/magnifying-glass-solid.svg +0 -0
  36. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/400.html +0 -0
  37. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/404.html +0 -0
  38. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/429.html +0 -0
  39. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/500.html +0 -0
  40. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/archives.html +0 -0
  41. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/article-review.html +0 -0
  42. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/article.html +0 -0
  43. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/category.html +0 -0
  44. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/collection.html +0 -0
  45. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/contest.html +0 -0
  46. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/contests.html +0 -0
  47. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/footer.html +0 -0
  48. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/iframe.html +0 -0
  49. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/index.html +0 -0
  50. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/member-instructables.html +0 -0
  51. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/member.html +0 -0
  52. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/privacypolicy.html +0 -0
  53. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/projects.html +0 -0
  54. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/sitemap.html +0 -0
  55. {structables-0.4.0 → structables-0.4.3}/src/structables/templates/style.html +0 -0
  56. {structables-0.4.0 → structables-0.4.3}/src/structables/utils/helpers.py +0 -0
@@ -0,0 +1,15 @@
1
+ .env
2
+ .vscode
3
+ __pycache__/
4
+ docker-compose.yml
5
+ privacy.md
6
+ privacy.txt
7
+ *.pyc
8
+ venv/
9
+ /output
10
+ /cache/
11
+
12
+ # Special rules needed for building the Docker image
13
+ /dist/*
14
+ !/dist/css/
15
+ !/dist/css/*
@@ -4,17 +4,15 @@ on:
4
4
  push:
5
5
  tags:
6
6
  - "*"
7
+ workflow_dispatch:
7
8
 
8
9
  jobs:
9
10
  docker:
10
11
  name: Docker Build and Push to Docker Hub
12
+ runs-on: docker-privileged
11
13
  container:
12
- image: node:22-bookworm
14
+ image: catthehacker/ubuntu:act-latest
13
15
  steps:
14
- - name: Install dependencies
15
- run: |
16
- apt update
17
- apt install -y docker.io
18
16
  - name: Set up QEMU
19
17
  uses: docker/setup-qemu-action@v3
20
18
  - name: Set up Docker Buildx
@@ -30,4 +28,4 @@ jobs:
30
28
  push: true
31
29
  tags: |
32
30
  privatecoffee/structables:latest
33
- privatecoffee/structables:${{ env.GITHUB_REF_NAME }}
31
+ privatecoffee/structables:${{ github.ref_name }}
@@ -9,6 +9,7 @@ on:
9
9
  jobs:
10
10
  publish:
11
11
  name: Publish to PyPI
12
+ runs-on: docker
12
13
  container:
13
14
  image: node:22-bookworm
14
15
 
@@ -7,6 +7,7 @@ privacy.txt
7
7
  *.pyc
8
8
  venv/
9
9
  /output
10
+ /cache/
10
11
 
11
12
  # Special rules needed for building the Docker image
12
13
  /dist/*
@@ -2,6 +2,7 @@ FROM alpine:3.21
2
2
 
3
3
  ENV APP_ENV=/opt/venv
4
4
  ENV PATH="${APP_ENV}/bin:$PATH"
5
+ ENV PORT=8002
5
6
 
6
7
  RUN apk add --no-cache py3-pip uwsgi-python3 && \
7
8
  python3 -m venv $APP_ENV
@@ -12,10 +13,8 @@ RUN $APP_ENV/bin/pip install --no-cache-dir pip && \
12
13
  $APP_ENV/bin/pip install /app && \
13
14
  adduser -S -D -H structables
14
15
 
15
- COPY entrypoint.sh /entrypoint.sh
16
-
17
16
  EXPOSE 8002
18
17
 
19
18
  USER structables
20
19
 
21
- ENTRYPOINT ["/entrypoint.sh"]
20
+ ENTRYPOINT ["/app/entrypoint.sh"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structables
3
- Version: 0.4.0
3
+ Version: 0.4.3
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
@@ -672,7 +672,7 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
672
672
  Classifier: Operating System :: OS Independent
673
673
  Classifier: Programming Language :: Python :: 3
674
674
  Requires-Python: >=3.10
675
- Requires-Dist: bs4
675
+ Requires-Dist: beautifulsoup4
676
676
  Requires-Dist: flask
677
677
  Requires-Dist: markdown2[all]
678
678
  Description-Content-Type: text/markdown
@@ -694,11 +694,12 @@ An open source alternative front-end to Instructables. This is a fork of <a href
694
694
 
695
695
  <!-- START_INSTANCE_LIST type:eq=clearnet -->
696
696
 
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 🇺🇸 | |
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
+ | [structables.franklyflawless.org](https://structables.franklyflawless.org) | [FranklyFlawless](https://franklyflawless.org) | Germany 🇩🇪 🇪🇺 | |
702
703
 
703
704
  <!-- END_INSTANCE_LIST -->
704
705
 
@@ -737,6 +738,8 @@ Of course, you can also join our [Matrix room](https://matrix.pcof.fi/#/#structa
737
738
 
738
739
  ### Production: Docker
739
740
 
741
+ **Note:** The Docker image is now hosted on [git.private.coffee](https://git.private.coffee/privatecoffee/structables/packages) instead of Docker Hub. Please use the new image name `git.private.coffee/privatecoffee/structables` instead of `privatecoffee/structables` to continue receiving updates.
742
+
740
743
  1. Copy `.env.example` to `.env` and adjust the settings as necessary
741
744
  2. Copy `docker-compose-example.yml` to `docker-compose.yml` and adjust it as necessary, for example modifying resource limits or changing the port/host configuration
742
745
  3. Build and run the Docker container:
@@ -15,11 +15,12 @@ An open source alternative front-end to Instructables. This is a fork of <a href
15
15
 
16
16
  <!-- START_INSTANCE_LIST type:eq=clearnet -->
17
17
 
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 🇺🇸 | |
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
+ | [structables.franklyflawless.org](https://structables.franklyflawless.org) | [FranklyFlawless](https://franklyflawless.org) | Germany 🇩🇪 🇪🇺 | |
23
24
 
24
25
  <!-- END_INSTANCE_LIST -->
25
26
 
@@ -58,6 +59,8 @@ Of course, you can also join our [Matrix room](https://matrix.pcof.fi/#/#structa
58
59
 
59
60
  ### Production: Docker
60
61
 
62
+ **Note:** The Docker image is now hosted on [git.private.coffee](https://git.private.coffee/privatecoffee/structables/packages) instead of Docker Hub. Please use the new image name `git.private.coffee/privatecoffee/structables` instead of `privatecoffee/structables` to continue receiving updates.
63
+
61
64
  1. Copy `.env.example` to `.env` and adjust the settings as necessary
62
65
  2. Copy `docker-compose-example.yml` to `docker-compose.yml` and adjust it as necessary, for example modifying resource limits or changing the port/host configuration
63
66
  3. Build and run the Docker container:
@@ -5,12 +5,13 @@ services:
5
5
  build: .
6
6
  ports:
7
7
  - "127.0.0.1:8002:8002"
8
+ volumes:
9
+ - ./cache:/app/structables_cache
8
10
  env_file: .env
9
11
  security_opt:
10
12
  - no-new-privileges:true
11
13
  cap_drop:
12
14
  - ALL
13
- read_only: true
14
15
  deploy:
15
16
  resources:
16
17
  limits:
@@ -2,15 +2,16 @@ services:
2
2
  structables:
3
3
  container_name: structables
4
4
  restart: unless-stopped
5
- image: privatecoffee/structables:latest
5
+ image: git.private.coffee/privatecoffee/structables:latest
6
6
  ports:
7
7
  - "127.0.0.1:8002:8002"
8
+ volumes:
9
+ - ./cache:/app/structables_cache
8
10
  env_file: .env
9
11
  security_opt:
10
12
  - no-new-privileges:true
11
13
  cap_drop:
12
14
  - ALL
13
- read_only: true
14
15
  deploy:
15
16
  resources:
16
17
  limits:
@@ -50,5 +50,15 @@
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"
53
63
  }
54
64
  ]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "structables"
7
- version = "0.4.0"
7
+ version = "0.4.3"
8
8
  authors = [
9
9
  { name="Private.coffee Team", email="support@private.coffee" },
10
10
  ]
@@ -19,7 +19,7 @@ classifiers = [
19
19
  ]
20
20
  dependencies = [
21
21
  "flask",
22
- "bs4",
22
+ "beautifulsoup4",
23
23
  "markdown2[all]",
24
24
  ]
25
25
 
@@ -1,8 +1,13 @@
1
1
  import os
2
2
  import tempfile
3
+ import logging
4
+
5
+ from pathlib import Path
3
6
 
4
7
  from .utils.helpers import get_typesense_api_key
5
8
 
9
+ logger = logging.getLogger(__name__)
10
+
6
11
 
7
12
  class Config:
8
13
  DEBUG = os.environ.get("FLASK_DEBUG", os.environ.get("STRUCTABLES_DEBUG", False))
@@ -24,10 +29,20 @@ class Config:
24
29
  )
25
30
  CACHE_DIR = os.environ.get("STRUCTABLES_CACHE_DIR")
26
31
 
27
- if CACHE_DIR is None:
28
- CACHE_DIR = os.path.join(
29
- tempfile.gettempdir(), "structables_cache"
30
- )
32
+ if CACHE_ENABLED and CACHE_DIR is None:
33
+ try:
34
+ CACHE_DIR = Path(tempfile.gettempdir()) / "structables_cache"
35
+ except FileNotFoundError:
36
+ CACHE_DIR = Path(os.getcwd()) / "structables_cache"
37
+
38
+ try:
39
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
40
+ (CACHE_DIR / "test").write_text("test")
41
+ (CACHE_DIR / "test").unlink()
42
+ except Exception as e:
43
+ logger.error(f"Could not create or write to cache directory: {e} - disabling cache")
44
+ CACHE_ENABLED = False
45
+ CACHE_DIR = None
31
46
 
32
47
  CACHE_MAX_AGE = int(
33
48
  os.environ.get("STRUCTABLES_CACHE_MAX_AGE", 60 * 60 * 24 * 7)
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python
2
+
3
+ from flask import Flask
4
+ import logging
5
+ import time
6
+
7
+ from .config import Config
8
+ from .routes import init_routes
9
+ from .utils.data import maybe_update_data
10
+
11
+ # Configure logging
12
+ logger = logging.getLogger(__name__)
13
+
14
+ app = Flask(__name__, template_folder="templates", static_folder="static")
15
+ app.config.from_object(Config)
16
+
17
+ logger.debug("Initializing routes")
18
+ init_routes(app)
19
+ logger.debug("Performing initial data update")
20
+ maybe_update_data(app)
21
+
22
+
23
+ def main():
24
+ if app.config["DEBUG"]:
25
+ logging.basicConfig(
26
+ level=logging.DEBUG,
27
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
28
+ )
29
+ else:
30
+ logging.basicConfig(
31
+ level=logging.INFO,
32
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
33
+ )
34
+
35
+ logger.info(
36
+ f"Starting Structables on {app.config['LISTEN_HOST']}:{app.config['PORT']}"
37
+ )
38
+ app.run(
39
+ port=app.config["PORT"],
40
+ host=app.config["LISTEN_HOST"],
41
+ debug=app.config["DEBUG"],
42
+ )
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
@@ -6,6 +6,7 @@ from urllib.parse import quote
6
6
  from werkzeug.exceptions import InternalServerError
7
7
  from markdown2 import Markdown
8
8
  from traceback import print_exc
9
+
9
10
  import pathlib
10
11
  import json
11
12
  import logging
@@ -13,6 +14,7 @@ import logging
13
14
  from ..utils.data import update_data
14
15
  from ..utils.helpers import explore_lists, proxy
15
16
  from .category import project_list
17
+ from ..utils.data import maybe_update_data
16
18
 
17
19
  logger = logging.getLogger(__name__)
18
20
 
@@ -22,6 +24,9 @@ def init_main_routes(app):
22
24
  def route_explore():
23
25
  logger.debug("Rendering explore page")
24
26
 
27
+ # Check if data needs to be updated
28
+ maybe_update_data(app)
29
+
25
30
  try:
26
31
  logger.debug("Fetching data from instructables.com")
27
32
  data = urlopen("https://www.instructables.com/")
@@ -130,8 +135,9 @@ def init_main_routes(app):
130
135
 
131
136
  try:
132
137
  logger.debug(f"Fetching article data from instructables.com for: {article}")
138
+ article_path = quote(article)
133
139
  data = urlopen(
134
- f"https://www.instructables.com/json-api/showInstructableModel?urlString={article}"
140
+ f"https://www.instructables.com/json-api/showInstructableModel?urlString={article_path}"
135
141
  )
136
142
  data = json.loads(data.read().decode())
137
143
  logger.debug("Successfully fetched article data")
@@ -7,13 +7,12 @@ import logging
7
7
  import os
8
8
  import hashlib
9
9
  import time
10
- import threading
11
10
  import shutil
12
11
 
13
12
  logger = logging.getLogger(__name__)
14
13
 
15
- # Cache cleanup thread reference
16
- cache_cleanup_thread = None
14
+ # Track last cache cleanup time
15
+ last_cache_cleanup = 0
17
16
 
18
17
 
19
18
  def get_cache_path(app, url):
@@ -94,24 +93,35 @@ def get_content_type(cache_path):
94
93
  return "application/octet-stream"
95
94
 
96
95
 
97
- def cache_cleanup(app):
98
- """Clean up the cache directory to stay within size limits.
99
-
100
- This function removes the oldest files first until the cache size
101
- is below the maximum size.
96
+ def maybe_cleanup_cache(app):
97
+ """Clean up the cache directory if it's time to do so.
102
98
 
103
99
  Args:
104
100
  app: The Flask app instance.
105
101
  """
102
+ global last_cache_cleanup
103
+
106
104
  # If caching is disabled, don't do anything
107
105
  if not app.config["CACHE_ENABLED"]:
108
106
  return
109
107
 
108
+ # Check if it's time to run cleanup
109
+ current_time = time.time()
110
+ cleanup_interval = app.config["CACHE_CLEANUP_INTERVAL"]
111
+
112
+ if current_time - last_cache_cleanup < cleanup_interval:
113
+ logger.debug(
114
+ f"Cache cleanup skipped. Time since last cleanup: {current_time - last_cache_cleanup:.2f} seconds"
115
+ )
116
+ return
117
+
110
118
  logger.debug("Starting cache cleanup")
119
+ last_cache_cleanup = current_time
111
120
 
112
121
  try:
113
122
  cache_dir = app.config["CACHE_DIR"]
114
123
  max_size = app.config["CACHE_MAX_SIZE"]
124
+ max_age = app.config["CACHE_MAX_AGE"]
115
125
 
116
126
  # Get all cache files with their modification times
117
127
  cache_files = []
@@ -120,6 +130,10 @@ def cache_cleanup(app):
120
130
  for filename in os.listdir(cache_dir):
121
131
  file_path = os.path.join(cache_dir, filename)
122
132
  if os.path.isfile(file_path):
133
+ # Skip metadata files in the count
134
+ if file_path.endswith(".meta"):
135
+ continue
136
+
123
137
  file_size = os.path.getsize(file_path)
124
138
  file_time = os.path.getmtime(file_path)
125
139
  total_size += file_size
@@ -127,7 +141,34 @@ def cache_cleanup(app):
127
141
 
128
142
  logger.debug(f"Current cache size: {total_size / (1024 * 1024):.2f} MB")
129
143
 
130
- # If we're over the size limit, remove oldest files first
144
+ # First, remove expired files
145
+ current_time = time.time()
146
+ expired_files = [
147
+ (path, mtime, size)
148
+ for path, mtime, size in cache_files
149
+ if current_time - mtime > max_age
150
+ ]
151
+
152
+ for file_path, _, file_size in expired_files:
153
+ try:
154
+ os.remove(file_path)
155
+ # Also remove metadata file if it exists
156
+ meta_path = file_path + ".meta"
157
+ if os.path.exists(meta_path):
158
+ os.remove(meta_path)
159
+ total_size -= file_size
160
+ logger.debug(f"Removed expired cache file: {file_path}")
161
+ except OSError:
162
+ logger.warning(f"Failed to remove expired cache file: {file_path}")
163
+
164
+ # Remove files from the list that we've already deleted
165
+ cache_files = [
166
+ (path, mtime, size)
167
+ for path, mtime, size in cache_files
168
+ if (path, mtime, size) not in expired_files
169
+ ]
170
+
171
+ # If we're still over the size limit, remove oldest files first
131
172
  if total_size > max_size:
132
173
  logger.debug("Cache size exceeds limit, cleaning up")
133
174
  # Sort by modification time (oldest first)
@@ -146,7 +187,7 @@ def cache_cleanup(app):
146
187
  os.remove(meta_path)
147
188
 
148
189
  total_size -= file_size
149
- logger.debug(f"Removed cache file: {file_path}")
190
+ logger.debug(f"Removed old cache file: {file_path}")
150
191
  except OSError:
151
192
  logger.warning(f"Failed to remove cache file: {file_path}")
152
193
 
@@ -158,40 +199,6 @@ def cache_cleanup(app):
158
199
  logger.error(f"Error during cache cleanup: {str(e)}")
159
200
 
160
201
 
161
- def start_cache_cleanup_thread(app):
162
- """Start a background thread to periodically clean up the cache.
163
-
164
- Args:
165
- app: The Flask app instance.
166
- """
167
- global cache_cleanup_thread
168
-
169
- # If thread is already running, don't start another one
170
- if cache_cleanup_thread is not None and cache_cleanup_thread.is_alive():
171
- return
172
-
173
- # If caching is disabled, don't start the thread
174
- if not app.config["CACHE_ENABLED"]:
175
- logger.debug("Caching is disabled, not starting cache cleanup thread")
176
- return
177
-
178
- def cleanup_worker():
179
- while True:
180
- try:
181
- with app.app_context():
182
- cache_cleanup(app)
183
- cleanup_interval = app.config["CACHE_CLEANUP_INTERVAL"]
184
- time.sleep(cleanup_interval)
185
- except Exception as e:
186
- logger.error(f"Error in cache cleanup worker: {str(e)}")
187
- # Sleep a bit to avoid tight loop in case of recurring errors
188
- time.sleep(60)
189
-
190
- cache_cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
191
- cache_cleanup_thread.start()
192
- logger.debug("Started cache cleanup background thread")
193
-
194
-
195
202
  def init_proxy_routes(app):
196
203
  # Create cache directory if it doesn't exist and caching is enabled
197
204
  if app.config["CACHE_ENABLED"]:
@@ -215,6 +222,9 @@ def init_proxy_routes(app):
215
222
 
216
223
  logger.debug(f"Proxy request for URL: {url}, filename: {filename}")
217
224
 
225
+ # Clean up the cache if needed
226
+ maybe_cleanup_cache(app)
227
+
218
228
  if url is not None:
219
229
  if url.startswith("https://cdn.instructables.com/") or url.startswith(
220
230
  "https://content.instructables.com/"
@@ -21,21 +21,41 @@
21
21
  {% include "footer.html" %}
22
22
  {% if config["THEME"] == "auto" %}
23
23
  <script>
24
+ // Create theme switcher elements
25
+ const themeSwitchWrapper = document.querySelector('.theme-switch-wrapper');
26
+ const sunIcon = document.createElement('span');
27
+ sunIcon.className = 'theme-icon sun-icon';
28
+ const moonIcon = document.createElement('span');
29
+ moonIcon.className = 'theme-icon moon-icon';
30
+ const themeSwitchLabel = document.createElement('label');
31
+ themeSwitchLabel.className = 'theme-switch';
32
+ const themeSwitchInput = document.createElement('input');
33
+ themeSwitchInput.type = 'checkbox';
34
+ themeSwitchInput.id = 'checkbox';
35
+ const sliderDiv = document.createElement('div');
36
+ sliderDiv.className = 'slider';
37
+
38
+ // Append elements to the theme switch wrapper
39
+ themeSwitchLabel.appendChild(themeSwitchInput);
40
+ themeSwitchLabel.appendChild(sliderDiv);
41
+ themeSwitchWrapper.appendChild(sunIcon);
42
+ themeSwitchWrapper.appendChild(themeSwitchLabel);
43
+ themeSwitchWrapper.appendChild(moonIcon);
44
+
24
45
  // Theme toggle functionality
25
- const toggleSwitch = document.querySelector('#checkbox');
26
46
  const currentTheme = localStorage.getItem('theme');
27
47
 
28
48
  // Set theme based on saved preference or system preference
29
49
  if (currentTheme) {
30
50
  document.documentElement.setAttribute('data-theme', currentTheme);
31
51
  if (currentTheme === 'dark') {
32
- toggleSwitch.checked = true;
52
+ themeSwitchInput.checked = true;
33
53
  }
34
54
  } else {
35
55
  // Check if user prefers dark mode
36
56
  if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
37
57
  document.documentElement.setAttribute('data-theme', 'dark');
38
- toggleSwitch.checked = true;
58
+ themeSwitchInput.checked = true;
39
59
  localStorage.setItem('theme', 'dark');
40
60
  }
41
61
  }
@@ -51,7 +71,7 @@
51
71
  }
52
72
  }
53
73
 
54
- toggleSwitch.addEventListener('change', switchTheme, false);
74
+ themeSwitchInput.addEventListener('change', switchTheme, false);
55
75
  </script>
56
76
  {% endif %}
57
77
  </body>
@@ -20,16 +20,8 @@
20
20
  <li class="nav-item">
21
21
  <a class="nav-link" href="/sitemap/">Sitemap</a>
22
22
  </li>
23
- {% if config["THEME"] == "auto" %}
24
23
  <li class="nav-item theme-switch-wrapper">
25
- <span class="theme-icon sun-icon"></span>
26
- <label class="theme-switch" for="checkbox">
27
- <input type="checkbox" id="checkbox" />
28
- <div class="slider"></div>
29
- </label>
30
- <span class="theme-icon moon-icon"></span>
31
24
  </li>
32
- {% endif %}
33
25
  </ul>
34
26
  <form class="search-form" action="/search" method="post">
35
27
  <input class="search-input"
@@ -1,10 +1,14 @@
1
1
  from urllib.request import urlopen
2
2
  import logging
3
+ import time
3
4
  from bs4 import BeautifulSoup
4
5
  from .helpers import proxy, projects_search
5
6
 
6
7
  logger = logging.getLogger(__name__)
7
8
 
9
+ # Track the last data update time
10
+ last_data_update = 0
11
+
8
12
  def update_data(app):
9
13
  """Update the application's cached data.
10
14
 
@@ -73,4 +77,23 @@ def update_data(app):
73
77
  logger.debug(f"Updated global projects list with {len(app.global_ibles['/projects'])} projects")
74
78
  logger.debug("Data update completed successfully")
75
79
  except Exception as e:
76
- logger.error(f"Error updating data: {str(e)}")
80
+ logger.error(f"Error updating data: {str(e)}")
81
+
82
+
83
+ def maybe_update_data(app):
84
+ """Updates the data if it's time to do so.
85
+
86
+ This replaces the background thread with a request-triggered update.
87
+
88
+ Args:
89
+ app (Flask): The Flask app instance.
90
+ """
91
+ global last_data_update
92
+ current_time = time.time()
93
+
94
+ # Update every 5 minutes (300 seconds)
95
+ if current_time - last_data_update >= 300:
96
+ logger.debug("Running scheduled data update")
97
+ update_data(app)
98
+ last_data_update = current_time
99
+ logger.debug("Data update complete")
@@ -1,65 +0,0 @@
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()
File without changes
File without changes
File without changes
File without changes
File without changes