mirror of
https://github.com/ThePhaseless/Byparr.git
synced 2026-09-24 22:03:30 +01:00
Compare commits
374
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4f722d121 | ||
|
|
38b3407b15 | ||
|
|
81959df037 | ||
|
|
05f9f44c79 | ||
|
|
08cfd95d6a | ||
|
|
f60ecbf2ee | ||
|
|
630389aebe | ||
|
|
603cd0f9a7 | ||
|
|
c75c79a2ad | ||
|
|
f3fe419eae | ||
|
|
e1a2e77036 | ||
|
|
1bdd65499d | ||
|
|
e15d1facf2 | ||
|
|
38d5b7dd21 | ||
|
|
96d96c32a6 | ||
|
|
c9967c85aa | ||
|
|
ed86a79c4d | ||
|
|
d6b2036956 | ||
|
|
dd74f80c18 | ||
|
|
2d9e415eb0 | ||
|
|
a328e52471 | ||
|
|
e3d1cdfb3e | ||
|
|
ae12dc05e4 | ||
|
|
d5a1bed3c1 | ||
|
|
3830149ba0 | ||
|
|
7a9a64a4b2 | ||
|
|
16efa73f48 | ||
|
|
a5d6a27b2c | ||
|
|
20ab7491e1 | ||
|
|
9aa041a022 | ||
|
|
725339f316 | ||
|
|
fed8e5d0a6 | ||
|
|
e612d82c2c | ||
|
|
79188f171c | ||
|
|
2292ce7479 | ||
|
|
8ea5d32e03 | ||
|
|
a4ba7c257d | ||
|
|
44bab14090 | ||
|
|
a903353fef | ||
|
|
3ec4ea5375 | ||
|
|
27f67be245 | ||
|
|
f78c18ed25 | ||
|
|
b8cf0b776e | ||
|
|
6fca51f408 | ||
|
|
5a4de1204e | ||
|
|
1da4d608a1 | ||
|
|
db0924d9ce | ||
|
|
b8d2b80746 | ||
|
|
537d7f803c | ||
|
|
1273dd51c2 | ||
|
|
332402652d | ||
|
|
061f0d44de | ||
|
|
b7914268bc | ||
|
|
2e8ed3fa9b | ||
|
|
6f6941e423 | ||
|
|
25ae01ba71 | ||
|
|
a065b4b7ae | ||
|
|
508568ded7 | ||
|
|
72f54fc0a9 | ||
|
|
1499cfa3df | ||
|
|
cf46f481e1 | ||
|
|
c90b0ab91e | ||
|
|
f5e75b451c | ||
|
|
0fbae3f9dd | ||
|
|
70f99e40fc | ||
|
|
6a56098a33 | ||
|
|
7dd1dd633f | ||
|
|
e825e588a8 | ||
|
|
43fe522417 | ||
|
|
ad88b7b505 | ||
|
|
78deb9ed34 | ||
|
|
65f39b4448 | ||
|
|
565b076c67 | ||
|
|
0ea76ac000 | ||
|
|
52f4b6110a | ||
|
|
8c9795150e | ||
|
|
44c3d5b512 | ||
|
|
817610d216 | ||
|
|
bf1daabaf9 | ||
|
|
9445ffc5f6 | ||
|
|
67303951bb | ||
|
|
42303d5582 | ||
|
|
35c6189f6d | ||
|
|
1e6bd9e656 | ||
|
|
7bd7925530 | ||
|
|
f0dd9517e7 | ||
|
|
af72a32870 | ||
|
|
d0dea96f63 | ||
|
|
d7138776a5 | ||
|
|
7e2c895a11 | ||
|
|
29793054f8 | ||
|
|
9fbf4982c8 | ||
|
|
f60d828914 | ||
|
|
bdcf756b7e | ||
|
|
61ef009944 | ||
|
|
02fdfb619a | ||
|
|
978efa7a5b | ||
|
|
a912a32eea | ||
|
|
b8a3995c8c | ||
|
|
59104a3c6e | ||
|
|
a9edd671a2 | ||
|
|
5c5451785e | ||
|
|
4d11b950a1 | ||
|
|
df161e6d2e | ||
|
|
8e53210014 | ||
|
|
43cd9297cb | ||
|
|
19670c8e06 | ||
|
|
eba16fc3de | ||
|
|
ecbb2ab182 | ||
|
|
36421b2c1b | ||
|
|
ac953fe704 | ||
|
|
30f25acd55 | ||
|
|
554347ce6f | ||
|
|
58a486c41d | ||
|
|
dc80b0cccf | ||
|
|
84a70ad35b | ||
|
|
6618bcf651 | ||
|
|
620c768375 | ||
|
|
9244768ce4 | ||
|
|
9bd13e19a0 | ||
|
|
ecd9e0aee2 | ||
|
|
78137997e1 | ||
|
|
03c98375f2 | ||
|
|
4bfb7845dd | ||
|
|
d7c22f042e | ||
|
|
33a1327f93 | ||
|
|
e52616934b | ||
|
|
892daccc60 | ||
|
|
3d91874ddb | ||
|
|
3026ed4a41 | ||
|
|
4b5d82b4f1 | ||
|
|
56d9a4659b | ||
|
|
273622c30c | ||
|
|
d0b51020a0 | ||
|
|
d17def34ba | ||
|
|
40a4bf3d8f | ||
|
|
e0bb4115fb | ||
|
|
9e2d40845c | ||
|
|
6c3da87c00 | ||
|
|
78d97dfd0d | ||
|
|
4461f5a59a | ||
|
|
7dac738ac5 | ||
|
|
ead7a09ce2 | ||
|
|
bf85a854fa | ||
|
|
9a20451ad9 | ||
|
|
150bf80583 | ||
|
|
bcce6779cc | ||
|
|
cd7e803411 | ||
|
|
5dc76167c2 | ||
|
|
8ae150fd87 | ||
|
|
0325877e19 | ||
|
|
c4159e5c38 | ||
|
|
83169ce019 | ||
|
|
1c0929fbc5 | ||
|
|
9170967455 | ||
|
|
d5d0bf7923 | ||
|
|
17854c70ed | ||
|
|
e30954c98c | ||
|
|
2556a1bc88 | ||
|
|
6c065d4dd3 | ||
|
|
096eda0fa3 | ||
|
|
2ebb1bd820 | ||
|
|
bf37539890 | ||
|
|
d7ef83b8c6 | ||
|
|
1a7cd9d657 | ||
|
|
f9387e6942 | ||
|
|
9d8c8db979 | ||
|
|
979e0b4803 | ||
|
|
3dbe3c424c | ||
|
|
bbb7c73065 | ||
|
|
9a353b3da2 | ||
|
|
140a97003e | ||
|
|
c9e29f578d | ||
|
|
c37ab51179 | ||
|
|
962db9409c | ||
|
|
96a9cc7ba9 | ||
|
|
bf9460b960 | ||
|
|
c0547192dd | ||
|
|
008077076c | ||
|
|
b33444dc48 | ||
|
|
b8c8576e5a | ||
|
|
905d802a26 | ||
|
|
f99429fdea | ||
|
|
1a4ec03587 | ||
|
|
ee2630dce2 | ||
|
|
82130b77c0 | ||
|
|
faf7e5e51a | ||
|
|
597faa48dc | ||
|
|
a28dbbe972 | ||
|
|
82124dee9b | ||
|
|
0551c7b82b | ||
|
|
b483dba080 | ||
|
|
7533d039ac | ||
|
|
38b2ea0798 | ||
|
|
58ae7964df | ||
|
|
5283fb2d0e | ||
|
|
b3e7662782 | ||
|
|
074a3dd712 | ||
|
|
cb48f16370 | ||
|
|
637de7572d | ||
|
|
7b55252a73 | ||
|
|
cbe4cf57a5 | ||
|
|
0775d107ad | ||
|
|
d36972b80e | ||
|
|
27ec17f7f2 | ||
|
|
0b2bf9e12a | ||
|
|
8c6906c916 | ||
|
|
3103051ccf | ||
|
|
8551bdfef8 | ||
|
|
cdebe4be0e | ||
|
|
543296ce68 | ||
|
|
7210ae4bc0 | ||
|
|
f782e1bed6 | ||
|
|
49329ec950 | ||
|
|
2ae289ef0c | ||
|
|
8ef570ec46 | ||
|
|
e899f8d5ac | ||
|
|
ab0e7f9e43 | ||
|
|
d26974e22a | ||
|
|
994dcb2dc4 | ||
|
|
0d4d5093ca | ||
|
|
fd8c7156fd | ||
|
|
39a5045836 | ||
|
|
6dc1133daf | ||
|
|
794ebdf00e | ||
|
|
c366ce7db6 | ||
|
|
00e443a5eb | ||
|
|
b9f3153689 | ||
|
|
4453fa6a93 | ||
|
|
c40ad328f1 | ||
|
|
428fbd4b24 | ||
|
|
471f17a0d3 | ||
|
|
c81b93a161 | ||
|
|
f2532385d6 | ||
|
|
8d9f433aca | ||
|
|
52ad4c74c7 | ||
|
|
b64ff25ad2 | ||
|
|
8b9ef2604a | ||
|
|
57dbc43697 | ||
|
|
77fd2a47f7 | ||
|
|
0e75f2a3d1 | ||
|
|
d0a1489898 | ||
|
|
583f5fcebc | ||
|
|
3c25ce471b | ||
|
|
b01ee50444 | ||
|
|
f117e4705a | ||
|
|
b19b1b852c | ||
|
|
70d78859ad | ||
|
|
f5c3cae846 | ||
|
|
db18c01f51 | ||
|
|
3991a0bceb | ||
|
|
88abca13c5 | ||
|
|
3b7f501404 | ||
|
|
e41c831c73 | ||
|
|
c409ca22f9 | ||
|
|
8e5b7061e8 | ||
|
|
aabff0a65f | ||
|
|
58f230dce3 | ||
|
|
0535c5d9bd | ||
|
|
d26ddf3bcf | ||
|
|
361e11cda2 | ||
|
|
e33d7ab77b | ||
|
|
4e3876e0b7 | ||
|
|
f4dcaf453e | ||
|
|
95b5b1375f | ||
|
|
b3d7060431 | ||
|
|
2dfc5b2201 | ||
|
|
55a557468e | ||
|
|
2da2d08718 | ||
|
|
a12594b11a | ||
|
|
8c249fbbbd | ||
|
|
8b0caf6adc | ||
|
|
dbc91bc982 | ||
|
|
e0a2a2a035 | ||
|
|
5003544d5b | ||
|
|
a2fd8a056b | ||
|
|
4bc53e5311 | ||
|
|
23a5d53e9a | ||
|
|
b924d27d26 | ||
|
|
ca80bd0285 | ||
|
|
464a618f29 | ||
|
|
093d318dbc | ||
|
|
d452d43ee8 | ||
|
|
b7f9c1467c | ||
|
|
2e819c9adf | ||
|
|
d77dc06b35 | ||
|
|
29a5432684 | ||
|
|
b204acb826 | ||
|
|
8a90428f4b | ||
|
|
5424ea0ab7 | ||
|
|
718e6da2b6 | ||
|
|
60bf8332d0 | ||
|
|
d2f7f3448d | ||
|
|
7d530daf88 | ||
|
|
7dc16dfe7c | ||
|
|
87db1a3955 | ||
|
|
bf0648e49b | ||
|
|
70b75f4db4 | ||
|
|
2ab91102b0 | ||
|
|
5c87b29599 | ||
|
|
76a93f44a1 | ||
|
|
9dff932766 | ||
|
|
749fab3ff1 | ||
|
|
77290081b7 | ||
|
|
843fe88e5a | ||
|
|
0c2206911d | ||
|
|
cbce34366b | ||
|
|
29a31ab757 | ||
|
|
ee9e8ca98f | ||
|
|
e25415012a | ||
|
|
b518904196 | ||
|
|
a633a91dcf | ||
|
|
22fba55373 | ||
|
|
6e825e15a4 | ||
|
|
ac4eadea9d | ||
|
|
0d79b22ceb | ||
|
|
d8d92ec3de | ||
|
|
79fb20082f | ||
|
|
2844f51fb1 | ||
|
|
092b9fb492 | ||
|
|
b1a67da658 | ||
|
|
40b790ceee | ||
|
|
312ea474d9 | ||
|
|
98719476c4 | ||
|
|
e05c34aa8a | ||
|
|
76502ee508 | ||
|
|
fb905a251a | ||
|
|
ed71354e19 | ||
|
|
8c8b7e3324 | ||
|
|
d9c26877ec | ||
|
|
d377d63400 | ||
|
|
7c61d37ea8 | ||
|
|
3fa2b49b6d | ||
|
|
1d9740cdd8 | ||
|
|
d5a1680b4e | ||
|
|
58200970de | ||
|
|
8311b7b148 | ||
|
|
86fd1e2c5f | ||
|
|
bccd8c8401 | ||
|
|
51f01e2f97 | ||
|
|
7a47e446a0 | ||
|
|
9808c42a8a | ||
|
|
f4c13957bf | ||
|
|
25d708de01 | ||
|
|
6e45f4c316 | ||
|
|
4f225438a7 | ||
|
|
eaf83e62ff | ||
|
|
336d20a3a5 | ||
|
|
6c124dcfd0 | ||
|
|
2ad7ee262e | ||
|
|
75bac7e203 | ||
|
|
8501e28f85 | ||
|
|
3682ccc0fa | ||
|
|
b0c8d5d387 | ||
|
|
8c428276d6 | ||
|
|
003e7d0139 | ||
|
|
2d1b2964af | ||
|
|
427c758de6 | ||
|
|
031235dba1 | ||
|
|
2a3a76b6ef | ||
|
|
104ef5968a | ||
|
|
2341ad5887 | ||
|
|
a5c2355f21 | ||
|
|
d3617797e6 | ||
|
|
e449a183ae | ||
|
|
335e9e486f | ||
|
|
70cb18ae81 | ||
|
|
d8fb8feaeb | ||
|
|
4831e1247b | ||
|
|
87ee96ef5c | ||
|
|
2a50a019e1 | ||
|
|
0ddf1e03c0 | ||
|
|
b6c51470ff | ||
|
|
640a4bd8d8 |
@@ -0,0 +1,4 @@
|
||||
FROM ghcr.io/astral-sh/uv:debian
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
RUN apt update && apt upgrade -y && apt install -y chromium chromium-driver xvfb git
|
||||
@@ -3,26 +3,36 @@
|
||||
{
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"build": {
|
||||
"dockerfile": "../Dockerfile",
|
||||
"target": "devcontainer"
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"runArgs": ["--security-opt", "label=disable", "--privileged"],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"charliermarsh.ruff",
|
||||
"ms-python.python",
|
||||
"tamasfe.even-better-toml"
|
||||
"-ms-python.autopep8",
|
||||
"-ms-python.vscode-pylance"
|
||||
],
|
||||
"settings": {
|
||||
"python.venvPath": "./venv"
|
||||
}
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "uv sync --group test",
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "pip3 install --user -r requirements.txt",
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
"name": "Byparr Dev Container",
|
||||
"runArgs": [
|
||||
"-p",
|
||||
"8181:8191"
|
||||
],
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"moby": false
|
||||
}
|
||||
"ghcr.io/devcontainers-extra/features/act:1": {},
|
||||
"ghcr.io/devcontainers/features/desktop-lite:1": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
.gitignore
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
.extentions/
|
||||
@@ -9,32 +9,15 @@ body:
|
||||
label: "I've completed the following steps:"
|
||||
options:
|
||||
- label: Read Loop Warning on Readme
|
||||
- label: "Used --init/init: true in docker run/compose.yaml"
|
||||
- label: Done the troubleshooting from Readme
|
||||
- label: Checked if such issue already exists
|
||||
- label: Checked other websites with Cloudflare Turnstile
|
||||
- label: I am able to access the webpage from my browser
|
||||
- label: (Docker only) Checked that the problem does also occur on main tag
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: "If checked other websites with Cloudflare Turnstile:"
|
||||
options:
|
||||
- label: Other websites do work.
|
||||
- label: Other websites do not work.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: "The issue is still present in the latest main tag:"
|
||||
options:
|
||||
- label: "Yes"
|
||||
- label: "No"
|
||||
- label: Checked other websites with Cloudflare Turnstile
|
||||
|
||||
- type: input
|
||||
id: docker-host
|
||||
attributes:
|
||||
label: OS/Docker Host
|
||||
placeholder: e.g. Docker, WSL2, Ubuntu, Windows
|
||||
label: OS
|
||||
placeholder: e.g. Docker, WSL2, Ubuntu, Windows
|
||||
|
||||
- type: input
|
||||
id: platform-model
|
||||
@@ -51,7 +34,7 @@ body:
|
||||
- type: input
|
||||
id: website
|
||||
attributes:
|
||||
label: Example website/URL that does not work
|
||||
label: Website
|
||||
placeholder: e.g. https://nowsecure.nl
|
||||
|
||||
- type: textarea
|
||||
@@ -59,7 +42,7 @@ body:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: "Example: The application crashes when trying to load a new file"
|
||||
|
||||
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
name: Cleanup PR Docker Images
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Delete PR Docker images
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner.toLowerCase();
|
||||
const repo = context.repo.repo.toLowerCase();
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
|
||||
// Get the package
|
||||
const packageName = `${repo}`;
|
||||
|
||||
console.log(`Cleaning up images for PR #${prNumber}`);
|
||||
|
||||
// Check if owner is an org or user
|
||||
let isOrg = false;
|
||||
try {
|
||||
await github.rest.orgs.get({ org: owner });
|
||||
isOrg = true;
|
||||
console.log(`Repository owner is an organization`);
|
||||
} catch (error) {
|
||||
console.log(`Repository owner is a user account`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all versions of the package
|
||||
let versions;
|
||||
if (isOrg) {
|
||||
versions = await github.rest.packages.getAllPackageVersionsForPackageOwnedByOrg({
|
||||
package_type: 'container',
|
||||
package_name: packageName,
|
||||
org: owner,
|
||||
per_page: 100
|
||||
});
|
||||
} else {
|
||||
versions = await github.rest.packages.getAllPackageVersionsForPackageOwnedByUser({
|
||||
package_type: 'container',
|
||||
package_name: packageName,
|
||||
username: owner,
|
||||
per_page: 100
|
||||
});
|
||||
}
|
||||
|
||||
// Filter versions that match this PR (pr-123, pr-123-amd64, pr-123-arm64)
|
||||
const prVersions = versions.data.filter(version => {
|
||||
const tags = version.metadata?.container?.tags || [];
|
||||
// Match tags like: pr-123, pr-123-amd64, pr-123-arm64
|
||||
return tags.some(tag =>
|
||||
tag === `pr-${prNumber}` ||
|
||||
tag === `pr-${prNumber}-amd64` ||
|
||||
tag === `pr-${prNumber}-arm64`
|
||||
);
|
||||
});
|
||||
|
||||
console.log(`Found ${prVersions.length} image version(s) to delete`);
|
||||
|
||||
// Delete each version
|
||||
for (const version of prVersions) {
|
||||
console.log(`Deleting version ${version.id} with tags: ${version.metadata?.container?.tags?.join(', ')}`);
|
||||
try {
|
||||
if (isOrg) {
|
||||
await github.rest.packages.deletePackageVersionForOrg({
|
||||
package_type: 'container',
|
||||
package_name: packageName,
|
||||
org: owner,
|
||||
package_version_id: version.id
|
||||
});
|
||||
} else {
|
||||
await github.rest.packages.deletePackageVersionForUser({
|
||||
package_type: 'container',
|
||||
package_name: packageName,
|
||||
username: owner,
|
||||
package_version_id: version.id
|
||||
});
|
||||
}
|
||||
console.log(`✓ Deleted version ${version.id}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete version ${version.id}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Cleanup completed');
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log('No package found - nothing to clean up');
|
||||
} else {
|
||||
console.error('Error during cleanup:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -9,29 +9,11 @@ on:
|
||||
schedule:
|
||||
- cron: "25 0 * * *"
|
||||
push:
|
||||
branches: ["*"]
|
||||
branches: ["main"]
|
||||
# Publish semver tags as releases.
|
||||
tags: ["v*.*.*"]
|
||||
paths:
|
||||
- "src/**"
|
||||
- "main.py"
|
||||
- "tests/**"
|
||||
- "Dockerfile"
|
||||
- "pyproject.toml"
|
||||
- "uv.lock"
|
||||
- "compose.yaml"
|
||||
- ".github/workflows/docker-publish.yml"
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "src/**"
|
||||
- "main.py"
|
||||
- "tests/**"
|
||||
- "Dockerfile"
|
||||
- "pyproject.toml"
|
||||
- "uv.lock"
|
||||
- "compose.yaml"
|
||||
- ".github/workflows/docker-publish.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -40,10 +22,6 @@ env:
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -54,14 +32,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Test
|
||||
id: test
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
@@ -72,8 +50,6 @@ jobs:
|
||||
|
||||
build:
|
||||
needs: test
|
||||
# Skip build for Renovate PRs
|
||||
if: github.event.pull_request.user.login != 'renovate[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -86,31 +62,26 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare variables
|
||||
id: vars
|
||||
run: |
|
||||
SURFIX=$(echo ${{ matrix.platform }} | cut -d'/' -f2)
|
||||
echo "SURFIX=$SURFIX" >> $GITHUB_OUTPUT
|
||||
|
||||
# Use PR number for PRs, SHA for everything else
|
||||
if [ "${{ github.event_name }}" == "pull_request" ]; then
|
||||
echo "LOCAL_TAG=pr-${{ github.event.pull_request.number }}-$SURFIX" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "LOCAL_TAG=${{ github.sha }}-$SURFIX" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
# Generate a unique local tag for the image
|
||||
echo "LOCAL_TAG=${{ github.sha }}-$SURFIX" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up BuildKit Docker container builder
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log into registry
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -119,19 +90,19 @@ jobs:
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
tags: type=raw,value=${{ steps.vars.outputs.LOCAL_TAG }}
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image for each platform
|
||||
# Build and export Docker image for each platform (without pushing)
|
||||
- name: Build Docker image
|
||||
id: build
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
pull: true
|
||||
push: true
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
@@ -144,6 +115,7 @@ jobs:
|
||||
merge-and-push:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -151,7 +123,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Install the cosign tool
|
||||
- name: Install cosign
|
||||
@@ -159,11 +131,11 @@ jobs:
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log into registry
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -172,7 +144,7 @@ jobs:
|
||||
# Extract Docker metadata for tagging
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
@@ -202,16 +174,9 @@ jobs:
|
||||
|
||||
echo $args
|
||||
|
||||
# Use PR-based tags for PRs, SHA-based tags for everything else
|
||||
if [ "${{ github.event_name }}" == "pull_request" ]; then
|
||||
docker buildx imagetools create $args \
|
||||
${image}:pr-${{ github.event.pull_request.number }}-amd64 \
|
||||
${image}:pr-${{ github.event.pull_request.number }}-arm64
|
||||
else
|
||||
docker buildx imagetools create $args \
|
||||
${image}:${{github.sha}}-amd64 \
|
||||
${image}:${{github.sha}}-arm64
|
||||
fi
|
||||
docker buildx imagetools create $args \
|
||||
${image}:${{github.sha}}-amd64 \
|
||||
${image}:${{github.sha}}-arm64
|
||||
|
||||
# Sign the manifest
|
||||
- name: Sign the manifests
|
||||
|
||||
+1
-4
@@ -168,7 +168,4 @@ core
|
||||
screenshots/
|
||||
|
||||
# Downloaded files
|
||||
downloaded_files/
|
||||
|
||||
# Browser Data
|
||||
browser_data/
|
||||
downloaded_files/
|
||||
Vendored
+18
-16
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"LOG_LEVEL": "DEBUG"
|
||||
},
|
||||
"purpose": ["debug-test", "debug-in-terminal"],
|
||||
"justMyCode": false,
|
||||
"name": "Python Debugger: Main",
|
||||
"program": "main.py",
|
||||
"request": "launch",
|
||||
"type": "debugpy"
|
||||
}
|
||||
]
|
||||
}
|
||||
"configurations": [
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"LOG_LEVEL": "DEBUG"
|
||||
},
|
||||
"justMyCode": false,
|
||||
"name": "Python Debugger: Main",
|
||||
"program": "main.py",
|
||||
"request": "launch",
|
||||
"type": "debugpy"
|
||||
},
|
||||
],
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0"
|
||||
}
|
||||
Vendored
+22
-17
@@ -1,18 +1,23 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"python.analysis.autoImportCompletions": true,
|
||||
"python.analysis.packageIndexDepths": [
|
||||
{
|
||||
"depth": 5,
|
||||
"includeAllSymbols": true,
|
||||
"name": ""
|
||||
}
|
||||
],
|
||||
"python.terminal.activateEnvironment": true,
|
||||
"python.testing.pytestArgs": ["tests", "-n", "auto", "--retries=3"],
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.unittestEnabled": false,
|
||||
"cSpell.words": ["camoufox", "domcontentloaded", "networkidle"]
|
||||
}
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"python.analysis.autoImportCompletions": true,
|
||||
"python.analysis.packageIndexDepths": [
|
||||
{
|
||||
"depth": 5,
|
||||
"includeAllSymbols": true,
|
||||
"name": ""
|
||||
}
|
||||
],
|
||||
"python.analysis.typeCheckingMode": "standard",
|
||||
"python.terminal.activateEnvironment": true,
|
||||
"python.testing.pytestArgs": [
|
||||
"tests",
|
||||
"-n",
|
||||
"auto",
|
||||
"--retries=3",
|
||||
],
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.unittestEnabled": false
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
# Copilot Instructions
|
||||
|
||||
## Project overview
|
||||
|
||||
- FastAPI service that mimics FlareSolverr-style API for bypassing anti-bot pages using Camoufox + Playwright captcha solver.
|
||||
- Entry point: main app in main.py; routes and request flow defined in src/endpoints.py and src/models.py.
|
||||
- Browser lifecycle is owned by get_camoufox() in src/utils.py, which yields a page, solver, and context for each request.
|
||||
|
||||
## Architecture and data flow
|
||||
|
||||
- Request flow: POST /v1 -> read_item() -> browser.goto() -> wait for load states -> detect challenge title -> solve captcha -> return LinkResponse.
|
||||
- Challenge detection uses CHALLENGE_TITLES from src/consts.py; update this map when adding providers.
|
||||
- Health check hits /v1 internally with <https://google.com> and fails if status is not OK.
|
||||
- Logging: LogRequest middleware logs only POST /v1 timing and outcome; other paths pass through.
|
||||
|
||||
## Key modules and patterns
|
||||
|
||||
- Models use Pydantic v2 with camelCase aliasing for responses (see src/models.py).
|
||||
- LinkResponse.invalid() is the standard error response shape; keep fields consistent with FlareSolverr style.
|
||||
- get_camoufox() constructs AsyncCamoufox with addons and optional proxy config from env vars.
|
||||
|
||||
## Config and environment
|
||||
|
||||
- Core env vars in src/consts.py: HOST, PORT, PROXY_SERVER, PROXY_USERNAME, PROXY_PASSWORD, LOG_LEVEL, VERSION.
|
||||
- VERSION strips leading "v" for tag-style values.
|
||||
|
||||
## Developer workflows
|
||||
|
||||
- Local run: uv sync && uv run main.py
|
||||
- Init mode: uv run main.py --init (pre-warms health check via browser setup)
|
||||
- Tests: uv sync --group test && uv run pytest --retries 3
|
||||
- Docker troubleshooting: docker build --target test .
|
||||
|
||||
## Tests and external dependencies
|
||||
|
||||
- tests/main_test.py calls real websites; tests are network-dependent and may be skipped based on upstream status.
|
||||
- HTTP client tests use starlette.testclient + httpx; avoid mocking unless needed for local-only changes.
|
||||
+22
-43
@@ -1,61 +1,40 @@
|
||||
# Ubuntu is required by playwright
|
||||
FROM ubuntu:latest AS base
|
||||
FROM debian:bullseye-slim AS base
|
||||
|
||||
ARG GITHUB_BUILD=false \
|
||||
VERSION
|
||||
|
||||
ENV GITHUB_BUILD=${GITHUB_BUILD}\
|
||||
ENV HOME=/root \
|
||||
GITHUB_BUILD=${GITHUB_BUILD}\
|
||||
VERSION=${VERSION}\
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
# prevents python creating .pyc files
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
PORT=8191 \
|
||||
XDG_CACHE_HOME=/cache \
|
||||
HOME=/tmp
|
||||
DISPLAY=:0
|
||||
ENV PATH="${HOME}/.local/bin:$PATH"
|
||||
|
||||
RUN apt-get update &&\
|
||||
apt-get install -y --no-install-recommends curl ca-certificates &&\
|
||||
apt-get clean &&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
FROM base AS devcontainer
|
||||
RUN apt-get update &&\
|
||||
apt-get install -y --no-install-recommends git &&\
|
||||
uvx playwright install-deps firefox &&\
|
||||
uvx camoufox fetch &&\
|
||||
apt-get clean &&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
ENTRYPOINT [ "sleep", "infinity" ]
|
||||
|
||||
FROM base AS app
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests xauth xvfb scrot curl chromium chromium-driver ca-certificates
|
||||
|
||||
ADD https://astral.sh/uv/install.sh install.sh
|
||||
RUN sh install.sh
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN mkdir -p /cache &&\
|
||||
uv sync &&\
|
||||
uv run camoufox fetch &&\
|
||||
apt-get update &&\
|
||||
uv run playwright install-deps firefox &&\
|
||||
uv cache clean &&\
|
||||
apt-get clean &&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN --mount=type=cache,target=${HOME}/.cache/uv uv sync
|
||||
|
||||
# SeleniumBase does not come with an arm64 chromedriver binary
|
||||
RUN cd .venv/lib/*/site-packages/seleniumbase/drivers && ln -s /usr/bin/chromedriver uc_driver
|
||||
|
||||
COPY . .
|
||||
|
||||
# Make app and cache world-readable; addon scripts dir world-writable (runtime writes)
|
||||
RUN chmod -R o+rX /app /cache &&\
|
||||
find /app/.venv -path "*/camoufox_add_init_script/addon" -type d -exec chmod -R o+rwX {} +
|
||||
FROM base AS test
|
||||
|
||||
FROM app AS test
|
||||
RUN \
|
||||
uv sync --group test &&\
|
||||
uv run pytest --retries 3
|
||||
RUN --mount=type=cache,target=${HOME}/.cache/uv uv sync --group test
|
||||
RUN ./test.sh
|
||||
|
||||
FROM app
|
||||
USER 1000
|
||||
EXPOSE $PORT
|
||||
HEALTHCHECK --interval=15m --timeout=30s --start-period=5s --retries=3 CMD curl "http://localhost:${PORT}/health"
|
||||
ENTRYPOINT ["/app/.venv/bin/python", "main.py"]
|
||||
FROM base
|
||||
|
||||
EXPOSE 8191
|
||||
HEALTHCHECK --interval=15m --timeout=30s --start-period=5s --retries=3 CMD [ "curl", "http://localhost:8191/health" ]
|
||||
CMD ["./cmd.sh"]
|
||||
@@ -1,73 +1,19 @@
|
||||
# Byparr
|
||||
|
||||
<p align="center">
|
||||
<img src="icon/logo-byparr.svg" alt="Byparr logo" width="120" />
|
||||
</p>
|
||||
An alternative to [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) as a drop-in replacement, built with [seleniumbase](https://seleniumbase.io/) and [FastAPI](https://fastapi.tiangolo.com).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This software does not **guarantee** (only greatly increases the chance) that any challenge will be bypassed. While this tool passes the initial browser check, Cloudflare and other captcha providers likely require valid network traffic originating from the user’s public IP address to mark a connection as legitimate. If any website does not pass the challenge, please run troubleshooting steps and check if other websites work before you create an GitHub issue.
|
||||
|
||||
## Options
|
||||
|
||||
| Environment Variable | Default | Description |
|
||||
| -------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `HOST` | `0.0.0.0` | Host address to bind the server to. Use `0.0.0.0` to bind to all IPv4 interfaces, `::` for all IPv6 interfaces, or `127.0.0.1`/`localhost` for local access only. |
|
||||
| `PORT` | `8191` | Port to bind the server to. |
|
||||
| `PROXY_SERVER` | None | Proxy to use in format: `protocol://host:port`. |
|
||||
| `PROXY_USERNAME` | None | Username for proxy authentication. |
|
||||
| `PROXY_PASSWORD` | None | Password for proxy authentication. |
|
||||
|
||||
## Proxy Recommendation
|
||||
|
||||
Recently I've partnered with a _new in town_ proxy service - ProxyBase - to offer affordable proxy services that seems to work seamlessly with Byparr! Using my affiliate code `byparr` (case sensitive!) when signing up will not only get you access to their cost-effective (**$0.69/GB with occasional promotions** _at the time of writing_) proxy network but will also help support the continued development of this project. ProxyBase's proxies can significantly improve your success rate when bypassing anti-bot challenges. [Check out ProxyBase](https://client.proxybase.org/signup?ref=byparr) and enhance your Byparr experience!
|
||||
|
||||
## Tags
|
||||
|
||||
- `v*.*.*`/`latest` - Releases considered stable
|
||||
- `main` - Latest release from main branch (untested)
|
||||
- `pr-{number}` - Pull request images for testing (automatically cleaned up when PR closes)
|
||||
|
||||
## Usage
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Support for NAS devices (like Synology) is minimal. Please report issues, but do not expect it to be fixed quickly. The only ARM device I have is a free Ampere Oracle VM, so I can only test ARM support on that. See [#22](https://github.com/ThePhaseless/Byparr/issues/22) and [#3](https://github.com/ThePhaseless/Byparr/issues/3)
|
||||
|
||||
### Docker Compose setup
|
||||
|
||||
1. Review settings in `compose.yaml`.
|
||||
2. Start the service:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Docker install
|
||||
|
||||
1. Pull and run the image:
|
||||
|
||||
```bash
|
||||
docker run -p 8191:8191 ghcr.io/thephaseless/byparr:latest
|
||||
```
|
||||
|
||||
2. Optional: set env vars using `-e` or `--env-file`.
|
||||
|
||||
### Local install
|
||||
|
||||
1. Install ([or update when Python version changes](https://github.com/astral-sh/uv/issues/17887)) [uv](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
2. Clone this repo - `git clone https://github.com/ThePhaseless/Byparr`
|
||||
3. Run `uv run main.py`
|
||||
4. Enjoy!
|
||||
|
||||
### API Docs
|
||||
|
||||
Once running, open:
|
||||
|
||||
- `http://localhost:8191/docs`
|
||||
- `http://localhost:8191/` (redirects to `/docs`)
|
||||
> [!NOTE]
|
||||
> Thanks to FastAPI implementation, now you can also see the API documentation at `/docs` or `/` (redirect to `/docs`) endpoints.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Docker troubleshooting
|
||||
### Docker
|
||||
|
||||
1. Clone repo to the host that has issues with Byparr.
|
||||
2. Run `docker build --target test .`
|
||||
@@ -75,7 +21,7 @@ Once running, open:
|
||||
1. If run successfully, try updating container or if already on newest stable release create an issue for creating new release with new dependencies
|
||||
2. If build fails, try troubleshooting on another host/using other method
|
||||
|
||||
### Local troubleshooting
|
||||
### Local
|
||||
|
||||
1. Download [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||
2. Download dependencies using `uv sync --group test`
|
||||
@@ -84,3 +30,43 @@ Once running, open:
|
||||
5. Depending of the test success:
|
||||
1. If run successfully, try updating container or if already on newest stable release create an issue for creating new release with new dependencies
|
||||
2. If test fails, try troubleshooting on another host/using other method
|
||||
|
||||
## Options
|
||||
|
||||
| Environment Variable | Default | Description |
|
||||
| -------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `USE_XVFB` | `SeleniumBase default` | Use virtual desktop with Xvfb. (Linux only) (Can cause performance hog [#14](https://github.com/ThePhaseless/Byparr/issues/14)) |
|
||||
| `USE_HEADLESS` | `SeleniumBase default` | Use headless chromium. |
|
||||
| `PROXY` | None | Proxy to use in format: `protocol://username:password@host:port`. [SOCKS5 with authentication is not supported by Chrome](https://stackoverflow.com/questions/75602916/connection-to-private-proxy-socks5-with-chrome-webrequest-onauthrequired-and), see `compose.yaml` file for a workaround |
|
||||
|
||||
## Tags
|
||||
|
||||
- `v*.*.*`/`latest` - Releases considered stable
|
||||
- `main` - Latest release from main branch (untested)
|
||||
|
||||
## Usage
|
||||
|
||||
### Docker Compose
|
||||
|
||||
See `compose.yaml`
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker run -p 8191:8191 ghcr.io/thephaseless/byparr:latest
|
||||
```
|
||||
|
||||
### Local
|
||||
|
||||
```bash
|
||||
uv sync && ./cmd.sh
|
||||
```
|
||||
|
||||
## Need help with / TODO
|
||||
|
||||
- [x] Slimming container (only ~650 MB now!)
|
||||
- [x] Add more anti-bot challenges
|
||||
- [x] Add doc strings
|
||||
- [x] Implement versioning
|
||||
- [x] Proxy support
|
||||
- [x] Add more architectures support
|
||||
|
||||
+17
-5
@@ -1,11 +1,23 @@
|
||||
services:
|
||||
byparr:
|
||||
image: ghcr.io/thephaseless/byparr:latest
|
||||
image: ghcr.io/thephaseless/byparr
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
# Uncomment below to use byparr outside of internal network
|
||||
# ports:
|
||||
# - "8191:8191"
|
||||
# environment: # Uncomment if using pproxy
|
||||
# - proxy=http://pproxy:8080
|
||||
volumes:
|
||||
- ./screenshots:/app/screenshots # For screenshots when exception occurs
|
||||
ports:
|
||||
- "8191:8191"
|
||||
# # Uncomment the following lines to enable pproxy
|
||||
# pproxy:
|
||||
# tty: true # Required for pproxy to work
|
||||
# container_name: pproxy
|
||||
# restart: unless-stopped
|
||||
# image: mosajjal/pproxy:latest
|
||||
# command:
|
||||
# - -vv
|
||||
# - -l http://:8080
|
||||
# - -r socks5://host:port#username:password
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M243.9 511.9c-58.1-3.2-110.4-24-155-61.8-7-5.9-21.2-20.1-27.2-27.2-20-23.7-35.2-49.2-45.4-76.5-8.3-22-13-42.6-15.5-67.3-1.1-10.8-1.1-35.7 0-46.5 5.6-54.9 25.6-101.9 61-143.8 5.9-7 20.1-21.2 27.2-27.2 23.7-20 49.2-35.2 76.5-45.4 22-8.3 42.7-13 67.3-15.5 10.8-1.1 35.7-1.1 46.5 0 29.1 3 55.1 9.7 80 20.7 39.2 17.3 72.8 43.6 99.6 78 5.3 6.7 14.8 21 19.4 29.1 15.8 27.6 26.4 58.3 31.1 89.8 2 13.6 2.5 21.5 2.5 37.5s-.5 23.9-2.5 37.5c-7.1 47.8-27.1 91.6-59.2 129.5-5.9 7-20.1 21.2-27.2 27.2-16.4 13.8-34.2 25.8-51.8 34.6-28.8 14.5-59.1 23.1-91.7 26.3-7.4.8-28.9 1.3-35.6 1" style="fill:#fff"/><path d="M246.5 395.1c-1.7-.2-3.9-.3-5-.2-1 .1-3.5 0-5.5-.2-2-.3-6.1-.8-9.3-1.2s-6.7-.9-7.8-1.2c-1.2-.3-7.7-3.2-14.5-6.6s-14.3-7-16.8-8.1c-8.5-3.7-14.5-8.1-18.1-13.4-2.1-3.1-3.7-4.4-8.2-6.7-4-2-6-4.1-10.9-11-2-2.9-5-6.8-6.6-8.7-10.9-12.9-11.1-13.3-12.4-19.5-.5-2.5-1.6-5.4-2.9-7.8-4-7.4-5.4-10.8-6.4-14.9-.5-2.3-1.7-6.6-2.6-9.6-1.5-5.1-1.7-6.1-2.1-15.8-.4-9.4-.4-10.9.5-16.1.5-3.2 1.1-7.4 1.2-9.4.2-2 .6-5.4 1-7.6s.9-6.5 1.2-9.5c1-9.8 2.6-14.1 9.1-23.8 4.2-6.2 5.2-8.5 6-13.5.6-3.9 1.4-5.7 4.1-8.9 1.2-1.5 3.5-5.4 5.1-8.6 3.1-6.4 3.2-6.5 8.2-9.9 1.9-1.3 5.8-4 8.6-6s6.3-4.4 7.6-5.4c1.7-1.2 3.1-2.7 4.3-4.6 4.1-6.6 6.1-8 16.3-11.7 3.1-1.1 7.5-3.1 9.8-4.5 5.3-3.1 9-4.4 15-5 2.8-.3 6.8-1.2 9.8-2.1 4.5-1.4 5.6-1.6 10.4-1.6 5.7 0 6.4-.2 13.2-3.1 4.8-2.1 8.9-2.5 14.8-1.5 2.7.5 7.2 1.1 10.1 1.5 6.2.8 6.1.8 11.2 3.1 3.4 1.5 4.9 1.9 9.3 2.3 7 .7 7.9 1 16.1 5.1 3.9 2 9.5 4.5 12.4 5.7 9.8 3.9 14.3 7.2 22.1 15.7 3.3 3.6 8.6 9.2 11.9 12.5 14.5 14.5 14.9 15 16.5 19.4.8 2.2 2.6 6 3.9 8.3 5.2 9.2 12.2 23.8 13.4 28 .7 2.3 1.9 6.5 2.7 9.3 2.1 6.9 2.4 14.2 1 23.6-1.2 8-1.2 11.9-.3 18 .6 3.6.6 5.5.3 9.4-.7 6.7-1.6 10-4.1 14.1-1.3 2.2-2.4 4.7-2.9 6.6-1.8 7.8-5.6 16.3-10.7 24.3-1.4 2.2-3.4 5.5-4.3 7.4-2.2 4.3-4.2 6.6-8.2 9.6-3.7 2.7-14.5 14-17.7 18.4-2.2 3.1-7.1 7.4-10.6 9.4-1.1.6-4.3 1.8-7.2 2.6s-7.1 2.5-9.5 3.7c-3.6 1.8-5 3-8.6 6.6-2.3 2.4-4.9 4.7-5.7 5.1-1.6.8-5.5 1.5-10.7 2-1.7.2-4.6.8-6.4 1.5-7 2.6-7.3 2.6-16.1 2.7-6.9.1-9.3.3-12.3 1.2-4.9.9-8.8 1.2-12.7.6" style="fill:#cc7a3d"/><path d="M237.7 473.6c-42-3.8-79.9-18.3-113-43.2C56 378.7 24.1 291.7 43.1 207.6c6.6-29.2 19.8-57.8 37.7-81.9 47.7-64.1 126.8-97.1 205.8-85.9 36 5.1 70.5 19.3 99.7 41C455.7 132.5 488 220 468.9 304.4c-11.4 50.4-40.9 95.7-82.6 126.8-29.1 21.7-63.7 35.9-99.7 41-15.1 2.2-34.5 2.7-48.9 1.4m33.7-5.2c29-2.5 54.4-9.5 79-21.8 41.4-20.6 75.5-54.8 96.1-96.1 16.9-34 24.9-74.2 21.8-109.9-3.7-42.4-17.6-79.2-42.3-112-34.4-45.8-84.7-75.5-141.4-83.5-25.2-3.6-49.5-2.5-75.2 3.3-49.6 11.2-93.7 40-124.4 81.2-21.3 28.7-35 62-40.1 97.8-2.8 19.4-2.8 38 0 57.4 8.1 56.7 37.7 107 83.5 141.4 11.1 8.3 21 14.5 33 20.5 34.1 16.9 74.4 24.8 110 21.7" style="fill:#b1b1b1"/><path d="M253.9 367.3c-2.9-.7-4.3-1.4-6-2.9-2.9-2.6-3.9-4.9-3.9-8.9 0-3.9 1-6.3 3.9-8.9 6-5.4 15.8-3.1 19 4.3 1.1 2.6 1 7-.3 9.6-1.2 2.4-3.7 4.9-6 5.8-1.7.8-5.3 1.3-6.7 1m68.4-29.3c-9.2-3.4-11-15.4-3.1-21.1 8.1-5.9 20 1 18.8 11-.5 4-3 7.6-6.7 9.5-2.4 1.2-6.6 1.5-9 .6m-139.8-.2c-3.4-1.1-6.2-3.5-7.9-6.7-1.3-2.4-1.2-7.7.2-10.2 1.2-2.3 3.9-4.8 6.3-5.8 2.6-1.1 7-1 9.6.3 4.2 2.1 6.7 6 6.7 10.7 0 6.5-4.7 11.5-11 11.9-1.5.1-3.3 0-3.9-.2m43.2-22.8c-2.4-.8-4.1-1.9-5.8-3.8-5.4-6-3.1-15.8 4.3-19 2.6-1.1 7-1 9.6.3 2.5 1.2 4.9 3.7 5.9 6.2.9 2.1 1 6.3.1 8.8-.8 2.3-3.5 5.3-5.9 6.6-1.9 1-6.3 1.5-8.2.9m54.6 0c-2.4-.8-4.1-1.9-5.8-3.8-5.4-6-3.1-15.8 4.3-19 2.6-1.1 7-1 9.6.3 2.5 1.2 4.9 3.7 5.9 6.2.9 2.1 1 6.3.1 8.8-.8 2.3-3.5 5.3-5.9 6.6-1.9 1-6.3 1.5-8.2.9m-27.6-47c-2.2-.7-4-1.9-5.5-3.5-2.5-2.7-3.2-4.5-3.2-8.5 0-3.9.7-5.8 3.2-8.5s4.8-3.6 8.8-3.6 6.3.9 8.8 3.6 3.2 4.5 3.2 8.5c0 2.5-.3 4-.9 5.2-1.2 2.3-3.5 4.7-5.7 5.8-1.9 1.1-6.7 1.6-8.7 1m99.7 0c-2.4-.8-4.1-1.9-5.8-3.8-5.4-6-3.1-15.8 4.3-19 2.6-1.1 7-1 9.6.3 2.5 1.2 4.9 3.7 5.9 6.2.9 2.1 1 6.3.1 8.8-.8 2.3-3.5 5.3-5.9 6.6-1.9 1-6.3 1.5-8.2.9m-198.9-.7c-8.9-2.5-11.8-13.8-5.1-20.1 2.6-2.5 4.5-3.2 8.2-3.2 3.9 0 6.3 1 8.9 3.9 2.1 2.3 3 4.7 2.9 8.2 0 7.6-7.5 13.2-14.9 11.2m42.9-.4c-2.3-1.2-5.2-4.1-6.1-6.4-1-2.4-.9-7.1.3-9.5 1.1-2.3 4.1-5.2 6.4-6.2 2.4-1 7.1-.9 9.5.3 2.3 1.1 5.2 4.1 6.2 6.4 1 2.4.9 7.1-.3 9.5-1.1 2.3-4.1 5.2-6.4 6.2-2.5 1.1-7.2.9-9.6-.3m109-.3c-11.1-5.6-7-22.5 5.5-22.4 3.7 0 6.1 1.1 8.6 3.9 2.2 2.4 3.2 4.9 3.2 8s-1 5.6-3.2 8c-2.5 2.8-5 3.9-8.6 3.9-2.3-.1-3.5-.4-5.5-1.4m-79.7-46.3c-8.9-2.5-11.8-13.8-5.1-20.1 2.6-2.5 4.5-3.2 8.2-3.2 3.9 0 6.3 1 8.9 3.9 2.1 2.3 3 4.7 2.9 8.2-.1 7.6-7.6 13.3-14.9 11.2m54.5 0c-8-2.2-11.3-12-6.4-18.5 5.7-7.5 16.1-6.4 20.5 2 1.3 2.4 1.2 7.7-.2 10.2-2.6 5.1-8.6 7.8-13.9 6.3m43.2-22.8c-8-2.2-11.3-12-6.4-18.5 5.7-7.5 16.1-6.4 20.5 2 1.3 2.4 1.2 7.7-.2 10.2-2.6 5.2-8.6 7.8-13.9 6.3m-140.6-.5c-8.7-2.4-11.6-13-5.4-19.7 2.5-2.6 4.8-3.6 8.6-3.6 3.9 0 6.3 1.1 8.8 3.8 2.2 2.5 3 4.7 3 8 .1 8-7.3 13.6-15 11.5m70.3-28.9c-8-2.2-11.3-12-6.4-18.5 5.7-7.5 16.1-6.4 20.5 2 1.3 2.4 1.2 7.7-.2 10.2-2.6 5.1-8.5 7.8-13.9 6.3" style="fill:#884717"/></svg>
|
||||
|
Before Width: | Height: | Size: 4.8 KiB |
@@ -1,20 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
from src.consts import HOST, LOG_LEVEL, PORT, VERSION
|
||||
from src.endpoints import health_check, router
|
||||
from src.consts import LOG_LEVEL, VERSION
|
||||
from src.endpoints import router
|
||||
from src.middlewares import LogRequest
|
||||
from src.utils import get_camoufox, logger
|
||||
from src.utils import logger
|
||||
|
||||
logger.info("Using version %s", VERSION)
|
||||
logger.info("Log level set to %s", logging.getLevelName(LOG_LEVEL))
|
||||
|
||||
app = FastAPI(debug=LOG_LEVEL == logging.DEBUG, log_level=LOG_LEVEL)
|
||||
app.add_middleware(GZipMiddleware)
|
||||
@@ -23,19 +20,5 @@ app.add_middleware(LogRequest)
|
||||
app.include_router(router=router)
|
||||
|
||||
|
||||
async def init():
|
||||
"""Initialize the application."""
|
||||
async for browser in get_camoufox():
|
||||
await health_check(browser)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check for --init flag to run the app in development mode
|
||||
if "--init" in sys.argv:
|
||||
logger.info("Running initialization script...")
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(init())
|
||||
logger.info("Initialization complete.")
|
||||
else:
|
||||
uvicorn.run(app, host=HOST, port=PORT)
|
||||
uvicorn.run(app, host="0.0.0.0", port=8191, log_level=LOG_LEVEL) # noqa: S104
|
||||
|
||||
+17
-15
@@ -2,29 +2,29 @@
|
||||
[project]
|
||||
authors = [{ name = "ThePhaseless", email = "kukubaorch@gmail.com" }]
|
||||
license = { text = "LICENSE" }
|
||||
requires-python = "==3.14.*"
|
||||
requires-python = "<4.0,>=3.12"
|
||||
name = "Byparr"
|
||||
version = "0.1.0"
|
||||
description = "API for getting cookies for Cloudflare challenges"
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"camoufox[geoip]==0.4.*",
|
||||
"fastapi[standard]==0.135.*",
|
||||
"playwright-captcha==0.1.*",
|
||||
"pydantic==2.*",
|
||||
"fastapi[standard]==0.115.12",
|
||||
"pyautogui==0.9.54",
|
||||
"pydantic==2.11.3",
|
||||
"seleniumbase==4.37.2",
|
||||
"uvicorn==0.34.0",
|
||||
]
|
||||
urls = { repository = "https://github.com/ThePhaseless/Byparr" }
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
"httpx==0.28.*",
|
||||
"pytest==9.0.*",
|
||||
"pytest-asyncio==1.3.*",
|
||||
"pytest-retry==1.7.*",
|
||||
"pytest-xdist==3.8.*",
|
||||
"setuptools>=80.9.0",
|
||||
"httpx==0.28.1",
|
||||
"pytest-asyncio==0.26.0",
|
||||
"pytest-progress==1.3.0",
|
||||
"pytest-retry==1.7.0",
|
||||
"pytest-xdist==3.6.1",
|
||||
]
|
||||
dev = ["deptry==0.25.*", "ruff==0.15.*"]
|
||||
dev = ["deptry==0.23.0", "ruff==0.11.5"]
|
||||
|
||||
[tool.deptry.per_rule_ignores]
|
||||
DEP002 = ["pyautogui"]
|
||||
@@ -58,6 +58,8 @@ ignore = [
|
||||
select = ["ALL"]
|
||||
extend-safe-fixes = ["D415"]
|
||||
|
||||
[tool.pyright]
|
||||
reportMissingTypeStubs = false
|
||||
typeCheckingMode = "strict"
|
||||
[tool.pytest]
|
||||
log_cli = "True"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"forkProcessing": "enabled",
|
||||
"packageRules": [
|
||||
{
|
||||
"automerge": true,
|
||||
|
||||
+35
-24
@@ -1,32 +1,43 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from playwright_captcha import CaptchaType
|
||||
from playwright_captcha.utils.camoufox_add_init_script.add_init_script import (
|
||||
get_addon_path,
|
||||
|
||||
def get_version_from_env():
|
||||
"""
|
||||
Retrieve the version from the environment variable 'VERSION'.
|
||||
|
||||
This function checks the 'VERSION' environment variable for a value
|
||||
that starts with 'v' and returns the version without the prefix.
|
||||
|
||||
Returns:
|
||||
str | None: The version string without the 'v' prefix, or None if
|
||||
the 'VERSION' environment variable is not set or does not start
|
||||
with 'v'.
|
||||
|
||||
"""
|
||||
version_env = os.getenv("VERSION")
|
||||
if not version_env:
|
||||
return None
|
||||
|
||||
return version_env.removeprefix("v")
|
||||
|
||||
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL") or "INFO"
|
||||
LOG_LEVEL = logging.getLevelNamesMapping()[LOG_LEVEL.upper()]
|
||||
|
||||
VERSION = get_version_from_env() or "unknown"
|
||||
|
||||
USE_XVFB = os.getenv("USE_XVFB") in ["true", "1"] if os.getenv("USE_XVFB") else None
|
||||
|
||||
USE_HEADLESS = (
|
||||
os.getenv("USE_HEADLESS") in ["true", "1"] if os.getenv("USE_HEADLESS") else None
|
||||
)
|
||||
|
||||
LOG_LEVEL = logging.getLevelNamesMapping()[os.getenv("LOG_LEVEL", "INFO").upper()]
|
||||
|
||||
VERSION = os.getenv("VERSION", "unknown").removeprefix("v")
|
||||
|
||||
ADDON_PATH = str(Path(get_addon_path()).absolute())
|
||||
MAX_ATTEMPTS = sys.maxsize
|
||||
|
||||
PROXY_SERVER = os.getenv("PROXY_SERVER")
|
||||
PROXY_USERNAME = os.getenv("PROXY_USERNAME")
|
||||
PROXY_PASSWORD = os.getenv("PROXY_PASSWORD")
|
||||
|
||||
HOST = os.getenv("HOST", "0.0.0.0") # noqa: S104
|
||||
PORT = int(os.getenv("PORT", "8191"))
|
||||
|
||||
CHALLENGE_TITLES_MAP: dict[CaptchaType, list[str]] = {
|
||||
# Cloudflare
|
||||
CaptchaType.CLOUDFLARE_INTERSTITIAL: ["Just a moment..."],
|
||||
}
|
||||
PROXY = os.getenv("PROXY")
|
||||
|
||||
CHALLENGE_TITLES = [
|
||||
title for titles in CHALLENGE_TITLES_MAP.values() for title in titles
|
||||
# Cloudflare
|
||||
"Just a moment...",
|
||||
# DDoS-GUARD
|
||||
"DDoS-Guard",
|
||||
]
|
||||
|
||||
+31
-52
@@ -1,28 +1,23 @@
|
||||
import time
|
||||
import warnings
|
||||
from asyncio import wait_for
|
||||
from http import HTTPStatus
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import RedirectResponse
|
||||
from playwright_captcha import CaptchaType
|
||||
from sbase import BaseCase
|
||||
|
||||
from src.consts import CHALLENGE_TITLES
|
||||
from src.models import (
|
||||
HealthcheckResponse,
|
||||
LinkRequest,
|
||||
LinkResponse,
|
||||
Solution,
|
||||
)
|
||||
from src.utils import CamoufoxDepClass, TimeoutTimer, get_camoufox, logger
|
||||
|
||||
warnings.filterwarnings("ignore", category=SyntaxWarning)
|
||||
|
||||
from .utils import get_sb, logger, save_screenshot
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
CamoufoxDep = Annotated[CamoufoxDepClass, Depends(get_camoufox)]
|
||||
SeleniumDep = Annotated[BaseCase, Depends(get_sb)]
|
||||
|
||||
|
||||
@router.get("/", include_in_schema=False)
|
||||
@@ -33,9 +28,9 @@ def read_root():
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health_check(sb: CamoufoxDep):
|
||||
def health_check(sb: SeleniumDep):
|
||||
"""Health check endpoint."""
|
||||
health_check_request = await read_item(
|
||||
health_check_request = read_item(
|
||||
LinkRequest.model_construct(url="https://google.com"),
|
||||
sb,
|
||||
)
|
||||
@@ -46,61 +41,45 @@ async def health_check(sb: CamoufoxDep):
|
||||
detail="Health check failed",
|
||||
)
|
||||
|
||||
return HealthcheckResponse(user_agent=health_check_request.solution.user_agent)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/v1")
|
||||
async def read_item(request: LinkRequest, dep: CamoufoxDep) -> LinkResponse:
|
||||
def read_item(request: LinkRequest, sb: SeleniumDep) -> LinkResponse:
|
||||
"""Handle POST requests."""
|
||||
start_time = int(time.time() * 1000)
|
||||
sb.uc_open_with_reconnect(request.url)
|
||||
logger.debug(f"Got webpage: {request.url}")
|
||||
source_bs = sb.get_beautiful_soup()
|
||||
title_tag = source_bs.title
|
||||
if title_tag and title_tag.string in CHALLENGE_TITLES:
|
||||
logger.debug("Challenge detected")
|
||||
sb.uc_gui_click_captcha()
|
||||
logger.info("Clicked captcha")
|
||||
|
||||
timer = TimeoutTimer(duration=request.max_timeout)
|
||||
if sb.get_title() in CHALLENGE_TITLES:
|
||||
save_screenshot(sb)
|
||||
raise HTTPException(status_code=500, detail="Could not bypass challenge")
|
||||
|
||||
request.url = request.url.replace('"', "").strip()
|
||||
try:
|
||||
page_request = await dep.page.goto(
|
||||
request.url, timeout=timer.remaining() * 1000
|
||||
)
|
||||
status = page_request.status if page_request else HTTPStatus.OK
|
||||
await dep.page.wait_for_load_state(
|
||||
state="domcontentloaded", timeout=timer.remaining() * 1000
|
||||
)
|
||||
await dep.page.wait_for_load_state(
|
||||
"networkidle", timeout=timer.remaining() * 1000
|
||||
)
|
||||
cookies = sb.get_cookies()
|
||||
for cookie in cookies:
|
||||
name = cookie["name"]
|
||||
value = cookie["value"]
|
||||
cookie["size"] = len(f"{name}={value}".encode())
|
||||
|
||||
if await dep.page.title() in CHALLENGE_TITLES:
|
||||
logger.info("Challenge detected, attempting to solve...")
|
||||
# Solve the captcha
|
||||
await wait_for(
|
||||
dep.solver.solve_captcha( # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType]
|
||||
captcha_container=dep.page,
|
||||
captcha_type=CaptchaType.CLOUDFLARE_INTERSTITIAL,
|
||||
wait_checkbox_attempts=1,
|
||||
wait_checkbox_delay=0.5,
|
||||
),
|
||||
timeout=timer.remaining(),
|
||||
)
|
||||
status = HTTPStatus.OK
|
||||
logger.debug("Challenge solved successfully.")
|
||||
except TimeoutError as e:
|
||||
logger.error("Timed out while solving the challenge")
|
||||
raise HTTPException(
|
||||
status_code=408,
|
||||
detail="Timed out while solving the challenge",
|
||||
) from e
|
||||
|
||||
cookies = await dep.context.cookies()
|
||||
cookie["session"] = False
|
||||
if "expiry" in cookie:
|
||||
cookie["expires"] = cookie["expiry"]
|
||||
|
||||
return LinkResponse(
|
||||
message="Success",
|
||||
solution=Solution(
|
||||
user_agent=await dep.page.evaluate("navigator.userAgent"),
|
||||
url=dep.page.url,
|
||||
status=status,
|
||||
userAgent=sb.get_user_agent(),
|
||||
url=sb.get_current_url(),
|
||||
status=200,
|
||||
cookies=cookies,
|
||||
headers=page_request.headers if page_request else {},
|
||||
response=await dep.page.content(),
|
||||
headers={},
|
||||
response=str(sb.get_beautiful_soup()),
|
||||
),
|
||||
start_timestamp=start_time,
|
||||
)
|
||||
|
||||
+4
-5
@@ -1,17 +1,16 @@
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from src.models import LinkRequest
|
||||
from src.utils import logger
|
||||
|
||||
|
||||
class LogRequest(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
|
||||
async def dispatch(self, request, call_next):
|
||||
"""Log requests."""
|
||||
if request.url.path != "/v1" or request.method != "POST":
|
||||
if request.url.path != "/v1":
|
||||
return await call_next(request)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
@@ -25,6 +24,6 @@ class LogRequest(BaseHTTPMiddleware):
|
||||
if response.status_code == HTTPStatus.OK:
|
||||
logger.info(f"Done {request_body.url} in {process_time:.2f}s")
|
||||
else:
|
||||
logger.warning(f"Failed {request_body.url} in {process_time:.2f}s")
|
||||
logger.info(f"Failed {request_body.url} in {process_time:.2f}s")
|
||||
|
||||
return response
|
||||
|
||||
+23
-19
@@ -1,10 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from http.client import INTERNAL_SERVER_ERROR
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from playwright.sync_api import Cookie
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
@@ -17,27 +16,32 @@ class LinkRequest(BaseModel):
|
||||
description="Type of request, currently only supports GET requests. This string is purely for compatibility with FlareSolverr.",
|
||||
)
|
||||
url: str = Field(pattern=r"^https?://", default="https://")
|
||||
max_timeout: int = Field(
|
||||
default=60,
|
||||
description="Maximum timeout in seconds for resolving the anti-bot challenge.",
|
||||
)
|
||||
|
||||
|
||||
class HealthcheckResponse(BaseModel):
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
msg: str = "Byparr is working!"
|
||||
version: str = consts.VERSION
|
||||
user_agent: str
|
||||
max_timeout: int = Field(default=60)
|
||||
|
||||
|
||||
class Solution(BaseModel):
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
url: str
|
||||
status: int
|
||||
cookies: list[Cookie] = []
|
||||
user_agent: str = ""
|
||||
headers: dict[str, Any] = {}
|
||||
response: str = ""
|
||||
cookies: list
|
||||
userAgent: str # noqa: N815 # Ignore to preserve compatibility
|
||||
headers: dict[str, Any]
|
||||
response: str
|
||||
|
||||
@classmethod
|
||||
def invalid(cls, url: str):
|
||||
"""
|
||||
Return an empty Solution with default values.
|
||||
|
||||
Useful for returning an error response.
|
||||
"""
|
||||
return cls(
|
||||
url=url,
|
||||
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
cookies=[],
|
||||
userAgent="",
|
||||
headers={},
|
||||
response="",
|
||||
)
|
||||
|
||||
|
||||
class LinkResponse(BaseModel):
|
||||
@@ -59,6 +63,6 @@ class LinkResponse(BaseModel):
|
||||
return cls(
|
||||
status="error",
|
||||
message="Invalid request",
|
||||
solution=Solution(url=url, status=INTERNAL_SERVER_ERROR),
|
||||
solution=Solution.invalid(url),
|
||||
start_timestamp=int(time.time() * 1000),
|
||||
)
|
||||
|
||||
+29
-103
@@ -1,33 +1,11 @@
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Annotated, NamedTuple, cast
|
||||
from time import gmtime, strftime
|
||||
|
||||
from camoufox import AsyncCamoufox
|
||||
from fastapi import Header
|
||||
from playwright.async_api import Browser, BrowserContext, Page
|
||||
from playwright_captcha import (
|
||||
ClickSolver,
|
||||
FrameworkType,
|
||||
)
|
||||
from pydantic import BaseModel, Field
|
||||
from fastapi import Header, HTTPException
|
||||
from httpx import codes
|
||||
from sbase import SB, BaseCase
|
||||
|
||||
from src.consts import (
|
||||
ADDON_PATH,
|
||||
LOG_LEVEL,
|
||||
MAX_ATTEMPTS,
|
||||
PROXY_PASSWORD,
|
||||
PROXY_SERVER,
|
||||
PROXY_USERNAME,
|
||||
)
|
||||
|
||||
solver_logger = logging.getLogger("playwright_captcha")
|
||||
solver_logger.handlers.clear()
|
||||
if LOG_LEVEL == logging.DEBUG:
|
||||
solver_logger.addHandler(logging.StreamHandler())
|
||||
solver_logger.setLevel(LOG_LEVEL)
|
||||
else:
|
||||
solver_logger.handlers.append(logging.NullHandler())
|
||||
from src.consts import LOG_LEVEL, PROXY, USE_HEADLESS
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
logger.setLevel(LOG_LEVEL)
|
||||
@@ -35,82 +13,30 @@ if len(logger.handlers) == 0:
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
|
||||
|
||||
class TimeoutTimer(BaseModel):
|
||||
duration: int # in seconds
|
||||
start_time: float = Field(default_factory=time.perf_counter)
|
||||
def get_sb(
|
||||
proxy: str | None = Header(
|
||||
default=PROXY,
|
||||
examples=["protocol://username:password@host:port"],
|
||||
description="Override default proxy address",
|
||||
),
|
||||
):
|
||||
"""Get SeleniumBase instance."""
|
||||
if proxy and proxy.startswith("socks5://") and "@" in proxy:
|
||||
raise HTTPException(
|
||||
status_code=codes.BAD_REQUEST,
|
||||
detail="SOCKS5 proxy with authentication is not supported. Check README for more info.",
|
||||
)
|
||||
|
||||
def remaining(self) -> float:
|
||||
"""Get remaining time in seconds."""
|
||||
return max(0, self.duration - (time.perf_counter() - self.start_time))
|
||||
with SB(
|
||||
uc=True,
|
||||
headless=USE_HEADLESS,
|
||||
locale_code="en",
|
||||
ad_block=True,
|
||||
proxy=proxy,
|
||||
) as sb:
|
||||
yield sb
|
||||
|
||||
|
||||
class CamoufoxDepClass(NamedTuple):
|
||||
page: Page
|
||||
solver: ClickSolver
|
||||
context: BrowserContext
|
||||
|
||||
|
||||
async def get_camoufox(
|
||||
x_proxy_server: Annotated[
|
||||
str | None,
|
||||
Header(
|
||||
alias="X-Proxy-Server",
|
||||
description="Override proxy server for this request in protocol://host:port format.",
|
||||
),
|
||||
] = None,
|
||||
x_proxy_username: Annotated[
|
||||
str | None,
|
||||
Header(
|
||||
alias="X-Proxy-Username",
|
||||
),
|
||||
] = None,
|
||||
x_proxy_password: Annotated[
|
||||
str | None,
|
||||
Header(
|
||||
alias="X-Proxy-Password",
|
||||
),
|
||||
] = None,
|
||||
) -> AsyncGenerator[CamoufoxDepClass]:
|
||||
"""Get Camoufox instance."""
|
||||
header_server = x_proxy_server
|
||||
header_username = x_proxy_username
|
||||
header_password = x_proxy_password
|
||||
|
||||
proxy_config = None
|
||||
|
||||
if header_server:
|
||||
proxy_config = {
|
||||
"server": header_server,
|
||||
"username": header_username,
|
||||
"password": header_password,
|
||||
}
|
||||
elif PROXY_SERVER:
|
||||
proxy_config = {
|
||||
"server": PROXY_SERVER,
|
||||
"username": PROXY_USERNAME,
|
||||
"password": PROXY_PASSWORD,
|
||||
}
|
||||
|
||||
async with AsyncCamoufox(
|
||||
main_world_eval=True,
|
||||
addons=[ADDON_PATH],
|
||||
geoip=True,
|
||||
proxy=proxy_config,
|
||||
locale="en-US",
|
||||
headless=True,
|
||||
humanize=True,
|
||||
i_know_what_im_doing=True,
|
||||
config={"forceScopeAccess": True}, # add this when creating Camoufox instance
|
||||
disable_coop=True, # add this when creating Camoufox instance
|
||||
) as browser_raw:
|
||||
# Cast to Browser since AsyncCamoufox always returns a Browser, not BrowserContext
|
||||
browser = cast("Browser", browser_raw)
|
||||
context = await browser.new_context()
|
||||
page = await context.new_page()
|
||||
async with ClickSolver(
|
||||
framework=FrameworkType.CAMOUFOX,
|
||||
page=page,
|
||||
max_attempts=MAX_ATTEMPTS,
|
||||
attempt_delay=1,
|
||||
) as solver:
|
||||
yield CamoufoxDepClass(page, solver, context)
|
||||
def save_screenshot(sb: BaseCase):
|
||||
"""Save screenshot on HTTPException."""
|
||||
sb.save_screenshot(f"screenshots/{strftime('%Y-%m-%d %H:%M:%S', gmtime())}.png")
|
||||
|
||||
+10
-13
@@ -1,5 +1,4 @@
|
||||
from http import HTTPStatus
|
||||
from json import JSONDecodeError
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -12,11 +11,10 @@ client = TestClient(app)
|
||||
|
||||
test_websites = [
|
||||
"https://ext.to/",
|
||||
# "https://www.ygg.re/",
|
||||
"https://www.ygg.re/",
|
||||
"https://extratorrent.st/",
|
||||
"https://idope.se/",
|
||||
"https://speed.cd/login",
|
||||
'https://www.yggtorrent.top/engine/search?do=search&order=desc&sort=publish_date&name="UNESCAPED"+"DOUBLEQUOTES"&category=2145',
|
||||
"https://1337x.to/home/",
|
||||
]
|
||||
|
||||
|
||||
@@ -31,20 +29,19 @@ def test_bypass(website: str):
|
||||
website,
|
||||
)
|
||||
if (
|
||||
test_request.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
test_request.status_code != HTTPStatus.OK
|
||||
and "Just a moment..." not in test_request.text
|
||||
):
|
||||
try:
|
||||
error_details = test_request.json()
|
||||
except JSONDecodeError:
|
||||
error_details = test_request.text
|
||||
pytest.skip(
|
||||
f"Skipping {website} - ({test_request.status_code}) {error_details}"
|
||||
)
|
||||
pytest.skip(f"Skipping {website} due to {test_request.status_code}")
|
||||
|
||||
response = client.post(
|
||||
"/v1",
|
||||
json=LinkRequest.model_construct(url=website, cmd="request.get").model_dump(),
|
||||
json={
|
||||
**LinkRequest.model_construct(
|
||||
url=website, max_timeout=30, cmd="request.get"
|
||||
).model_dump(),
|
||||
# "proxy": "203.174.15.83:8080",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
Reference in New Issue
Block a user