diff --git a/.bumpversion.toml b/.bumpversion.toml new file mode 100644 index 0000000..894b569 --- /dev/null +++ b/.bumpversion.toml @@ -0,0 +1,21 @@ +[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 = false +sign_tags = false +tag_name = "v{new_version}" +tag_message = "Bump version: {current_version} → {new_version}" +allow_dirty = false +commit = false +message = "Bump version: {current_version} → {new_version}" +moveable_tags = [] +commit_args = "" +setup_hooks = [] +pre_commit_hooks = [] +post_commit_hooks = [] diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..d0d5e93 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,120 @@ +on: + workflow_dispatch: + inputs: + release_notes: + description: Release notes (use \n for newlines) + type: string + required: false + github_release: + description: 'Create Gitea Release' + default: true + type: boolean + docker_release: + description: 'Push Docker images' + default: true + type: boolean + bump: + description: 'Bump type' + required: true + default: 'patch' + type: choice + options: + - 'major' + - 'minor' + - 'patch' + + + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ secrets.REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.TOKEN }} + + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Set up Python + run: uv python install + with: + python-version-file: "pyproject.toml" + + - name: Install the project + run: uv sync --locked --all-extras --dev + - name: Set Git identity + run: | + git config user.name "Gitea CI" + git config user.email "ci@git.theprivateserver.de" + - name: Bump version + id: bump + run: | + uv tool install bump-my-version + uv tool run bump-my-version bump ${{ github.event.inputs.bump }} + # echo the version to github env, the version is shown by using uv tool run bump-my-version show current_version + echo "VERSION<> $GITHUB_ENV + echo "$(uv tool run bump-my-version show current_version)" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + - name: Check version + run: echo ${{ env.VERSION }} + - name: Push changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ github.ref }} + + - name: Add release notes to environment + id: add_release_notes + run: | + echo "RELEASE_NOTES<> $GITHUB_ENV + echo "${{ github.event.inputs.release_notes }}" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Build and store Docker image + if: ${{ github.event.inputs.docker_release == 'true' }} + 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 . + - name: Generate changelog + id: changelog + uses: metcalfc/changelog-generator@v4.6.2 + with: + token: ${{ secrets.TOKEN }} + - name: Get the changelog + run: | + cat << "EOF" + ${{ steps.changelog.outputs.changelog }} + EOF + + - name: Create release + id: create_release + if: ${{ github.event.inputs.github_release == 'true' }} + uses: softprops/action-gh-release@master + with: + tag_name: ${{ env.VERSION }} + release_name: Release ${{ env.VERSION }} + body: ${{ steps.changelog.outputs.changelog }} + draft: false + prerelease: false + make_latest: true + env: + GITHUB_TOKEN: ${{ secrets.TOKEN }} + diff --git a/.gitignore b/.gitignore index ed468fc..7037468 100644 --- a/.gitignore +++ b/.gitignore @@ -227,7 +227,6 @@ docs/ config.yaml **/tempCodeRunnerFile.py -uv.lock .history .venv venv diff --git a/pyproject.toml b/pyproject.toml index 85fada5..1b6eb5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,16 +5,22 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ - "flask>=3.1.0", + "anilistapi", + "flask>=3.1.1", "httpx>=0.28.1", + "httpx-retries>=0.3.2", "jinja2>=3.1.6", + "komcache", + "komconfig", "komgapi", + "komsuite-nyaapy", "limit>=0.2.3", "quart>=0.20.0", ] [tool.uv.sources] -anilistapi = { workspace = true } -komconfig = { workspace = true } -komcache = { workspace = true } +komsuite-nyaapy = { workspace = true } komgapi = { workspace = true } +komcache = { workspace = true } +komconfig = { workspace = true } +anilistapi = { workspace = true } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fed2509 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,88 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml -o requirements.txt +-e file:///home/alexander/GitHub/KomSuite/packages/KomConfig + # via komgapi +-e file:///home/alexander/GitHub/KomSuite/packages/komgAPI + # via kompage (pyproject.toml) +aiofiles==24.1.0 + # via quart +antlr4-python3-runtime==4.9.3 + # via omegaconf +anyio==4.9.0 + # via httpx +blinker==1.9.0 + # via + # flask + # quart +certifi==2025.4.26 + # via + # httpcore + # httpx +click==8.2.0 + # via + # flask + # quart +flask==3.1.1 + # via + # kompage (pyproject.toml) + # quart +h11==0.16.0 + # via + # httpcore + # hypercorn + # wsproto +h2==4.2.0 + # via hypercorn +hpack==4.1.0 + # via h2 +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via + # kompage (pyproject.toml) + # komgapi +hypercorn==0.17.3 + # via quart +hyperframe==6.1.0 + # via h2 +idna==3.10 + # via + # anyio + # httpx +itsdangerous==2.2.0 + # via + # flask + # quart +jinja2==3.1.6 + # via + # kompage (pyproject.toml) + # flask + # quart +limit==0.2.3 + # via kompage (pyproject.toml) +loguru==0.7.3 + # via komgapi +markupsafe==3.0.2 + # via + # flask + # jinja2 + # quart + # werkzeug +omegaconf==2.3.0 + # via komconfig +priority==2.0.0 + # via hypercorn +pyyaml==6.0.2 + # via omegaconf +quart==0.20.0 + # via kompage (pyproject.toml) +sniffio==1.3.1 + # via anyio +typing-extensions==4.13.2 + # via komgapi +werkzeug==3.1.3 + # via + # flask + # quart +wsproto==1.2.0 + # via hypercorn diff --git a/src/app.py b/src/app.py index 41d80d9..766d15f 100644 --- a/src/app.py +++ b/src/app.py @@ -12,7 +12,14 @@ from komcache import KomCache from komgapi import komgapi as KOMGAPI from typing import Any, Dict, List from limit import limit +import loguru +import sys +from komsuite_nyaapy import Nyaa +log = loguru.logger +log.remove() +log.add("application.log", rotation="1 week", retention="1 month") +log.add(sys.stdout) app = Quart(__name__) cache = KomCache() @@ -32,19 +39,28 @@ komga_series = komga.seriesList() @limit(90, 60) async def fetch_data( - data: Dict[str, Any], query: str = REQUESTS_QUERY + inputdata: Dict[str, Any], check_downloads: bool = False ) -> List[Dict[str, Any]]: + log.error(f"fetch_data called with data: {inputdata}") async with httpx.AsyncClient() as client: try: - variables: Dict[str, Any] = {"search": data["query"]} - if data.get("genres"): - variables["genres"] = data["genres"] - if data.get("tags"): - variables["tags"] = data["tags"] - if data.get("type"): - if data["type"] in ["MANGA", "NOVEL"]: - variables["format"] = data["type"] - print(data["query"], variables) + # Log the incoming data for debugging + + # Ensure 'query' key exists in the data dictionary + if "query" not in inputdata or not inputdata["query"]: + raise ValueError("Missing or empty 'query' key in data") + + variables: Dict[str, Any] = {"search": inputdata["query"]} + if inputdata.get("genres"): + variables["genres"] = inputdata["genres"] + if inputdata.get("tags"): + variables["tags"] = inputdata["tags"] + if inputdata.get("type"): + if inputdata["type"] in ["MANGA", "NOVEL"]: + variables["format"] = inputdata["type"] + + log.debug(f"GraphQL variables: {variables}") + response = await client.post( "https://graphql.anilist.co", json={ @@ -55,6 +71,7 @@ async def fetch_data( response.raise_for_status() data = response.json() + log.debug(f"GraphQL response: {data}") results: List[Dict[str, Any]] = [] for item in data.get("data", {}).get("Page", {}).get("media", []): @@ -67,7 +84,6 @@ async def fetch_data( args=(manga.id,), ) komga_request = bool(requested) - results.append( { "id": manga.id, @@ -87,18 +103,30 @@ async def fetch_data( "isAdult": manga.isAdult, "in_komga": in_komga, "requested": komga_request, + "siteUrl": manga.siteUrl, } ) + if check_downloads: + for result in results: + downloads = Nyaa().search(result.get("title"), 3, 1) + else: + downloads = [] + result["download"] = len(downloads) if downloads else 0 + log.debug(f"Fetched {len(results)} results for query: {inputdata['query']}") + log.error(f"Results: {results}") return results + except ValueError as ve: + log.error(f"ValueError in fetch_data: {ve}") + return [] except httpx.RequestError as e: - print(f"An error occurred while requesting data: {e}") + log.error(f"HTTP request error in fetch_data: {e}") return [] except Exception as e: - print(f"Unexpected error: {e}") + log.error(f"Unexpected error in fetch_data: {e}") return [] - +@limit(90, 60) async def fetch_requested_data(data: list[int]) -> List[Dict[str, Any]]: requested_data: list[dict[str, Any]] = [] for manga_id in data: @@ -130,7 +158,7 @@ async def fetch_requested_data(data: list[int]) -> List[Dict[str, Any]]: else manga.title.romaji ) requested = cache.fetch_one( - query="SELECT manga_id, grabbed FROM manga_requests WHERE manga_id = ?", + query="SELECT grabbed FROM manga_requests WHERE manga_id = ?", args=(manga.id,), ) komga_request = bool(requested) @@ -143,19 +171,22 @@ async def fetch_requested_data(data: list[int]) -> List[Dict[str, Any]]: else manga.title.romaji, "image": manga.coverImage.get("large") if manga.coverImage - else "https://demofree.sirv.com/nope-not-here.jpg", + else None, "status": manga.status, "type": manga.format, "genres": manga.genres or [], "tags": [tag.name for tag in (manga.tags or [])], "description": manga.description.replace("
", "\n") if manga.description - else "No description available", + else None, "isAdult": manga.isAdult, - "in_komga": in_komga, - "requested": komga_request, + "in_komga": bool(in_komga), + "requested": bool(komga_request), } ) + log.debug( + requested_data, + ) except httpx.RequestError as e: print(f"An error occurred while requesting data: {e}") @@ -211,6 +242,43 @@ async def search(): return jsonify(results) +@app.route("/api/search", methods=["GET"]) +async def api_search(): + """ + API endpoint for searching manga. + + Parameters: + - q: Query string to search for + + Returns: + - JSON array of matching manga entries + """ + try: + query = request.args.get("q") + + if not query: + return jsonify({"error": "Missing required parameter: q"}), 400 + + # Build search parameters + search_data: Dict[str, Any] = {"query": query} + + # Add optional filters if provided + + log.info(f"API search request: {search_data}") + + # Fetch results using existing fetch_data function + results = await fetch_data(search_data, check_downloads=False) + + if not results: + return jsonify({"results": [], "count": 0}), 404 + + return jsonify({"results": results, "count": len(results)}) + + except Exception as e: + log.error(f"Error in API search: {e}") + return jsonify({"error": "An unexpected error occurred"}), 500 + + @app.route("/", methods=["GET"]) async def index(): return await render_template("index.html", komga_series=komga_series) @@ -241,9 +309,11 @@ async def requests_page(): entries = await fetch_requested_data(req_ids) else: entries = [] - print(entries) + data = [] + for entry in entries: + data.append(dict(entry)) - return await render_template("requests.html", requests=entries) + return await render_template("requests.html", requests=data) if __name__ == "__main__": diff --git a/src/static/style.css b/src/static/style.css index cc986aa..514b1e3 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -194,14 +194,7 @@ body.nsfw-disabled .image-container.nsfw:hover img { flex-wrap: wrap; } -.info { - /* Implement design here */ - background-color: #4CAF50; -} -.request { - /* Implement design here */ -} /* ========== KOMGA SPECIFIC STYLES ========== */ diff --git a/src/templates/index.html b/src/templates/index.html index 7387e41..dd30f1c 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -46,6 +46,8 @@ Unblur NSFW images + +