├── .dockerignore ├── .env.sample ├── .github └── workflows │ ├── docker-build.yml │ ├── docker-dev.yml │ └── docker-main.yml ├── .gitignore ├── .gitpod.yml ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── README.md ├── SETUP.md ├── app.py ├── images └── show-url-decoded.png ├── requirements.txt ├── self notes.todo └── src ├── .editorconfig ├── __init__.py ├── common ├── __init__.py ├── _http.py └── trailer.py ├── providers ├── __init__.py ├── boobpedia.py ├── javdatabase.py ├── javdb.py ├── javlibrary.py ├── r18.py └── rss.py ├── routes ├── __init__.py ├── jav.py ├── non_jav.py └── rss.py └── tests ├── data ├── javdatabase.doa-017.json ├── javdatabase.ebod-391.json ├── javdatabase.ebod-875.json ├── javdatabase.mkck-275.json ├── javlibrary.doa-017.json ├── javlibrary.ebod-391.json ├── javlibrary.ssis-001.json ├── javlibrary.ssis-100.json ├── r18.doa-017.json ├── r18.ebod-391.json ├── r18.ebod-875.json └── r18.mkck-275.json ├── javdatabase.py ├── javlibrary.py └── r18.py /.dockerignore: -------------------------------------------------------------------------------- 1 | # Cached builds 2 | __pycache__/ 3 | *.pyc 4 | *.pyo 5 | *.pyd 6 | *.so 7 | *.dll 8 | *.egg 9 | *.egg-info/ 10 | *.env 11 | 12 | # Build directories 13 | docs/_build/ 14 | build/ 15 | dist/ 16 | 17 | 18 | # IDEs 19 | .idea/ 20 | 21 | # Testing and temporary files 22 | .tox/ 23 | 24 | # Custom 25 | .venv 26 | .vscode 27 | var* 28 | *.log 29 | 30 | # Dev 31 | test* 32 | old_* 33 | site 34 | *.sql 35 | 36 | # changelog 37 | CHANGELOG.md 38 | 39 | thunder-tests 40 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | API_USER=admin 2 | API_PASS=admin 3 | PORT=8000 4 | 5 | # proxy config (currently only javlibrary uses proxy) 6 | HTTP_PROXY="" 7 | HTTPS_PROXY="" 8 | 9 | # Required for javdb.com 10 | JDB_SESSION="" # value from _jdb_session 11 | REMEMBER_ME_TOKEN="" # value from remember_me_token 12 | 13 | PRIORITY_LIST="['r18', 'jvdtbs', 'jvlib', 'javdb']" -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: build-layer 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | push: 8 | branches: 9 | - "build" 10 | jobs: 11 | qemu: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | ref: build 19 | - 20 | name: Set up QEMU 21 | uses: docker/setup-qemu-action@v2.0.0 22 | - 23 | name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v2.0.0 25 | - 26 | name: Login to DockerHub 27 | uses: docker/login-action@v2 28 | with: 29 | username: ${{secrets.DOCKER_USERNAME}} 30 | password: ${{secrets.DOCKER_PASSWORD}} 31 | - 32 | name: Build and push 33 | uses: docker/build-push-action@v3 34 | with: 35 | context: . 36 | file: Dockerfile.build 37 | cache-from: type=registry,ref=iamrony777/javinfo-api:${{ github.ref_name }}-cache 38 | cache-to: type=registry,ref=iamrony777/javinfo-api:${{ github.ref_name }}-cache,mode=max 39 | platforms: linux/amd64, linux/arm64 40 | push: true 41 | tags: iamrony777/javinfo-api:build-layer 42 | -------------------------------------------------------------------------------- /.github/workflows/docker-dev.yml: -------------------------------------------------------------------------------- 1 | name: docker-dev 2 | 3 | on: 4 | push: 5 | branches: 6 | - "dev" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v3 15 | - 16 | name: Set up QEMU 17 | uses: docker/setup-qemu-action@v2.0.0 18 | - 19 | name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v2.0.0 21 | - 22 | name: Login to DockerHub 23 | uses: docker/login-action@v2 24 | with: 25 | username: ${{secrets.DOCKER_USERNAME}} 26 | password: ${{secrets.DOCKER_PASSWORD}} 27 | - 28 | name: Build and push 29 | uses: docker/build-push-action@v3 30 | with: 31 | context: . 32 | file: Dockerfile.build 33 | platforms: linux/amd64 34 | push: true 35 | tags: iamrony777/javinfo-api:dev 36 | cache-from: type=registry,ref=iamrony777/javinfo-api:${{ github.ref_name }}-cache 37 | cache-to: type=registry,ref=iamrony777/javinfo-api:${{ github.ref_name }}-cache,mode=max 38 | build-args: | 39 | PLATFORM=container 40 | CREATE_REDIS=false 41 | -------------------------------------------------------------------------------- /.github/workflows/docker-main.yml: -------------------------------------------------------------------------------- 1 | name: docker-main 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v3 15 | - 16 | name: Set up QEMU 17 | uses: docker/setup-qemu-action@v2.0.0 18 | - 19 | name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v2.0.0 21 | - 22 | name: Login to DockerHub 23 | uses: docker/login-action@v2 24 | with: 25 | username: ${{secrets.DOCKER_USERNAME}} 26 | password: ${{secrets.DOCKER_PASSWORD}} 27 | - 28 | name: Build and push - Standalone 29 | uses: docker/build-push-action@v3 30 | with: 31 | context: . 32 | platforms: linux/amd64, linux/arm/v8, linux/arm/v7, linux/arm64 33 | push: true 34 | tags: iamrony777/javinfo-api:latest, iamrony777/javinfo-api:standalone 35 | build-args: | 36 | PLATFORM=container 37 | CREATE_REDIS=true 38 | - 39 | name: Build and push - No Redis , Only api 40 | uses: docker/build-push-action@v3 41 | with: 42 | context: . 43 | platforms: linux/amd64, linux/arm/v8, linux/arm/v7, linux/arm64 44 | push: true 45 | tags: iamrony777/javinfo-api:server 46 | build-args: | 47 | PLATFORM=container 48 | CREATE_REDIS=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cached builds 2 | __pycache__/ 3 | *.pyc 4 | *.pyo 5 | *.pyd 6 | *.so 7 | *.dll 8 | *.egg 9 | *.egg-info/ 10 | 11 | # Build directories 12 | docs/_build/ 13 | build/ 14 | dist/ 15 | 16 | 17 | # IDEs 18 | .idea/ 19 | 20 | # Testing and temporary files 21 | .tox/ 22 | 23 | # Custom 24 | .venv 25 | .env 26 | .vscode 27 | var* 28 | *.log 29 | 30 | # Dev 31 | # test* 32 | old_* 33 | site 34 | *.sql 35 | 36 | # changelog 37 | CHANGELOG.md 38 | 39 | thunder-tests 40 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: iamrony777/gitpod-images:python 2 | tasks: 3 | - before: poetry config virtualenvs.in-project true && poetry install && poetry update 4 | command: | 5 | if [[ -n "$VIRTUAL_ENV" ]]; then echo "$VIRTUAL_ENV"; else poetry shell && exit 0; fi 6 | wget -q -O '/home/gitpod/.local/wait-for-it' 'https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh' 7 | chmod 755 '/home/gitpod/.local/wait-for-it' && '/home/gitpod/.local/wait-for-it' -t 0 127.0.0.1:23000 8 | for exts in /home/gitpod/extensions/*; do gitpod-code --install-extension "$exts"; done && exit 0 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine3.18 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt . 6 | RUN pip install -r requirements.txt 7 | 8 | COPY . . 9 | 10 | EXPOSE 3000 11 | CMD ["python", "app.py"] -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | fastapi = "*" 8 | uvicorn = "*" 9 | cloudscraper = "*" 10 | lxml = "*" 11 | loguru = "*" 12 | cssselect = "*" 13 | parsel = "*" 14 | python-dotenv = "*" 15 | googletrans = "==3.1.0a0" 16 | httpx = {extras = ["http2"], version = "*"} 17 | 18 | [dev-packages] 19 | pytest = "*" 20 | rich = "*" 21 | 22 | [requires] 23 | python_version = "3.11" 24 | python_full_version = "3.11.4" 25 | 26 | [scripts] 27 | test = "pytest src/tests/" 28 | start = "uvicorn app:app" 29 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "be91aa7593c1b3f3eac8c1a80960b38750ecbe44ab20ccf9c76bfdcfd74f8f20" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_full_version": "3.11.4", 9 | "python_version": "3.11" 10 | }, 11 | "sources": [ 12 | { 13 | "name": "pypi", 14 | "url": "https://pypi.org/simple", 15 | "verify_ssl": true 16 | } 17 | ] 18 | }, 19 | "default": { 20 | "annotated-types": { 21 | "hashes": [ 22 | "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", 23 | "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" 24 | ], 25 | "markers": "python_version >= '3.8'", 26 | "version": "==0.6.0" 27 | }, 28 | "anyio": { 29 | "hashes": [ 30 | "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", 31 | "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6" 32 | ], 33 | "markers": "python_version >= '3.8'", 34 | "version": "==4.3.0" 35 | }, 36 | "certifi": { 37 | "hashes": [ 38 | "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", 39 | "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" 40 | ], 41 | "markers": "python_version >= '3.6'", 42 | "version": "==2024.2.2" 43 | }, 44 | "chardet": { 45 | "hashes": [ 46 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 47 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 48 | ], 49 | "version": "==3.0.4" 50 | }, 51 | "charset-normalizer": { 52 | "hashes": [ 53 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 54 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 55 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 56 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 57 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 58 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 59 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 60 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 61 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 62 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 63 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 64 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 65 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 66 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 67 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 68 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 69 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 70 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 71 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 72 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 73 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 74 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 75 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 76 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 77 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 78 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 79 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 80 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 81 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 82 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 83 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 84 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 85 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 86 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 87 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 88 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 89 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 90 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 91 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 92 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 93 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 94 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 95 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 96 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 97 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 98 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 99 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 100 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 101 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 102 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 103 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 104 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 105 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 106 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 107 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 108 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 109 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 110 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 111 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 112 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 113 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 114 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 115 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 116 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 117 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 118 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 119 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 120 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 121 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 122 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 123 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 124 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 125 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 126 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 127 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 128 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 129 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 130 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 131 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 132 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 133 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 134 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 135 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 136 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 137 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 138 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 139 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 140 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 141 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 142 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 143 | ], 144 | "markers": "python_full_version >= '3.7.0'", 145 | "version": "==3.3.2" 146 | }, 147 | "click": { 148 | "hashes": [ 149 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 150 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 151 | ], 152 | "markers": "python_version >= '3.7'", 153 | "version": "==8.1.7" 154 | }, 155 | "cloudscraper": { 156 | "hashes": [ 157 | "sha256:429c6e8aa6916d5bad5c8a5eac50f3ea53c9ac22616f6cb21b18dcc71517d0d3", 158 | "sha256:76f50ca529ed2279e220837befdec892626f9511708e200d48d5bb76ded679b0" 159 | ], 160 | "index": "pypi", 161 | "version": "==1.2.71" 162 | }, 163 | "cssselect": { 164 | "hashes": [ 165 | "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc", 166 | "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e" 167 | ], 168 | "index": "pypi", 169 | "markers": "python_version >= '3.7'", 170 | "version": "==1.2.0" 171 | }, 172 | "fastapi": { 173 | "hashes": [ 174 | "sha256:5df913203c482f820d31f48e635e022f8cbfe7350e4830ef05a3163925b1addc", 175 | "sha256:6feac43ec359dfe4f45b2c18ec8c94edb8dc2dfc461d417d9e626590c071baad" 176 | ], 177 | "index": "pypi", 178 | "markers": "python_version >= '3.8'", 179 | "version": "==0.110.1" 180 | }, 181 | "googletrans": { 182 | "hashes": [ 183 | "sha256:d20373a7975791318a7e5d6c6e3205012d7a990b8fabbfc6b0c16017a6dfae04" 184 | ], 185 | "index": "pypi", 186 | "markers": "python_version >= '3.6'", 187 | "version": "==3.1.0a0" 188 | }, 189 | "h11": { 190 | "hashes": [ 191 | "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1", 192 | "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1" 193 | ], 194 | "markers": "python_version >= '3.7'", 195 | "version": "==0.9.0" 196 | }, 197 | "h2": { 198 | "hashes": [ 199 | "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5", 200 | "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14" 201 | ], 202 | "version": "==3.2.0" 203 | }, 204 | "hpack": { 205 | "hashes": [ 206 | "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89", 207 | "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2" 208 | ], 209 | "version": "==3.0.0" 210 | }, 211 | "hstspreload": { 212 | "hashes": [ 213 | "sha256:25fbfdf1dc1d7a5621221e220caedef58aadcbd375e8fb71e1b6056c9e5f4123", 214 | "sha256:d379d0593f4b8819bd42e758c697f43a95a8202f8f5412bd8db820b37cdf96ac" 215 | ], 216 | "markers": "python_version >= '3.6'", 217 | "version": "==2024.4.1" 218 | }, 219 | "httpcore": { 220 | "hashes": [ 221 | "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0", 222 | "sha256:ecc5949310d9dae4de64648a4ce529f86df1f232ce23dcfefe737c24d21dfbe9" 223 | ], 224 | "markers": "python_version >= '3.6'", 225 | "version": "==0.9.1" 226 | }, 227 | "httpx": { 228 | "extras": [ 229 | "http2" 230 | ], 231 | "hashes": [ 232 | "sha256:32d930858eab677bc29a742aaa4f096de259f1c78c68a90ad11f5c3c04f08335", 233 | "sha256:3642bd13e90b80ba8a243a730275eb10a4c26ec96f5fc16b87e458d4ab21efae" 234 | ], 235 | "markers": "python_version >= '3.6'", 236 | "version": "==0.13.3" 237 | }, 238 | "hyperframe": { 239 | "hashes": [ 240 | "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40", 241 | "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f" 242 | ], 243 | "version": "==5.2.0" 244 | }, 245 | "idna": { 246 | "hashes": [ 247 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 248 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 249 | ], 250 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 251 | "version": "==2.10" 252 | }, 253 | "jmespath": { 254 | "hashes": [ 255 | "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", 256 | "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" 257 | ], 258 | "markers": "python_version >= '3.7'", 259 | "version": "==1.0.1" 260 | }, 261 | "loguru": { 262 | "hashes": [ 263 | "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", 264 | "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac" 265 | ], 266 | "index": "pypi", 267 | "markers": "python_version >= '3.5'", 268 | "version": "==0.7.2" 269 | }, 270 | "lxml": { 271 | "hashes": [ 272 | "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04", 273 | "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0", 274 | "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739", 275 | "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a", 276 | "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1", 277 | "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218", 278 | "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9", 279 | "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188", 280 | "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138", 281 | "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585", 282 | "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637", 283 | "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe", 284 | "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d", 285 | "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1", 286 | "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095", 287 | "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9", 288 | "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81", 289 | "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57", 290 | "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536", 291 | "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a", 292 | "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052", 293 | "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01", 294 | "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98", 295 | "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433", 296 | "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1", 297 | "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f", 298 | "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4", 299 | "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b", 300 | "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6", 301 | "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8", 302 | "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5", 303 | "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306", 304 | "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5", 305 | "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f", 306 | "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4", 307 | "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be", 308 | "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919", 309 | "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af", 310 | "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66", 311 | "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1", 312 | "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af", 313 | "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec", 314 | "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b", 315 | "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289", 316 | "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a", 317 | "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d", 318 | "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102", 319 | "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9", 320 | "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc", 321 | "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45", 322 | "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa", 323 | "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a", 324 | "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c", 325 | "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461", 326 | "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708", 327 | "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca", 328 | "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd", 329 | "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913", 330 | "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da", 331 | "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0", 332 | "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5", 333 | "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5", 334 | "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96", 335 | "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41", 336 | "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3", 337 | "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456", 338 | "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c", 339 | "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867", 340 | "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0", 341 | "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213", 342 | "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619", 343 | "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240", 344 | "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c", 345 | "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377", 346 | "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b", 347 | "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c", 348 | "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54", 349 | "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b", 350 | "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53", 351 | "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029", 352 | "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6", 353 | "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885", 354 | "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94", 355 | "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134", 356 | "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8", 357 | "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9", 358 | "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863", 359 | "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b", 360 | "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806", 361 | "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11", 362 | "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9", 363 | "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817", 364 | "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95", 365 | "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8", 366 | "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc", 367 | "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47", 368 | "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b", 369 | "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0", 370 | "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a", 371 | "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f", 372 | "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56", 373 | "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef", 374 | "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851", 375 | "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7", 376 | "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62", 377 | "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4", 378 | "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a", 379 | "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c", 380 | "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533", 381 | "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f", 382 | "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e", 383 | "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a", 384 | "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3", 385 | "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b", 386 | "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4", 387 | "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0", 388 | "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d", 389 | "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3", 390 | "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5", 391 | "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534", 392 | "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4", 393 | "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144", 394 | "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd", 395 | "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd", 396 | "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860", 397 | "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704", 398 | "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8", 399 | "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d", 400 | "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9", 401 | "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f", 402 | "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad", 403 | "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc", 404 | "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510", 405 | "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937", 406 | "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a", 407 | "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460", 408 | "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85", 409 | "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86", 410 | "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0", 411 | "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246", 412 | "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7", 413 | "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa", 414 | "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08", 415 | "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270", 416 | "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a", 417 | "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169", 418 | "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e", 419 | "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75", 420 | "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd", 421 | "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354", 422 | "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c", 423 | "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1", 424 | "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb", 425 | "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f", 426 | "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef" 427 | ], 428 | "index": "pypi", 429 | "markers": "python_version >= '3.6'", 430 | "version": "==5.2.1" 431 | }, 432 | "packaging": { 433 | "hashes": [ 434 | "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", 435 | "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" 436 | ], 437 | "markers": "python_version >= '3.7'", 438 | "version": "==24.0" 439 | }, 440 | "parsel": { 441 | "hashes": [ 442 | "sha256:14e00dc07731c9030db620c195fcae884b5b4848e9f9c523c6119f708ccfa9ac", 443 | "sha256:c4a777ee6c3ff5e39652b58e351c5cf02c12ff420d05b07a7966aebb68ab1700" 444 | ], 445 | "index": "pypi", 446 | "markers": "python_version >= '3.8'", 447 | "version": "==1.9.1" 448 | }, 449 | "pydantic": { 450 | "hashes": [ 451 | "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352", 452 | "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383" 453 | ], 454 | "markers": "python_version >= '3.8'", 455 | "version": "==2.7.0" 456 | }, 457 | "pydantic-core": { 458 | "hashes": [ 459 | "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6", 460 | "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb", 461 | "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0", 462 | "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6", 463 | "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47", 464 | "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a", 465 | "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a", 466 | "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac", 467 | "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88", 468 | "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db", 469 | "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d", 470 | "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d", 471 | "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9", 472 | "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e", 473 | "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b", 474 | "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d", 475 | "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649", 476 | "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c", 477 | "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1", 478 | "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09", 479 | "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0", 480 | "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90", 481 | "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d", 482 | "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294", 483 | "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144", 484 | "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b", 485 | "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1", 486 | "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b", 487 | "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2", 488 | "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad", 489 | "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622", 490 | "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17", 491 | "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06", 492 | "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc", 493 | "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50", 494 | "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d", 495 | "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59", 496 | "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539", 497 | "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a", 498 | "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b", 499 | "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5", 500 | "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9", 501 | "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278", 502 | "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6", 503 | "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44", 504 | "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0", 505 | "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb", 506 | "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80", 507 | "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5", 508 | "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570", 509 | "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b", 510 | "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de", 511 | "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6", 512 | "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8", 513 | "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203", 514 | "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7", 515 | "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048", 516 | "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae", 517 | "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89", 518 | "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f", 519 | "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926", 520 | "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2", 521 | "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76", 522 | "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d", 523 | "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411", 524 | "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9", 525 | "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2", 526 | "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586", 527 | "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35", 528 | "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c", 529 | "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143", 530 | "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6", 531 | "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60", 532 | "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b", 533 | "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226", 534 | "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519", 535 | "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31", 536 | "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7", 537 | "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b" 538 | ], 539 | "markers": "python_version >= '3.8'", 540 | "version": "==2.18.1" 541 | }, 542 | "pyparsing": { 543 | "hashes": [ 544 | "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", 545 | "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" 546 | ], 547 | "markers": "python_full_version >= '3.6.8'", 548 | "version": "==3.1.2" 549 | }, 550 | "python-dotenv": { 551 | "hashes": [ 552 | "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", 553 | "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" 554 | ], 555 | "index": "pypi", 556 | "markers": "python_version >= '3.8'", 557 | "version": "==1.0.1" 558 | }, 559 | "requests": { 560 | "hashes": [ 561 | "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", 562 | "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" 563 | ], 564 | "markers": "python_version >= '3.7'", 565 | "version": "==2.31.0" 566 | }, 567 | "requests-toolbelt": { 568 | "hashes": [ 569 | "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", 570 | "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" 571 | ], 572 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 573 | "version": "==1.0.0" 574 | }, 575 | "rfc3986": { 576 | "hashes": [ 577 | "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", 578 | "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" 579 | ], 580 | "version": "==1.5.0" 581 | }, 582 | "sniffio": { 583 | "hashes": [ 584 | "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", 585 | "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" 586 | ], 587 | "markers": "python_version >= '3.7'", 588 | "version": "==1.3.1" 589 | }, 590 | "starlette": { 591 | "hashes": [ 592 | "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee", 593 | "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823" 594 | ], 595 | "markers": "python_version >= '3.8'", 596 | "version": "==0.37.2" 597 | }, 598 | "typing-extensions": { 599 | "hashes": [ 600 | "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", 601 | "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" 602 | ], 603 | "markers": "python_version >= '3.8'", 604 | "version": "==4.11.0" 605 | }, 606 | "urllib3": { 607 | "hashes": [ 608 | "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", 609 | "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" 610 | ], 611 | "markers": "python_version >= '3.8'", 612 | "version": "==2.2.1" 613 | }, 614 | "uvicorn": { 615 | "hashes": [ 616 | "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de", 617 | "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0" 618 | ], 619 | "index": "pypi", 620 | "markers": "python_version >= '3.8'", 621 | "version": "==0.29.0" 622 | }, 623 | "w3lib": { 624 | "hashes": [ 625 | "sha256:c4432926e739caa8e3f49f5de783f336df563d9490416aebd5d39fb896d264e7", 626 | "sha256:ed5b74e997eea2abe3c1321f916e344144ee8e9072a6f33463ee8e57f858a4b1" 627 | ], 628 | "markers": "python_version >= '3.7'", 629 | "version": "==2.1.2" 630 | } 631 | }, 632 | "develop": { 633 | "iniconfig": { 634 | "hashes": [ 635 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 636 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 637 | ], 638 | "markers": "python_version >= '3.7'", 639 | "version": "==2.0.0" 640 | }, 641 | "markdown-it-py": { 642 | "hashes": [ 643 | "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", 644 | "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" 645 | ], 646 | "markers": "python_version >= '3.8'", 647 | "version": "==3.0.0" 648 | }, 649 | "mdurl": { 650 | "hashes": [ 651 | "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", 652 | "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" 653 | ], 654 | "markers": "python_version >= '3.7'", 655 | "version": "==0.1.2" 656 | }, 657 | "packaging": { 658 | "hashes": [ 659 | "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", 660 | "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" 661 | ], 662 | "markers": "python_version >= '3.7'", 663 | "version": "==24.0" 664 | }, 665 | "pluggy": { 666 | "hashes": [ 667 | "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", 668 | "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" 669 | ], 670 | "markers": "python_version >= '3.8'", 671 | "version": "==1.4.0" 672 | }, 673 | "pygments": { 674 | "hashes": [ 675 | "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", 676 | "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" 677 | ], 678 | "markers": "python_version >= '3.7'", 679 | "version": "==2.17.2" 680 | }, 681 | "pytest": { 682 | "hashes": [ 683 | "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", 684 | "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044" 685 | ], 686 | "index": "pypi", 687 | "markers": "python_version >= '3.8'", 688 | "version": "==8.1.1" 689 | }, 690 | "rich": { 691 | "hashes": [ 692 | "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", 693 | "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" 694 | ], 695 | "index": "pypi", 696 | "markers": "python_full_version >= '3.7.0'", 697 | "version": "==13.7.1" 698 | } 699 | } 700 | } 701 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Work in progress 2 | ~check latest [deployment](../../deployments/cyclic:dev) for testing/preview or [click here](https://javinfo-api.cyclic.cloud)~ 3 | 4 | for testing/preview [clicl here](https://api.javinfo.eu.org/docs) 5 | 6 | check [SETUP](./SETUP.md) for local development 7 | 8 | ~currently testing on [Cyclic.sh](https://app.cyclic.sh/#/join/iamrony777) serverless infrastructure. v1 required a server but i want to make v3 fully serverless. Cloudflare blocks requests made from vercel's serverless instances~ 9 | 10 | [Cyclic.sh](https://app.cyclic.sh/#/join/iamrony777) Use my referral link to earn $10 in credit 11 | -------------------------------------------------------------------------------- /SETUP.md: -------------------------------------------------------------------------------- 1 | # Using pipenv [source](https://pipenv.pypa.io/en/latest/) 2 | 3 | 4 | ```bash 5 | pip install --user pipenv 6 | ``` 7 | 8 | 9 | # basic structure explaination 10 | 11 | ```bash 12 | src/api -> api related folder (e.g. routers, auths) 13 | src/providers -> basically websracpers 14 | src/common -> commonly used modules 15 | sr/app.py 16 | ``` 17 | 18 | # misc 19 | file: `.env` 20 | 21 | ```env 22 | PRIORITY_LIST="['r18', 'jvdtbs', 'jvlib', 'javdb']" 23 | 24 | # proxy config (currently only javlibrary uses proxy) 25 | HTTP_PROXY="" 26 | HTTPS_PROXY="" 27 | ``` 28 | # javdb.com cookies 29 | 30 | 1. Login into javdb.com 31 | 2. Open developer tools (Right click -> Inspect / Ctrl + Shift + I ) 32 | 3. Navigate to Storage -> Cookies -> `https://javdb.com` 33 | 4. Now copy `_jdb_session` and `remember_me_token` key's values save in corresponding environment variables 34 | 35 | ```env 36 | JDB_SESSION="" 37 | REMEMBER_ME_TOKEN="" 38 | ``` 39 | 40 | * Remeber to copy decoded values 41 | ![show url decoded](./images/show-url-decoded.png) -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fastapi import FastAPI 3 | from fastapi.requests import Request 4 | from fastapi.responses import RedirectResponse 5 | import uvicorn 6 | 7 | from src.routes import jav, non_jav, rss 8 | 9 | app = FastAPI() 10 | app.include_router(jav.router) 11 | app.include_router(non_jav.router) 12 | app.include_router(rss.router) 13 | 14 | @app.get("/") 15 | async def root(request: Request): 16 | return RedirectResponse("/docs") 17 | 18 | 19 | if __name__ == "__main__": 20 | uvicorn.run( 21 | app=app, host="0.0.0.0", port=int(os.getenv("PORT", "3000")), use_colors=False 22 | ) 23 | -------------------------------------------------------------------------------- /images/show-url-decoded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamrony777/javinfo-api/c5cb126ace2250f20453b1da4b1add1f8defdd07/images/show-url-decoded.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | annotated-types==0.6.0; python_version >= '3.8' 3 | anyio==3.7.1; python_version >= '3.7' 4 | certifi==2023.11.17; python_version >= '3.6' 5 | chardet==3.0.4 6 | charset-normalizer==3.3.2; python_full_version >= '3.7.0' 7 | click==8.1.7; python_version >= '3.7' 8 | cloudscraper==1.2.71 9 | cssselect==1.2.0; python_version >= '3.7' 10 | fastapi==0.104.1; python_version >= '3.8' 11 | googletrans==3.1.0a0; python_version >= '3.6' 12 | h11==0.9.0; python_version >= '3.7' 13 | h2==3.2.0 14 | hpack==3.0.0 15 | hstspreload==2023.1.1; python_version >= '3.6' 16 | httpcore==0.9.1; python_version >= '3.6' 17 | httpx==0.13.3; python_version >= '3.6' 18 | hyperframe==5.2.0 19 | idna==2.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 20 | jmespath==1.0.1; python_version >= '3.7' 21 | loguru==0.7.2; python_version >= '3.5' 22 | lxml==4.9.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' 23 | packaging==23.2; python_version >= '3.7' 24 | parsel==1.8.1; python_version >= '3.7' 25 | pydantic==2.5.2; python_version >= '3.7' 26 | pydantic-core==2.14.5; python_version >= '3.7' 27 | pyparsing==3.1.1; python_full_version >= '3.6.8' 28 | python-dotenv==1.0.0; python_version >= '3.8' 29 | requests==2.31.0; python_version >= '3.7' 30 | requests-toolbelt==1.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 31 | rfc3986==1.5.0 32 | sniffio==1.3.0; python_version >= '3.7' 33 | starlette==0.27.0; python_version >= '3.7' 34 | typing-extensions==4.8.0; python_version >= '3.8' 35 | urllib3==2.1.0; python_version >= '3.8' 36 | uvicorn==0.24.0.post1; python_version >= '3.8' 37 | w3lib==2.1.2; python_version >= '3.7' 38 | -------------------------------------------------------------------------------- /self notes.todo: -------------------------------------------------------------------------------- 1 | @19/09/2023 2 | starting with providers. r18.dev first 3 | 4 | @20/09/2023 5 | using `cloudscraper` as main http module to bypass cloudflare 403 error, and its working. 6 | finished javdatabase scrapper, starting r18.dev and javlibrary. will finish by tonight 12AM , now its 12PM 7 | following a common schema for responses from proviers 8 | 9 | @21/09/2023 10 | write tests for r18, init javlibrary 11 | added ebod-875 to tests. providers complete -> r18,javdatabase 12 | 13 | @22/09/2023 14 | deployed and tested on cyclic.sh (serverless) [success] -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = false -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | import ast 2 | # import json 3 | from src.providers import Javdatabase, R18, Javlibrary, Javdb 4 | import concurrent.futures 5 | import os 6 | # import logging 7 | 8 | if os.path.isfile(".env"): 9 | from dotenv import load_dotenv 10 | 11 | load_dotenv(".env") 12 | 13 | r18Provider = R18() 14 | jvdtbsProvider = Javdatabase() 15 | jvlibProvideer = Javlibrary() 16 | javdbProvider = Javdb() 17 | 18 | priority = { 19 | "r18": 1, 20 | "jvdtbs": 2, 21 | "jvlib": 3, 22 | "javdb": 4, 23 | } 24 | 25 | 26 | def search_all_providers(code: str, provider: str = "all"): 27 | executors_list = [] 28 | 29 | with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: 30 | if provider == "r18": 31 | executors_list.append(executor.submit(r18Provider.search, code)) 32 | if provider == "jvdtbs": 33 | executors_list.append(executor.submit(jvdtbsProvider.search, code)) 34 | if provider == "jvlib": 35 | executors_list.append(executor.submit(jvlibProvideer.search, code)) 36 | if provider == "javdb": 37 | executors_list.append(executor.submit(javdbProvider.search, code)) 38 | if provider == "all" or provider is None: 39 | if ( 40 | os.getenv("PRIORITY_LIST") is not None 41 | and os.getenv("PRIORITY_LIST") != "" 42 | ): 43 | print("Using priority list: " + os.getenv("PRIORITY_LIST")) 44 | providers = ast.literal_eval(os.getenv("PRIORITY_LIST")) 45 | else: 46 | print("Using default priority list") 47 | providers = sorted(priority.keys(), key=lambda x: priority[x]) 48 | 49 | for provider in providers: 50 | if provider == "r18": 51 | executors_list.append(executor.submit(r18Provider.search, code)) 52 | elif provider == "jvdtbs": 53 | executors_list.append(executor.submit(jvdtbsProvider.search, code)) 54 | elif provider == "jvlib": 55 | executors_list.append(executor.submit(jvlibProvideer.search, code)) 56 | elif provider == "javdb": 57 | executors_list.append(executor.submit(javdbProvider.search, code)) 58 | 59 | concurrent.futures.wait( 60 | executors_list, 61 | return_when=concurrent.futures.ALL_COMPLETED, 62 | ) 63 | 64 | results = [future.result() for future in executors_list] 65 | 66 | # print(json.dumps(results, indent=2, ensure_ascii=False)) 67 | 68 | if results: 69 | for result in results: 70 | if "statusCode" not in result: 71 | return result 72 | return results[0] 73 | -------------------------------------------------------------------------------- /src/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamrony777/javinfo-api/c5cb126ace2250f20453b1da4b1add1f8defdd07/src/common/__init__.py -------------------------------------------------------------------------------- /src/common/_http.py: -------------------------------------------------------------------------------- 1 | from requests import Session 2 | from urllib.parse import urljoin 3 | 4 | 5 | class CustomSession(Session): 6 | def __init__(self, base_url=None): 7 | super().__init__() 8 | self.base_url = base_url 9 | 10 | def request(self, method, url, *args, **kwargs): 11 | joined_url = urljoin(self.base_url, url) 12 | return super().request(method, joined_url, *args, **kwargs) 13 | -------------------------------------------------------------------------------- /src/common/trailer.py: -------------------------------------------------------------------------------- 1 | """ 2 | fetch trailer/preview from https://www.dmm.co.jp/service/-/html5_player/ 3 | """ 4 | 5 | import re 6 | from lxml import html 7 | import requests 8 | 9 | 10 | def getPreview(code: str): 11 | response = requests.get( 12 | f"https://www.dmm.co.jp/service/-/html5_player/=/cid={code}/mtype=AhRVShI_/service=mono/floor=dvd/mode=/", 13 | allow_redirects=True, 14 | ) 15 | 16 | page: html.HtmlElement = html.fromstring( 17 | html=response.content, base_url="https://www.dmm.co.jp" 18 | ) 19 | src = re.findall( 20 | pattern=r"\"src\":\"(.*?)\"", 21 | string=page.cssselect("div > script")[0].text, 22 | flags=re.MULTILINE, 23 | ) 24 | 25 | return ( 26 | "https:" + src[0].replace("\\", "") 27 | if len(src) > 0 and src[0].endswith(".mp4") 28 | else None 29 | ) 30 | 31 | 32 | if __name__ == "__main__": 33 | print(getPreview("mkck275")) 34 | -------------------------------------------------------------------------------- /src/providers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from enum import Enum 3 | from .javdatabase import Javdatabase 4 | from .r18 import R18 5 | from .javlibrary import Javlibrary 6 | from .javdb import Javdb 7 | 8 | 9 | class Providers(str, Enum): 10 | """ 11 | Enumeration of providers for the API. 12 | 13 | This class represents the available providers for the API. Each provider has a unique value 14 | that can be used to identify it. 15 | """ 16 | 17 | r18 = "r18" 18 | jvdtbs = "jvdtbs" 19 | jvlib = "jvlib" 20 | javdb = "javdb" 21 | all = "all" 22 | -------------------------------------------------------------------------------- /src/providers/boobpedia.py: -------------------------------------------------------------------------------- 1 | """ 2 | An api wrapper of Boobpedia (https://www.boobpedia.com/boobs/Main_Page) 3 | """ 4 | 5 | from datetime import datetime 6 | from urllib.parse import urljoin 7 | from cloudscraper import create_scraper 8 | from lxml import html 9 | 10 | 11 | class Boobpedia: 12 | def __init__(self, base_url: str = "https://www.boobpedia.com") -> None: 13 | self.base_url = base_url 14 | self.headers = { 15 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36", 16 | "Accept": "*/*", 17 | } 18 | self.client = create_scraper( 19 | browser={"browser": "chrome", "platform": "linux", "desktop": True} 20 | ) 21 | self.parser = html.HTMLParser(encoding="UTF-8") 22 | 23 | # def __main_page(self): 24 | 25 | ## interesting links 26 | # 1. Today's birthday (/Category:October_9_birthdays) 27 | 28 | def todaysBirthdays(self): 29 | result = [] 30 | curr = datetime.now() 31 | url = urljoin( 32 | base=self.base_url, 33 | url=f'/boobs/Category:{curr.strftime("%B")}_{curr.day}_birthdays', 34 | ) 35 | 36 | response = self.client.get(url, timeout=5) 37 | response: html.HtmlElement = html.fromstring( 38 | html=response.content, base_url=self.base_url, parser=self.parser 39 | ) 40 | 41 | for el in response.xpath("//div[@class='mw-category']/*/ul/li/a"): 42 | result.append( 43 | { 44 | "name": el.text, 45 | "link": urljoin(base=self.base_url, url=el.get("href")), 46 | } 47 | ) 48 | 49 | return result 50 | 51 | 52 | # if __name__ == "__main__": 53 | # import asyncio 54 | 55 | # boobpedia = Boobpedia() 56 | # print(boobpedia.todaysBirthdays()) 57 | -------------------------------------------------------------------------------- /src/providers/javdatabase.py: -------------------------------------------------------------------------------- 1 | """ 2 | javdatabase.com scrapper 3 | Author @github.com/iamrony777 4 | """ 5 | 6 | import json 7 | import logging 8 | import re 9 | from urllib.parse import urljoin 10 | from cloudscraper import create_scraper 11 | from lxml import html 12 | from os.path import basename, splitext 13 | 14 | 15 | class Javdatabase: 16 | """ 17 | Scrapes from javdatabase.com (default url) 18 | 19 | constuctor: 20 | base_url: str = "https://javdatabase.com/" 21 | 22 | methods: 23 | search(code: str) -> providerResponse 24 | 25 | """ 26 | 27 | def __init__(self, base_url: str = "https://javdatabase.com/") -> None: 28 | # __handler = logging.StreamHandler() 29 | # __handler.setLevel(logging.DEBUG) 30 | # __handler.setFormatter( 31 | # logging.Formatter("%(name)s - %(levelname)s - %(message)s") 32 | # ) 33 | self.base_url = base_url 34 | self.headers = { 35 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36", 36 | "Accept": "*/*", 37 | } 38 | self.client = create_scraper( 39 | browser={"browser": "chrome", "platform": "linux", "desktop": True}, 40 | ) 41 | self.parser = html.HTMLParser(encoding="UTF-8") 42 | self.logger = logging.getLogger(splitext(basename(__file__))[0]) 43 | # self.logger.addHandler(__handler) 44 | self.logger.setLevel(logging.DEBUG) 45 | 46 | def __getJsonResult(self, code: str, page: html.HtmlElement): 47 | result = {"id": code} 48 | result["title"] = page.cssselect(".entry-header > h1")[0].text.replace( 49 | f"{code} - ", "" 50 | ) 51 | result["title_ja"] = None 52 | result["page"] = urljoin(base=self.base_url, url=f"movies/{code.lower()}/") 53 | 54 | ## result.poster 55 | try: 56 | result["poster"] = page.xpath('//meta[@property="og:image"]')[0].get( 57 | "content" 58 | ) 59 | except IndexError: 60 | try: 61 | result["poster"] = page.xpath('//meta[@name="twitter:image"]')[0].get( 62 | "content" 63 | ) 64 | except IndexError: 65 | result["poster"] = None 66 | 67 | ## result.preview 68 | try: 69 | result["preview"] = page.xpath("//iframe")[0].get("src") 70 | except IndexError: 71 | result["preview"] = None 72 | 73 | ## result.details 74 | result["details"] = { 75 | "director": None, 76 | "release_date": None, 77 | "runtime": None, 78 | "studio": None, 79 | } 80 | ### result.details.director 81 | try: 82 | result["details"]["director"] = page.cssselect( 83 | "div.movietable > table > tr:nth-child(11) > td:nth-child(2) > span > a" 84 | )[0].text 85 | except IndexError: 86 | pass 87 | 88 | ## result.details.release_date 89 | try: 90 | result["details"]["release_date"] = page.cssselect( 91 | "div.movietable > table > tr:nth-child(14) > td:nth-child(2)" 92 | )[0].text 93 | except IndexError: 94 | pass 95 | 96 | ## result.details.runtime 97 | try: 98 | result["details"]["runtime"] = re.match( 99 | r"\d+", 100 | page.cssselect( 101 | "div.movietable > table > tr:nth-child(15) > td:nth-child(2)" 102 | )[0].text, 103 | )[0] 104 | except IndexError: 105 | pass 106 | ## result.details.studio 107 | try: 108 | result["details"]["studio"] = page.cssselect( 109 | "div.movietable > table > tr:nth-child(10) > td:nth-child(2) > span > a" 110 | )[0].text 111 | except IndexError: 112 | pass 113 | 114 | ## result.actress 115 | result["actress"] = [] 116 | try: 117 | for actr in page.cssselect("div.idol-thumb > a > img"): 118 | result["actress"].append( 119 | {"name": actr.attrib["alt"], "image": actr.attrib["data-src"]} 120 | ) 121 | except KeyError: 122 | pass 123 | 124 | ## result.screenshots 125 | result["screenshots"] = [] 126 | 127 | el = ( 128 | 2 if not result["actress"] else 3 129 | ) ## if actress section is not available then screenshot section changes it position 130 | try: 131 | for ss in page.cssselect(f".entry-content > div:nth-child({el}) > a"): 132 | result["screenshots"].append(ss.attrib["href"]) 133 | except KeyError: 134 | pass 135 | 136 | ## result.tags 137 | result["tags"] = [] 138 | try: 139 | _ = 7 140 | while True: 141 | for tags in page.cssselect( 142 | f"div.movietable > table > tr:nth-child({_}) > td:nth-child(2) > span > a" # change value of tr:nth-child() if tags arent available 143 | ): 144 | result["tags"].append(tags.text.strip()) 145 | 146 | if result["tags"]: 147 | break 148 | _ += 1 149 | except KeyError: 150 | self.logger.debug("keyerror:tags") 151 | return result 152 | 153 | def search(self, code: str) -> dict: 154 | """public method: search""" 155 | resp = self.client.get( 156 | urljoin(base=self.base_url, url=f"movies/{code.lower()}"), 157 | allow_redirects=True, 158 | timeout=5, 159 | ) 160 | self.logger.debug("search:status_code: %s", resp.status_code) 161 | 162 | if not resp.ok: 163 | return {"statusCode": resp.status_code} 164 | else: 165 | return self.__getJsonResult( 166 | code=code, 167 | page=html.fromstring( 168 | html=resp.content, base_url=self.base_url, parser=self.parser 169 | ), 170 | ) 171 | 172 | 173 | if __name__ == "__main__": 174 | print(json.dumps(Javdatabase().search("MFOD-023"), ensure_ascii=False, indent=2)) 175 | -------------------------------------------------------------------------------- /src/providers/javdb.py: -------------------------------------------------------------------------------- 1 | """ 2 | Javdb.com scrapper 3 | Author @github.com/iamrony777 4 | """ 5 | 6 | import difflib 7 | import re 8 | from os import getenv 9 | from urllib.parse import quote, urljoin 10 | from googletrans import Translator 11 | from lxml import html 12 | from cloudscraper import create_scraper 13 | from os.path import basename, splitext 14 | import logging 15 | 16 | 17 | class Javdb: 18 | """ 19 | Simple Javdb API client. 20 | 21 | It provides methods to interact with the Javdb API and retrieve data. 22 | 23 | """ 24 | 25 | def __init__(self, base_url: str = "https://javdb.com/") -> None: 26 | """ 27 | Initializes the class with the given base URL. 28 | 29 | Parameters: 30 | base_url (str): The base URL to be used for the API requests. Defaults to "https://javdb.com/". 31 | 32 | Returns: 33 | None 34 | """ 35 | # __handler = logging.StreamHandler() 36 | # __handler.setLevel(logging.DEBUG) 37 | # __handler.setFormatter(logging.Formatter('%(name)s - %(levelname)s - %(message)s')) 38 | 39 | self.base_url = base_url 40 | self.cookies = { 41 | "redirect_to": "/", 42 | "theme": "auto", 43 | "locale": "en", 44 | "over18": "1", 45 | "remember_me_token": quote(getenv("REMEMBER_ME_TOKEN")), 46 | "_jdb_session": quote(getenv("JDB_SESSION")), 47 | } 48 | self.client = create_scraper( 49 | browser={"browser": "chrome", "platform": "linux", "desktop": True}, 50 | ) 51 | self.parser = html.HTMLParser(encoding="UTF-8") 52 | self.translator = Translator() 53 | 54 | self.logger = logging.getLogger(splitext(basename(__file__))[0]) 55 | # self.logger.addHandler(__handler) 56 | self.logger.setLevel(logging.DEBUG) 57 | 58 | def __fixPreivewUrl(self, url: str | None): 59 | if url: 60 | if url.startswith("//"): 61 | url = f"https:{url}" 62 | return url 63 | 64 | def __getJsonResult(self, page: html.HtmlElement): 65 | if bool(re.search(r"Sign in", page.text_content())): 66 | return {"statusCode": 403, "message": "Unauthorized, Sign in required"} 67 | _id = page.xpath('//a[@title="Copy ID"]')[0].get("data-clipboard-text") 68 | 69 | result = {"id": _id} 70 | _title_ja = page.cssselect("div.video-detail > h2 > strong.current-title")[ 71 | 0 72 | ].text 73 | 74 | result["title"] = self.translator.translate(_title_ja, dest="en").text 75 | result["title_ja"] = _title_ja 76 | result["page"] = page.xpath('//link[@rel="canonical"]')[0].get("href") 77 | result["poster"] = page.cssselect("div.column.column-video-cover > a > img")[ 78 | 0 79 | ].get("src") 80 | result["preview"] = self.__fixPreivewUrl( 81 | page.cssselect("#preview-video > source")[0].get("src") 82 | ) 83 | 84 | ## result.details 85 | result["details"] = { 86 | "director": None, 87 | "release_date": None, 88 | "runtime": None, 89 | "studio": None, 90 | } 91 | 92 | result["actress"] = [] 93 | result["screenshots"] = [] 94 | result["tags"] = [] 95 | 96 | for item in page.xpath('//div/nav[@class="panel movie-panel-info"]/div'): 97 | try: 98 | if item.find("strong").text == "Director:": 99 | result["details"]["director"] = item.find("span/a").text 100 | 101 | if item.find("strong").text == "Duration:": 102 | result["details"]["runtime"] = re.match( 103 | pattern=r"\d+", string=item.find("span").text 104 | ).group() 105 | 106 | if item.find("strong").text == "Released Date:": 107 | result["details"]["release_date"] = item.find("span").text 108 | 109 | if item.find("strong").text == "Publisher:": 110 | result["details"]["studio"] = item.find("span/a").text 111 | 112 | if item.find("strong").text == "Actor(s):": 113 | for actress in item.findall("span/a"): 114 | # result["actress"].append(actress.find('a').text) 115 | result["actress"].append({"name": actress.text, "image": None}) 116 | 117 | if item.find("strong").text == "Tags:": 118 | for tag in item.findall("span/a"): 119 | result["tags"].append(tag.text) 120 | 121 | except AttributeError: 122 | pass 123 | 124 | for item in page.xpath('//a[@class="tile-item"][@data-fancybox="gallery"]'): 125 | result["screenshots"].append(item.get("href")) 126 | 127 | return result 128 | 129 | def search(self, code: str) -> dict: 130 | """ 131 | public method: search 132 | """ 133 | resp = self.client.get( 134 | urljoin(self.base_url, "/search"), 135 | params={"q": code, "f": "all", "locale": "en", "over18": 1}, 136 | cookies=self.cookies, 137 | allow_redirects=True, 138 | timeout=5, 139 | ) 140 | 141 | resultObj = [] 142 | self.logger.debug(f"search:status_code: {resp.status_code}") 143 | 144 | if resp.status_code == 200: 145 | page: html.HtmlElement = html.fromstring( 146 | html=resp.content, base_url=self.base_url, parser=self.parser 147 | ) 148 | 149 | for eachItem in page.cssselect("div > div.movie-list > div.item"): 150 | # save results for getting the closest match 151 | resultObj.append( 152 | { 153 | "id": eachItem.find('a/div[@class="video-title"]/strong').text, 154 | "url": eachItem.find("a").get("href"), 155 | } 156 | ) 157 | if ( 158 | code == eachItem.find('a/div[@class="video-title"]/strong').text 159 | ): # when the code is found 160 | # self.logger.debug("search:code: found") 161 | resp = self.client.get( 162 | urljoin(base=self.base_url, url=eachItem.find("a").get("href")), 163 | cookies=self.cookies, 164 | allow_redirects=True, 165 | timeout=5, 166 | ) 167 | # self.logger.debug("search:code: scraped") 168 | return self.__getJsonResult( 169 | page=html.fromstring( 170 | html=resp.content, 171 | base_url=self.base_url, 172 | parser=self.parser, 173 | ), 174 | ) 175 | 176 | # get the most similar 177 | most_similar = difflib.get_close_matches( 178 | code, [i["id"] for i in resultObj], n=1, cutoff=0.6 179 | ) 180 | if most_similar: 181 | self.logger.debug("search:most_similar: found %s", most_similar[0]) 182 | resp = self.client.get( 183 | urljoin( 184 | base=self.base_url, 185 | url=next( 186 | obj["url"] 187 | for obj in resultObj 188 | if obj["id"] == most_similar[0] 189 | ), 190 | ), 191 | cookies=self.cookies, 192 | allow_redirects=True, 193 | timeout=5, 194 | ) 195 | 196 | if bool(re.search(pattern=r"plans", string=resp.url)): 197 | self.logger.error("search:most_similar: vip required") 198 | return { 199 | "statusCode": 402, 200 | "message": "VIP permission is required to watch the movie", 201 | } 202 | 203 | self.logger.debug("search:most_similar: scraped") 204 | return self.__getJsonResult( 205 | page=html.fromstring( 206 | html=resp.content, 207 | base_url=self.base_url, 208 | parser=self.parser, 209 | ), 210 | ) 211 | 212 | self.logger.debug("search:code: not found") 213 | return {"statusCode": 404, "message": "Not Found"} 214 | 215 | if not resp or resp.status_code != 200: 216 | self.logger.debug("search:code:status: %s", resp.status_code) 217 | return {"statusCode": resp.status_code} 218 | 219 | 220 | if __name__ == "__main__": 221 | import json 222 | from dotenv import load_dotenv 223 | 224 | load_dotenv(".env") 225 | print(json.dumps(Javdb().search("FC2 3185212"), indent=4, ensure_ascii=False)) 226 | -------------------------------------------------------------------------------- /src/providers/javlibrary.py: -------------------------------------------------------------------------------- 1 | """ 2 | Javlibrary.com/en/ scrapper 3 | Author @github.com/iamrony777 4 | """ 5 | 6 | import re 7 | import os 8 | from lxml import html 9 | from urllib.parse import urljoin 10 | 11 | # from cloudscraper import create_scraper 12 | from httpx import Client 13 | from src.common.trailer import getPreview 14 | 15 | class Javlibrary: 16 | """ 17 | Simple Javlibrary API client. 18 | 19 | It provides methods to interact with the Javlibrary API and retrieve data. 20 | 21 | """ 22 | 23 | def __init__(self, base_url: str = "https://www.javlibrary.com/en/") -> None: 24 | """ 25 | Initializes the class with the given base URL. 26 | 27 | Parameters: 28 | base_url (str): The base URL to be used for the API requests. Defaults to "https://www.javlibrary.com/en/". 29 | 30 | Returns: 31 | None 32 | """ 33 | self.base_url = base_url 34 | self.parser = html.HTMLParser(encoding="UTF-8") 35 | # self.client = create_scraper( 36 | # browser={"browser": "chrome", "platform": "linux", "desktop": True}, 37 | # ) 38 | self.client = Client( 39 | verify=False, 40 | timeout=60, 41 | http2=True, 42 | headers={ 43 | "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" 44 | }, 45 | ) 46 | 47 | if os.getenv("HTTP_PROXY"): 48 | self.client.proxies.update( 49 | { 50 | "http://": os.getenv("HTTP_PROXY"), 51 | } 52 | ) 53 | 54 | if os.getenv("HTTPS_PROXY"): 55 | self.client.proxies.update( 56 | { 57 | "https://": os.getenv("HTTPS_PROXY"), 58 | } 59 | ) 60 | 61 | def __getJapanesePage(self, url: str) -> html.HtmlElement: 62 | return html.fromstring( 63 | html=self.client.get( 64 | url=urljoin(base="https://www.javlibrary.com/ja/", url=url), 65 | cookies={"over18": "18"}, 66 | allow_redirects=True, 67 | ).content, 68 | base_url=urljoin(base="https://www.javlibrary.com/ja/", url=url), 69 | parser=self.parser, 70 | ) 71 | 72 | def __getJsonResult(self, page: html.HtmlElement): 73 | _id = page.cssselect("#video_id > table > tr > td.text")[0].text 74 | 75 | result = {"id": _id} 76 | result["title"] = page.cssselect("#video_title > h3 > a")[0].text.replace( 77 | f"{_id} ", "" 78 | ) 79 | result["title_ja"] = ( 80 | self.__getJapanesePage( 81 | page.cssselect("#video_title > h3 > a")[0] 82 | .get("href") 83 | .replace("/en/", "./") 84 | ) 85 | .cssselect("#video_title > h3 > a")[0] 86 | .text.replace(f"{_id} ", "") 87 | ) 88 | result["page"] = urljoin( 89 | base=self.base_url, 90 | url=page.cssselect("#video_title > h3 > a")[0].get("href"), 91 | ) 92 | result["poster"] = page.cssselect("#video_jacket_img")[0].get("src") 93 | result["preview"] = ( 94 | getPreview( 95 | page.cssselect("div.previewthumbs > a.btn_videoplayer")[0].get( 96 | "attr-data" 97 | ) 98 | ) 99 | if page.cssselect("div.previewthumbs > a.btn_videoplayer") 100 | else None 101 | ) 102 | 103 | ## result.details 104 | result["details"] = { 105 | "director": None, 106 | "release_date": None, 107 | "runtime": None, 108 | "studio": None, 109 | } 110 | 111 | try: 112 | result["details"]["director"] = page.xpath( 113 | '//td/span[@class="director"]/a/text()' 114 | )[0].strip() 115 | except IndexError: 116 | pass 117 | 118 | try: 119 | result["details"]["runtime"] = int( 120 | page.cssselect("#video_length > table > tr > td:nth-child(2) > span")[ 121 | 0 122 | ].text 123 | ) 124 | except IndexError: 125 | pass 126 | 127 | try: 128 | result["details"]["release_date"] = page.cssselect( 129 | "#video_date > table > tr > td.text" 130 | )[0].text.strip() 131 | except IndexError: 132 | pass 133 | 134 | try: 135 | result["details"]["studio"] = page.xpath( 136 | '//td/span[@class="maker"]/a/text()' 137 | )[0] 138 | except IndexError: 139 | pass 140 | 141 | result["actress"] = [] 142 | try: 143 | for act in page.xpath('//*/span[@class="star"]'): 144 | result["actress"].append({"name": act.find("a").text, "image": None}) 145 | except TypeError: 146 | pass 147 | 148 | result["screenshots"] = [] 149 | try: 150 | for ss in page.cssselect("div.previewthumbs > a"): 151 | if ss.get("href") != "#": 152 | result["screenshots"].append(ss.get("href")) 153 | except [IndexError, TypeError, KeyError]: 154 | pass 155 | 156 | result["tags"] = [] 157 | try: 158 | for t in page.cssselect("#video_genres > table > tr > td.text > span > a"): 159 | result["tags"].append(t.text) 160 | except [IndexError, TypeError, KeyError]: 161 | pass 162 | return result 163 | 164 | def search(self, code: str) -> dict: 165 | """ 166 | Searches by Movie Code 167 | 168 | Args: 169 | code (str): The code to search for. (Format: XXX-000) 170 | 171 | Returns: 172 | dict: A dictionary containing the search result. If the code is not found, 173 | the dictionary contains the following keys: 174 | - "statusCode": The status code of the search request (404). 175 | - "error": The error message indicating that no result was found. 176 | If the code is found, the dictionary contains the search result in JSON format. 177 | 178 | Raises: 179 | None 180 | """ 181 | # first search for checking availability 182 | code = code.upper() 183 | resp = self.client.get( 184 | url=urljoin(base=self.base_url, url="vl_searchbyid.php"), 185 | params={"keyword": code}, 186 | cookies={"over18": "18"}, 187 | allow_redirects=True, 188 | ) 189 | if resp.status_code == 200 and bool( 190 | re.search(pattern=r"\?keyword=[a-zA-Z0-9]+", string=resp.url) 191 | ): # duplicate or no results found 192 | page: html.HtmlElement = html.fromstring( 193 | html=resp.content, base_url=self.base_url 194 | ) 195 | try: ## No result found 196 | return { 197 | "statusCode": 404, 198 | "error": page.cssselect("#rightcolumn > p > em")[0].text, 199 | } 200 | except IndexError: ## Duplicate/Many results 201 | for each in page.cssselect("div.videothumblist > div.videos > div"): 202 | if code == each.find("a/div").text: 203 | resp = self.client.get( 204 | url=urljoin( 205 | base=self.base_url, 206 | url=each.find("a").get("href"), 207 | ), 208 | cookies={"over18": "18"}, 209 | allow_redirects=True, 210 | timeout=5, 211 | ) 212 | return self.__getJsonResult( 213 | page=html.fromstring( 214 | html=resp.content, 215 | base_url=self.base_url, 216 | parser=self.parser, 217 | ) 218 | ) 219 | continue 220 | elif resp.status_code == 200 and bool( 221 | re.search(pattern=r"\?v=[a-zA-Z0-9]+", string=resp.url) 222 | ): # redirected to actual page 223 | return self.__getJsonResult( 224 | page=html.fromstring( 225 | html=resp.content, 226 | base_url=self.base_url, 227 | parser=self.parser, 228 | ) 229 | ) 230 | elif not resp.status_code == 200: 231 | return {"statusCode": resp.status_code, "url": str(resp.url)} 232 | 233 | 234 | if __name__ == "__main__": 235 | print(Javlibrary().search("SSIS-001")) 236 | -------------------------------------------------------------------------------- /src/providers/r18.py: -------------------------------------------------------------------------------- 1 | """ 2 | fetch data from r18.dev's internal api 3 | Author @github.com/iamrony777 4 | """ 5 | 6 | 7 | from src.common._http import CustomSession 8 | from urllib.parse import urljoin 9 | 10 | 11 | class R18: 12 | def __init__(self, base_url: str = "https://r18.dev") -> None: 13 | self.base_url = base_url 14 | self.client = CustomSession(base_url=base_url) 15 | 16 | def __getJsonResult(self, data: dict[str]): 17 | result = {"id": data["dvd_id"]} 18 | result["title"] = data["title_en"] 19 | result["title_ja"] = data["title_ja"] 20 | result[ 21 | "page" 22 | ] = f"https://r18.dev/videos/vod/movies/detail/-/id={data['content_id']}/" 23 | result["poster"] = data.get("jacket_full_url", None) 24 | result["preview"] = data.get("sample_url", None) 25 | 26 | ## result.details 27 | result["details"] = { 28 | "director": None, 29 | "release_date": None, 30 | "runtime": None, 31 | "studio": None, 32 | } 33 | result["details"]["director"] = ( 34 | data["directors"][0]["name_romaji"] if data["directors"] else None 35 | ) 36 | result["details"]["release_date"] = data.get("release_date", None) 37 | result["details"]["runtime"] = data.get("runtime_mins", None) 38 | result["details"]["studio"] = data.get("maker_name_en", None) 39 | 40 | result["actress"] = [ 41 | { 42 | "name": a["name_romaji"], 43 | "image": urljoin( 44 | "https://pics.dmm.co.jp/mono/actjpgs/", a["image_url"] 45 | ), 46 | } 47 | for a in data["actresses"] 48 | ] 49 | 50 | result["screenshots"] = [ss["image_full"] for ss in data["gallery"]] 51 | result["tags"] = [c["name_en"] for c in data["categories"]] 52 | return result 53 | 54 | def search(self, code: str) -> dict: 55 | resp = self.client.get(url=f"/videos/vod/movies/detail/-/dvd_id={code}/json") 56 | if resp.ok: 57 | resp = self.client.get( 58 | url=f"/videos/vod/movies/detail/-/combined={resp.json()['content_id']}/json", 59 | timeout=5 60 | ) 61 | return self.__getJsonResult(resp.json()) 62 | else: 63 | return {"statusCode": resp.status_code} 64 | 65 | 66 | if __name__ == "__main__": 67 | from json import dumps 68 | 69 | print(dumps(R18().search("EBOD-875"), indent=2, ensure_ascii=False)) 70 | -------------------------------------------------------------------------------- /src/providers/rss.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from enum import Enum 3 | from os import getenv 4 | import re 5 | from urllib.parse import urljoin 6 | from lxml import etree, html 7 | import logging 8 | from os.path import basename, splitext 9 | from cloudscraper import create_scraper 10 | 11 | 12 | class Providers(str, Enum): 13 | """ 14 | Enumeration of providers for the API. 15 | 16 | This class represents the available providers for the API. Each provider has a unique value 17 | that can be used to identify it. 18 | """ 19 | 20 | r18 = "r18" 21 | jvdtbs = "jvdtbs" 22 | jvlib = "jvlib" 23 | javdb = "javdb" 24 | all = "all" 25 | 26 | 27 | class RSS: 28 | def __init__(self) -> None: 29 | __handler = logging.StreamHandler() 30 | __handler.setLevel(logging.DEBUG) 31 | __handler.setFormatter( 32 | logging.Formatter("%(name)s - %(levelname)s - %(message)s") 33 | ) 34 | self.parser = html.HTMLParser(encoding="utf-8") 35 | self.rss: etree._Element = etree.Element( 36 | "rss", version="2.0", nsmap={"atom": "http://www.w3.org/2005/Atom"} 37 | ) 38 | # self.provider = provider 39 | self.logger = logging.getLogger(splitext(basename(__file__))[0]) 40 | self.logger.addHandler(__handler) 41 | self.logger.setLevel(logging.DEBUG) 42 | self.headers = { 43 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36", 44 | "Accept": "*/*", 45 | } 46 | self.client = create_scraper( 47 | browser={"browser": "chrome", "platform": "linux", "desktop": True}, 48 | ) 49 | # javdatabase - init 50 | self.javdatabase_base_url = "https://javdatabase.com/" 51 | 52 | def generate_channel(self) -> etree._Element: 53 | channel: etree._Element = etree.SubElement(self.rss, "channel") 54 | 55 | # channel.title 56 | etree.SubElement(channel, "title").text = "NSFW API - JAV RSS" 57 | 58 | # channel.description 59 | etree.SubElement( 60 | channel, "description" 61 | ).text = "A rss endpoint to list latest javs" 62 | 63 | # channel.link 64 | etree.SubElement(channel, "link").text = "https:/nsfw-api.eu.org" 65 | 66 | # channel.logo 67 | etree.SubElement(channel, "icon").text = "image" 68 | gen = etree.SubElement( 69 | channel, 70 | "generator", 71 | uri="https://github.com/iamrony777/nsfw-api", 72 | version="3.0.0", 73 | ) 74 | gen.text = "RSS Generator by NSFW API" 75 | 76 | updated = etree.SubElement(channel, "pubDate") 77 | updated.text = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S %z") 78 | 79 | # rss.atom 80 | etree.SubElement( 81 | channel, 82 | etree.QName("http://www.w3.org/2005/Atom", "link"), 83 | attrib={ 84 | # "href": getenv("VERCEL_URL") + el_title.replace("r/", "/r/"), 85 | "rel": "self", 86 | "type": "application/rss+xml", 87 | }, 88 | ) 89 | 90 | return channel 91 | 92 | def generate_entries(self, provider: Providers) -> etree._Element: 93 | channel = self.generate_channel() 94 | if provider == Providers.jvdtbs: 95 | return self.generate_jvdtbs_entries(channel) 96 | # if provider == Providers.jvlib: 97 | # return self.generate_jvlib_entries() 98 | 99 | 100 | def generate_jvdtbs_entries(self, channel: etree._Element) -> etree._Element | None: 101 | resp = self.client.get( 102 | url=urljoin(self.javdatabase_base_url, "/movies"), 103 | allow_redirects=True, 104 | ) 105 | 106 | if not resp.ok: 107 | self.logger.debug("search:code:status: %s", resp.status_code) 108 | return None 109 | 110 | data_collumn = html.fromstring( 111 | resp.content, self.javdatabase_base_url, self.parser 112 | ) 113 | 114 | for element in data_collumn.xpath('(//div[@class="row"])[2]')[0]: 115 | item = etree.SubElement(channel, "item") 116 | title = etree.SubElement(item, "title") 117 | description = etree.Element("div", attrib={"class": "col-md-9"}) 118 | 119 | for child in element.find("div/div").iterchildren(): 120 | if child.tag == "p": 121 | title.text = f"[{child.find('a').text.strip()}] " 122 | # etree.SubElement(item, "title").text = child.find("a").text.strip() 123 | etree.SubElement(item, "link").text = child.find("a").get("href") 124 | if ( 125 | child.tag == "div" 126 | and child.attrib.get("class") == "movie-cover-thumb" 127 | ): 128 | etree.SubElement( 129 | etree.SubElement( 130 | description, 131 | "a", 132 | attrib={ 133 | "referrerpolicy": "no-referrer", 134 | "class": "bigImage", 135 | "title": child.find("a/img").get("alt"), 136 | }, 137 | ), 138 | "img", 139 | attrib={ 140 | "src": child.find("a/img").get("data-src"), 141 | "alt": child.find("a/img").get("alt"), 142 | }, 143 | ) 144 | etree.SubElement(item, "description").text = etree.CDATA( 145 | html.tostring(description, encoding="unicode", method="xml") 146 | ) 147 | 148 | if (child.tag == 'div' and child.attrib.get("class") == "mt-auto"): 149 | title.text += child.find('a').text.strip() 150 | # etree.SubElement(item, 'title').text = title 151 | 152 | 153 | return item 154 | 155 | def generate_jvlib_entries(self, channel: etree._Element) -> etree._Element | None: 156 | pass 157 | 158 | if __name__ == "__main__": 159 | rss = RSS() 160 | rss.generate_entries(Providers.jvdtbs) 161 | print( 162 | etree.tostring( 163 | rss.rss, 164 | pretty_print=True, 165 | xml_declaration=True, 166 | encoding="utf-8", 167 | standalone=True, 168 | ).decode("utf-8") 169 | ) 170 | -------------------------------------------------------------------------------- /src/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamrony777/javinfo-api/c5cb126ace2250f20453b1da4b1add1f8defdd07/src/routes/__init__.py -------------------------------------------------------------------------------- /src/routes/jav.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from fastapi import APIRouter, Request 3 | from fastapi.responses import JSONResponse, Response 4 | from pydantic import BaseModel 5 | from src import search_all_providers 6 | 7 | 8 | class Providers(str, Enum): 9 | """ 10 | Enumeration of providers for the API. 11 | 12 | This class represents the available providers for the API. Each provider has a unique value 13 | that can be used to identify it. 14 | """ 15 | 16 | r18 = "r18" 17 | jvdtbs = "jvdtbs" 18 | jvlib = "jvlib" 19 | javdb = "javdb" 20 | all = "all" 21 | 22 | 23 | class Details(BaseModel): 24 | director: str 25 | release_date: str 26 | runtime: int 27 | studio: str 28 | 29 | 30 | class Actress(BaseModel): 31 | name: str 32 | image: str 33 | 34 | 35 | class SuccessfulResponse(BaseModel): 36 | id: str 37 | title: str 38 | title_ja: str | None 39 | page: str 40 | poster: str 41 | preview: str | None 42 | details: Details 43 | actress: list[Actress] 44 | screenshots: list[str] 45 | tags: list[str] 46 | screenshots: list[str] 47 | tags: list[str] 48 | 49 | 50 | class UnSucessfulResponse(BaseModel): 51 | statuCode: int 52 | message: str | None 53 | 54 | 55 | router = APIRouter( 56 | prefix="/jav", 57 | tags=["jav"], 58 | responses={ 59 | 200: { 60 | "model": SuccessfulResponse, 61 | }, 62 | 404: {"model": UnSucessfulResponse}, 63 | }, 64 | ) 65 | 66 | 67 | @router.get("/search") 68 | async def search( 69 | req: Request, 70 | code: str, 71 | provider: Providers = Providers.all, 72 | # includeActressUrl: bool = True, 73 | ): 74 | response = search_all_providers(code, provider) 75 | if "statusCode" in response: 76 | return Response(status_code=response['statusCode']) 77 | return JSONResponse(content=response) 78 | -------------------------------------------------------------------------------- /src/routes/non_jav.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from src.providers import boobpedia 3 | 4 | router = APIRouter(tags=["non-jav", "boobpedia"]) 5 | boobpedia = boobpedia.Boobpedia() 6 | 7 | 8 | @router.get("/boobpedia/todaysBirthdays", tags=["boobpedia"]) 9 | async def todaysBirthday(): 10 | """ 11 | Get today's birthdays from the Boobpedia API. 12 | 13 | Returns: 14 | The list of today's birthdays. 15 | 16 | Raises: 17 | None. 18 | """ 19 | return boobpedia.todaysBirthdays() 20 | -------------------------------------------------------------------------------- /src/routes/rss.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from fastapi import APIRouter, Request 3 | from fastapi.responses import JSONResponse, Response 4 | from pydantic import BaseModel 5 | from lxml import etree 6 | from src.providers.rss import RSS 7 | 8 | 9 | class Providers(str, Enum): 10 | """ 11 | Enumeration of providers for the API. 12 | 13 | This class represents the available providers for the API. Each provider has a unique value 14 | that can be used to identify it. 15 | """ 16 | 17 | r18 = "r18" 18 | jvdtbs = "jvdtbs" 19 | jvlib = "jvlib" 20 | javdb = "javdb" 21 | all = "all" 22 | 23 | 24 | router = APIRouter( 25 | prefix="/rss", 26 | tags=["rss","jav"], 27 | # responses={ 28 | # 200: { 29 | # "model": SuccessfulResponse, 30 | # }, 31 | # 404: {"model": UnSucessfulResponse}, 32 | # }, 33 | ) 34 | 35 | 36 | @router.get("/javdatabase") 37 | async def javdatabase_feed(): 38 | rss = RSS() 39 | rss.generate_entries(Providers.jvdtbs) 40 | 41 | return Response( 42 | content=etree.tostring( 43 | rss.rss, 44 | pretty_print=True, 45 | xml_declaration=True, 46 | encoding="utf-8", 47 | standalone=True, 48 | ).decode("utf-8"), 49 | media_type="application/xml; charset=utf-8", 50 | ) 51 | -------------------------------------------------------------------------------- /src/tests/data/javdatabase.doa-017.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "DOA-017", 3 | "title": "First-Rate K-Cup Tits. Glamorously And Explosively Big Tits.", 4 | "title_ja": null, 5 | "page": "https://javdatabase.com/movies/doa-017/", 6 | "poster": "https://pics.dmm.co.jp/digital/video/doa00017/doa00017pl.jpg", 7 | "preview": null, 8 | "details": { 9 | "director": null, 10 | "release_date": "2022-02-26", 11 | "runtime": "119", 12 | "studio": "Black Dog/Daydreamers" 13 | }, 14 | "actress": [], 15 | "screenshots": [ 16 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-1.jpg", 17 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-2.jpg", 18 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-3.jpg", 19 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-4.jpg", 20 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-5.jpg", 21 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-6.jpg" 22 | ], 23 | "tags": [ 24 | "Big Tits", 25 | "Confinement", 26 | "Creampie", 27 | "Daydreamers", 28 | "Other Fetishes", 29 | "Titty Fuck" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/tests/data/javdatabase.ebod-391.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "EBOD-391", 3 | "title": "Real Breast Milk Married Woman - Ema Kisaki", 4 | "title_ja": null, 5 | "page": "https://javdatabase.com/movies/ebod-391/", 6 | "poster": "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391pl.jpg", 7 | "preview": null, 8 | "details": { 9 | "director": "Hao * Minami", 10 | "release_date": "2014-08-09", 11 | "runtime": "119", 12 | "studio": "E-BODY" 13 | }, 14 | "actress": [ 15 | { 16 | "name": "Ema Kisaki (HARUKI", 17 | "image": "https://www.javdatabase.com/idolimages/thumb/ema-kisaki-haruki.webp" 18 | }, 19 | { 20 | "name": "Haruki Kato", 21 | "image": "https://www.javdatabase.com/idolimages/thumb/haruki-kato.webp" 22 | } 23 | ], 24 | "screenshots": [ 25 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-1.jpg", 26 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-2.jpg", 27 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-3.jpg", 28 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-4.jpg", 29 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-5.jpg", 30 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-6.jpg" 31 | ], 32 | "tags": [ 33 | "Big Tits", 34 | "Breast Milk", 35 | "Featured Actress", 36 | "Gal", 37 | "Hi-Def", 38 | "Nymphomaniac", 39 | "Slender" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/tests/data/javdatabase.ebod-875.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "EBOD-875", 3 | "title": "No.1 In The Fudor Popularity Ranking For Three Consecutive Months! Legendary Soap Girl With Big Tits And Big Buttocks Who Serves Customers In Yoshiwara In Summer And Overseas In Winter Makes Her AV Debut! Reiran Akame", 4 | "title_ja": null, 5 | "page": "https://javdatabase.com/movies/ebod-875/", 6 | "poster": "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875pl.jpg", 7 | "preview": null, 8 | "details": { 9 | "director": "Zaku Arai", 10 | "release_date": "2021-12-17", 11 | "runtime": "149", 12 | "studio": "E-BODY" 13 | }, 14 | "actress": [ 15 | { 16 | "name": "Reiran Akame", 17 | "image": "https://www.javdatabase.com/idolimages/thumb/reiran-akame.webp" 18 | } 19 | ], 20 | "screenshots": [ 21 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-1.jpg", 22 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-2.jpg", 23 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-3.jpg", 24 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-4.jpg", 25 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-5.jpg", 26 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-6.jpg" 27 | ], 28 | "tags": [ 29 | "Big Tits", 30 | "Debut", 31 | "Featured Actress", 32 | "Hi-Def", 33 | "Lotion", 34 | "Threesome / Foursome", 35 | "Titty Fuck" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/tests/data/javdatabase.mkck-275.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "MKCK-275", 3 | "title": "Colossal Tits And Huge Ass Erection Temptation By An Exhibitionist!! Slutty Girls With Obscene Erotic Bodies Giving 102 Ejaculations 8 Hours", 4 | "title_ja": null, 5 | "page": "https://javdatabase.com/movies/mkck-275/", 6 | "poster": "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275pl.jpg", 7 | "preview": null, 8 | "details": { 9 | "director": null, 10 | "release_date": "2020-12-12", 11 | "runtime": "477", 12 | "studio": "E-BODY" 13 | }, 14 | "actress": [ 15 | { 16 | "name": "Eimi Fukada", 17 | "image": "https://www.javdatabase.com/idolimages/thumb/eimi-fukada.webp" 18 | }, 19 | { 20 | "name": "Elly Akira (Elly Arai, Yuka Osawa)", 21 | "image": "https://www.javdatabase.com/idolimages/thumb/elly-akira-elly-arai-yuka-osawa.webp" 22 | }, 23 | { 24 | "name": "Hazuki Wakamiya", 25 | "image": "https://www.javdatabase.com/idolimages/thumb/hazuki-wakamiya.webp" 26 | }, 27 | { 28 | "name": "Kaho Imai", 29 | "image": "https://www.javdatabase.com/idolimages/thumb/kaho-imai.webp" 30 | }, 31 | { 32 | "name": "Misato Nonomiya", 33 | "image": "https://www.javdatabase.com/idolimages/thumb/misato-nonomiya.webp" 34 | }, 35 | { 36 | "name": "Ruka Inaba", 37 | "image": "https://www.javdatabase.com/idolimages/thumb/ruka-inaba.webp" 38 | }, 39 | { 40 | "name": "Saki Okuda", 41 | "image": "https://www.javdatabase.com/idolimages/thumb/saki-okuda.webp" 42 | }, 43 | { 44 | "name": "Valenta Rich", 45 | "image": "https://www.javdatabase.com/idolimages/thumb/valenta-rich.webp" 46 | }, 47 | { 48 | "name": "Yuri Honma", 49 | "image": "https://www.javdatabase.com/idolimages/thumb/yuri-honma.webp" 50 | }, 51 | { 52 | "name": "Yuria Kanae", 53 | "image": "https://www.javdatabase.com/idolimages/thumb/yuria-kanae.webp" 54 | } 55 | ], 56 | "screenshots": [ 57 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-1.jpg", 58 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-2.jpg", 59 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-3.jpg", 60 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-4.jpg", 61 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-5.jpg", 62 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-6.jpg" 63 | ], 64 | "tags": [ 65 | "Big Asses", 66 | "Big Tits", 67 | "Compilation", 68 | "Older Sister", 69 | "Over 4 Hours", 70 | "Slut" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /src/tests/data/javlibrary.doa-017.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "DOA-017", 3 | "title": "Superb K Cup Explosion Big Breasts Glamorous", 4 | "title_ja": "極上Kカップ爆裂巨乳グラマラス", 5 | "page": "https://www.javlibrary.com/en/?v=javme4bzyy", 6 | "poster": "https://pics.dmm.co.jp/mono/movie/adult/doa017/doa017pl.jpg", 7 | "preview": null, 8 | "details": { 9 | "director": null, 10 | "release_date": "2022-03-01", 11 | "runtime": 120, 12 | "studio": "Black Dog / Mousozoku" 13 | }, 14 | "actress": [ 15 | { 16 | "name": "Hazuki Mion", 17 | "image": null 18 | } 19 | ], 20 | "screenshots": [ 21 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-1.jpg", 22 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-2.jpg", 23 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-3.jpg", 24 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-4.jpg", 25 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-5.jpg", 26 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-6.jpg", 27 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-7.jpg", 28 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-8.jpg", 29 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-9.jpg", 30 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-10.jpg", 31 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-11.jpg" 32 | ], 33 | "tags": [ 34 | "Creampie", 35 | "Other Fetish", 36 | "Big Tits", 37 | "Titty Fuck", 38 | "Confinement" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/tests/data/javlibrary.ebod-391.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "EBOD-391", 3 | "title": "Breast Milk Real Gal Married Emma NozomiSaki", 4 | "title_ja": "母乳本物ギャル人妻 希咲エマ", 5 | "page": "https://www.javlibrary.com/en/?v=javlijzd7i", 6 | "poster": "https://pics.dmm.co.jp/mono/movie/adult/ebod391/ebod391pl.jpg", 7 | "preview": null, 8 | "details": { 9 | "director": "Minami★Haou", 10 | "release_date": "2014-08-13", 11 | "runtime": 120, 12 | "studio": "E-body" 13 | }, 14 | "actress": [ 15 | { 16 | "name": "Kisaki Ema", 17 | "image": null 18 | } 19 | ], 20 | "screenshots": [ 21 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-1.jpg", 22 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-2.jpg", 23 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-3.jpg", 24 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-4.jpg", 25 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-5.jpg", 26 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-6.jpg", 27 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-7.jpg", 28 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-8.jpg", 29 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-9.jpg", 30 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-10.jpg" 31 | ], 32 | "tags": [ 33 | "Solowork", 34 | "Gal", 35 | "Big Tits", 36 | "Breast Milk", 37 | "Nasty, Hardcore", 38 | "Slender" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/tests/data/javlibrary.ssis-001.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "SSIS-001", 3 | "title": "Three Days Without Her Who Was Absorbed Only In Cheating SEX With Two Of Her Roommates At The End Of A Month Of Abstinence. Tsukasa Aoi Sayaka Otoshiro", 4 | "title_ja": "一ヶ月間の禁欲の果てに彼女のルームメイト2人と浮気SEXだけに没頭した彼女不在の3日間。 葵つかさ 乙白さやか", 5 | "page": "https://www.javlibrary.com/en/?v=javmezzbqu", 6 | "poster": "https://pics.dmm.co.jp/mono/movie/adult/ssis001/ssis001pl.jpg", 7 | "preview": "https://cc3001.dmm.com/litevideo/freepv/s/ssi/ssis00001/ssis00001_mhb_w.mp4", 8 | "details": { 9 | "director": "Hasami Kuka", 10 | "release_date": "2021-02-19", 11 | "runtime": 150, 12 | "studio": "S1 NO.1 STYLE" 13 | }, 14 | "actress": [ 15 | { 16 | "name": "Aoi Tsukasa", 17 | "image": null 18 | }, 19 | { 20 | "name": "Otsushiro Sayaka", 21 | "image": null 22 | } 23 | ], 24 | "screenshots": [ 25 | "https://pics.dmm.co.jp/digital/video/ssis00001/ssis00001jp-1.jpg", 26 | "https://pics.dmm.co.jp/digital/video/ssis00001/ssis00001jp-2.jpg", 27 | "https://pics.dmm.co.jp/digital/video/ssis00001/ssis00001jp-3.jpg", 28 | "https://pics.dmm.co.jp/digital/video/ssis00001/ssis00001jp-4.jpg", 29 | "https://pics.dmm.co.jp/digital/video/ssis00001/ssis00001jp-5.jpg", 30 | "https://pics.dmm.co.jp/digital/video/ssis00001/ssis00001jp-6.jpg", 31 | "https://pics.dmm.co.jp/digital/video/ssis00001/ssis00001jp-7.jpg", 32 | "https://pics.dmm.co.jp/digital/video/ssis00001/ssis00001jp-8.jpg", 33 | "https://pics.dmm.co.jp/digital/video/ssis00001/ssis00001jp-9.jpg", 34 | "https://pics.dmm.co.jp/digital/video/ssis00001/ssis00001jp-10.jpg" 35 | ], 36 | "tags": [ 37 | "3P, 4P", 38 | "Beautiful Girl", 39 | "Breasts", 40 | "Drama", 41 | "Cuckold", 42 | "Risky Mosaic" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/tests/data/javlibrary.ssis-100.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "SSIS-100", 3 | "title": "Intense Iki 108 Times! Convulsions 4800 Times! Iki Tide 1500cc! Pure White G Cup Girl Eros Awakening First Big / Spasm / Spasm Special Hayano Uta", 4 | "title_ja": "激イキ108回!痙攣4800回!イキ潮1500cc! 純白Gカップ少女エロス覚醒 はじめての大・痙・攣スペシャル はやのうた", 5 | "page": "https://www.javlibrary.com/en/?v=javme5iotm", 6 | "poster": "https://pics.dmm.co.jp/mono/movie/adult/ssis100/ssis100pl.jpg", 7 | "preview": "https://cc3001.dmm.com/litevideo/freepv/s/ssi/ssis00100/ssis00100_mhb_w.mp4", 8 | "details": { 9 | "director": "Hasami Kuka", 10 | "release_date": "2021-06-19", 11 | "runtime": 120, 12 | "studio": "S1 NO.1 STYLE" 13 | }, 14 | "actress": [ 15 | { 16 | "name": "Hayano Uta", 17 | "image": null 18 | } 19 | ], 20 | "screenshots": [ 21 | "https://pics.dmm.co.jp/digital/video/ssis00100/ssis00100jp-1.jpg", 22 | "https://pics.dmm.co.jp/digital/video/ssis00100/ssis00100jp-2.jpg", 23 | "https://pics.dmm.co.jp/digital/video/ssis00100/ssis00100jp-3.jpg", 24 | "https://pics.dmm.co.jp/digital/video/ssis00100/ssis00100jp-4.jpg", 25 | "https://pics.dmm.co.jp/digital/video/ssis00100/ssis00100jp-5.jpg", 26 | "https://pics.dmm.co.jp/digital/video/ssis00100/ssis00100jp-6.jpg", 27 | "https://pics.dmm.co.jp/digital/video/ssis00100/ssis00100jp-7.jpg", 28 | "https://pics.dmm.co.jp/digital/video/ssis00100/ssis00100jp-8.jpg", 29 | "https://pics.dmm.co.jp/digital/video/ssis00100/ssis00100jp-9.jpg", 30 | "https://pics.dmm.co.jp/digital/video/ssis00100/ssis00100jp-10.jpg" 31 | ], 32 | "tags": [ 33 | "Solowork", 34 | "Big Tits", 35 | "Beautiful Girl", 36 | "Nasty, Hardcore", 37 | "Squirting", 38 | "Risky Mosaic" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/tests/data/r18.doa-017.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "DOA-017", 3 | "title": "First-Rate K-Cup Tits. Glamorously And Explosively Big Tits.", 4 | "title_ja": "極上Kカップ爆裂巨乳グラマラス", 5 | "page": "https://r18.dev/videos/vod/movies/detail/-/id=doa00017/", 6 | "poster": "https://pics.dmm.co.jp/digital/video/doa00017/doa00017pl.jpg", 7 | "preview": "https://cc3001.dmm.co.jp/litevideo/freepv/d/doa/doa017/doa017_mhb_w.mp4", 8 | "details": { 9 | "director": null, 10 | "release_date": "2022-03-01", 11 | "runtime": 119, 12 | "studio": "Black Dog/Daydreamers" 13 | }, 14 | "actress": [], 15 | "screenshots": [ 16 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-1.jpg", 17 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-2.jpg", 18 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-3.jpg", 19 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-4.jpg", 20 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-5.jpg", 21 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-6.jpg", 22 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-7.jpg", 23 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-8.jpg", 24 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-9.jpg", 25 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-10.jpg", 26 | "https://pics.dmm.co.jp/digital/video/doa00017/doa00017jp-11.jpg" 27 | ], 28 | "tags": [ 29 | "Big Tits", 30 | "Other Fetishes", 31 | "Creampie", 32 | "Confinement", 33 | "Titty Fuck", 34 | "Daydreamers" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/tests/data/r18.ebod-391.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "EBOD-391", 3 | "title": "Real Breast Milk Married Woman - Ema Kisaki", 4 | "title_ja": "母乳本物ギャル人妻 希咲エマ", 5 | "page": "https://r18.dev/videos/vod/movies/detail/-/id=ebod00391/", 6 | "poster": "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391pl.jpg", 7 | "preview": "https://cc3001.dmm.co.jp/litevideo/freepv/e/ebo/ebod00391/ebod00391_dmb_w.mp4", 8 | "details": { 9 | "director": "Hao * Minami", 10 | "release_date": "2014-08-13", 11 | "runtime": 119, 12 | "studio": "E-BODY" 13 | }, 14 | "actress": [ 15 | { 16 | "name": "Ema Kisaki (HARUKI, Haruki Kato)", 17 | "image": "https://pics.dmm.co.jp/mono/actjpgs/kisaki_ema.jpg" 18 | } 19 | ], 20 | "screenshots": [ 21 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-1.jpg", 22 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-2.jpg", 23 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-3.jpg", 24 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-4.jpg", 25 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-5.jpg", 26 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-6.jpg", 27 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-7.jpg", 28 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-8.jpg", 29 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-9.jpg", 30 | "https://pics.dmm.co.jp/digital/video/ebod00391/ebod00391jp-10.jpg" 31 | ], 32 | "tags": [ 33 | "Gal", 34 | "Big Tits", 35 | "Slender", 36 | "Featured Actress", 37 | "Nymphomaniac", 38 | "Breast Milk", 39 | "Hi-Def", 40 | "Exclusive Distribution" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/tests/data/r18.ebod-875.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "EBOD-875", 3 | "title": "No.1 In The Fudor Popularity Ranking For Three Consecutive Months! Legendary Soap Girl With Big Tits And Big Buttocks Who Serves Customers In Yoshiwara In Summer And Overseas In Winter Makes Her AV Debut! Reiran Akame", 4 | "title_ja": "フードル人気ランキング3ヶ月連続1位!! 夏は吉原、冬は海外で接客する伝説の爆乳爆尻ソープ嬢AVデビュー 赤目レイラン", 5 | "page": "https://r18.dev/videos/vod/movies/detail/-/id=ebod00875/", 6 | "poster": "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875pl.jpg", 7 | "preview": "https://cc3001.dmm.co.jp/litevideo/freepv/e/ebo/ebod00875/ebod00875_mhb_w.mp4", 8 | "details": { 9 | "director": "Zaku Arai", 10 | "release_date": "2021-12-21", 11 | "runtime": 149, 12 | "studio": "E-BODY" 13 | }, 14 | "actress": [ 15 | { 16 | "name": "Reiran Akame", 17 | "image": "https://pics.dmm.co.jp/mono/actjpgs/akame_reiran.jpg" 18 | } 19 | ], 20 | "screenshots": [ 21 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-1.jpg", 22 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-2.jpg", 23 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-3.jpg", 24 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-4.jpg", 25 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-5.jpg", 26 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-6.jpg", 27 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-7.jpg", 28 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-8.jpg", 29 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-9.jpg", 30 | "https://pics.dmm.co.jp/digital/video/ebod00875/ebod00875jp-10.jpg" 31 | ], 32 | "tags": [ 33 | "Big Tits", 34 | "Featured Actress", 35 | "Titty Fuck", 36 | "Threesome / Foursome", 37 | "Lotion", 38 | "Debut", 39 | "Hi-Def", 40 | "Exclusive Distribution" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/tests/data/r18.mkck-275.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "MKCK-275", 3 | "title": "Colossal Tits And Huge Ass Erection Temptation By An Exhibitionist!! Slutty Girls With Obscene Erotic Bodies Giving 102 Ejaculations 8 Hours", 4 | "title_ja": "爆乳&巨尻露出で強●勃起!!無理やり102射精させる淫乱エロボディ痴女たち8時間", 5 | "page": "https://r18.dev/videos/vod/movies/detail/-/id=mkck00275/", 6 | "poster": "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275pl.jpg", 7 | "preview": "https://cc3001.dmm.co.jp/litevideo/freepv/m/mkc/mkck00275/mkck00275_mhb_w.mp4", 8 | "details": { 9 | "director": null, 10 | "release_date": "2020-12-13", 11 | "runtime": 477, 12 | "studio": "E-BODY" 13 | }, 14 | "actress": [ 15 | { 16 | "name": "Hazuki Wakamiya", 17 | "image": "https://pics.dmm.co.jp/mono/actjpgs/wakamiya_hazuki.jpg" 18 | }, 19 | { 20 | "name": "Eimi Fukada", 21 | "image": "https://pics.dmm.co.jp/mono/actjpgs/hukada_eimi.jpg" 22 | }, 23 | { 24 | "name": "Valenta Rich", 25 | "image": "https://pics.dmm.co.jp/mono/actjpgs/varentaritti.jpg" 26 | }, 27 | { 28 | "name": "Saki Okuda", 29 | "image": "https://pics.dmm.co.jp/mono/actjpgs/okuda_saki.jpg" 30 | }, 31 | { 32 | "name": "Elly Akira (Elly Arai, Yuka Osawa)", 33 | "image": "https://pics.dmm.co.jp/mono/actjpgs/oosawa_yuka2.jpg" 34 | }, 35 | { 36 | "name": "Yuri Honma", 37 | "image": "https://pics.dmm.co.jp/mono/actjpgs/honma_yuri.jpg" 38 | }, 39 | { 40 | "name": "Kaho Imai", 41 | "image": "https://pics.dmm.co.jp/mono/actjpgs/imai_kaho.jpg" 42 | }, 43 | { 44 | "name": "Ruka Inaba", 45 | "image": "https://pics.dmm.co.jp/mono/actjpgs/inaba_ruka.jpg" 46 | }, 47 | { 48 | "name": "Yuria Kanae", 49 | "image": "https://pics.dmm.co.jp/mono/actjpgs/kanae_yuria.jpg" 50 | } 51 | ], 52 | "screenshots": [ 53 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-1.jpg", 54 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-2.jpg", 55 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-3.jpg", 56 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-4.jpg", 57 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-5.jpg", 58 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-6.jpg", 59 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-7.jpg", 60 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-8.jpg", 61 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-9.jpg", 62 | "https://pics.dmm.co.jp/digital/video/mkck00275/mkck00275jp-10.jpg" 63 | ], 64 | "tags": [ 65 | "Slut", 66 | "Older Sister", 67 | "Big Tits", 68 | "Big Asses", 69 | "Compilation", 70 | "Over 4 Hours", 71 | "Exclusive Distribution" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /src/tests/javdatabase.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | from src.providers import Javdatabase 4 | 5 | # Define the codes to test 6 | codes = ["ebod-391", "mkck-275", "doa-017", "ebod-875"] 7 | provider = Javdatabase() 8 | 9 | 10 | @pytest.mark.parametrize("code", codes) 11 | def test_javdatabase(code: str): 12 | # Load the expected result from the json file 13 | with open(f"src/tests/data/r18.{code}.json", "r") as f: 14 | expected_result: str = f.read().strip() 15 | 16 | # Run the search function and get the result 17 | result = json.dumps(provider.search(code.upper()), indent=2, ensure_ascii=False) 18 | 19 | # Compare the result with the expected result 20 | assert ( 21 | result == expected_result 22 | ), f"For code: {code}, expected: {expected_result}, but got: {result}" 23 | 24 | 25 | # pytest.main(["-x", __file__]) 26 | -------------------------------------------------------------------------------- /src/tests/javlibrary.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | import sys 4 | from src.providers import Javlibrary 5 | 6 | # Define the codes to test 7 | codes = ["ebod-391", "ssis-100", "doa-017", "ssis-001"] 8 | provider = Javlibrary() 9 | 10 | 11 | @pytest.mark.parametrize("code", codes) 12 | def test_javlibrary(code: str): 13 | # Load the expected result from the json file 14 | with open(f"src/tests/data/r18.{code}.json", "r") as f: 15 | expected_result: str = f.read().strip() 16 | 17 | # Run the search function and get the result 18 | result = json.dumps(provider.search(code.upper()), indent=2, ensure_ascii=False) 19 | 20 | # Compare the result with the expected result 21 | assert ( 22 | result == expected_result 23 | ), f"For code: {code}, expected: {expected_result}, but got: {result}" 24 | 25 | 26 | # pytest.main(["-x", __file__]) 27 | -------------------------------------------------------------------------------- /src/tests/r18.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | from src.providers import R18 4 | 5 | # Define the codes to test 6 | codes = ["ebod-391", "mkck-275", "doa-017", "ebod-875"] 7 | provider = R18() 8 | 9 | 10 | @pytest.mark.parametrize("code", codes) 11 | def test_r18(code: str): 12 | # Load the expected result from the json file 13 | with open(f"src/tests/data/r18.{code}.json", "r") as f: 14 | expected_result: str = f.read().strip() 15 | 16 | # Run the search function and get the result 17 | result = json.dumps(provider.search(code.upper()), indent=2, ensure_ascii=False) 18 | 19 | # Compare the result with the expected result 20 | assert ( 21 | result == expected_result 22 | ), f"For code: {code}, expected: {expected_result}, but got: {result}" 23 | 24 | 25 | # pytest.main(["-x", __file__]) 26 | --------------------------------------------------------------------------------