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.
- {structables-0.3.15 → structables-0.4.0}/.forgejo/workflows/docker.yml +2 -2
- {structables-0.3.15 → structables-0.4.0}/.forgejo/workflows/release.yml +1 -1
- {structables-0.3.15 → structables-0.4.0}/.gitignore +1 -0
- {structables-0.3.15 → structables-0.4.0}/Dockerfile +1 -1
- {structables-0.3.15 → structables-0.4.0}/PKG-INFO +42 -7
- {structables-0.3.15 → structables-0.4.0}/README.md +40 -5
- structables-0.4.0/instances.json +54 -0
- {structables-0.3.15 → structables-0.4.0}/pyproject.toml +1 -1
- structables-0.4.0/renovate.json +6 -0
- structables-0.4.0/requirements-dev.txt +6 -0
- structables-0.4.0/src/structables/config.py +44 -0
- structables-0.4.0/src/structables/main.py +65 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/routes/category.py +21 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/routes/contest.py +39 -4
- {structables-0.3.15 → structables-0.4.0}/src/structables/routes/main.py +125 -49
- {structables-0.3.15 → structables-0.4.0}/src/structables/routes/member.py +22 -3
- structables-0.4.0/src/structables/routes/proxy.py +358 -0
- structables-0.4.0/src/structables/static/css/iframe.css +114 -0
- structables-0.4.0/src/structables/static/css/style.css +1190 -0
- structables-0.4.0/src/structables/static/img/magnifying-glass-solid.svg +2 -0
- structables-0.4.0/src/structables/templates/base.html +58 -0
- structables-0.4.0/src/structables/templates/category.html +35 -0
- structables-0.4.0/src/structables/templates/footer.html +17 -0
- structables-0.4.0/src/structables/templates/header.html +49 -0
- structables-0.4.0/src/structables/templates/iframe.html +23 -0
- structables-0.4.0/src/structables/templates/index.html +37 -0
- structables-0.4.0/src/structables/templates/projects.html +49 -0
- structables-0.4.0/src/structables/utils/data.py +76 -0
- structables-0.4.0/src/structables/utils/helpers.py +551 -0
- structables-0.3.15/src/structables/config.py +0 -13
- structables-0.3.15/src/structables/main.py +0 -43
- structables-0.3.15/src/structables/routes/proxy.py +0 -60
- structables-0.3.15/src/structables/static/css/style.css +0 -282
- structables-0.3.15/src/structables/static/dist/css/bootstrap.min.css +0 -6
- structables-0.3.15/src/structables/static/dist/css/bootstrap.min.css.map +0 -1
- structables-0.3.15/src/structables/static/img/magnifying-glass-solid.svg +0 -1
- structables-0.3.15/src/structables/templates/base.html +0 -27
- structables-0.3.15/src/structables/templates/category.html +0 -68
- structables-0.3.15/src/structables/templates/footer.html +0 -21
- structables-0.3.15/src/structables/templates/header.html +0 -39
- structables-0.3.15/src/structables/templates/iframe.html +0 -12
- structables-0.3.15/src/structables/templates/index.html +0 -45
- structables-0.3.15/src/structables/templates/projects.html +0 -56
- structables-0.3.15/src/structables/utils/data.py +0 -56
- structables-0.3.15/src/structables/utils/helpers.py +0 -462
- {structables-0.3.15 → structables-0.4.0}/.env.example +0 -0
- {structables-0.3.15 → structables-0.4.0}/LICENSE +0 -0
- {structables-0.3.15 → structables-0.4.0}/cronjob.txt +0 -0
- {structables-0.3.15 → structables-0.4.0}/docker-compose-example-dev.yml +0 -0
- {structables-0.3.15 → structables-0.4.0}/docker-compose-example.yml +0 -0
- {structables-0.3.15 → structables-0.4.0}/entrypoint.sh +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/__init__.py +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/routes/__init__.py +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/static/img/logo.png +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/static/img/logo_lg.png +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/400.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/404.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/429.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/500.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/archives.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/article-review.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/article.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/collection.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/contest.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/contests.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/member-instructables.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/member.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/privacypolicy.html +0 -0
- {structables-0.3.15 → structables-0.4.0}/src/structables/templates/sitemap.html +0 -0
- {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:
|
|
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@
|
|
28
|
+
uses: docker/build-push-action@v6
|
|
29
29
|
with:
|
|
30
30
|
push: true
|
|
31
31
|
tags: |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: structables
|
|
3
|
-
Version: 0.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
]
|
|
@@ -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 =
|
|
65
|
-
headers = {"x-typesense-api-key": app.
|
|
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
|
-
|
|
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 = {
|