commit 01b3246533329ae5f661e3116fc1cb8110a39b28 Author: WorldTeacher Date: Sun May 4 12:29:59 2025 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed468fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,234 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# ---> Qt +# C++ objects and libs +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.so.* +*.dll +*.dylib + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +*.qmlc +*.jsc +Makefile* +*build-* +*.qm +*.prl + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database +compile_commands.json + +# QtCreator local machine specific files for imported projects +*creator.user* + +*_qmlcache.qrc + + +.history +depend +output/output/LOGtoJSON.exe + +.pytest_cache +output +docs/ +config.yaml +**/tempCodeRunnerFile.py + +uv.lock +.history +.venv +venv +*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aec92ac --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 WorldTeacher + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..841917d --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +from anilistapi import AnilistAPI + +api = AnilistAPI() + +print(api.get_tags()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..47a5c29 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "kompage" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "flask>=3.1.0", + "httpx>=0.28.1", + "jinja2>=3.1.6", + "quart>=0.20.0", +] + +[tool.uv.sources] +anilistapi = { workspace = true } +komconfig = { workspace = true } +komcache = { workspace = true } diff --git a/requests.db b/requests.db new file mode 100644 index 0000000..afe76b7 Binary files /dev/null and b/requests.db differ diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..508d206 --- /dev/null +++ b/src/app.py @@ -0,0 +1,120 @@ +from quart import Quart, render_template, request, jsonify +import httpx +from anilistapi.queries.manga import REQUESTS_QUERY, TAGS_QUERY, GENRES_QUERY +from anilistapi.schemas.manga import Manga +from komcache import KomCache + +app = Quart(__name__) + +cache = KomCache() +cache.create_table( + "CREATE TABLE IF NOT EXISTS manga_requests (id INTEGER PRIMARY KEY, manga_id INTEGER, manga_title TEXT)" +) + + +async def fetch_data(query): + # Simulated API response + async with httpx.AsyncClient() as client: + try: + response = await client.post( + f"https://graphql.anilist.co", + json={"query": REQUESTS_QUERY, "variables": {"search": query}}, + ) + response.raise_for_status() + data = response.json() + + results = [] + for item in data.get("data", {}).get("Page", {}).get("media", []): + manga = Manga(**item) + + results.append( + { + "id": manga.id, + "title": manga.title.romaji if manga.title else "Untitled", + "image": manga.coverImage.get("large") + if manga.coverImage + else "https://demofree.sirv.com/nope-not-here.jpg", + "status": manga.status, + "type": manga.type, + "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", + "isAdult": manga.isAdult, + } + ) + + return results + except Exception as e: + print(f"Error fetching data: {e}") + return [] + + +@app.route("/api/genres") +async def get_genres(): + async with httpx.AsyncClient() as client: + try: + response = await client.post( + f"https://graphql.anilist.co", + json={"query": GENRES_QUERY}, + ) + response.raise_for_status() + data = response.json() + + results = data.get("data", {}).get("genres", []) + return jsonify(results) + except Exception as e: + print(f"Error fetching genres: {e}") + return jsonify([]) + + +@app.route("/api/tags") +async def get_tags(): + async with httpx.AsyncClient() as client: + try: + response = await client.post( + f"https://graphql.anilist.co", + json={"query": TAGS_QUERY}, + ) + response.raise_for_status() + data = response.json() + results = data.get("data", {}).get("tags", []) + return jsonify(results) + except Exception as e: + print(f"Error fetching genres: {e}") + return jsonify([]) + + +@app.route("/", methods=["GET", "POST"]) +async def index(): + query = None + results = [] + + if request.method == "POST": + form = await request.form + query = form.get("query") + if query: + results = await fetch_data(query) + + return await render_template("index.html", results=results) + + +@app.route("/request", methods=["POST"]) +async def log_request(): + data = await request.get_json() + print(data) + item = data.get("item") + if item: + asynccache = KomCache() + manga_title = data.get("title") + asynccache.insert( + "INSERT INTO manga_requests (manga_id, manga_title) VALUES (?, ?)", + (item, manga_title), + ) + return jsonify({"status": "success"}) + return jsonify({"status": "failed"}), 400 + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=5000) diff --git a/src/query.json b/src/query.json new file mode 100644 index 0000000..0fe6970 --- /dev/null +++ b/src/query.json @@ -0,0 +1,112 @@ +{ + "query": "query( + $page:Int = 1 + $id:Int + $type:MediaType + $isAdult:Boolean = false + $search:String + $format: [MediaFormat + ] + $status:MediaStatus + $countryOfOrigin:CountryCode + $source:MediaSource + $season:MediaSeason + $seasonYear:Int + $year:String + $onList:Boolean + $yearLesser:FuzzyDateInt + $yearGreater:FuzzyDateInt + $episodeLesser:Int + $episodeGreater:Int + $durationLesser:Int + $durationGreater:Int + $chapterLesser:Int + $chapterGreater:Int + $volumeLesser:Int + $volumeGreater:Int + $licensedBy: [Int + ] + $isLicensed:Boolean + $genres: [String + ] + $excludedGenres: [String + ] + $tags: [String + ] + $excludedTags: [String + ] + $minimumTagRank:Int + $sort: [MediaSort + ]=[POPULARITY_DESC,SCORE_DESC + ]) + { + Page(page:$page,perPage: 20) + { + pageInfo{ + total + perPage + currentPage + lastPage + hasNextPage + } + media( + id:$id + type:$type + season:$season + format_in:$format + status:$status + countryOfOrigin:$countryOfOrigin + source:$source + search:$search + onList:$onList + seasonYear:$seasonYear + startDate_like:$year + startDate_lesser:$yearLesser + startDate_greater:$yearGreater + episodes_lesser:$episodeLesser + episodes_greater:$episodeGreater + duration_lesser:$durationLesser + duration_greater:$durationGreater + chapters_lesser:$chapterLesser + chapters_greater:$chapterGreater + volumes_lesser:$volumeLesser + volumes_greater:$volumeGreater + licensedById_in:$licensedBy + isLicensed:$isLicensed + genre_in:$genres + genre_not_in:$excludedGenres + tag_in:$tags + tag_not_in:$excludedTags + minimumTagRank:$minimumTagRank + sort:$sort + isAdult:$isAdult + ) + { + id title{ + userPreferred + } + coverImage{extraLarge large color + } + startDate{year month day + } + endDate{year month day + } + bannerImage season seasonYear description type format status(version: 2)episodes duration chapters volumes genres isAdult averageScore popularity nextAiringEpisode{airingAt timeUntilAiring episode + } + mediaListEntry{id status + } + studios(isMain:true){edges{isMain node{id name + } + } + } + } + } + }","variables": {"page": 1,"type": "MANGA", + "genres": [ + "Action", + "Romance" + ], + "sort": "SEARCH_MATCH", + "search": "Assassin" +} +} \ No newline at end of file diff --git a/src/static/style.css b/src/static/style.css new file mode 100644 index 0000000..4edc623 --- /dev/null +++ b/src/static/style.css @@ -0,0 +1,122 @@ +/* ========== GRID LAYOUT ========== */ +.results { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 20px; + margin-top: 20px; +} + +/* Responsive: show 1 per row on small screens */ +@media screen and (max-width: 768px) { + .results { + grid-template-columns: 1fr; + } +} + +/* ========== CARD STYLING ========== */ +.card { + position: relative; + padding: 10px; + border: 1px solid #ccc; + background: #fafafa; + text-align: center; + transition: box-shadow 0.2s; +} + +.card:hover { + box-shadow: 0 0 10px #aaa; +} + +.card img { + width: 100%; + height: auto; +} + +/* ========== ACTION BUTTONS ========== */ +.actions { + margin-top: 10px; + display: flex; + justify-content: space-around; +} + +/* ========== MODAL ========== */ +.modal { + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + background-color: #fefefe; + margin: 5% auto; + padding: 20px; + border: 1px solid #888; + width: 50%; + border-radius: 10px; +} + +.close { + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +/* ========== IMAGE CONTAINER ========== */ +.image-container { + position: relative; + overflow: hidden; +} + +.image-container img { + width: 100%; + height: auto; + display: block; + transition: filter 0.3s ease; +} + + + +/* ========== BLUR CONTROL VIA BODY CLASS ========== */ +body.nsfw-disabled .image-container.nsfw img { + filter: blur(8px); +} + +body.nsfw-disabled .image-container.nsfw:hover img { + filter: none; + /* Remove blur on hover, set as important */ +} + + +/* ========== BADGE ========== */ +.adult-badge { + position: absolute; + top: 8px; + left: 8px; + background-color: rgba(255, 0, 0, 0.8); + color: white; + padding: 4px 8px; + font-size: 12px; + font-weight: bold; + border-radius: 4px; + z-index: 1; + pointer-events: none; +} + +.search-bar { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 20px; +} + +.search-bar select { + padding: 5px; + border: 1px solid #ccc; + border-radius: 4px; +} \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html new file mode 100644 index 0000000..959c1d7 --- /dev/null +++ b/src/templates/index.html @@ -0,0 +1,174 @@ + + + + + Anime Search and Request Page + + + + +

Search Manga

+ + + + + + + {% if results %} +
+ +
+ +
+ {% for result in results %} +
+
+ + Cover + {% if result.isAdult %} +
18+
+ {% endif %} +
+ + +

{{ result.title }}

+
+ + + +
+
+ {% endfor %} +
+ {% endif %} + + + + + + + + + + \ No newline at end of file