mirror of
https://github.com/ThePhaseless/Byparr.git
synced 2026-09-24 22:03:30 +01:00
Compare commits
341
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,14 @@ 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"
|
||||
|
||||
- type: input
|
||||
id: docker-host
|
||||
attributes:
|
||||
label: OS/Docker Host
|
||||
placeholder: e.g. Docker, WSL2, Ubuntu, Windows
|
||||
label: OS
|
||||
placeholder: e.g. Docker, Ubuntu Linux
|
||||
|
||||
- type: input
|
||||
id: platform-model
|
||||
@@ -51,7 +33,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
|
||||
@@ -61,28 +43,6 @@ body:
|
||||
placeholder: "Example: The application crashes when trying to load a new file"
|
||||
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Docker Compose config
|
||||
placeholder: |
|
||||
```yaml
|
||||
services:
|
||||
...
|
||||
```
|
||||
- type: textarea
|
||||
id: prowlarr-logs
|
||||
attributes:
|
||||
label: Prowlarr Logs
|
||||
placeholder: |
|
||||
```
|
||||
put logs between 3x `
|
||||
```
|
||||
|
||||
- type: textarea
|
||||
id: byparr-logs
|
||||
attributes:
|
||||
label: Byparr Logs
|
||||
placeholder: |
|
||||
```
|
||||
put logs between 3x `
|
||||
```
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
|
||||
@@ -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,31 +90,30 @@ 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 }}
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
build-args: |
|
||||
GITHUB_BUILD=true
|
||||
VERSION=${{ github.ref_type == 'tag' && github.ref_name || github.sha }}
|
||||
build-args: GITHUB_BUILD=true,VERSION=${{ github.ref_type == 'tag' && github.ref_name || github.sha }}
|
||||
|
||||
merge-and-push:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -151,7 +121,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Install the cosign tool
|
||||
- name: Install cosign
|
||||
@@ -159,11 +129,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 +142,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 +172,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
|
||||
> Due to recent challenge changes, this software does not guarantee that the Cloudflare challenge will be bypassed. Cloudflare likely requires valid network traffic originating from the user’s public IP address to mark a connection as legitimate. While this tool may bypass the initial browser check, it does not ensure that requests will consistently pass Cloudflare's validation. More testing and data are required to understand how Cloudflare identifies connections and requests as valid. Invalid requests will result in Byparr's looping and eventually time-outing.
|
||||
|
||||
> [!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
|
||||
|
||||
| Env | 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. (format: `username:password@host:port`) |
|
||||
|
||||
## 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
|
||||
|
||||
+7
-6
@@ -1,11 +1,12 @@
|
||||
services:
|
||||
byparr:
|
||||
image: ghcr.io/thephaseless/byparr:latest
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
image: ghcr.io/thephaseless/byparr
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
# Uncomment below to use byparr outside of internal network
|
||||
# ports:
|
||||
# - "8191:8191"
|
||||
environment:
|
||||
- LOG_LEVEL=INFO
|
||||
volumes:
|
||||
- ./screenshots:/app/screenshots # For screenshots when exception occurs
|
||||
ports:
|
||||
- "8191:8191"
|
||||
|
||||
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.11",
|
||||
"pyautogui==0.9.54",
|
||||
"pydantic==2.10.6",
|
||||
"seleniumbase==4.35.7",
|
||||
"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.25.3",
|
||||
"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.0"]
|
||||
|
||||
[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",
|
||||
]
|
||||
|
||||
Regular → Executable
+24
-55
@@ -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,35 @@ 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)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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()
|
||||
if sb.get_title() in CHALLENGE_TITLES:
|
||||
save_screenshot(sb)
|
||||
raise HTTPException(status_code=500, detail="Could not bypass challenge")
|
||||
|
||||
return LinkResponse(
|
||||
message="Success",
|
||||
solution=Solution(
|
||||
user_agent=await dep.page.evaluate("navigator.userAgent"),
|
||||
url=dep.page.url,
|
||||
status=status,
|
||||
cookies=cookies,
|
||||
headers=page_request.headers if page_request else {},
|
||||
response=await dep.page.content(),
|
||||
userAgent=sb.get_user_agent(),
|
||||
url=sb.get_current_url(),
|
||||
status=200,
|
||||
cookies=sb.get_cookies(),
|
||||
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
|
||||
|
||||
+28
-24
@@ -1,12 +1,11 @@
|
||||
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 fastapi import Body
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
from src import consts
|
||||
|
||||
@@ -17,36 +16,40 @@ 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):
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
status: str = "ok"
|
||||
message: str
|
||||
solution: Solution
|
||||
start_timestamp: int
|
||||
end_timestamp: int = Field(default_factory=lambda: int(time.time() * 1000))
|
||||
start_timestamp: int = Field(alias="startTimestamp", default_factory=lambda: int(time.time() * 1000))
|
||||
end_timestamp: int = Field(alias="endTimestamp", default_factory=lambda: int(time.time() * 1000))
|
||||
version: str = consts.VERSION
|
||||
|
||||
@classmethod
|
||||
@@ -59,6 +62,7 @@ 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),
|
||||
)
|
||||
end_timestamp=int(time.time() * 1000),
|
||||
)
|
||||
+22
-103
@@ -1,33 +1,10 @@
|
||||
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 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 +12,24 @@ 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 remaining(self) -> float:
|
||||
"""Get remaining time in seconds."""
|
||||
return max(0, self.duration - (time.perf_counter() - self.start_time))
|
||||
def get_sb(
|
||||
proxy: str | None = Header(
|
||||
default=None,
|
||||
examples=["username:password@host:port"],
|
||||
description="Override default proxy from env",
|
||||
),
|
||||
):
|
||||
"""Get SeleniumBase instance."""
|
||||
with SB(
|
||||
uc=True,
|
||||
headless=USE_HEADLESS,
|
||||
locale_code="en",
|
||||
ad_block=True,
|
||||
proxy=proxy or 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