From 8ddea25e5ba0e62426c26a561d70c3a7d03f49d8 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Fri, 23 May 2025 16:42:27 +0200 Subject: [PATCH] Add issue templates and CI workflows; remove obsolete files - Created bug report and feature request templates for better issue tracking. - Added a build workflow for automated package management and release. - Removed outdated CodeQL analysis and Python publish workflows. - Updated project metadata in pyproject.toml and README.md. - Refactored torrent handling and site interaction modules. --- .gitea/ISSUE_TEMPLATE/bug.yml | 34 ++++ .gitea/ISSUE_TEMPLATE/feature.yml | 23 +++ .gitea/workflows/build.yml | 88 ++++++++++ .github/workflows/codeql-analysis.yml | 70 -------- .github/workflows/python-publish.yml | 39 ----- HOW-TO-CONTRIBUTE.md | 45 ----- README.md | 5 + nyaapy/torrent.py | 31 ---- pyproject.toml | 31 ++-- requirements.txt | 3 - setup.cfg | 2 - setup.py | 25 --- src/komsuite_nyaapy/__init__.py | 4 + .../komsuite_nyaapy/modules}/anime_site.py | 27 +-- .../komsuite_nyaapy/modules}/magnet.py | 0 .../komsuite_nyaapy/modules}/parser.py | 5 +- src/komsuite_nyaapy/modules/torrent.py | 155 ++++++++++++++++++ src/komsuite_nyaapy/sites/nyaa.py | 11 ++ 18 files changed, 349 insertions(+), 249 deletions(-) create mode 100644 .gitea/ISSUE_TEMPLATE/bug.yml create mode 100644 .gitea/ISSUE_TEMPLATE/feature.yml create mode 100644 .gitea/workflows/build.yml delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 .github/workflows/python-publish.yml delete mode 100644 HOW-TO-CONTRIBUTE.md delete mode 100644 nyaapy/torrent.py delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py create mode 100644 src/komsuite_nyaapy/__init__.py rename {nyaapy => src/komsuite_nyaapy/modules}/anime_site.py (81%) rename {nyaapy => src/komsuite_nyaapy/modules}/magnet.py (100%) rename {nyaapy => src/komsuite_nyaapy/modules}/parser.py (98%) create mode 100644 src/komsuite_nyaapy/modules/torrent.py create mode 100644 src/komsuite_nyaapy/sites/nyaa.py diff --git a/.gitea/ISSUE_TEMPLATE/bug.yml b/.gitea/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..8f78e72 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,34 @@ +name: Bug Report +description: Report a bug in this project +labels: + - kind/bug + - triage +body: + + - type: textarea + id: bug + attributes: + label: Describe the bug + description: | + A clear and concise description of what the bug is. + What did you expect to happen? What happened instead? + Include screenshots if applicable. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: | + A clear and concise description of how to reproduce the bug. + Include steps, code snippets, or screenshots if applicable. + validations: + required: true + - type: textarea + id: additional-info + attributes: + label: Additional information + description: | + Add any other context or screenshots about the bug here. + + \ No newline at end of file diff --git a/.gitea/ISSUE_TEMPLATE/feature.yml b/.gitea/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..cb6338e --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,23 @@ +name: Feature request +description: Suggest an idea for this project +labels: + - kind/feature + - triage + +body: + - type: textarea + id: feature + attributes: + label: Describe the feature + description: | + A clear and concise description of what the feature is. + What is the problem it solves? What are you trying to accomplish? + Include screenshots if applicable. + validations: + required: true + - type: textarea + id: additional-info + attributes: + label: Additional information + description: | + Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..8373538 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,88 @@ +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 + 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@master + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Set up Python + run: uv python install + - 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: 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 package + run: uv build + - name: Publish package + env: + USERNAME: ${{ github.repository_owner }} + run: uv publish --publish-url https://git.theprivateserver.de/api/packages/$USERNAME/pypi/ -t ${{ secrets.TOKEN }} + - name: Generate changelog + id: changelog + uses: metcalfc/changelog-generator@v4.6.2 + with: + myToken: ${{ secrets.GITHUB_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 + files: | + dist/* + env: + GITHUB_TOKEN: ${{ secrets.TOKEN }} \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 34b9728..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,70 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '38 17 * * 0' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index 432b5cd..0000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,39 +0,0 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Upload Python Package - -on: - release: - types: [published] - -permissions: - contents: read - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.9.0 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/HOW-TO-CONTRIBUTE.md b/HOW-TO-CONTRIBUTE.md deleted file mode 100644 index 15ff81b..0000000 --- a/HOW-TO-CONTRIBUTE.md +++ /dev/null @@ -1,45 +0,0 @@ -# How to contribute with this project - -## Getting started - -1. Star the repo, it will help me a lot. -2. Make a fork for you. -3. Clone the repo into your local machine. -4. Create a new branch for your changes. -5. Start hacking :-) - -## Not familiarized with the Python workflow? - -1. Be sure that you have Python 3 and virtualenv installed (if not, install them) -2. Create a new virtualenv - -``` - python -m virtualenv env -p python3 -``` - -3. And activate it! -4. Now it's time to install the dependencies. - -``` - pip install -r requirements.txt -``` - -5. And now you're ready to hack. - -## Hacking - -1. Please, make sure your code is valid according the PEP8 specification -2. If you want to add a new feature, not planned yet, open an issue first. -3. Fix the issues tagged as bug, then we can fix the issues related with features and improvements. -4. Add comments to your code, don't be shy. - -## Hacktoberfest plus - -If this is your first time, follow this steps: - - 1. Go to `Getting started` and follow the instructions there. - 2. Push your changes into your fork. - 3. Open a PR across forks from your `dev` branch to parent's repo `dev` branch (NOT MASTER) - 4. Wait for feedback - -Happy coding! diff --git a/README.md b/README.md index 020c6bc..0dcbd15 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +# Hard Fork +## TBD + + +# Original Repo

diff --git a/nyaapy/torrent.py b/nyaapy/torrent.py deleted file mode 100644 index 3aca6f3..0000000 --- a/nyaapy/torrent.py +++ /dev/null @@ -1,31 +0,0 @@ -from enum import Enum - - -def json_to_class(data): - # We check if the data passed is a list or not - if isinstance(data, list): - object_list = [] - for item in data: - object_list.append(Torrent(item)) - # Return a list of Torrent objects - return object_list - else: - return Torrent(data) - - -# This deals with converting the dict to an object -class Torrent(object): - def __init__(self, my_dict): - for key in my_dict: - setattr(self, key, my_dict[key]) - - -class TorrentSite(Enum): - """ - Contains torrent sites - """ - - NYAASI = "https://nyaa.si" - SUKEBEINYAASI = "https://sukebei.nyaa.si" - - NYAALAND = "https://nyaa.land" diff --git a/pyproject.toml b/pyproject.toml index 8cf2911..092a07e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,18 @@ -[tool.poetry] -name = "nyaapy" -version = "0.7" -description = "Unofficial Python wrapper for NyaaPantsu API and Nyaa.si" -authors = ["Juanjo Salvador "] +[project] +name = "komsuite-nyaapy" +version = "0.1.0" +description = "A rewritten hard fork of the original NyaaPy library." license = "MIT" readme = "README.md" +authors = [{ name = "WorldTeacher", email = "coding_contact@pm.me" }] +requires-python = ">=3.13" +dependencies = [ + "bencodepy>=0.9.5", + "lxml>=5.3.1", + "regex>=2024.11.6", +] -[tool.poetry.dependencies] -python = "^3.10" -requests = "^2.32.3" -lxml = "^5.2.2" - -[tool.poetry.group.dev.dependencies] -pytest = "^8.2.2" -black = "^24.4.2" -mypy = "^1.10.1" -types-requests = "^2.32.0.20240712" -lxml-stubs = "^0.5.1" [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 19198a3..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests>=2.20.0 -beautifulsoup4==4.6.0 -lxml \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b88034e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md diff --git a/setup.py b/setup.py deleted file mode 100644 index 0a6c413..0000000 --- a/setup.py +++ /dev/null @@ -1,25 +0,0 @@ -from setuptools import setup, find_packages # type: ignore -from os import path - -currdir = path.abspath(path.dirname(__file__)) -with open(path.join(currdir, "README.md"), encoding="utf-8") as f: - long_desc = f.read() - -setup( - name="nyaapy", - version="0.6.3", - install_requires=[ - "requests", - "lxml", - ], - url="https://github.com/juanjosalvador/nyaapy", - long_description=long_desc, - long_description_content_type="text/markdown", - download_url=("https://github.com/juanjosalvador/" "nyaapy/archive/0.6.3.tar.gz"), - license="MIT", - author="Juanjo Salvador", - author_email="juanjosalvador@netc.eu", - description="Allows you to make requests on Nyaa.si and nyaa.pantsu.cat", - packages=find_packages(exclude=["tests"]), - zip_safe=False, -) diff --git a/src/komsuite_nyaapy/__init__.py b/src/komsuite_nyaapy/__init__.py new file mode 100644 index 0000000..56831da --- /dev/null +++ b/src/komsuite_nyaapy/__init__.py @@ -0,0 +1,4 @@ +from .modules.anime_site import AnimeTorrentSite +from .modules.torrent import TorrentSite +from .sites.nyaa import Nyaa, SukebeiNyaa +from .modules.torrent import Torrent diff --git a/nyaapy/anime_site.py b/src/komsuite_nyaapy/modules/anime_site.py similarity index 81% rename from nyaapy/anime_site.py rename to src/komsuite_nyaapy/modules/anime_site.py index 7cd8844..e7d8fc6 100644 --- a/nyaapy/anime_site.py +++ b/src/komsuite_nyaapy/modules/anime_site.py @@ -1,11 +1,11 @@ import requests -from nyaapy import torrent -from nyaapy.parser import parse_nyaa, parse_single, parse_nyaa_rss +from komsuite_nyaapy.modules import torrent +from komsuite_nyaapy.modules.parser import parse_nyaa, parse_single, parse_nyaa_rss class AnimeTorrentSite: SITE = torrent.TorrentSite.NYAASI - URL = "https://nyaa.si" + URL = SITE @classmethod def last_uploads(self, number_of_results: int): @@ -21,18 +21,20 @@ class AnimeTorrentSite: return torrent.json_to_class(json_data) @classmethod - def search(self, keyword: str, **kwargs): + def search( + self, + keyword: str, + category: int = 0, + subcategory: int = 0, + filters: int = 0, + page: int = 0, + sorting: str = "id", + order: str = "desc", + **kwargs, + ): base_url = self.URL user = kwargs.get("user", None) - category = kwargs.get("category", 0) - subcategory = kwargs.get("subcategory", 0) - filters = kwargs.get("filters", 0) - page = kwargs.get("page", 0) - sorting = kwargs.get( - "sort", "id" - ) # Sorting by id = sorting by date, this is the default. - order = kwargs.get("order", "desc") user_uri = f"user/{user}" if user else "" @@ -62,7 +64,6 @@ class AnimeTorrentSite: if not user: search_uri += "&page=rss" - http_response = requests.get(search_uri) http_response.raise_for_status() diff --git a/nyaapy/magnet.py b/src/komsuite_nyaapy/modules/magnet.py similarity index 100% rename from nyaapy/magnet.py rename to src/komsuite_nyaapy/modules/magnet.py diff --git a/nyaapy/parser.py b/src/komsuite_nyaapy/modules/parser.py similarity index 98% rename from nyaapy/parser.py rename to src/komsuite_nyaapy/modules/parser.py index 836ec1e..42ec87f 100644 --- a/nyaapy/parser.py +++ b/src/komsuite_nyaapy/modules/parser.py @@ -1,6 +1,6 @@ from lxml import etree -from nyaapy.magnet import magnet_builder -from nyaapy.torrent import TorrentSite +from .magnet import magnet_builder +from .torrent import TorrentSite def nyaa_categories(b): @@ -115,7 +115,6 @@ def parse_nyaa(request_text, limit, site): for td in tr.xpath("./td"): for link in td.xpath("./a"): - href = link.attrib.get("href").split("/")[-1] # Only caring about non-comment pages. diff --git a/src/komsuite_nyaapy/modules/torrent.py b/src/komsuite_nyaapy/modules/torrent.py new file mode 100644 index 0000000..a134361 --- /dev/null +++ b/src/komsuite_nyaapy/modules/torrent.py @@ -0,0 +1,155 @@ +from enum import Enum +from dataclasses import dataclass +from dataclasses import field +import os +import bencodepy +import regex +from typing import Optional +import loguru +import sys + +log = loguru.logger +log.remove() +log.add("cli.log", rotation="1 week", retention="1 month") +log.add(sys.stdout, level="INFO") + + +def json_to_class(data): + # We check if the data passed is a list or not + if isinstance(data, list): + object_list = [] + for item in data: + object_list.append(Torrent(**item)) + # Return a list of Torrent objects + return object_list + else: + return [Torrent(**data)] + + +# This deals with converting the dict to an object +@dataclass +class Torrent: + id: int = 0 + category: Optional[str] = None + category: Optional[str] = None + url: Optional[str] = None + name: Optional[str] = None + download_url: Optional[str] = None + magnet: Optional[str] = None + size: Optional[str] = None + seeders: int = 0 + leechers: int = 0 + completed_downloads: int = 0 + date: Optional[str] = None + type: Optional[str] = None + volumes: list[int] = field(default_factory=list) + contents: list[str] = field(default_factory=list) + filetypes: list[str] = field(default_factory=list) + + def __post_init__(self): + self.get_contents + self.seeders = int(self.seeders) + self.leechers = int(self.leechers) if self.leechers is not None else 0 + + @property + def filesizes(self): + data_count = self.size.split(" ")[0] + data_value = self.size.split(" ")[1] + mib = 1 + kib = 2 + gib = 3 + + return float(data_count) * ( + 1024 + ** (mib if data_value == "MiB" else kib if data_value == "KiB" else gib) + ) + + @property + def get_contents(self): + os.system(f"wget {self.download_url}> /dev/null 2>&1") + with open(f"{self.download_url.split('/')[-1]}", "rb") as f: + data = bencodepy.decode(f.read()) + + info = data[b"info"] + filetypes: list[str] = [] + + # For multi-file torrents + if b"files" in info: + files: list[str] = [] + for file in info[b"files"]: # type: ignore + path_parts = [part.decode("utf-8") for part in file[b"path"]] # type: ignore + files.append("/".join(path_parts)) # type: ignore + filetypes.append(path_parts[-1].split(".")[-1]) # type: ignore + self.filetypes = list(set(filetypes)) + self.contents = files + # check each file for the presence of r"^(.*?)\s(vol\.\s\d{2})|(v\d{2,3})" + volumes: list[int] = [] + for file in files: + match = regex.findall( + r"(vol\.?\s*\d{1,3}|v\d{1,3}(?:[-v]\d{1,3})?)", + file, + regex.IGNORECASE, + ) + if match: + match = ( + "".join(match[0]) + .strip() + .replace("vol", "") + .replace("v", "") + .replace(".", "") + ) + if "-" in match: + match = match.split("-") + # if difference between numbers is greater than 1, add missing numbers to volumes + if int(match[1]) - int(match[0]) > 1: + for i in range(int(match[0]) - 1, int(match[1]) + 1): + volumes.append(i) + else: + volumes.append(int(match[0])) + volumes.append(int(match[1])) + else: + if match.isdigit(): + volumes.append(int(match)) + + volumes = sorted(volumes) + self.volumes = volumes + + # For single-file torrents + # elif b"name" in info: + # return [info[b"name"].decode("utf-8")] + else: + self.volumes = [0] + # log.debug("Filetypes: {}, Volumes: {}".format(self.filetypes, self.volumes)) #! enable for debug + os.remove(f"{self.download_url.split('/')[-1]}") + + +class TorrentSite(Enum): + """ + Contains torrent sites + """ + + NYAASI = "https://nyaa.si" + SUKEBEINYAASI = "https://sukebei.nyaa.si" + + NYAALAND = "https://nyaa.land" + + @classmethod + def list_sites(cls): + return [site.value for site in cls] + + @classmethod + def get_site(cls, site): + if isinstance(site, TorrentSite): + return site.value + for s in cls: + if s.value == site: + return s + return None + + @classmethod + def get_site_url(cls, site): + return cls.site.value + + @classmethod + def nyaa(cls): + return (cls.NYAASI, cls.NYAASI.value) diff --git a/src/komsuite_nyaapy/sites/nyaa.py b/src/komsuite_nyaapy/sites/nyaa.py new file mode 100644 index 0000000..0496e6c --- /dev/null +++ b/src/komsuite_nyaapy/sites/nyaa.py @@ -0,0 +1,11 @@ +from komsuite_nyaapy import AnimeTorrentSite, TorrentSite + + +class SukebeiNyaa(AnimeTorrentSite): + SITE = TorrentSite.SUKEBEINYAASI + URL = TorrentSite.get_site(SITE) + + +class Nyaa(AnimeTorrentSite): + SITE = TorrentSite.NYAASI + URL = TorrentSite.get_site(SITE)