diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 0a9a187..9b7af1e 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -32,6 +32,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -53,7 +56,7 @@ jobs: python-version-file: "pyproject.toml" - name: Install the project - run: uv sync --locked --all-extras --dev + run: uv sync --all-extras --dev - name: Create requirements.txt run: | @@ -66,25 +69,15 @@ jobs: git config user.name "Gitea CI" git config user.email "ci@git.theprivateserver.de" - - name: Create release notes - run: | - mkdir release_notes - echo -e "${{ inputs.release_notes }}" >> release_notes/release_notes.md - echo "Release notes:" - cat release_notes/release_notes.md - echo "" - - - name: Build and store Docker image - if: ${{ github.event.inputs.docker_release == 'true' }} + - name: Build Changelog + id: build_changelog + uses: https://github.com/mikepenz/release-changelog-builder-action@v5 + with: + platform: "gitea" + baseURL: "http://gitea:3000" + configuration: ".gitea/changelog-config.json" env: - TAG: ${{ github.sha }} - run: | - REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') - docker buildx build \ - --platform linux/amd64 \ - --tag ${{ secrets.REGISTRY }}/${REPO_NAME}:latest \ - --tag ${{ secrets.REGISTRY }}/${REPO_NAME}:${TAG} \ - --push . + GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }} - name: Bump version @@ -98,6 +91,15 @@ jobs: echo "EOF" >> $GITHUB_ENV - name: Check version run: echo ${{ env.VERSION }} + - name: Build and store Docker image + if: ${{ github.event.inputs.docker_release == 'true' }} + run: | + REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + docker buildx build \ + --platform linux/amd64 \ + --tag ${{ secrets.REGISTRY }}/${REPO_NAME}:latest \ + --tag ${{ secrets.REGISTRY }}/${REPO_NAME}:${{ env.VERSION }} \ + --push . - name: Push changes uses: ad-m/github-push-action@master with: @@ -108,7 +110,7 @@ jobs: if: ${{ github.event.inputs.github_release == 'true' }} uses: softprops/action-gh-release@master with: - tag_name: ${{ env.VERSION }} + tag_name: v${{ env.VERSION }} release_name: Release ${{ env.VERSION }} body_path: release_notes/release_notes.md draft: false diff --git a/Dockerfile b/Dockerfile index 005db73..1d5544a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use the official Python image as the base image -FROM python:3.11-slim +FROM python:3.13-slim # Set the working directory in the container WORKDIR /app @@ -7,11 +7,14 @@ WORKDIR /app # Copy the application files into the container COPY . /app -# Install dependencies -RUN pip install --no-cache-dir -r requirements.txt +# Install dependencies with an external pip index +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt \ + --extra-index-url https://git.theprivateserver.de/api/packages/KomSuite/pypi/simple/ # Expose the port the app runs on EXPOSE 5001 -# Set the default command to run the application -CMD ["python", "src/app.py"] +# Set the default command to run the application using hypercorn +# run main.py using uv run python +CMD ["python", "main.py"] diff --git a/main.py b/main.py index 841917d..61a0c7e 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ -from anilistapi import AnilistAPI +from src.app import app -api = AnilistAPI() +if __name__ == "__main__": + import uvicorn -print(api.get_tags()) + uvicorn.run(app, host="0.0.0.0", port=5001) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 02eba76..5247f22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "flask>=3.1.1", "httpx>=0.28.1", "httpx-retries>=0.3.2", + "hypercorn>=0.17.3", "jinja2>=3.1.6", "komcache>=0.1.0", "komconfig>=0.2.0", @@ -16,6 +17,7 @@ dependencies = [ "komsuite-nyaapy>=0.1.0", "limit>=0.2.3", "quart>=0.20.0", + "uvicorn>=0.34.2", ] @@ -23,3 +25,26 @@ dependencies = [ [[tool.uv.index]] name = "gitea" url = "https://git.theprivateserver.de/api/packages/KomSuite/pypi/simple/" + + +[tool.bumpversion] +current_version = "0.1.0" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +serialize = ["{major}.{minor}.{patch}"] +search = "{current_version}" +replace = "{new_version}" +regex = false +ignore_missing_version = false +ignore_missing_files = false +tag = true +sign_tags = false +tag_name = "v{new_version}" +tag_message = "Bump version: {current_version} → {new_version}" +allow_dirty = true +commit = true +message = "Bump version: {current_version} → {new_version}" +moveable_tags = [] +commit_args = "" +setup_hooks = [] +pre_commit_hooks = [] +post_commit_hooks = [] diff --git a/requirements.txt b/requirements.txt index 81cbe4e..4c81c65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -54,6 +54,7 @@ click==8.2.1 \ # via # flask # quart + # uvicorn colorama==0.4.6 ; sys_platform == 'win32' \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 @@ -66,12 +67,33 @@ flask==3.1.1 \ # via # kompage # quart +greenlet==3.2.2 \ + --hash=sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207 \ + --hash=sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4 \ + --hash=sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825 \ + --hash=sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708 \ + --hash=sha256:2593283bf81ca37d27d110956b79e8723f9aa50c4bcdc29d3c0543d4743d2763 \ + --hash=sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf \ + --hash=sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d \ + --hash=sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59 \ + --hash=sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51 \ + --hash=sha256:6fadd183186db360b61cb34e81117a096bff91c072929cd1b529eb20dd46e6c5 \ + --hash=sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf \ + --hash=sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b \ + --hash=sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240 \ + --hash=sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485 \ + --hash=sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8 \ + --hash=sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418 \ + --hash=sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325 \ + --hash=sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421 + # via sqlalchemy h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via # httpcore # hypercorn + # uvicorn # wsproto h2==4.2.0 \ --hash=sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0 \ @@ -99,7 +121,9 @@ httpx-retries==0.4.0 \ hypercorn==0.17.3 \ --hash=sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547 \ --hash=sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165 - # via quart + # via + # kompage + # quart hyperframe==6.1.0 \ --hash=sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5 \ --hash=sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08 @@ -124,13 +148,13 @@ jinja2==3.1.6 \ # flask # kompage # quart -komcache==0.1.0 \ - --hash=sha256:67df7d393d27385d605cb4c92af8a20331a0582d988ac1fcaccd2c6f819b4ff6 \ - --hash=sha256:cfb87e7800e9e9ace4454446e001298a4763a2e39f271d16343f6ba6d8596676 +komcache==0.1.2 \ + --hash=sha256:b474c0f00e72b9e6d9f1fd765b69fbd4bdf0fd37e094692d69a3565b49c7c017 \ + --hash=sha256:d56d01a81751113433325a9e25afbb985eff2530fc9b0b7cc3dac960308b4c35 # via kompage -komconfig==0.2.0 \ - --hash=sha256:31b1e06e821cfaba8bbc8c077dbb596989a277c33c99096870df4cba39bc0f15 \ - --hash=sha256:913bedd6eb38dad81460afed96352f3af7518e9ea76d575b6cf2d190adb4a162 +komconfig==0.2.2 \ + --hash=sha256:6bfda853ac3ee513380db2df896830099eba8a6d6e0ed1484049839e9dcb5845 \ + --hash=sha256:a2b15158aedb2fc8f55c9d8fc15d6bda9791f2b9690e2e066d3010b1f681061e # via # komcache # komgapi @@ -207,6 +231,10 @@ priority==2.0.0 \ --hash=sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa \ --hash=sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0 # via hypercorn +pymysql==1.1.1 \ + --hash=sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c \ + --hash=sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0 + # via komcache pyyaml==6.0.2 \ --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ @@ -249,14 +277,32 @@ sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc # via anyio -typing-extensions==4.13.2 \ - --hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \ - --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef - # via komgapi +sqlalchemy==2.0.41 \ + --hash=sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443 \ + --hash=sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23 \ + --hash=sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576 \ + --hash=sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df \ + --hash=sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1 \ + --hash=sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f \ + --hash=sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d \ + --hash=sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc \ + --hash=sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a \ + --hash=sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9 + # via komcache +typing-extensions==4.14.0 \ + --hash=sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4 \ + --hash=sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af + # via + # komgapi + # sqlalchemy urllib3==2.4.0 \ --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \ --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813 # via requests +uvicorn==0.34.3 \ + --hash=sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885 \ + --hash=sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a + # via kompage werkzeug==3.1.3 \ --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 diff --git a/src/app.py b/src/app.py index 766d15f..e101942 100644 --- a/src/app.py +++ b/src/app.py @@ -22,22 +22,23 @@ log.add("application.log", rotation="1 week", retention="1 month") log.add(sys.stdout) app = Quart(__name__) -cache = KomCache() -cache.create_table( - "CREATE TABLE IF NOT EXISTS manga_requests (id INTEGER PRIMARY KEY, manga_id INTEGER, grabbed BOOLEAN DEFAULT 0)" -) settings = KomConfig() + +# else: +# cache.create_table( +# "CREATE TABLE IF NOT EXISTS manga_requests (id INT AUTO_INCREMENT PRIMARY KEY, anilist_id INT NOT NULL, grabbed BOOLEAN DEFAULT 0)" + komga = KOMGAPI( url=settings.komga.url, username=settings.komga.user, password=settings.komga.password, ) -komga_series = komga.seriesList() +# komga_series = komga.seriesList() -@limit(90, 60) +@limit(limit=1, every=2) async def fetch_data( inputdata: Dict[str, Any], check_downloads: bool = False ) -> List[Dict[str, Any]]: @@ -67,6 +68,11 @@ async def fetch_data( "query": REQUESTS_QUERY, "variables": variables, }, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": "KomPage Searcher API/1.0 (contact: discord-id: 908987973264089139)", + }, ) response.raise_for_status() @@ -74,14 +80,16 @@ async def fetch_data( log.debug(f"GraphQL response: {data}") results: List[Dict[str, Any]] = [] + cache = KomCache() for item in data.get("data", {}).get("Page", {}).get("media", []): manga = Manga(**item) in_komga = komga.getSeries( manga.title.english if manga.title.english else manga.title.romaji ) + requested = cache.fetch_one( - query="SELECT manga_id, grabbed FROM manga_requests WHERE manga_id = ?", - args=(manga.id,), + query="SELECT manga_id, grabbed FROM manga_requests WHERE manga_id = :id", + args={"id": manga.id}, ) komga_request = bool(requested) results.append( @@ -126,9 +134,12 @@ async def fetch_data( log.error(f"Unexpected error in fetch_data: {e}") return [] -@limit(90, 60) + +@limit(limit=1, every=2) async def fetch_requested_data(data: list[int]) -> List[Dict[str, Any]]: requested_data: list[dict[str, Any]] = [] + + cache = KomCache() for manga_id in data: async with httpx.AsyncClient() as client: try: @@ -158,8 +169,8 @@ async def fetch_requested_data(data: list[int]) -> List[Dict[str, Any]]: else manga.title.romaji ) requested = cache.fetch_one( - query="SELECT grabbed FROM manga_requests WHERE manga_id = ?", - args=(manga.id,), + query="SELECT grabbed FROM manga_requests WHERE manga_id = :id", + args={"id": manga.id}, ) komga_request = bool(requested) @@ -281,18 +292,28 @@ async def api_search(): @app.route("/", methods=["GET"]) async def index(): - return await render_template("index.html", komga_series=komga_series) + return await render_template("index.html") # , komga_series=komga_series) @app.route("/request", methods=["POST"]) async def log_request(): data = await request.get_json() + log.debug(f"Received request data: {data}") item = data.get("item") if item: + data = await fetch_requested_data([item]) + if not data: + return jsonify({"status": "failed", "message": "Item not found"}), 404 + data = data[0] + title = data.get("title") + image_url = data.get("image") + log.debug( + f"Logging request for item: {item}, title: {title}, image_url: {image_url}" + ) asynccache = KomCache() asynccache.insert( - "INSERT INTO manga_requests (manga_id) VALUES (?)", - (item,), + f"INSERT INTO manga_requests (manga_id, title, image) VALUES ({item}, '{title}', :image)", + args={"image": image_url}, ) return jsonify({"status": "success"}) return jsonify({"status": "failed"}), 400 @@ -300,21 +321,42 @@ async def log_request(): @app.route("/requests", methods=["GET"]) async def requests_page(): + cache = KomCache() requests = ( - cache.fetch_all(query="SELECT manga_id, grabbed FROM manga_requests") or [] + cache.fetch_all( + query="SELECT manga_id, title, image grabbed FROM manga_requests" + ) + or [] ) entries: List[Dict[str, Any]] = [] req_ids = [req[0] for req in requests] if req_ids: - entries = await fetch_requested_data(req_ids) + entries = [ + {"manga_id": req[0], "title": req[1], "image": req[2]} for req in requests + ] else: entries = [] - data = [] - for entry in entries: - data.append(dict(entry)) - return await render_template("requests.html", requests=data) + return await render_template("requests.html", requests=entries) +@app.route("/delete", methods=["POST"]) +async def delete_request(): + # Delete a request from the database. ID is sent after the /delete endpoint, so: /delete/ + data = await request.get_json() + item_id = data.get("item") + + if item_id: + asynccache = KomCache() + asynccache.query( + "DELETE FROM manga_requests WHERE manga_id = :id", args={"id": item_id} + ) + return jsonify({"status": "success"}) + return jsonify({"status": "failed"}), 400 if __name__ == "__main__": - app.run(debug=True, host="0.0.0.0", port=5001) + log.info("Starting Komga Manga Grabber API") + # use hypercorn to run the app + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=5001) + # run in test mode diff --git a/src/static/style.css b/src/static/style.css index 514b1e3..a2282ef 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -55,6 +55,7 @@ margin-right: 0.5em; } + .actions .info:hover { background-color: #0056b3; } @@ -230,4 +231,21 @@ body.nsfw-disabled .image-container.nsfw:hover img { transform: none !important; box-shadow: none !important; z-index: auto !important; +} +.card .delete-button { + position: absolute; + top: 5px; + right: 5px; + background-color: #dc3545; + color: white; + border: none; + padding: 0.5em; + cursor: pointer; + border-radius: 50%; + z-index: 3; + opacity: 1; +} + +.card:hover .delete-button { + display: block; } \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html index dd30f1c..f03a0fd 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -77,7 +77,8 @@ {% if not result.in_komga %} - + + {% endif %} @@ -113,7 +114,9 @@ fetch("/request", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ item: item }) + body: JSON.stringify({ + item: item + }) }) .then(res => res.json()) .then(data => alert(data.status === "success" ? "Request logged!" : "Failed")); @@ -304,7 +307,7 @@ const card = document.createElement('div'); card.className = `card ${result.in_komga ? 'komga' : ''}`; card.className += ` ${result.type.toLowerCase()}`; - // card.className += ` ${result.requested ? 'requested' : ''}`; + const imageContainer = document.createElement('div'); imageContainer.className = `image-container ${result.isAdult ? 'nsfw' : ''}`; @@ -335,14 +338,16 @@ const requestButton = document.createElement('button'); requestButton.textContent = 'Request'; requestButton.className = 'request'; - requestButton.onclick = () => sendRequest(result.id); + requestButton.onclick = () => sendRequest(result); // Disable request button if the result is in Komga if (result.in_komga) { requestButton.disabled = true; } if (result.requested) { + requestButton.textContent = 'Requested'; requestButton.disabled = true; + card.style.border = '3px solid orange'; } actions.appendChild(infoButton); diff --git a/src/templates/requests.html b/src/templates/requests.html index e4d8d66..ed53646 100644 --- a/src/templates/requests.html +++ b/src/templates/requests.html @@ -4,6 +4,7 @@ Requested Manga + @@ -14,12 +15,38 @@ +
- {% if requests %} + {% if results %} + {% for result in results %} + +
+ +
+ + Cover + {% if result.isAdult %} +
18+
+ {% endif %} +
+ +

{{ result.title }}

+
+ + + +
+
+ {% endfor %} + {% endif %} +
+ + {% if requests %} +
{% for request in requests %} -
+
+ Cover {% if request.isAdult %}
18+
@@ -28,17 +55,19 @@

{{ request.title }}

- + +
{% endfor %} - {% endif %}
- {% if not requests %} -

No requests found.

+ {% else %} +

No requests found. This may be because the database disconnected. Please refresh using the button, if the message + persists, there are no requests

+ {% endif %} - +