├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ └── feature-request.md ├── pull_request_template.md ├── stale.yml └── workflows │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── install.mjs ├── .npmignore ├── .prettierignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ └── hianime │ ├── animeAZList.test.ts │ ├── animeAboutInfo.test.ts │ ├── animeCategory.test.ts │ ├── animeEpisodeSrcs.test.ts │ ├── animeEpisodes.test.ts │ ├── animeGenre.test.ts │ ├── animeProducer.test.ts │ ├── animeQtip.test.ts │ ├── animeSearch.test.ts │ ├── animeSearchSuggestion.test.ts │ ├── episodeServers.test.ts │ ├── estimatedSchedule.test.ts │ ├── homePage.test.ts │ └── nextEpisodeSchedule.test.ts ├── package.json ├── pnpm-lock.yaml ├── prettier.config.mjs ├── scripts └── format-package-json.js ├── src ├── config │ ├── client.ts │ ├── error.ts │ └── logger.ts ├── extractors │ ├── index.ts │ ├── megacloud.decodedpng.ts │ ├── megacloud.getsrcs.ts │ ├── megacloud.ts │ ├── rapidcloud.ts │ ├── streamsb.ts │ └── streamtape.ts ├── hianime │ ├── error.ts │ ├── hianime.ts │ ├── scrapers │ │ ├── animeAZList.ts │ │ ├── animeAboutInfo.ts │ │ ├── animeCategory.ts │ │ ├── animeEpisodeSrcs.ts │ │ ├── animeEpisodes.ts │ │ ├── animeGenre.ts │ │ ├── animeProducer.ts │ │ ├── animeQtip.ts │ │ ├── animeSearch.ts │ │ ├── animeSearchSuggestion.ts │ │ ├── episodeServers.ts │ │ ├── estimatedSchedule.ts │ │ ├── homePage.ts │ │ └── index.ts │ └── types │ │ ├── anime.ts │ │ ├── animeSearch.ts │ │ ├── extractor.ts │ │ └── scrapers │ │ ├── animeAZList.ts │ │ ├── animeAboutInfo.ts │ │ ├── animeCategory.ts │ │ ├── animeEpisodeSrcs.ts │ │ ├── animeEpisodes.ts │ │ ├── animeGenre.ts │ │ ├── animeProducer.ts │ │ ├── animeQtip.ts │ │ ├── animeSearch.ts │ │ ├── animeSearchSuggestion.ts │ │ ├── episodeServers.ts │ │ ├── estimatedSchedule.ts │ │ ├── homePage.ts │ │ └── index.ts ├── index.ts └── utils │ ├── constants.ts │ ├── index.ts │ └── methods.ts ├── tsconfig.json ├── tsup.config.ts └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | 4 | [*.{js,mjs,ts}] 5 | indent_size = 4 6 | trim_trailing_whitespace = true 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ghoshRitesh12 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 🐛 2 | description: Create a report to help this package improve 3 | labels: [bug] 4 | assignees: [ghoshRitesh12] 5 | body: 6 | - type: input 7 | id: describe-the-bug 8 | attributes: 9 | label: Describe the bug 10 | description: | 11 | A clear and concise description of what the bug is. 12 | placeholder: | 13 | Example: "This scraper is not working..." 14 | validations: 15 | required: true 16 | 17 | - type: textarea 18 | id: expected-behavior 19 | attributes: 20 | label: Expected behavior 21 | placeholder: | 22 | Example: 23 | "This should happen..." 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | id: actual-behavior 29 | attributes: 30 | label: Actual behavior 31 | placeholder: | 32 | Example: 33 | "This happened instead..." 34 | validations: 35 | required: true 36 | 37 | - type: textarea 38 | id: additional-context 39 | attributes: 40 | label: Additional context 41 | description: | 42 | Add any other context about the problem here. 43 | placeholder: | 44 | Example: 45 | "Also ..." 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 🆕 3 | about: Suggest an idea for this package 4 | title: "" 5 | labels: ["enhancement"] 6 | assignees: ["ghoshRitesh12"] 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **What kind of change does this PR introduce?** 4 | 5 | 6 | 7 | **Did you add tests for your changes?** 8 | 9 | **If relevant, did you update the documentation?** 10 | 11 | **Summary** 12 | 13 | 14 | 15 | 16 | **Other information** 17 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Comment to post when closing a stale issue. Set to `false` to disable 2 | closeComment: > 3 | We are closing this issue. If the issue still persists in the latest version of 4 | aniwatch package, please reopen the issue and update the description. We will try our 5 | best to accomodate it! 6 | # Number of days of inactivity before an issue becomes stale 7 | daysUntilStale: 60 8 | # Number of days of inactivity before a stale issue is closed 9 | daysUntilClose: 30 10 | # Issues with these labels will never be considered stale 11 | exemptLabels: 12 | - provider request 13 | - enhancement 14 | - help wanted 15 | # Comment to post when marking an issue as stale. 16 | markComment: > 17 | We're marking this issue as wontfix because it has not had recent activity. It will be closed if no further activity occurs 18 | within the next 30 days. Thank you for your contributions. 19 | # Only mark issues. 20 | only: issues 21 | # Label to use when marking an issue as stale 22 | staleLabel: wontfix 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish Package" 2 | on: 3 | release: 4 | types: [published] 5 | 6 | concurrency: ${{ github.workflow }}-${{ github.ref }} 7 | 8 | env: 9 | HUSKY: 0 10 | 11 | jobs: 12 | publish_package: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup pnpm 19 | uses: pnpm/action-setup@v4 20 | with: 21 | version: 9 22 | 23 | - name: Setup NodeJS 24 | uses: actions/setup-node@v4 25 | with: 26 | cache: pnpm 27 | node-version: 20 28 | registry-url: https://registry.npmjs.org/ 29 | 30 | - name: Install dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | - name: Lint and build 34 | run: pnpm run ci 35 | 36 | - name: Publish package 37 | run: npm publish 38 | env: 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | 41 | # - name: Setup .npmrc file to publish to GitHub Packages 42 | # uses: actions/setup-node@v4 43 | # with: 44 | # cache: pnpm 45 | # node-version: 20 46 | # registry-url: https://npm.pkg.github.com 47 | # scope: "@${{ github.repository_owner }}" 48 | # - run: npm run addscope 49 | # - name: Publish to GitHub Packages 50 | # run: npm publish 51 | # env: 52 | # NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Create Release" 2 | on: 3 | push: 4 | branches: ["main"] 5 | 6 | jobs: 7 | changelog: 8 | runs-on: ubuntu-latest 9 | 10 | permissions: 11 | contents: write 12 | 13 | steps: 14 | - name: Checkouts repo 15 | uses: actions/checkout@v4 16 | 17 | - name: Conventional changelog action 18 | id: changelog 19 | uses: TriPSs/conventional-changelog-action@v5 20 | with: 21 | skip-on-empty: false 22 | git-user-name: "github-actions[bot]" 23 | git-user-email: "github-actions[bot]@users.noreply.github.com" 24 | pre-commit: ./scripts/format-package-json.js 25 | github-token: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: Create release 28 | if: ${{ steps.changelog.outputs.skipped == 'false' }} 29 | uses: ncipollo/release-action@v1 30 | with: 31 | generateReleaseNotes: true 32 | token: ${{ secrets.RELEASE_CHANGELOG_SECRET }} 33 | tag: ${{ steps.changelog.outputs.tag }} 34 | name: ${{ steps.changelog.outputs.tag }} 35 | body: ${{ steps.changelog.outputs.clean_changelog }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | node_modules 4 | dist 5 | TESTS 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if ! head -1 "$1" | grep -qE "^(feat|fix|chore|docs|test|style|refactor|perf|build|ci|revert)(\(.+?\))?: .{1,}$"; then 3 | echo "Aborting commit. Your commit message is invalid." >&2 4 | exit 1 5 | fi 6 | if ! head -1 "$1" | grep -qE "^.{1,88}$"; then 7 | echo "Aborting commit. Your commit message is too long." >&2 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /.husky/install.mjs: -------------------------------------------------------------------------------- 1 | // Skip Husky install in production and CI 2 | if (process.env.NODE_ENV === "production" || process.env.CI === "true") { 3 | process.exit(0); 4 | } 5 | const husky = (await import("husky")).default; 6 | console.log(husky()); 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .husky 3 | node_modules 4 | src 5 | __tests__ 6 | 7 | README.md 8 | CHANGELOG.md 9 | CONTRIBUTING.md 10 | CODE_OF_CONDUCT.md 11 | 12 | tsconfig.json 13 | tsup.config.ts 14 | vitest.config.ts 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # *.md 3 | *.html 4 | *.json 5 | 6 | *.yaml 7 | *.yml 8 | *.log 9 | *.txt -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.23.0](https://github.com/ghoshRitesh12/aniwatch/compare/v2.22.1...v2.23.0) (2025-05-11) 2 | 3 | 4 | ### Features 5 | 6 | * add pre-commit hook for package.json formatting in release workflow ([ab68040](https://github.com/ghoshRitesh12/aniwatch/commit/ab680405d767ad175ee5acbcf811cd42fa549920)) 7 | * add preCommit function to format package.json ([20b9de5](https://github.com/ghoshRitesh12/aniwatch/commit/20b9de5bc29cd1f3c8a1d80078680e7ae12aeec7)) 8 | 9 | 10 | 11 | ## [2.22.1](https://github.com/ghoshRitesh12/aniwatch/compare/v2.22.0...v2.22.1) (2025-05-11) 12 | 13 | 14 | 15 | # [2.22.0](https://github.com/ghoshRitesh12/aniwatch/compare/v2.21.2...v2.22.0) (2025-05-11) 16 | 17 | 18 | ### Features 19 | 20 | * add custom logger ([6038366](https://github.com/ghoshRitesh12/aniwatch/commit/6038366bc328bc70fd60af8a311041f486145698)) 21 | * add husky custom install script ([673a96b](https://github.com/ghoshRitesh12/aniwatch/commit/673a96b36a4680a637910e3a5713f2deeb395bfb)) 22 | * add prettierignore file ([08c58d9](https://github.com/ghoshRitesh12/aniwatch/commit/08c58d9a74adb52e903fc308ed677835fb583884)) 23 | 24 | 25 | 26 | ## [2.21.2](https://github.com/ghoshRitesh12/aniwatch/compare/v2.21.1...v2.21.2) (2025-04-16) 27 | 28 | 29 | 30 | ## [2.21.1](https://github.com/ghoshRitesh12/aniwatch/compare/v2.21.0...v2.21.1) (2025-04-14) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * possible edge case of invalid tzOffset ([5075279](https://github.com/ghoshRitesh12/aniwatch/commit/5075279d9a07bc5b56ee8fb58292f6cd226e9366)) 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Our Responsibilities 40 | 41 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 42 | 43 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 44 | 45 | ## Scope 46 | 47 | This Code of Conduct applies within all community spaces, and also applies when 48 | an individual is officially representing the community in public spaces. 49 | Examples of representing our community include using an official e-mail address, 50 | posting via an official social media account, or acting as an appointed 51 | representative at an online or offline event. 52 | 53 | ## Enforcement 54 | 55 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [support@github.com](mailto:support@github.com).The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 56 | 57 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 58 | 59 | ## Attribution 60 | 61 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). 62 | 63 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 64 | 65 | [homepage]: https://www.contributor-covenant.org 66 | 67 | For answers to common questions about this code of conduct, see the FAQ at 68 | [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at 69 | [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to aniwatch 2 | 3 | Thank you for your interest in contributing to aniwatch. We appreciate whatever form of contribution you are willing to make. Every contribution counts ✨ 4 | 5 | ## Table of Contents 6 | 7 | - [Types of contributions we are looking for](#types-of-contributions-we-are-looking-for) 8 | - [Ground Rules & Expectations](#ground-rules--expectations) 9 | - [How To Contribute](#how-to-contribute) 10 | - [Prerequisites](#prerequisites) 11 | - [Clone the repository](#clone-the-repository) 12 | - [Project Structure](#project-structure) 13 | - [Commit Messages](#commit-messages) 14 | 15 | ## Types of contributions we are looking for 16 | 17 | In short, we welcome any sort of contribution you are willing to make as each and every contribution counts. We gladly accept contributions such as: 18 | 19 | - Documentation improvements: from minor typos to major document overhauls 20 | - Helping others by answering questions in pull requests. 21 | - Fixing known [bugs](https://github.com/ghoshRitesh12/aniwatch/issues?q=is%3Aopen). 22 | 23 | ## Ground Rules & Expectations 24 | 25 | Before we begin, here are a few things we anticipate from you and that you should expect from others: 26 | 27 | - Be respectful and thoughtful in your conversations around this project. Each person may have their own views and opinions about the project. Try to listen to each other and reach an agreement or compromise. 28 | 29 | ## How To Contribute 30 | 31 | If you'd like to contribute, start by searching through the [issues](https://github.com/ghoshRitesh12/aniwatch/issues) and [pull requests](https://github.com/ghoshRitesh12/aniwatch/pulls) to see whether someone else has raised a similar idea or question. 32 | 33 | If you don't see your idea listed, and you think it fits into the goals of this guide, you may do one of the following: 34 | 35 | - **If your contribution is minor,** such as a typo fix or new provider, consider opening a pull request. 36 | - **If your contribution is major,** such as a major refactor, start by opening an issue first. That way, other people can weigh in on the discussion before you do any work. 37 | 38 | ## Prerequisites 39 | 40 | To contribute to this project, you must know the following: 41 | 42 | - [NodeJS](https://nodejs.org/) 43 | - [TypeScript](https://www.typescriptlang.org/) 44 | - Web Scraping 45 | - [Cheerio](https://cheerio.js.org/) 46 | - [Axios](https://axios-http.com/docs/intro) 47 | - [CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) 48 | - [Browser Dev Tools](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools) 49 | 50 | ### Clone the repository 51 | 52 | 1. [Fork the repository](https://github.com/ghoshRitesh12/aniwatch/fork) 53 | 2. Clone your fork to your local machine using the following command (replace with your actual GitHub username) 54 | 55 | ```bash 56 | git clone https://github.com//aniwatch 57 | ``` 58 | 59 | 3. Creating a new branch
60 | Replace \ with any of the following naming conventions:
61 | - `feature/` - for adding new features 62 | - `bug/` - for fixing known bugs 63 | - `misc/` - for anything other than bug or features 64 | 65 | ```bash 66 | git checkout -b 67 | ``` 68 | 69 | ### Project Structure 70 | 71 | - `src` directory contains all the source code required for this project 72 | 73 | - `types` directory contains all types & interfaces used for this project 74 | - `parsers` directory contains all the parsing aka scraping logic 75 | - `utils` directory contains handy utility methods and properties 76 | - `config` directory contains api configuration related files 77 | - `extractors` directory contains anime streaming url extractor files 78 |

79 | 80 | - `test` directory contains all the tests that needs to be evaluated 81 | 82 | ## Commit Messages 83 | 84 | When you've made changes to one or more files, you have to commit that file. You also need a message for that commit. 85 | 86 | We follow [Conventional Commit Messages](https://www.conventionalcommits.org/en/v1.0.0/#summary). 87 | 88 | A brief overview: 89 | 90 | - `feat`: A feature, possibly improving something already existing 91 | - `fix`: A fix, for example of a bug 92 | - `perf`: Performance related change 93 | - `refactor`: Refactoring a specific section of the codebase 94 | - `style`: Everything related to styling code like whitespaces, tabs, indenting, etc. 95 | - `test`: Everything related to testing 96 | - `docs`: Everything related to documentation 97 | - `chore`: Code maintenance 98 | 99 | Examples: 100 | 101 | - `docs: fixed typo in readme` 102 | - `feat: added a new category parser` 103 | - `fix: fixed search results bug` 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ritesh Ghosh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 10 | 11 |

12 | 13 | #

Aniwatch

14 | 15 |
16 | 📦 A scraper package serving anime information from hianimez.to 17 |
18 | 19 | 22 | Bug report 23 | 24 | · 25 | 28 | Feature request 29 | 30 | 31 |
32 | 33 |
34 | 35 |
36 | 37 | [![Publish Package](https://github.com/ghoshRitesh12/aniwatch/actions/workflows/publish.yml/badge.svg)](https://github.com/ghoshRitesh12/aniwatch/actions/workflows/publish.yml) 38 | ![NPM Downloads](https://img.shields.io/npm/dw/aniwatch?logo=npm&logoColor=e78284&label=Downloads&labelColor=292e34&color=31c754) 39 | [![GitHub License](https://img.shields.io/github/license/ghoshRitesh12/aniwatch?logo=github&logoColor=%23959da5&labelColor=%23292e34&color=%2331c754)](https://github.com/ghoshRitesh12/aniwatch/blob/main/LICENSE) 40 | 41 | 42 | 43 |
44 | 45 |
46 | 47 | [![stars](https://img.shields.io/github/stars/ghoshRitesh12/aniwatch?style=social)](https://github.com/ghoshRitesh12/aniwatch/stargazers) 48 | [![forks](https://img.shields.io/github/forks/ghoshRitesh12/aniwatch?style=social)](https://github.com/ghoshRitesh12/aniwatch/network/members) 49 | [![issues](https://img.shields.io/github/issues/ghoshRitesh12/aniwatch?style=social&logo=github)](https://github.com/ghoshRitesh12/aniwatch/issues?q=is%3Aissue+is%3Aopen+) 50 | [![version](https://img.shields.io/github/v/release/ghoshRitesh12/aniwatch?display_name=release&style=social&logo=github)](https://github.com/ghoshRitesh12/aniwatch/releases/latest) 51 | 52 |
53 | 54 | > [!IMPORTANT] 55 | > 56 | > 1. This package is just an unofficial package for [hianimez.to](https://hianimez.to) and is in no other way officially related to the same. 57 | > 2. The content that this package provides is not mine, nor is it hosted by me. These belong to their respective owners. This package just demonstrates how to build a package that scrapes websites and uses their content. 58 | 59 | ## Table of Contents 60 | 61 | - [Quick Start](#quick-start) 62 | - [Installation](#installation) 63 | - [Example Usage](#example-usage) 64 | - [Documentation](#documentation) 65 | - [getHomePage](#gethomepage) 66 | - [getAZList](#getazlist) 67 | - [getQtipInfo](#getqtipinfo) 68 | - [getAnimeAboutInfo](#getanimeaboutinfo) 69 | - [getAnimeSearchResults](#getanimesearchresults) 70 | - [getAnimeSearchSuggestion](#getanimesearchsuggestion) 71 | - [getProducerAnimes](#getproduceranimes) 72 | - [getGenreAnime](#getgenreanime) 73 | - [getAnimeCategory](#getanimecategory) 74 | - [getEstimatedSchedule](#getestimatedschedule) 75 | - [getNextEpisodeSchedule](#getnextepisodeschedule) 76 | - [getAnimeEpisodes](#getanimeepisodes) 77 | - [getEpisodeServers](#getepisodeservers) 78 | - [getAnimeEpisodeSources](#getanimeepisodesources) 79 | - [Development](#development) 80 | - [Thanks](#thanks) 81 | - [Support](#support) 82 | - [License](#license) 83 | - [Contributors](#contributors) 84 | - [Star History](#star-history) 85 | 86 | ## Quick start 87 | 88 | ### Installation 89 | 90 | To use `aniwatch` package in your project, run: 91 | 92 | ```bash 93 | pnpm add aniwatch 94 | # or "yarn add aniwatch" 95 | # or "npm install aniwatch" 96 | ``` 97 | 98 | ### Example usage 99 | 100 | Example - getting information about an anime by providing it's unique anime id, using anime [Steins;Gate](https://www.imdb.com/title/tt1910272/) with `steinsgate-3` unique anime id as an example. 101 | 102 | ```javascript 103 | import { HiAnime, HiAnimeError } from "aniwatch"; 104 | 105 | const hianime = new HiAnime.Scraper(); 106 | 107 | try { 108 | const data: HiAnime.ScrapedAnimeAboutInfo = await hianime.getInfo( 109 | "steinsgate-3" 110 | ); 111 | console.log(data); 112 | } catch (err) { 113 | console.error(err instanceof HiAnimeError, err); 114 | } 115 | ``` 116 | 117 |
118 | 119 | 120 | 121 | ### `getHomePage` 122 | 123 | 124 | 125 | #### Sample Usage 126 | 127 | ```typescript 128 | import { HiAnime } from "aniwatch"; 129 | 130 | const hianime = new HiAnime.Scraper(); 131 | 132 | hianime 133 | .getHomePage() 134 | .then((data) => console.log(data)) 135 | .catch((err) => console.error(err)); 136 | ``` 137 | 138 | #### Response Schema 139 | 140 | ```javascript 141 | { 142 | genres: ["Action", "Cars", "Adventure", ...], 143 | latestEpisodeAnimes: [ 144 | { 145 | id: string, 146 | name: string, 147 | poster: string, 148 | type: string, 149 | episodes: { 150 | sub: number, 151 | dub: number, 152 | } 153 | }, 154 | {...}, 155 | ], 156 | spotlightAnimes: [ 157 | { 158 | id: string, 159 | name: string, 160 | jname: string, 161 | poster: string, 162 | description: string, 163 | rank: number, 164 | otherInfo: string[], 165 | episodes: { 166 | sub: number, 167 | dub: number, 168 | }, 169 | }, 170 | {...}, 171 | ], 172 | top10Animes: { 173 | today: [ 174 | { 175 | episodes: { 176 | sub: number, 177 | dub: number, 178 | }, 179 | id: string, 180 | name: string, 181 | poster: string, 182 | rank: number 183 | }, 184 | {...}, 185 | ], 186 | month: [...], 187 | week: [...] 188 | }, 189 | topAiringAnimes: [ 190 | { 191 | id: string, 192 | name: string, 193 | jname: string, 194 | poster: string, 195 | }, 196 | {...}, 197 | ], 198 | topUpcomingAnimes: [ 199 | { 200 | id: string, 201 | name: string, 202 | poster: string, 203 | duration: string, 204 | type: string, 205 | rating: string, 206 | episodes: { 207 | sub: number, 208 | dub: number, 209 | } 210 | }, 211 | {...}, 212 | ], 213 | trendingAnimes: [ 214 | { 215 | id: string, 216 | name: string, 217 | poster: string, 218 | rank: number, 219 | }, 220 | {...}, 221 | ], 222 | mostPopularAnimes: [ 223 | { 224 | id: string, 225 | name: string, 226 | poster: string, 227 | type: string, 228 | episodes: { 229 | sub: number, 230 | dub: number, 231 | } 232 | }, 233 | {...}, 234 | ], 235 | mostFavoriteAnimes: [ 236 | { 237 | id: string, 238 | name: string, 239 | poster: string, 240 | type: string, 241 | episodes: { 242 | sub: number, 243 | dub: number, 244 | } 245 | }, 246 | {...}, 247 | ], 248 | latestCompletedAnimes: [ 249 | { 250 | id: string, 251 | name: string, 252 | poster: string, 253 | type: string, 254 | episodes: { 255 | sub: number, 256 | dub: number, 257 | } 258 | }, 259 | {...}, 260 | ], 261 | } 262 | 263 | ``` 264 | 265 | [🔼 Back to Top](#table-of-contents) 266 | 267 |
268 | 269 |
270 | 271 | 272 | 273 | ### `getAZList` 274 | 275 | 276 | 277 | #### Parameters 278 | 279 | | Parameter | Type | Description | Required? | Default | 280 | | :----------: | :----: | :-------------------------------------------------------------------------------------------------: | :-------: | :-----: | 281 | | `sortOption` | string | The az-list sort option. Possible values include: "all", "other", "0-9" and all english alphabets . | Yes | -- | 282 | | `page` | number | The page number of the result. | No | `1` | 283 | 284 | #### Sample Usage 285 | 286 | ```javascript 287 | import { HiAnime } from "aniwatch"; 288 | 289 | const hianime = new HiAnime.Scraper(); 290 | 291 | hianime 292 | .getAZList("0-9", 1) 293 | .then((data) => console.log(data)) 294 | .catch((err) => console.error(err)); 295 | ``` 296 | 297 | #### Response Schema 298 | 299 | ```javascript 300 | { 301 | sortOption: "0-9", 302 | animes: [ 303 | { 304 | id: string, 305 | name: string, 306 | jname: string, 307 | poster: string, 308 | duration: string, 309 | type: string, 310 | rating: string, 311 | episodes: { 312 | sub: number , 313 | dub: number 314 | } 315 | }, 316 | {...} 317 | ], 318 | totalPages: 1, 319 | currentPage: 1, 320 | hasNextPage: false 321 | } 322 | ``` 323 | 324 | [🔼 Back to Top](#table-of-contents) 325 | 326 |
327 | 328 |
329 | 330 | 331 | 332 | ### `getQtipInfo` 333 | 334 | 335 | 336 | #### Parameters 337 | 338 | | Parameter | Type | Description | Required? | Default | 339 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 340 | | `animeId` | string | The unique anime id (in kebab case). | Yes | -- | 341 | 342 | #### Sample Usage 343 | 344 | ```javascript 345 | import { HiAnime } from "aniwatch"; 346 | 347 | const hianime = new HiAnime.Scraper(); 348 | 349 | hianime 350 | .getQtipInfo("one-piece-100") 351 | .then((data) => console.log(data)) 352 | .catch((err) => console.error(err)); 353 | ``` 354 | 355 | #### Response Schema 356 | 357 | ```javascript 358 | { 359 | anime: { 360 | id: "one-piece-100", 361 | name: "One Piece", 362 | malscore: string, 363 | quality: string, 364 | episodes: { 365 | sub: number, 366 | dub: number 367 | }, 368 | type: string, 369 | description: string, 370 | jname: string, 371 | synonyms: string, 372 | aired: string, 373 | status: string, 374 | genres: ["Action", "Adventure", "Comedy", "Drama", "Fantasy", "Shounen", "Drama", "Fantasy", "Shounen", "Fantasy", "Shounen", "Shounen", "Super Power"] 375 | } 376 | } 377 | ``` 378 | 379 | [🔼 Back to Top](#table-of-contents) 380 | 381 |
382 | 383 |
384 | 385 | 386 | 387 | ### `getAnimeAboutInfo` 388 | 389 | 390 | 391 | #### Parameters 392 | 393 | | Parameter | Type | Description | Required? | Default | 394 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 395 | | `animeId` | string | The unique anime id (in kebab case). | Yes | -- | 396 | 397 | #### Sample Usage 398 | 399 | ```javascript 400 | import { HiAnime } from "aniwatch"; 401 | 402 | const hianime = new HiAnime.Scraper(); 403 | 404 | hianime 405 | .getInfo("steinsgate-3") 406 | .then((data) => console.log(data)) 407 | .catch((err) => console.error(err)); 408 | ``` 409 | 410 | #### Response Schema 411 | 412 | ```javascript 413 | { 414 | anime: [ 415 | info: { 416 | id: string, 417 | name: string, 418 | poster: string, 419 | description: string, 420 | stats: { 421 | rating: string, 422 | quality: string, 423 | episodes: { 424 | sub: number, 425 | dub: number 426 | }, 427 | type: string, 428 | duration: string 429 | }, 430 | promotionalVideos: [ 431 | { 432 | title: string | undefined, 433 | source: string | undefined, 434 | thumbnail: string | undefined 435 | }, 436 | {...}, 437 | ], 438 | characterVoiceActor: [ 439 | { 440 | character: { 441 | id: string, 442 | poster: string, 443 | name: string, 444 | cast: string 445 | }, 446 | voiceActor: { 447 | id: string, 448 | poster: string, 449 | name: string, 450 | cast: string 451 | } 452 | }, 453 | {...}, 454 | ] 455 | } 456 | moreInfo: { 457 | aired: string, 458 | genres: ["Action", "Mystery", ...], 459 | status: string, 460 | studios: string, 461 | duration: string 462 | ... 463 | } 464 | ], 465 | mostPopularAnimes: [ 466 | { 467 | episodes: { 468 | sub: number, 469 | dub: number, 470 | }, 471 | id: string, 472 | jname: string, 473 | name: string, 474 | poster: string, 475 | type: string 476 | }, 477 | {...}, 478 | ], 479 | recommendedAnimes: [ 480 | { 481 | id: string, 482 | name: string, 483 | poster: string, 484 | duration: string, 485 | type: string, 486 | rating: string, 487 | episodes: { 488 | sub: number, 489 | dub: number, 490 | } 491 | }, 492 | {...}, 493 | ], 494 | relatedAnimes: [ 495 | { 496 | id: string, 497 | name: string, 498 | poster: string, 499 | duration: string, 500 | type: string, 501 | rating: string, 502 | episodes: { 503 | sub: number, 504 | dub: number, 505 | } 506 | }, 507 | {...}, 508 | ], 509 | seasons: [ 510 | { 511 | id: string, 512 | name: string, 513 | title: string, 514 | poster: string, 515 | isCurrent: boolean 516 | }, 517 | {...} 518 | ] 519 | } 520 | ``` 521 | 522 | [🔼 Back to Top](#table-of-contents) 523 | 524 |
525 | 526 |
527 | 528 | 529 | 530 | ### `getAnimeSearchResults` 531 | 532 | 533 | 534 | #### Parameters 535 | 536 | | Parameter | Type | Description | Required? | Default | 537 | | :----------: | :----: | :---------------------------------------------------------------: | :-------: | :-----: | 538 | | `q` | string | The search query, i.e. the title of the item you are looking for. | Yes | -- | 539 | | `page` | number | The page number of the result. | No | `1` | 540 | | `type` | string | Type of the anime. eg: `movie` | No | -- | 541 | | `status` | string | Status of the anime. eg: `finished-airing` | No | -- | 542 | | `rated` | string | Rating of the anime. eg: `r+` or `pg-13` | No | -- | 543 | | `score` | string | Score of the anime. eg: `good` or `very-good` | No | -- | 544 | | `season` | string | Season of the aired anime. eg: `spring` | No | -- | 545 | | `language` | string | Language category of the anime. eg: `sub` or `sub-&-dub` | No | -- | 546 | | `start_date` | string | Start date of the anime(yyyy-mm-dd). eg: `2014-10-2` | No | -- | 547 | | `end_date` | string | End date of the anime(yyyy-mm-dd). eg: `2010-12-4` | No | -- | 548 | | `sort` | string | Order of sorting the anime result. eg: `recently-added` | No | -- | 549 | | `genres` | string | Genre of the anime, separated by commas. eg: `isekai,shounen` | No | -- | 550 | 551 | > [!TIP] 552 | > 553 | > For both `start_date` and `end_date`, year must be mentioned. If you wanna omit date or month specify `0` instead. Eg: omitting date -> 2014-10-0, omitting month -> 2014-0-12, omitting both -> 2014-0-0 554 | 555 | #### Sample Usage 556 | 557 | ```javascript 558 | import { HiAnime } from "aniwatch"; 559 | 560 | const hianime = new HiAnime.Scraper(); 561 | 562 | hianime 563 | .search("monster", 1, { 564 | genres: "seinen,psychological", 565 | }) 566 | .then((data) => { 567 | console.log(data); 568 | }) 569 | .catch((err) => { 570 | console.error(err); 571 | }); 572 | ``` 573 | 574 | #### Response Schema 575 | 576 | ```javascript 577 | { 578 | animes: [ 579 | { 580 | id: string, 581 | name: string, 582 | poster: string, 583 | duration: string, 584 | type: string, 585 | rating: string, 586 | episodes: { 587 | sub: number, 588 | dub: number, 589 | } 590 | }, 591 | {...}, 592 | ], 593 | mostPopularAnimes: [ 594 | { 595 | episodes: { 596 | sub: number, 597 | dub: number, 598 | }, 599 | id: string, 600 | jname: string, 601 | name: string, 602 | poster: string, 603 | type: string 604 | }, 605 | {...}, 606 | ], 607 | currentPage: 1, 608 | totalPages: 1, 609 | hasNextPage: false, 610 | searchQuery: string, 611 | searchFilters: { 612 | [filter_name]: [filter_value] 613 | ... 614 | } 615 | } 616 | ``` 617 | 618 | [🔼 Back to Top](#table-of-contents) 619 | 620 |
621 | 622 |
623 | 624 | 625 | 626 | ### `getAnimeSearchSuggestion` 627 | 628 | 629 | 630 | #### Parameters 631 | 632 | | Parameter | Type | Description | Required? | Default | 633 | | :-------: | :----: | :--------------------------: | :-------: | :-----: | 634 | | `q` | string | The search suggestion query. | Yes | -- | 635 | 636 | #### Sample Usage 637 | 638 | ```javascript 639 | import { HiAnime } from "aniwatch"; 640 | 641 | const hianime = new HiAnime.Scraper(); 642 | 643 | hianime 644 | .searchSuggestions("one piece") 645 | .then((data) => console.log(data)) 646 | .catch((err) => console.error(err)); 647 | ``` 648 | 649 | #### Response Schema 650 | 651 | ```javascript 652 | { 653 | suggestions: [ 654 | { 655 | id: string, 656 | name: string, 657 | poster: string, 658 | jname: string, 659 | moreInfo: ["Mar 4, 2000", "Movie", "50m"] 660 | }, 661 | {...}, 662 | ], 663 | } 664 | ``` 665 | 666 | [🔼 Back to Top](#table-of-contents) 667 | 668 |
669 | 670 |
671 | 672 | 673 | 674 | ### `getProducerAnimes` 675 | 676 | 677 | 678 | #### Parameters 679 | 680 | | Parameter | Type | Description | Required? | Default | 681 | | :-------: | :----: | :-----------------------------------------: | :-------: | :-----: | 682 | | `name` | string | The name of anime producer (in kebab case). | Yes | 683 | | `page` | number | The page number of the result. | No | `1` | 684 | 685 | #### Sample Usage 686 | 687 | ```javascript 688 | import { HiAnime } from "aniwatch"; 689 | 690 | const hianime = new HiAnime.Scraper(); 691 | 692 | hianime 693 | .getProducerAnimes("toei-animation", 2) 694 | .then((data) => console.log(data)) 695 | .catch((err) => console.error(err)); 696 | ``` 697 | 698 | #### Response Schema 699 | 700 | ```javascript 701 | { 702 | producerName: "Toei Animation Anime", 703 | animes: [ 704 | { 705 | id: string, 706 | name: string, 707 | poster: string, 708 | duration: string, 709 | type: string, 710 | rating: string, 711 | episodes: { 712 | sub: number, 713 | dub: number, 714 | } 715 | }, 716 | {...}, 717 | ], 718 | top10Animes: { 719 | today: [ 720 | { 721 | episodes: { 722 | sub: number, 723 | dub: number, 724 | }, 725 | id: string, 726 | name: string, 727 | poster: string, 728 | rank: number 729 | }, 730 | {...}, 731 | ], 732 | month: [...], 733 | week: [...] 734 | }, 735 | topAiringAnimes: [ 736 | { 737 | episodes: { 738 | sub: number, 739 | dub: number, 740 | }, 741 | id: string, 742 | jname: string, 743 | name: string, 744 | poster: string, 745 | type: string 746 | }, 747 | {...}, 748 | ], 749 | currentPage: 2, 750 | totalPages: 11, 751 | hasNextPage: true, 752 | } 753 | ``` 754 | 755 | [🔼 Back to Top](#table-of-contents) 756 | 757 |
758 | 759 |
760 | 761 | 762 | 763 | ### `getGenreAnime` 764 | 765 | 766 | 767 | #### Parameters 768 | 769 | | Parameter | Type | Description | Required? | Default | 770 | | :-------: | :----: | :--------------------------------------: | :-------: | :-----: | 771 | | `name` | string | The name of anime genre (in kebab case). | Yes | -- | 772 | | `page` | number | The page number of the result. | No | `1` | 773 | 774 | #### Sample Usage 775 | 776 | ```javascript 777 | import { HiAnime } from "aniwatch"; 778 | 779 | const hianime = new HiAnime.Scraper(); 780 | 781 | hianime 782 | .getGenreAnime("shounen", 2) 783 | .then((data) => console.log(data)) 784 | .catch((err) => console.error(err)); 785 | ``` 786 | 787 | #### Response Schema 788 | 789 | ```javascript 790 | { 791 | genreName: "Shounen Anime", 792 | animes: [ 793 | { 794 | id: string, 795 | name: string, 796 | poster: string, 797 | duration: string, 798 | type: string, 799 | rating: string, 800 | episodes: { 801 | sub: number, 802 | dub: number, 803 | } 804 | }, 805 | {...}, 806 | ], 807 | genres: ["Action", "Cars", "Adventure", ...], 808 | topAiringAnimes: [ 809 | { 810 | episodes: { 811 | sub: number, 812 | dub: number, 813 | }, 814 | id: string, 815 | jname: string, 816 | name: string, 817 | poster: string, 818 | type: string 819 | }, 820 | {...}, 821 | ], 822 | currentPage: 2, 823 | totalPages: 38, 824 | hasNextPage: true 825 | } 826 | ``` 827 | 828 | [🔼 Back to Top](#table-of-contents) 829 | 830 |
831 | 832 |
833 | 834 | 835 | 836 | ### `getAnimeCategory` 837 | 838 | 839 | 840 | #### Parameters 841 | 842 | | Parameter | Type | Description | Required? | Default | 843 | | :--------: | :----: | :----------------------------: | :-------: | :-----: | 844 | | `category` | string | The category of anime. | Yes | -- | 845 | | `page` | number | The page number of the result. | No | `1` | 846 | 847 | #### Sample Usage 848 | 849 | ```javascript 850 | import { HiAnime } from "aniwatch"; 851 | 852 | const hianime = new HiAnime.Scraper(); 853 | 854 | hianime 855 | .getCategoryAnime("subbed-anime") 856 | .then((data) => console.log(data)) 857 | .catch((err) => console.error(err)); 858 | 859 | // categories -> 860 | // "most-favorite", "most-popular", "subbed-anime", "dubbed-anime", 861 | // "recently-updated", "recently-added", "top-upcoming", "top-airing", 862 | // "movie", "special", "ova", "ona", "tv", "completed" 863 | ``` 864 | 865 | #### Response Schema 866 | 867 | ```javascript 868 | { 869 | category: "TV Series Anime", 870 | animes: [ 871 | { 872 | id: string, 873 | name: string, 874 | poster: string, 875 | duration: string, 876 | type: string, 877 | rating: string, 878 | episodes: { 879 | sub: number, 880 | dub: number, 881 | } 882 | }, 883 | {...}, 884 | ], 885 | genres: ["Action", "Cars", "Adventure", ...], 886 | top10Animes: { 887 | today: [ 888 | { 889 | episodes: { 890 | sub: number, 891 | dub: number, 892 | }, 893 | id: string, 894 | name: string, 895 | poster: string, 896 | rank: number 897 | }, 898 | {...}, 899 | ], 900 | month: [...], 901 | week: [...] 902 | }, 903 | currentPage: 2, 904 | totalPages: 100, 905 | hasNextPage: true 906 | } 907 | ``` 908 | 909 | [🔼 Back to Top](#table-of-contents) 910 | 911 |
912 | 913 |
914 | 915 | 916 | 917 | ### `getEstimatedSchedule` 918 | 919 | 920 | 921 | #### Parameters 922 | 923 | | Parameter | Type | Description | Required? | Default | 924 | | :-----------------: | :----: | :------------------------------------------------------------------: | :-------: | :-----: | 925 | | `date (yyyy-mm-dd)` | string | The date of the desired schedule. (months & days must have 2 digits) | Yes | -- | 926 | | `tzOffset` | number | The timezone offset in minutes (defaults to -330 i.e. IST) | No | `-330` | 927 | 928 | #### Sample Usage 929 | 930 | ```javascript 931 | import { HiAnime } from "aniwatch"; 932 | 933 | const hianime = new HiAnime.Scraper(); 934 | const timezoneOffset = -330; // IST offset in minutes 935 | 936 | hianime 937 | .getEstimatedSchedule("2025-06-09", timezoneOffset) 938 | .then((data) => console.log(data)) 939 | .catch((err) => console.error(err)); 940 | ``` 941 | 942 | #### Response Schema 943 | 944 | ```javascript 945 | { 946 | scheduledAnimes: [ 947 | { 948 | id: string, 949 | time: string, // 24 hours format 950 | name: string, 951 | jname: string, 952 | airingTimestamp: number, 953 | secondsUntilAiring: number 954 | }, 955 | {...} 956 | ] 957 | } 958 | ``` 959 | 960 | [🔼 Back to Top](#table-of-contents) 961 | 962 |
963 | 964 | ## 965 | 966 |
967 | 968 | 969 | 970 | ### `getNextEpisodeSchedule` 971 | 972 | 973 | 974 | #### Parameters 975 | 976 | | Parameter | Type | Description | Required? | Default | 977 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 978 | | `animeId` | string | The unique anime id (in kebab case). | Yes | -- | 979 | 980 | #### Sample Usage 981 | 982 | ```javascript 983 | import { HiAnime } from "aniwatch"; 984 | 985 | const hianime = new HiAnime.Scraper(); 986 | 987 | hianime 988 | .getNextEpisodeSchedule("one-piece-100") 989 | .then((data) => console.log(data)) 990 | .catch((err) => console.error(err)); 991 | ``` 992 | 993 | #### Response Schema 994 | 995 | ```javascript 996 | { 997 | airingISOTimestamp: string | null, 998 | airingTimestamp: number | null, 999 | secondsUntilAiring: number | null 1000 | } 1001 | ``` 1002 | 1003 | [🔼 Back to Top](#table-of-contents) 1004 | 1005 |
1006 | 1007 |
1008 | 1009 | 1010 | 1011 | ### `getAnimeEpisodes` 1012 | 1013 | 1014 | 1015 | #### Parameters 1016 | 1017 | | Parameter | Type | Description | Required? | Default | 1018 | | :-------: | :----: | :------------------: | :-------: | :-----: | 1019 | | `animeId` | string | The unique anime id. | Yes | -- | 1020 | 1021 | #### Sample Usage 1022 | 1023 | ```javascript 1024 | import { HiAnime } from "aniwatch"; 1025 | 1026 | const hianime = new HiAnime.Scraper(); 1027 | 1028 | hianime 1029 | .getEpisodes("steinsgate-3") 1030 | .then((data) => console.log(data)) 1031 | .catch((err) => console.error(err)); 1032 | ``` 1033 | 1034 | #### Response Schema 1035 | 1036 | ```javascript 1037 | { 1038 | totalEpisodes: 24, 1039 | episodes: [ 1040 | { 1041 | number: 1, 1042 | isFiller: false, 1043 | title: "Turning Point", 1044 | episodeId: "steinsgate-3?ep=213" 1045 | }, 1046 | {...} 1047 | ] 1048 | } 1049 | ``` 1050 | 1051 | [🔼 Back to Top](#table-of-contents) 1052 | 1053 |
1054 | 1055 |
1056 | 1057 | 1058 | 1059 | ### `getEpisodeServers` 1060 | 1061 | 1062 | 1063 | #### Parameters 1064 | 1065 | | Parameter | Type | Description | Required? | Default | 1066 | | :---------: | :----: | :--------------------: | :-------: | :-----: | 1067 | | `episodeId` | string | The unique episode id. | Yes | -- | 1068 | 1069 | #### Request sample 1070 | 1071 | ```javascript 1072 | import { HiAnime } from "aniwatch"; 1073 | 1074 | const hianime = new HiAnime.Scraper(); 1075 | 1076 | hianime 1077 | .getEpisodeServers("steinsgate-0-92?ep=2055") 1078 | .then((data) => console.log(data)) 1079 | .catch((err) => console.error(err)); 1080 | ``` 1081 | 1082 | #### Response Schema 1083 | 1084 | ```javascript 1085 | { 1086 | episodeId: "steinsgate-0-92?ep=2055", 1087 | episodeNo: 5, 1088 | sub: [ 1089 | { 1090 | serverId: 4, 1091 | serverName: "vidstreaming", 1092 | }, 1093 | {...} 1094 | ], 1095 | dub: [ 1096 | { 1097 | serverId: 1, 1098 | serverName: "megacloud", 1099 | }, 1100 | {...} 1101 | ], 1102 | raw: [ 1103 | { 1104 | serverId: 1, 1105 | serverName: "megacloud", 1106 | }, 1107 | {...} 1108 | ], 1109 | } 1110 | ``` 1111 | 1112 | [🔼 Back to Top](#table-of-contents) 1113 | 1114 |
1115 | 1116 |
1117 | 1118 | 1119 | 1120 | ### `getAnimeEpisodeSources` 1121 | 1122 | 1123 | 1124 | #### Parameters 1125 | 1126 | | Parameter | Type | Description | Required? | Default | 1127 | | :--------: | :----: | :--------------------------------------------------: | :-------: | :--------------: | 1128 | | `id` | string | The id of the episode. | Yes | -- | 1129 | | `server` | string | The name of the server. | No | `"vidstreaming"` | 1130 | | `category` | string | The category of the episode ('sub', 'dub' or 'raw'). | No | `"sub"` | 1131 | 1132 | #### Request sample 1133 | 1134 | ```javascript 1135 | import { HiAnime } from "aniwatch"; 1136 | 1137 | const hianime = new HiAnime.Scraper(); 1138 | 1139 | hianime 1140 | .getEpisodeSources("steinsgate-3?ep=230", "hd-1", "sub") 1141 | .then((data) => console.log(data)) 1142 | .catch((err) => console.error(err)); 1143 | ``` 1144 | 1145 | #### Response Schema 1146 | 1147 | ```javascript 1148 | { 1149 | headers: { 1150 | Referer: string, 1151 | "User-Agent": string, 1152 | ... 1153 | }, 1154 | sources: [ 1155 | { 1156 | url: string, // .m3u8 hls streaming file 1157 | isM3U8: boolean, 1158 | quality?: string, 1159 | }, 1160 | {...} 1161 | ], 1162 | subtitles: [ 1163 | { 1164 | lang: "English", 1165 | url: string, // .vtt subtitle file 1166 | }, 1167 | {...} 1168 | ], 1169 | anilistID: number | null, 1170 | malID: number | null, 1171 | } 1172 | ``` 1173 | 1174 | [🔼 Back to Top](#table-of-contents) 1175 | 1176 |
1177 | 1178 | ## Development 1179 | 1180 | Pull requests are always welcome. If you encounter any bug or want to add a new feature to this package, consider creating a new [issue](https://github.com/ghoshRitesh12/aniwatch/issues). If you wish to contribute to this project, read the [CONTRIBUTING.md](https://github.com/ghoshRitesh12/aniwatch/blob/main/CONTRIBUTING.md) file. 1181 | 1182 | ## Contributors 1183 | 1184 | Thanks to the following people for keeping this project alive and relevant. 1185 | 1186 | [![](https://contrib.rocks/image?repo=ghoshRitesh12/aniwatch)](https://github.com/ghoshRitesh12/aniwatch/graphs/contributors) 1187 | 1188 | ## Thanks 1189 | 1190 | - [consumet.ts](https://github.com/consumet/consumet.ts) 1191 | - [api.consumet.org](https://github.com/consumet/api.consumet.org) 1192 | 1193 | ## Support 1194 | 1195 | Don't forget to leave a star 🌟. You can also follow me on X (Twitter) [@riteshgsh](https://x.com/riteshgsh). 1196 | 1197 | ## License 1198 | 1199 | This project is licensed under the [MIT License](https://opensource.org/license/mit/) - see the [LICENSE](https://github.com/ghoshRitesh12/aniwatch/blob/main/LICENSE) file for more details. 1200 | 1201 | 1203 | 1204 | ## Star History 1205 | 1206 | 1210 | -------------------------------------------------------------------------------- /__tests__/hianime/animeAZList.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | // npx vitest run animeAZList.test.ts 5 | test("returns az list anime", async () => { 6 | const hianime = new HiAnime.Scraper(); 7 | const data = await hianime.getAZList("0-9", 1); 8 | 9 | expect(data.animes).not.toEqual([]); 10 | }); 11 | -------------------------------------------------------------------------------- /__tests__/hianime/animeAboutInfo.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | // npx vitest run animeAboutInfo.test.ts 5 | test("returns information about an anime", async () => { 6 | const hianime = new HiAnime.Scraper(); 7 | const data = await hianime.getInfo("steinsgate-3"); 8 | 9 | expect(data.anime.info.name).not.toEqual(null); 10 | expect(data.recommendedAnimes).not.toEqual([]); 11 | expect(data.mostPopularAnimes).not.toEqual([]); 12 | expect(Object.keys(data.anime.moreInfo)).not.toEqual([]); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/hianime/animeCategory.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | // npx vitest run animeCategory.test.ts 5 | test("returns animes belonging to a category", async () => { 6 | const hianime = new HiAnime.Scraper(); 7 | const data = await hianime.getCategoryAnime("subbed-anime"); 8 | 9 | expect(data.animes).not.toEqual([]); 10 | expect(data.genres).not.toEqual([]); 11 | expect(data.top10Animes.today).not.toEqual([]); 12 | expect(data.top10Animes.week).not.toEqual([]); 13 | expect(data.top10Animes.month).not.toEqual([]); 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/hianime/animeEpisodeSrcs.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | const animeEpisodeId = "attack-on-titan-112?ep=3304"; 5 | 6 | // npx vitest run animeEpisodeSrcs.test.ts 7 | test(`returns ${animeEpisodeId} episode streaming link(s)`, async () => { 8 | const hianime = new HiAnime.Scraper(); 9 | const data = await hianime.getEpisodeSources(animeEpisodeId, "hd-1", "sub"); 10 | 11 | expect(data.sources).not.toEqual([]); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/hianime/animeEpisodes.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | // npx vitest run animeEpisodes.test.ts 5 | test("returns episodes info of an anime", async () => { 6 | const hianime = new HiAnime.Scraper(); 7 | const data = await hianime.getEpisodes("steinsgate-3"); 8 | 9 | expect(data.totalEpisodes).not.toEqual(0); 10 | expect(data.episodes).not.toEqual([]); 11 | }); 12 | -------------------------------------------------------------------------------- /__tests__/hianime/animeGenre.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | // npx vitest run animeGenre.test.ts 5 | test("returns animes belonging to a genre", async () => { 6 | const hianime = new HiAnime.Scraper(); 7 | const data = await hianime.getGenreAnime("shounen", 2); 8 | 9 | expect(data.animes).not.toEqual([]); 10 | expect(data.genres).not.toEqual([]); 11 | expect(data.topAiringAnimes).not.toEqual([]); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/hianime/animeProducer.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | // npx vitest run animeProducer.test.ts 5 | test("returns animes produced by a producer", async () => { 6 | const hianime = new HiAnime.Scraper(); 7 | const data = await hianime.getProducerAnimes("toei-animation", 2); 8 | 9 | expect(data.animes).not.toEqual([]); 10 | expect(data.topAiringAnimes).not.toEqual([]); 11 | expect(data.top10Animes.today).not.toEqual([]); 12 | expect(data.top10Animes.week).not.toEqual([]); 13 | expect(data.top10Animes.month).not.toEqual([]); 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/hianime/animeQtip.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | const animeId = "one-piece-100"; 5 | 6 | // npx vitest run animeQtip.test.ts 7 | test(`returns ${animeId} anime qtip info`, async () => { 8 | const hianime = new HiAnime.Scraper(); 9 | const data = await hianime.getQtipInfo(animeId); 10 | 11 | expect(data.anime.id).not.toEqual(null); 12 | expect(data.anime.name).not.toEqual(null); 13 | expect(data.anime.description).not.toEqual(null); 14 | expect(data.anime.genres).not.toEqual([]); 15 | }); 16 | -------------------------------------------------------------------------------- /__tests__/hianime/animeSearch.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | // npx vitest run animeSearch.test.ts 5 | test("returns animes related to search query", async () => { 6 | const hianime = new HiAnime.Scraper(); 7 | 8 | const data = await hianime.search("monster", 1, { 9 | genres: "seinen,psychological", 10 | }); 11 | 12 | expect(data.animes).not.toEqual([]); 13 | expect(data.mostPopularAnimes).not.toEqual([]); 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/hianime/animeSearchSuggestion.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | // npx vitest run animeSearchSuggestion.test.ts 5 | test("returns animes search suggestions related to search query", async () => { 6 | const hianime = new HiAnime.Scraper(); 7 | const data = await hianime.searchSuggestions("one piece"); 8 | 9 | expect(data.suggestions).not.toEqual([]); 10 | }); 11 | -------------------------------------------------------------------------------- /__tests__/hianime/episodeServers.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | // npx vitest run episodeServers.test.ts 5 | test("returns episode source servers", async () => { 6 | const hianime = new HiAnime.Scraper(); 7 | const data = await hianime.getEpisodeServers("steinsgate-0-92?ep=2055"); 8 | 9 | expect(data.episodeId).not.toEqual(null); 10 | expect(data.episodeNo).not.toEqual(0); 11 | expect(data.sub).not.toEqual([]); 12 | expect(data.dub).not.toEqual([]); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/hianime/estimatedSchedule.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | function padZero(num: number) { 5 | return num < 10 ? `0${num}` : num.toString(); 6 | } 7 | 8 | // npx vitest run episodeSchedule.test.ts 9 | test("returns estimated schedule anime release", async () => { 10 | const hianime = new HiAnime.Scraper(); 11 | 12 | const d = new Date(); 13 | const data = await hianime.getEstimatedSchedule( 14 | `${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(d.getDate())}` 15 | ); 16 | 17 | expect(data.scheduledAnimes).not.toEqual([]); 18 | }); 19 | -------------------------------------------------------------------------------- /__tests__/hianime/homePage.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | // npx vitest run homePage.test.ts 5 | test("returns anime information present in homepage", async () => { 6 | const hianime = new HiAnime.Scraper(); 7 | const data = await hianime.getHomePage(); 8 | 9 | expect(data.spotlightAnimes).not.toEqual([]); 10 | expect(data.trendingAnimes).not.toEqual([]); 11 | expect(data.latestEpisodeAnimes).not.toEqual([]); 12 | expect(data.topUpcomingAnimes).not.toEqual([]); 13 | expect(data.topAiringAnimes).not.toEqual([]); 14 | expect(data.mostPopularAnimes).not.toEqual([]); 15 | expect(data.mostFavoriteAnimes).not.toEqual([]); 16 | expect(data.latestCompletedAnimes).not.toEqual([]); 17 | expect(data.genres).not.toEqual([]); 18 | 19 | expect(data.top10Animes.today).not.toEqual([]); 20 | expect(data.top10Animes.week).not.toEqual([]); 21 | expect(data.top10Animes.month).not.toEqual([]); 22 | }); 23 | -------------------------------------------------------------------------------- /__tests__/hianime/nextEpisodeSchedule.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { HiAnime } from "../../src/index.js"; 3 | 4 | function padZero(num: number) { 5 | return num < 10 ? `0${num}` : num.toString(); 6 | } 7 | 8 | // npx vitest run episodeSchedule.test.ts 9 | test("returns anime next episode schedule", async () => { 10 | const hianime = new HiAnime.Scraper(); 11 | 12 | const d = new Date(); 13 | const scheduleData = await hianime.getEstimatedSchedule( 14 | `${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(d.getDate())}` 15 | ); 16 | 17 | const animeId = scheduleData.scheduledAnimes[0].id!; 18 | const data = await hianime.getNextEpisodeSchedule(animeId); 19 | 20 | expect(data.airingISOTimestamp).not.toEqual(null); 21 | expect(data.airingTimestamp).not.toEqual(null); 22 | expect(data.secondsUntilAiring).not.toEqual(null); 23 | }); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aniwatch", 3 | "version": "2.23.0", 4 | "description": "📦 A scraper package serving anime information from hianimez.to", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "module": "dist/index.js", 9 | "type": "module", 10 | "scripts": { 11 | "lint": "tsc", 12 | "build": "tsup", 13 | "ci": "tsc && pnpm test && tsup", 14 | "test": "vitest run --config vitest.config.ts", 15 | "prepare": "node .husky/install.mjs", 16 | "format": "prettier --cache --write .", 17 | "format:check": "prettier --cache --check ." 18 | }, 19 | "author": { 20 | "name": "Ritesh Ghosh", 21 | "url": "https://github.com/ghoshRitesh12" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/ghoshRitesh12/aniwatch.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/ghoshRitesh12/aniwatch/issues" 29 | }, 30 | "homepage": "https://github.com/ghoshRitesh12/aniwatch#readme", 31 | "keywords": [ 32 | "anime", 33 | "hianime", 34 | "aniwatch", 35 | "hianimez.to", 36 | "aniwatch.to", 37 | "scraper", 38 | "package" 39 | ], 40 | "dependencies": { 41 | "axios": "^1.7.9", 42 | "cheerio": "1.0.0", 43 | "crypto-js": "^4.2.0", 44 | "pino": "^9.6.0" 45 | }, 46 | "devDependencies": { 47 | "@types/crypto-js": "^4.2.2", 48 | "@types/node": "^22.10.3", 49 | "husky": "^9.1.7", 50 | "pino-pretty": "^13.0.0", 51 | "prettier": "^3.5.3", 52 | "tsup": "^8.3.5", 53 | "typescript": "^5.7.2", 54 | "vitest": "^2.1.8" 55 | }, 56 | "files": [ 57 | "dist", 58 | "LICENSE", 59 | "package.json" 60 | ] 61 | } -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import("prettier").Config} 3 | */ 4 | export default { 5 | semi: true, 6 | tabWidth: 4, 7 | useTabs: false, 8 | printWidth: 80, 9 | singleQuote: false, 10 | arrowParens: "always", 11 | trailingComma: "es5", 12 | singleAttributePerLine: true, 13 | }; 14 | -------------------------------------------------------------------------------- /scripts/format-package-json.js: -------------------------------------------------------------------------------- 1 | import { readFile, writeFileSync } from "fs"; 2 | 3 | export function preCommit({ tag, version }) { 4 | readFile("package.json", "utf8", (err, data) => { 5 | if (err) { 6 | console.error("Error reading file:", err); 7 | return; 8 | } 9 | 10 | console.log({ tag, version }); 11 | 12 | const json = JSON.parse(data); 13 | const jsonString = JSON.stringify(json, null, 4); 14 | 15 | writeFileSync("package.json", jsonString); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/config/client.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, type AxiosRequestConfig } from "axios"; 2 | import { 3 | // SRC_BASE_URL, 4 | ACCEPT_HEADER, 5 | USER_AGENT_HEADER, 6 | ACCEPT_ENCODING_HEADER, 7 | } from "../utils/constants.js"; 8 | 9 | const clientConfig: AxiosRequestConfig = { 10 | timeout: 8000, 11 | // baseURL: SRC_BASE_URL, 12 | headers: { 13 | Accept: ACCEPT_HEADER, 14 | "User-Agent": USER_AGENT_HEADER, 15 | "Accept-Encoding": ACCEPT_ENCODING_HEADER, 16 | }, 17 | }; 18 | 19 | const client = axios.create(clientConfig); 20 | 21 | export { client, AxiosError }; 22 | -------------------------------------------------------------------------------- /src/config/error.ts: -------------------------------------------------------------------------------- 1 | export interface AniwatchError extends Error { 2 | scraper: string; 3 | status: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/config/logger.ts: -------------------------------------------------------------------------------- 1 | import { pino, type LoggerOptions } from "pino"; 2 | 3 | function isDevEnv(): boolean { 4 | return ( 5 | !process.env.NODE_ENV || 6 | process.env.NODE_ENV === "development" || 7 | process.env.NODE_ENV === "test" 8 | ); 9 | } 10 | 11 | const loggerOptions: LoggerOptions = { 12 | level: "info", 13 | transport: isDevEnv() 14 | ? { 15 | target: "pino-pretty", 16 | options: { 17 | colorize: true, 18 | translateTime: "SYS:standard", 19 | }, 20 | } 21 | : undefined, 22 | formatters: { 23 | level(label) { 24 | return { 25 | level: label.toUpperCase(), 26 | context: "aniwatch-pkg", 27 | }; 28 | }, 29 | }, 30 | redact: !isDevEnv() ? ["hostname"] : [], 31 | timestamp: pino.stdTimeFunctions.isoTime, 32 | }; 33 | 34 | export const log = pino(loggerOptions); 35 | -------------------------------------------------------------------------------- /src/extractors/index.ts: -------------------------------------------------------------------------------- 1 | import StreamSB from "./streamsb.js"; 2 | import StreamTape from "./streamtape.js"; 3 | import RapidCloud from "./rapidcloud.js"; 4 | import MegaCloud from "./megacloud.js"; 5 | 6 | export { StreamSB, StreamTape, RapidCloud, MegaCloud }; 7 | -------------------------------------------------------------------------------- /src/extractors/megacloud.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import crypto from "crypto"; 3 | import { HiAnimeError } from "../hianime/error.js"; 4 | import { getSources } from "./megacloud.getsrcs.js"; 5 | 6 | // https://megacloud.tv/embed-2/e-1/dBqCr5BcOhnD?k=1 7 | 8 | const megacloud = { 9 | script: "https://megacloud.tv/js/player/a/prod/e1-player.min.js?v=", 10 | sources: "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=", 11 | } as const; 12 | 13 | export type track = { 14 | file: string; 15 | kind: string; 16 | label?: string; 17 | default?: boolean; 18 | }; 19 | 20 | type intro_outro = { 21 | start: number; 22 | end: number; 23 | }; 24 | 25 | export type unencryptedSrc = { 26 | file: string; 27 | type: string; 28 | }; 29 | 30 | export type extractedSrc = { 31 | sources: string | unencryptedSrc[]; 32 | tracks: track[]; 33 | encrypted: boolean; 34 | intro: intro_outro; 35 | outro: intro_outro; 36 | server: number; 37 | }; 38 | 39 | type ExtractedData = Pick & { 40 | sources: { url: string; type: string }[]; 41 | }; 42 | 43 | class MegaCloud { 44 | // private serverName = "megacloud"; 45 | 46 | async extract(videoUrl: URL) { 47 | try { 48 | const extractedData: ExtractedData = { 49 | tracks: [], 50 | intro: { 51 | start: 0, 52 | end: 0, 53 | }, 54 | outro: { 55 | start: 0, 56 | end: 0, 57 | }, 58 | sources: [], 59 | }; 60 | 61 | const videoId = videoUrl?.href?.split("/")?.pop()?.split("?")[0]; 62 | const { data: srcsData } = await axios.get( 63 | megacloud.sources.concat(videoId || ""), 64 | { 65 | headers: { 66 | Accept: "*/*", 67 | "X-Requested-With": "XMLHttpRequest", 68 | "User-Agent": 69 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", 70 | Referer: videoUrl.href, 71 | }, 72 | } 73 | ); 74 | if (!srcsData) { 75 | throw new HiAnimeError( 76 | "Url may have an invalid video id", 77 | "getAnimeEpisodeSources", 78 | 400 79 | ); 80 | } 81 | 82 | // log.info(JSON.stringify(srcsData, null, 2)); 83 | 84 | const encryptedString = srcsData.sources; 85 | if (!srcsData.encrypted && Array.isArray(encryptedString)) { 86 | extractedData.intro = srcsData.intro; 87 | extractedData.outro = srcsData.outro; 88 | extractedData.tracks = srcsData.tracks; 89 | extractedData.sources = encryptedString.map((s) => ({ 90 | url: s.file, 91 | type: s.type, 92 | })); 93 | 94 | return extractedData; 95 | } 96 | 97 | let text: string; 98 | const { data } = await axios.get( 99 | megacloud.script.concat(Date.now().toString()) 100 | ); 101 | 102 | text = data; 103 | if (!text) { 104 | throw new HiAnimeError( 105 | "Couldn't fetch script to decrypt resource", 106 | "getAnimeEpisodeSources", 107 | 500 108 | ); 109 | } 110 | 111 | const vars = this.extractVariables(text); 112 | if (!vars.length) { 113 | throw new Error( 114 | "Can't find variables. Perhaps the extractor is outdated." 115 | ); 116 | } 117 | 118 | const { secret, encryptedSource } = this.getSecret( 119 | encryptedString as string, 120 | vars 121 | ); 122 | const decrypted = this.decrypt(encryptedSource, secret); 123 | try { 124 | const sources = JSON.parse(decrypted); 125 | extractedData.intro = srcsData.intro; 126 | extractedData.outro = srcsData.outro; 127 | extractedData.tracks = srcsData.tracks; 128 | extractedData.sources = sources.map((s: any) => ({ 129 | url: s.file, 130 | type: s.type, 131 | })); 132 | 133 | return extractedData; 134 | } catch (error) { 135 | throw new HiAnimeError( 136 | "Failed to decrypt resource", 137 | "getAnimeEpisodeSources", 138 | 500 139 | ); 140 | } 141 | } catch (err) { 142 | // log.info(err); 143 | throw err; 144 | } 145 | } 146 | 147 | private extractVariables(text: string) { 148 | // copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30' 149 | const regex = 150 | /case\s*0x[0-9a-f]+:(?![^;]*=partKey)\s*\w+\s*=\s*(\w+)\s*,\s*\w+\s*=\s*(\w+);/g; 151 | const matches = text.matchAll(regex); 152 | const vars = Array.from(matches, (match) => { 153 | const matchKey1 = this.matchingKey(match[1], text); 154 | const matchKey2 = this.matchingKey(match[2], text); 155 | try { 156 | return [parseInt(matchKey1, 16), parseInt(matchKey2, 16)]; 157 | } catch (e) { 158 | return []; 159 | } 160 | }).filter((pair) => pair.length > 0); 161 | 162 | return vars; 163 | } 164 | 165 | private getSecret(encryptedString: string, values: number[][]) { 166 | let secret = "", 167 | encryptedSource = "", 168 | encryptedSourceArray = encryptedString.split(""), 169 | currentIndex = 0; 170 | 171 | for (const index of values) { 172 | const start = index[0] + currentIndex; 173 | const end = start + index[1]; 174 | 175 | for (let i = start; i < end; i++) { 176 | secret += encryptedString[i]; 177 | encryptedSourceArray[i] = ""; 178 | } 179 | currentIndex += index[1]; 180 | } 181 | 182 | encryptedSource = encryptedSourceArray.join(""); 183 | 184 | return { secret, encryptedSource }; 185 | } 186 | 187 | private decrypt(encrypted: string, keyOrSecret: string, maybe_iv?: string) { 188 | let key; 189 | let iv; 190 | let contents; 191 | if (maybe_iv) { 192 | key = keyOrSecret; 193 | iv = maybe_iv; 194 | contents = encrypted; 195 | } else { 196 | // copied from 'https://github.com/brix/crypto-js/issues/468' 197 | const cypher = Buffer.from(encrypted, "base64"); 198 | const salt = cypher.subarray(8, 16); 199 | const password = Buffer.concat([ 200 | Buffer.from(keyOrSecret, "binary"), 201 | salt, 202 | ]); 203 | const md5Hashes = []; 204 | let digest = password; 205 | for (let i = 0; i < 3; i++) { 206 | md5Hashes[i] = crypto.createHash("md5").update(digest).digest(); 207 | digest = Buffer.concat([md5Hashes[i], password]); 208 | } 209 | key = Buffer.concat([md5Hashes[0], md5Hashes[1]]); 210 | iv = md5Hashes[2]; 211 | contents = cypher.subarray(16); 212 | } 213 | 214 | const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); 215 | const decrypted = 216 | decipher.update( 217 | contents as any, 218 | typeof contents === "string" ? "base64" : undefined, 219 | "utf8" 220 | ) + decipher.final(); 221 | 222 | return decrypted; 223 | } 224 | 225 | // function copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30' 226 | private matchingKey(value: string, script: string) { 227 | const regex = new RegExp(`,${value}=((?:0x)?([0-9a-fA-F]+))`); 228 | const match = script.match(regex); 229 | if (match) { 230 | return match[1].replace(/^0x/, ""); 231 | } else { 232 | throw new Error("Failed to match the key"); 233 | } 234 | } 235 | 236 | // https://megacloud.tv/embed-2/e-1/1hnXq7VzX0Ex?k=1 237 | async extract2(embedIframeURL: URL): Promise { 238 | try { 239 | const extractedData: ExtractedData = { 240 | tracks: [], 241 | intro: { 242 | start: 0, 243 | end: 0, 244 | }, 245 | outro: { 246 | start: 0, 247 | end: 0, 248 | }, 249 | sources: [], 250 | }; 251 | 252 | const xrax = embedIframeURL.pathname.split("/").pop() || ""; 253 | 254 | const resp = await getSources(xrax); 255 | if (!resp) return extractedData; 256 | 257 | if (Array.isArray(resp.sources)) { 258 | extractedData.sources = resp.sources.map((s) => ({ 259 | url: s.file, 260 | type: s.type, 261 | })); 262 | } 263 | extractedData.intro = resp.intro ? resp.intro : extractedData.intro; 264 | extractedData.outro = resp.outro ? resp.outro : extractedData.outro; 265 | extractedData.tracks = resp.tracks; 266 | 267 | return extractedData; 268 | } catch (err) { 269 | throw err; 270 | } 271 | } 272 | } 273 | 274 | export default MegaCloud; 275 | -------------------------------------------------------------------------------- /src/extractors/rapidcloud.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import CryptoJS from "crypto-js"; 3 | import { log } from "../config/logger.js"; 4 | import { substringAfter, substringBefore } from "../utils/index.js"; 5 | import type { Video, Subtitle, Intro } from "../hianime/types/extractor.js"; 6 | 7 | type extractReturn = { 8 | sources: Video[]; 9 | subtitles: Subtitle[]; 10 | }; 11 | 12 | // https://megacloud.tv/embed-2/e-1/IxJ7GjGVCyml?k=1 13 | class RapidCloud { 14 | // private serverName = "RapidCloud"; 15 | private sources: Video[] = []; 16 | 17 | // https://rapid-cloud.co/embed-6/eVZPDXwVfrY3?vast=1 18 | private readonly fallbackKey = "c1d17096f2ca11b7"; 19 | private readonly host = "https://rapid-cloud.co"; 20 | 21 | async extract(videoUrl: URL): Promise { 22 | const result: extractReturn & { intro?: Intro; outro?: Intro } = { 23 | sources: [], 24 | subtitles: [], 25 | }; 26 | 27 | try { 28 | const id = videoUrl.href.split("/").pop()?.split("?")[0]; 29 | const options = { 30 | headers: { 31 | "X-Requested-With": "XMLHttpRequest", 32 | }, 33 | }; 34 | 35 | let res = null; 36 | 37 | res = await axios.get( 38 | `https://${videoUrl.hostname}/embed-2/ajax/e-1/getSources?id=${id}`, 39 | options 40 | ); 41 | 42 | let { 43 | data: { sources, tracks, intro, outro, encrypted }, 44 | } = res; 45 | 46 | let decryptKey = await ( 47 | await axios.get( 48 | "https://raw.githubusercontent.com/cinemaxhq/keys/e1/key" 49 | ) 50 | ).data; 51 | 52 | decryptKey = substringBefore( 53 | substringAfter( 54 | decryptKey, 55 | '"blob-code blob-code-inner js-file-line">' 56 | ), 57 | "" 58 | ); 59 | 60 | if (!decryptKey) { 61 | decryptKey = await ( 62 | await axios.get( 63 | "https://raw.githubusercontent.com/cinemaxhq/keys/e1/key" 64 | ) 65 | ).data; 66 | } 67 | 68 | if (!decryptKey) decryptKey = this.fallbackKey; 69 | 70 | try { 71 | if (encrypted) { 72 | const sourcesArray = sources.split(""); 73 | let extractedKey = ""; 74 | let currentIndex = 0; 75 | 76 | for (const index of decryptKey) { 77 | const start = index[0] + currentIndex; 78 | const end = start + index[1]; 79 | 80 | for (let i = start; i < end; i++) { 81 | extractedKey += res.data.sources[i]; 82 | sourcesArray[i] = ""; 83 | } 84 | currentIndex += index[1]; 85 | } 86 | 87 | decryptKey = extractedKey; 88 | sources = sourcesArray.join(""); 89 | 90 | const decrypt = CryptoJS.AES.decrypt(sources, decryptKey); 91 | sources = JSON.parse(decrypt.toString(CryptoJS.enc.Utf8)); 92 | } 93 | } catch (err: any) { 94 | log.info(err.message); 95 | throw new Error( 96 | "Cannot decrypt sources. Perhaps the key is invalid." 97 | ); 98 | } 99 | 100 | this.sources = sources?.map((s: any) => ({ 101 | url: s.file, 102 | isM3U8: s.file.includes(".m3u8"), 103 | })); 104 | 105 | result.sources.push(...this.sources); 106 | 107 | if (videoUrl.href.includes(new URL(this.host).host)) { 108 | result.sources = []; 109 | this.sources = []; 110 | 111 | for (const source of sources) { 112 | const { data } = await axios.get(source.file, options); 113 | const m3u8data = data 114 | .split("\n") 115 | .filter( 116 | (line: string) => 117 | line.includes(".m3u8") && 118 | line.includes("RESOLUTION=") 119 | ); 120 | 121 | const secondHalf = m3u8data.map((line: string) => 122 | line 123 | .match(/RESOLUTION=.*,(C)|URI=.*/g) 124 | ?.map((s) => s.split("=")[1]) 125 | ); 126 | 127 | const TdArray = secondHalf.map((s: string[]) => { 128 | const f1 = s[0].split(",C")[0]; 129 | const f2 = s[1].replace(/"/g, ""); 130 | 131 | return [f1, f2]; 132 | }); 133 | 134 | for (const [f1, f2] of TdArray) { 135 | this.sources.push({ 136 | url: `${source.file?.split("master.m3u8")[0]}${f2.replace( 137 | "iframes", 138 | "index" 139 | )}`, 140 | quality: f1.split("x")[1] + "p", 141 | isM3U8: f2.includes(".m3u8"), 142 | }); 143 | } 144 | result.sources.push(...this.sources); 145 | } 146 | } 147 | 148 | result.intro = 149 | intro?.end > 1 150 | ? { start: intro.start, end: intro.end } 151 | : undefined; 152 | result.outro = 153 | outro?.end > 1 154 | ? { start: outro.start, end: outro.end } 155 | : undefined; 156 | 157 | result.sources.push({ 158 | url: sources[0].file, 159 | isM3U8: sources[0].file.includes(".m3u8"), 160 | quality: "auto", 161 | }); 162 | 163 | result.subtitles = tracks 164 | .map((s: any) => 165 | s.file 166 | ? { 167 | url: s.file, 168 | lang: s.label ? s.label : "Thumbnails", 169 | } 170 | : null 171 | ) 172 | .filter((s: any) => s); 173 | 174 | return result; 175 | } catch (err: any) { 176 | log.info(err.message); 177 | throw err; 178 | } 179 | } 180 | } 181 | 182 | export default RapidCloud; 183 | -------------------------------------------------------------------------------- /src/extractors/streamsb.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import type { Video } from "../hianime/types/extractor.js"; 3 | import { USER_AGENT_HEADER } from "../utils/index.js"; 4 | 5 | class StreamSB { 6 | // private serverName = "streamSB"; 7 | private sources: Video[] = []; 8 | 9 | private readonly host = "https://watchsb.com/sources50"; 10 | private readonly host2 = "https://streamsss.net/sources16"; 11 | 12 | private PAYLOAD(hex: string): string { 13 | // `5363587530696d33443675687c7c${hex}7c7c433569475830474c497a65767c7c73747265616d7362`; 14 | return `566d337678566f743674494a7c7c${hex}7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362`; 15 | } 16 | 17 | async extract(videoUrl: URL, isAlt: boolean = false): Promise { 18 | let headers: Record = { 19 | watchsb: "sbstream", 20 | Referer: videoUrl.href, 21 | "User-Agent": USER_AGENT_HEADER, 22 | }; 23 | let id = videoUrl.href.split("/e/").pop(); 24 | if (id?.includes("html")) { 25 | id = id.split(".html")[0]; 26 | } 27 | const bytes = new TextEncoder().encode(id); 28 | 29 | const res = await axios 30 | .get( 31 | `${isAlt ? this.host2 : this.host}/${this.PAYLOAD( 32 | Buffer.from(bytes).toString("hex") 33 | )}`, 34 | { headers } 35 | ) 36 | .catch(() => null); 37 | 38 | if (!res?.data.stream_data) { 39 | throw new Error("No source found. Try a different server"); 40 | } 41 | 42 | headers = { 43 | "User-Agent": USER_AGENT_HEADER, 44 | Referer: videoUrl.href.split("e/")[0], 45 | }; 46 | 47 | const m3u8_urls = await axios.get(res.data.stream_data.file, { 48 | headers, 49 | }); 50 | 51 | const videoList = m3u8_urls?.data?.split("#EXT-X-STREAM-INF:") ?? []; 52 | 53 | for (const video of videoList) { 54 | if (!video.includes("m3u8")) continue; 55 | 56 | const url = video.split("\n")[1]; 57 | const quality = video 58 | .split("RESOLUTION=")[1] 59 | .split(",")[0] 60 | .split("x")[1]; 61 | 62 | this.sources.push({ 63 | url: url, 64 | quality: `${quality}p`, 65 | isM3U8: true, 66 | }); 67 | } 68 | 69 | this.sources.push({ 70 | url: res.data.stream_data.file, 71 | quality: "auto", 72 | isM3U8: res.data.stream_data.file.includes(".m3u8"), 73 | }); 74 | 75 | return this.sources; 76 | } 77 | 78 | // private addSources(source: any): void { 79 | // this.sources.push({ 80 | // url: source.file, 81 | // isM3U8: source.file.includes(".m3u8"), 82 | // }); 83 | // } 84 | } 85 | 86 | export default StreamSB; 87 | -------------------------------------------------------------------------------- /src/extractors/streamtape.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { load, type CheerioAPI } from "cheerio"; 3 | import type { Video } from "../hianime/types/extractor.js"; 4 | 5 | class StreamTape { 6 | // private serverName = "StreamTape"; 7 | private sources: Video[] = []; 8 | 9 | async extract(videoUrl: URL): Promise { 10 | try { 11 | const { data } = await axios.get(videoUrl.href).catch(() => { 12 | throw new Error("Video not found"); 13 | }); 14 | 15 | const $: CheerioAPI = load(data); 16 | 17 | let [fh, sh] = $.html() 18 | ?.match(/robotlink'\).innerHTML = (.*)'/)![1] 19 | .split("+ ('"); 20 | 21 | sh = sh.substring(3); 22 | fh = fh.replace(/\'/g, ""); 23 | 24 | const url = `https:${fh}${sh}`; 25 | 26 | this.sources.push({ 27 | url: url, 28 | isM3U8: url.includes(".m3u8"), 29 | }); 30 | 31 | return this.sources; 32 | } catch (err) { 33 | throw new Error((err as Error).message); 34 | } 35 | } 36 | } 37 | export default StreamTape; 38 | -------------------------------------------------------------------------------- /src/hianime/error.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import type { AniwatchError } from "../config/error.js"; 3 | import { log } from "../config/logger.js"; 4 | 5 | const ANSI_ESC_CODE_COLOR_RED = "\x1b[31m"; 6 | const ANSI_ESC_CODE_COLOR_RESET = "\x1b[0m"; 7 | 8 | export class HiAnimeError extends Error implements AniwatchError { 9 | static DEFAULT_ERROR_STATUS = 500; 10 | static DEFAULT_ERROR_MESSAGE = "Something went wrong"; 11 | 12 | public scraper: string = HiAnimeError.DEFAULT_ERROR_MESSAGE; 13 | public status: number = HiAnimeError.DEFAULT_ERROR_STATUS; 14 | 15 | constructor(errMsg: string, scraperName: string, status?: number) { 16 | super(`${scraperName}: ${errMsg}`); 17 | 18 | this.name = HiAnimeError.name; 19 | this.scraper = scraperName; 20 | 21 | if (status) { 22 | this.status = 23 | status >= 400 && status < 600 24 | ? status 25 | : HiAnimeError.DEFAULT_ERROR_STATUS; // default status 26 | } 27 | 28 | if (Error.captureStackTrace) { 29 | Error.captureStackTrace(this, HiAnimeError); 30 | } 31 | 32 | this.logError(); 33 | } 34 | 35 | static wrapError( 36 | err: HiAnimeError | any, 37 | scraperName: string 38 | ): HiAnimeError { 39 | if (err instanceof HiAnimeError) { 40 | return err; 41 | } 42 | 43 | if (err instanceof AxiosError) { 44 | const statusText = 45 | err?.response?.statusText || HiAnimeError.DEFAULT_ERROR_MESSAGE; 46 | return new HiAnimeError( 47 | "fetchError: " + statusText, 48 | scraperName, 49 | err.status || HiAnimeError.DEFAULT_ERROR_STATUS 50 | ); 51 | } 52 | 53 | return new HiAnimeError( 54 | err?.message || HiAnimeError.DEFAULT_ERROR_MESSAGE, 55 | scraperName 56 | ); 57 | } 58 | 59 | public json(): { status: number; message: string } { 60 | return { 61 | status: this.status, 62 | message: this.message, 63 | }; 64 | } 65 | 66 | private logError() { 67 | log.error( 68 | ANSI_ESC_CODE_COLOR_RED + 69 | JSON.stringify( 70 | { 71 | status: this.status, 72 | scraper: this.scraper, 73 | message: this.message, 74 | }, 75 | null, 76 | 2 77 | ) + 78 | ANSI_ESC_CODE_COLOR_RESET 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/hianime/hianime.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getAZList, 3 | getHomePage, 4 | getGenreAnime, 5 | getAnimeQtipInfo, 6 | getAnimeEpisodes, 7 | getAnimeCategory, 8 | getAnimeAboutInfo, 9 | getEpisodeServers, 10 | getProducerAnimes, 11 | getEstimatedSchedule, 12 | getAnimeSearchResults, 13 | getAnimeEpisodeSources, 14 | getNextEpisodeSchedule, 15 | getAnimeSearchSuggestion, 16 | } from "./scrapers/index.js"; 17 | 18 | import { 19 | Servers, 20 | type AnimeServers, 21 | type AnimeCategories, 22 | type AZListSortOptions, 23 | } from "./types/anime.js"; 24 | import type { SearchFilters } from "./types/animeSearch.js"; 25 | 26 | class Scraper { 27 | /** 28 | * @param {string} animeId - unique anime id 29 | * @throws {HiAnimeError} 30 | * @example 31 | * import { HiAnime } from "aniwatch"; 32 | * 33 | * const hianime = new HiAnime.Scraper() 34 | * 35 | * hianime.getInfo("steinsgate-3") 36 | * .then((data) => console.log(data)) 37 | * .catch((err) => console.error(err)); 38 | * 39 | */ 40 | async getInfo(animeId: string) { 41 | return getAnimeAboutInfo(animeId); 42 | } 43 | 44 | /** 45 | * @param {string} category - anime category 46 | * @param {number} page - page number, defaults to `1` 47 | * @throws {HiAnimeError} 48 | * @example 49 | * import { HiAnime } from "aniwatch"; 50 | * 51 | * const hianime = new HiAnime.Scraper() 52 | * 53 | * hianime.getCategoryAnime("subbed-anime") 54 | * .then((data) => console.log(data)) 55 | * .catch((err) => console.error(err)); 56 | * 57 | */ 58 | async getCategoryAnime(category: AnimeCategories, page: number = 1) { 59 | return getAnimeCategory(category, page); 60 | } 61 | 62 | /** 63 | * @param {string} animeId - unique anime id 64 | * @throws {HiAnimeError} 65 | * @example 66 | * import { HiAnime } from "aniwatch"; 67 | * 68 | * const hianime = new HiAnime.Scraper() 69 | * 70 | * hianime.getEpisodes("steinsgate-3") 71 | * .then((data) => console.log(data)) 72 | * .catch((err) => console.error(err)); 73 | * 74 | */ 75 | async getEpisodes(animeId: string) { 76 | return getAnimeEpisodes(animeId); 77 | } 78 | 79 | /** 80 | * @param {string} episodeId - unique episode id 81 | * @throws {HiAnimeError} 82 | * @example 83 | * import { HiAnime } from "aniwatch"; 84 | * 85 | * const hianime = new HiAnime.Scraper() 86 | * 87 | * hianime.getEpisodeSources("steinsgate-3?ep=230", "hd-1", "sub") 88 | * .then((data) => console.log(data)) 89 | * .catch((err) => console.error(err)); 90 | * 91 | */ 92 | async getEpisodeSources( 93 | episodeId: string, 94 | server: AnimeServers = Servers.VidStreaming, 95 | category: "sub" | "dub" | "raw" = "sub" 96 | ) { 97 | return getAnimeEpisodeSources(episodeId, server, category); 98 | } 99 | 100 | /** 101 | * @param {string} genreName - anime genre name 102 | * @param {number} page - page number, defaults to `1` 103 | * @throws {HiAnimeError} 104 | * @example 105 | * import { HiAnime } from "aniwatch"; 106 | * 107 | * const hianime = new HiAnime.Scraper() 108 | * 109 | * hianime.getGenreAnime("shounen", 2) 110 | * .then((data) => console.log(data)) 111 | * .catch((err) => console.error(err)); 112 | * 113 | */ 114 | async getGenreAnime(genreName: string, page: number = 1) { 115 | return getGenreAnime(genreName, page); 116 | } 117 | 118 | /** 119 | * @param {string} producerName - anime producer name 120 | * @param {number} page - page number, defaults to `1` 121 | * @throws {HiAnimeError} 122 | * @example 123 | * import { HiAnime } from "aniwatch"; 124 | * 125 | * const hianime = new HiAnime.Scraper() 126 | * 127 | * hianime.getProducerAnimes("toei-animation", 2) 128 | * .then((data) => console.log(data)) 129 | * .catch((err) => console.error(err)); 130 | * 131 | */ 132 | async getProducerAnimes(producerName: string, page: number = 1) { 133 | return getProducerAnimes(producerName, page); 134 | } 135 | 136 | /** 137 | * @param {string} q - search query 138 | * @param {number} page - page number, defaults to `1` 139 | * @param {SearchFilters} filters - optional advance search filters 140 | * @throws {HiAnimeError} 141 | * @example 142 | * import { HiAnime } from "aniwatch"; 143 | * 144 | * const hianime = new HiAnime.Scraper(); 145 | * 146 | * hianime 147 | * .search("monster", 1, { 148 | * genres: "seinen,psychological", 149 | * }) 150 | * .then((data) => { 151 | * console.log(data); 152 | * }) 153 | * .catch((err) => { 154 | * console.error(err); 155 | * }); 156 | * 157 | */ 158 | async search(q: string, page: number = 1, filters: SearchFilters = {}) { 159 | return getAnimeSearchResults(q, page, filters); 160 | } 161 | 162 | /** 163 | * @param {string} q - search query 164 | * @throws {HiAnimeError} 165 | * @example 166 | * import { HiAnime } from "aniwatch"; 167 | * 168 | * const hianime = new HiAnime.Scraper() 169 | * 170 | * hianime.searchSuggestions("one piece") 171 | * .then((data) => console.log(data)) 172 | * .catch((err) => console.error(err)); 173 | * 174 | */ 175 | async searchSuggestions(q: string) { 176 | return getAnimeSearchSuggestion(q); 177 | } 178 | 179 | /** 180 | * @param {string} animeEpisodeId - unique anime episode id 181 | * @throws {HiAnimeError} 182 | * @example 183 | * import { HiAnime } from "aniwatch"; 184 | * 185 | * const hianime = new HiAnime.Scraper() 186 | * 187 | * hianime.getEpisodeServers("steinsgate-0-92?ep=2055") 188 | * .then((data) => console.log(data)) 189 | * .catch((err) => console.error(err)); 190 | * 191 | */ 192 | async getEpisodeServers(animeEpisodeId: string) { 193 | return getEpisodeServers(animeEpisodeId); 194 | } 195 | 196 | /** 197 | * @param {string} date - date in `YYYY-MM-DD` format 198 | * @param {number} tzOffset - timezone offset in minutes, defaults to `-330` (IST) 199 | * @throws {HiAnimeError} 200 | * @example 201 | * import { HiAnime } from "aniwatch"; 202 | * 203 | * const hianime = new HiAnime.Scraper() 204 | * const timezoneOffset = -330; // IST offset in minutes 205 | * 206 | * hianime.getEstimatedSchedule("2025-06-09", timezoneOffset) 207 | * .then((data) => console.log(data)) 208 | * .catch((err) => console.error(err)); 209 | * 210 | */ 211 | async getEstimatedSchedule(date: string, tzOffset: number = -330) { 212 | return getEstimatedSchedule(date, tzOffset); 213 | } 214 | 215 | /** 216 | * @param {string} animeId - unique anime id 217 | * @throws {HiAnimeError} 218 | * @example 219 | * import { HiAnime } from "aniwatch"; 220 | * 221 | * const hianime = new HiAnime.Scraper() 222 | * 223 | * hianime.getNextEpisodeSchedule("one-piece-100") 224 | * .then((data) => console.log(data)) 225 | * .catch((err) => console.error(err)); 226 | * 227 | */ 228 | async getNextEpisodeSchedule(animeId: string) { 229 | return getNextEpisodeSchedule(animeId); 230 | } 231 | 232 | /** 233 | * @throws {HiAnimeError} 234 | * @example 235 | * import { HiAnime } from "aniwatch"; 236 | * 237 | * const hianime = new HiAnime.Scraper() 238 | * 239 | * hianime.getHomePage() 240 | * .then((data) => console.log(data)) 241 | * .catch((err) => console.error(err)); 242 | * 243 | */ 244 | async getHomePage() { 245 | return getHomePage(); 246 | } 247 | 248 | /** 249 | * @param {AZListSortOptions} sortOption az-list sort option 250 | * @param {number} page - page number, defaults to `1` 251 | * @throws {HiAnimeError} 252 | * @example 253 | * import { HiAnime } from "aniwatch"; 254 | * 255 | * const hianime = new HiAnime.Scraper() 256 | * 257 | * hianime.getAZList("0-9", 1) 258 | * .then((data) => console.log(data)) 259 | * .catch((err) => console.error(err)); 260 | * 261 | */ 262 | async getAZList(sortOption: AZListSortOptions, page: number = 1) { 263 | return getAZList(sortOption, page); 264 | } 265 | 266 | /** 267 | * @param {string} animeId - unique anime id 268 | * @throws {HiAnimeError} 269 | * @example 270 | * import { HiAnime } from "aniwatch"; 271 | * 272 | * const hianime = new HiAnime.Scraper() 273 | * 274 | * hianime.getQtipInfo("one-piece-100") 275 | * .then((data) => console.log(data)) 276 | * .catch((err) => console.error(err)); 277 | * 278 | */ 279 | async getQtipInfo(animeId: string) { 280 | return getAnimeQtipInfo(animeId); 281 | } 282 | } 283 | 284 | export { Scraper }; 285 | export { 286 | SEARCH_PAGE_FILTERS, 287 | AZ_LIST_SORT_OPTIONS, 288 | } from "../utils/constants.js"; 289 | export * from "./types/anime.js"; 290 | export * from "./types/animeSearch.js"; 291 | export * from "./types/scrapers/index.js"; 292 | -------------------------------------------------------------------------------- /src/hianime/scrapers/animeAZList.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 2 | import { client } from "../../config/client.js"; 3 | import { HiAnimeError } from "../error.js"; 4 | import { 5 | AZ_LIST_SORT_OPTIONS, 6 | SRC_BASE_URL, 7 | extractAnimes, 8 | } from "../../utils/index.js"; 9 | import type { ScrapedAnimeAZList } from "../types/scrapers/index.js"; 10 | import type { AZListSortOptions } from "../hianime.js"; 11 | 12 | export async function getAZList( 13 | sortOption: AZListSortOptions, 14 | page: number 15 | ): Promise { 16 | const res: ScrapedAnimeAZList = { 17 | sortOption: sortOption.trim() as AZListSortOptions, 18 | animes: [], 19 | totalPages: 0, 20 | hasNextPage: false, 21 | currentPage: (Number(page) || 0) < 1 ? 1 : Number(page), 22 | }; 23 | sortOption = res.sortOption; 24 | page = res.currentPage; 25 | 26 | try { 27 | if ( 28 | sortOption === ("" as AZListSortOptions) || 29 | !Boolean(AZ_LIST_SORT_OPTIONS[sortOption]) 30 | ) { 31 | throw new HiAnimeError( 32 | "invalid az-list sort option", 33 | getAZList.name, 34 | 400 35 | ); 36 | } 37 | 38 | switch (sortOption) { 39 | case "all": 40 | sortOption = "" as AZListSortOptions; 41 | break; 42 | case "other": 43 | sortOption = "other"; 44 | break; 45 | default: 46 | sortOption = sortOption.toUpperCase() as AZListSortOptions; 47 | } 48 | 49 | const azURL: URL = new URL( 50 | `/az-list/${sortOption}?page=${page}`, 51 | SRC_BASE_URL 52 | ); 53 | 54 | const resp = await client.get(azURL.href); 55 | const $: CheerioAPI = load(resp.data); 56 | 57 | const selector: SelectorType = 58 | "#main-wrapper .tab-content .film_list-wrap .flw-item"; 59 | 60 | res.hasNextPage = 61 | $(".pagination > li").length > 0 62 | ? $(".pagination li.active").length > 0 63 | ? $(".pagination > li").last().hasClass("active") 64 | ? false 65 | : true 66 | : false 67 | : false; 68 | 69 | res.totalPages = 70 | Number( 71 | $('.pagination > .page-item a[title="Last"]') 72 | ?.attr("href") 73 | ?.split("=") 74 | .pop() ?? 75 | $('.pagination > .page-item a[title="Next"]') 76 | ?.attr("href") 77 | ?.split("=") 78 | .pop() ?? 79 | $(".pagination > .page-item.active a")?.text()?.trim() 80 | ) || 1; 81 | 82 | res.animes = extractAnimes($, selector, getAZList.name); 83 | 84 | if (res.animes.length === 0 && !res.hasNextPage) { 85 | res.totalPages = 0; 86 | } 87 | 88 | return res; 89 | } catch (err: any) { 90 | throw HiAnimeError.wrapError(err, getAZList.name); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/hianime/scrapers/animeAboutInfo.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 2 | import { client } from "../../config/client.js"; 3 | import { HiAnimeError } from "../error.js"; 4 | import { 5 | SRC_BASE_URL, 6 | extractAnimes, 7 | extractMostPopularAnimes, 8 | } from "../../utils/index.js"; 9 | import type { ScrapedAnimeAboutInfo } from "../types/scrapers/index.js"; 10 | 11 | export async function getAnimeAboutInfo( 12 | animeId: string 13 | ): Promise { 14 | const res: ScrapedAnimeAboutInfo = { 15 | anime: { 16 | info: { 17 | id: null, 18 | anilistId: null, 19 | malId: null, 20 | name: null, 21 | poster: null, 22 | description: null, 23 | stats: { 24 | rating: null, 25 | quality: null, 26 | episodes: { 27 | sub: null, 28 | dub: null, 29 | }, 30 | type: null, 31 | duration: null, 32 | }, 33 | promotionalVideos: [], 34 | charactersVoiceActors: [], 35 | }, 36 | moreInfo: {}, 37 | }, 38 | seasons: [], 39 | mostPopularAnimes: [], 40 | relatedAnimes: [], 41 | recommendedAnimes: [], 42 | }; 43 | 44 | try { 45 | if (animeId.trim() === "" || animeId.indexOf("-") === -1) { 46 | throw new HiAnimeError( 47 | "invalid anime id", 48 | getAnimeAboutInfo.name, 49 | 400 50 | ); 51 | } 52 | 53 | const animeUrl: URL = new URL(animeId, SRC_BASE_URL); 54 | const mainPage = await client.get(animeUrl.href); 55 | 56 | const $: CheerioAPI = load(mainPage.data); 57 | 58 | try { 59 | res.anime.info.anilistId = Number( 60 | JSON.parse($("body")?.find("#syncData")?.text())?.anilist_id 61 | ); 62 | res.anime.info.malId = Number( 63 | JSON.parse($("body")?.find("#syncData")?.text())?.mal_id 64 | ); 65 | } catch (err) { 66 | res.anime.info.anilistId = null; 67 | res.anime.info.malId = null; 68 | } 69 | 70 | const selector: SelectorType = "#ani_detail .container .anis-content"; 71 | 72 | res.anime.info.id = 73 | $(selector) 74 | ?.find(".anisc-detail .film-buttons a.btn-play") 75 | ?.attr("href") 76 | ?.split("/") 77 | ?.pop() || null; 78 | res.anime.info.name = 79 | $(selector) 80 | ?.find(".anisc-detail .film-name.dynamic-name") 81 | ?.text() 82 | ?.trim() || null; 83 | res.anime.info.description = 84 | $(selector) 85 | ?.find(".anisc-detail .film-description .text") 86 | .text() 87 | ?.split("[") 88 | ?.shift() 89 | ?.trim() || null; 90 | res.anime.info.poster = 91 | $(selector) 92 | ?.find(".film-poster .film-poster-img") 93 | ?.attr("src") 94 | ?.trim() || null; 95 | 96 | // stats 97 | res.anime.info.stats.rating = 98 | $(`${selector} .film-stats .tick .tick-pg`)?.text()?.trim() || null; 99 | res.anime.info.stats.quality = 100 | $(`${selector} .film-stats .tick .tick-quality`)?.text()?.trim() || 101 | null; 102 | res.anime.info.stats.episodes = { 103 | sub: 104 | Number( 105 | $(`${selector} .film-stats .tick .tick-sub`)?.text()?.trim() 106 | ) || null, 107 | dub: 108 | Number( 109 | $(`${selector} .film-stats .tick .tick-dub`)?.text()?.trim() 110 | ) || null, 111 | }; 112 | res.anime.info.stats.type = 113 | $(`${selector} .film-stats .tick`) 114 | ?.text() 115 | ?.trim() 116 | ?.replace(/[\s\n]+/g, " ") 117 | ?.split(" ") 118 | ?.at(-2) || null; 119 | res.anime.info.stats.duration = 120 | $(`${selector} .film-stats .tick`) 121 | ?.text() 122 | ?.trim() 123 | ?.replace(/[\s\n]+/g, " ") 124 | ?.split(" ") 125 | ?.pop() || null; 126 | 127 | // get promotional videos 128 | $( 129 | ".block_area.block_area-promotions .block_area-promotions-list .screen-items .item" 130 | ).each((_, el) => { 131 | res.anime.info.promotionalVideos.push({ 132 | title: $(el).attr("data-title"), 133 | source: $(el).attr("data-src"), 134 | thumbnail: $(el).find("img").attr("src"), 135 | }); 136 | }); 137 | 138 | // get characters and voice actors 139 | $( 140 | ".block_area.block_area-actors .block-actors-content .bac-list-wrap .bac-item" 141 | ).each((_, el) => { 142 | res.anime.info.charactersVoiceActors.push({ 143 | character: { 144 | id: 145 | $(el) 146 | .find($(".per-info.ltr .pi-avatar")) 147 | .attr("href") 148 | ?.split("/")[2] || "", 149 | poster: 150 | $(el) 151 | .find($(".per-info.ltr .pi-avatar img")) 152 | .attr("data-src") || "", 153 | name: $(el).find($(".per-info.ltr .pi-detail a")).text(), 154 | cast: $(el) 155 | .find($(".per-info.ltr .pi-detail .pi-cast")) 156 | .text(), 157 | }, 158 | voiceActor: { 159 | id: 160 | $(el) 161 | .find($(".per-info.rtl .pi-avatar")) 162 | .attr("href") 163 | ?.split("/")[2] || "", 164 | poster: 165 | $(el) 166 | .find($(".per-info.rtl .pi-avatar img")) 167 | .attr("data-src") || "", 168 | name: $(el).find($(".per-info.rtl .pi-detail a")).text(), 169 | cast: $(el) 170 | .find($(".per-info.rtl .pi-detail .pi-cast")) 171 | .text(), 172 | }, 173 | }); 174 | }); 175 | 176 | // more information 177 | $(`${selector} .anisc-info-wrap .anisc-info .item:not(.w-hide)`).each( 178 | (_, el) => { 179 | let key = $(el) 180 | .find(".item-head") 181 | .text() 182 | .toLowerCase() 183 | .replace(":", "") 184 | .trim(); 185 | key = key.includes(" ") ? key.replace(" ", "") : key; 186 | 187 | const value = [ 188 | ...$(el) 189 | .find("*:not(.item-head)") 190 | .map((_, el) => $(el).text().trim()), 191 | ] 192 | .map((i) => `${i}`) 193 | .toString() 194 | .trim(); 195 | 196 | if (key === "genres") { 197 | res.anime.moreInfo[key] = value 198 | .split(",") 199 | .map((i) => i.trim()); 200 | return; 201 | } 202 | if (key === "producers") { 203 | res.anime.moreInfo[key] = value 204 | .split(",") 205 | .map((i) => i.trim()); 206 | return; 207 | } 208 | res.anime.moreInfo[key] = value; 209 | } 210 | ); 211 | 212 | // more seasons 213 | const seasonsSelector: SelectorType = 214 | "#main-content .os-list a.os-item"; 215 | $(seasonsSelector).each((_, el) => { 216 | res.seasons.push({ 217 | id: $(el)?.attr("href")?.slice(1)?.trim() || null, 218 | name: $(el)?.attr("title")?.trim() || null, 219 | title: $(el)?.find(".title")?.text()?.trim(), 220 | poster: 221 | $(el) 222 | ?.find(".season-poster") 223 | ?.attr("style") 224 | ?.split(" ") 225 | ?.pop() 226 | ?.split("(") 227 | ?.pop() 228 | ?.split(")")[0] || null, 229 | isCurrent: $(el).hasClass("active"), 230 | }); 231 | }); 232 | 233 | const relatedAnimeSelector: SelectorType = 234 | "#main-sidebar .block_area.block_area_sidebar.block_area-realtime:nth-of-type(1) .anif-block-ul ul li"; 235 | res.relatedAnimes = extractMostPopularAnimes( 236 | $, 237 | relatedAnimeSelector, 238 | getAnimeAboutInfo.name 239 | ); 240 | 241 | const mostPopularSelector: SelectorType = 242 | "#main-sidebar .block_area.block_area_sidebar.block_area-realtime:nth-of-type(2) .anif-block-ul ul li"; 243 | res.mostPopularAnimes = extractMostPopularAnimes( 244 | $, 245 | mostPopularSelector, 246 | getAnimeAboutInfo.name 247 | ); 248 | 249 | const recommendedAnimeSelector: SelectorType = 250 | "#main-content .block_area.block_area_category .tab-content .flw-item"; 251 | res.recommendedAnimes = extractAnimes( 252 | $, 253 | recommendedAnimeSelector, 254 | getAnimeAboutInfo.name 255 | ); 256 | 257 | return res; 258 | } catch (err: any) { 259 | throw HiAnimeError.wrapError(err, getAnimeAboutInfo.name); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/hianime/scrapers/animeCategory.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 2 | import { client } from "../../config/client.js"; 3 | import { HiAnimeError } from "../error.js"; 4 | import { 5 | SRC_BASE_URL, 6 | extractAnimes, 7 | extractTop10Animes, 8 | } from "../../utils/index.js"; 9 | import type { AnimeCategories } from "../types/anime.js"; 10 | import type { ScrapedAnimeCategory } from "../types/scrapers/index.js"; 11 | 12 | export async function getAnimeCategory( 13 | category: AnimeCategories, 14 | page: number 15 | ): Promise { 16 | const res: ScrapedAnimeCategory = { 17 | animes: [], 18 | genres: [], 19 | top10Animes: { 20 | today: [], 21 | week: [], 22 | month: [], 23 | }, 24 | category, 25 | totalPages: 0, 26 | hasNextPage: false, 27 | currentPage: (Number(page) || 0) < 1 ? 1 : Number(page), 28 | }; 29 | 30 | try { 31 | if (category.trim() === "") { 32 | throw new HiAnimeError( 33 | "invalid anime category", 34 | getAnimeCategory.name, 35 | 400 36 | ); 37 | } 38 | page = res.currentPage; 39 | 40 | const scrapeUrl: URL = new URL(category, SRC_BASE_URL); 41 | const mainPage = await client.get(`${scrapeUrl}?page=${page}`); 42 | 43 | const $: CheerioAPI = load(mainPage.data); 44 | 45 | const selector: SelectorType = 46 | "#main-content .tab-content .film_list-wrap .flw-item"; 47 | 48 | const categoryNameSelector: SelectorType = 49 | "#main-content .block_area .block_area-header .cat-heading"; 50 | res.category = $(categoryNameSelector)?.text()?.trim() ?? category; 51 | 52 | res.hasNextPage = 53 | $(".pagination > li").length > 0 54 | ? $(".pagination li.active").length > 0 55 | ? $(".pagination > li").last().hasClass("active") 56 | ? false 57 | : true 58 | : false 59 | : false; 60 | 61 | res.totalPages = 62 | Number( 63 | $('.pagination > .page-item a[title="Last"]') 64 | ?.attr("href") 65 | ?.split("=") 66 | .pop() ?? 67 | $('.pagination > .page-item a[title="Next"]') 68 | ?.attr("href") 69 | ?.split("=") 70 | .pop() ?? 71 | $(".pagination > .page-item.active a")?.text()?.trim() 72 | ) || 1; 73 | 74 | res.animes = extractAnimes($, selector, getAnimeCategory.name); 75 | 76 | if (res.animes.length === 0 && !res.hasNextPage) { 77 | res.totalPages = 0; 78 | } 79 | 80 | const genreSelector: SelectorType = 81 | "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; 82 | $(genreSelector).each((_, el) => { 83 | res.genres.push(`${$(el).text().trim()}`); 84 | }); 85 | 86 | const top10AnimeSelector: SelectorType = 87 | '#main-sidebar .block_area-realtime [id^="top-viewed-"]'; 88 | 89 | $(top10AnimeSelector).each((_, el) => { 90 | const period = $(el).attr("id")?.split("-")?.pop()?.trim(); 91 | 92 | if (period === "day") { 93 | res.top10Animes.today = extractTop10Animes( 94 | $, 95 | period, 96 | getAnimeCategory.name 97 | ); 98 | return; 99 | } 100 | if (period === "week") { 101 | res.top10Animes.week = extractTop10Animes( 102 | $, 103 | period, 104 | getAnimeCategory.name 105 | ); 106 | return; 107 | } 108 | if (period === "month") { 109 | res.top10Animes.month = extractTop10Animes( 110 | $, 111 | period, 112 | getAnimeCategory.name 113 | ); 114 | } 115 | }); 116 | 117 | return res; 118 | } catch (err: any) { 119 | throw HiAnimeError.wrapError(err, getAnimeCategory.name); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/hianime/scrapers/animeEpisodeSrcs.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { load, type CheerioAPI } from "cheerio"; 3 | import { client } from "../../config/client.js"; 4 | import { HiAnimeError } from "../error.js"; 5 | import { 6 | SRC_AJAX_URL, 7 | SRC_BASE_URL, 8 | retrieveServerId, 9 | USER_AGENT_HEADER, 10 | } from "../../utils/index.js"; 11 | import { 12 | RapidCloud, 13 | StreamSB, 14 | StreamTape, 15 | MegaCloud, 16 | } from "../../extractors/index.js"; 17 | import { log } from "../../config/logger.js"; 18 | import { type AnimeServers, Servers } from "../types/anime.js"; 19 | import type { ScrapedAnimeEpisodesSources } from "../types/scrapers/index.js"; 20 | 21 | // vidtreaming -> 4 22 | // rapidcloud -> 1 23 | // streamsb -> 5 24 | // streamtape -> 3 25 | 26 | async function _getAnimeEpisodeSources( 27 | episodeId: string, 28 | server: AnimeServers = Servers.VidStreaming, 29 | category: "sub" | "dub" | "raw" = "sub" 30 | ): Promise { 31 | if (episodeId.startsWith("http")) { 32 | const serverUrl = new URL(episodeId); 33 | switch (server) { 34 | case Servers.VidStreaming: 35 | case Servers.VidCloud: 36 | return { 37 | headers: { Referer: `${serverUrl.origin}/` }, 38 | // disabled for the timebeing 39 | // ...(await new MegaCloud().extract(serverUrl)), 40 | ...(await new MegaCloud().extract2(serverUrl)), 41 | }; 42 | case Servers.StreamSB: 43 | return { 44 | headers: { 45 | Referer: serverUrl.href, 46 | watchsb: "streamsb", 47 | "User-Agent": USER_AGENT_HEADER, 48 | }, 49 | sources: await new StreamSB().extract(serverUrl, true), 50 | }; 51 | case Servers.StreamTape: 52 | return { 53 | headers: { 54 | Referer: serverUrl.href, 55 | "User-Agent": USER_AGENT_HEADER, 56 | }, 57 | sources: await new StreamTape().extract(serverUrl), 58 | }; 59 | default: // vidcloud 60 | return { 61 | headers: { Referer: serverUrl.href }, 62 | ...(await new RapidCloud().extract(serverUrl)), 63 | }; 64 | } 65 | } 66 | 67 | const epId = new URL(`/watch/${episodeId}`, SRC_BASE_URL).href; 68 | log.info(`EPISODE_ID: ${epId}`); 69 | 70 | try { 71 | const resp = await client.get( 72 | `${SRC_AJAX_URL}/v2/episode/servers?episodeId=${epId.split("?ep=")[1]}`, 73 | { 74 | headers: { 75 | Referer: epId, 76 | "X-Requested-With": "XMLHttpRequest", 77 | }, 78 | } 79 | ); 80 | 81 | const $: CheerioAPI = load(resp.data.html); 82 | 83 | let serverId: string | null = null; 84 | 85 | try { 86 | log.info(`THE SERVER: ${JSON.stringify(server)}`); 87 | 88 | switch (server) { 89 | case Servers.VidCloud: { 90 | serverId = retrieveServerId($, 1, category); 91 | if (!serverId) throw new Error("RapidCloud not found"); 92 | break; 93 | } 94 | case Servers.VidStreaming: { 95 | serverId = retrieveServerId($, 4, category); 96 | log.info(`SERVER_ID: ${serverId}`); 97 | if (!serverId) throw new Error("VidStreaming not found"); 98 | break; 99 | } 100 | case Servers.StreamSB: { 101 | serverId = retrieveServerId($, 5, category); 102 | if (!serverId) throw new Error("StreamSB not found"); 103 | break; 104 | } 105 | case Servers.StreamTape: { 106 | serverId = retrieveServerId($, 3, category); 107 | if (!serverId) throw new Error("StreamTape not found"); 108 | break; 109 | } 110 | } 111 | } catch (err) { 112 | throw new HiAnimeError( 113 | "Couldn't find server. Try another server", 114 | getAnimeEpisodeSources.name, 115 | 500 116 | ); 117 | } 118 | 119 | const { 120 | data: { link }, 121 | } = await client.get( 122 | `${SRC_AJAX_URL}/v2/episode/sources?id=${serverId}` 123 | ); 124 | log.info(`THE LINK: ${link}`); 125 | 126 | return await _getAnimeEpisodeSources(link, server); 127 | } catch (err: any) { 128 | throw HiAnimeError.wrapError(err, getAnimeEpisodeSources.name); 129 | } 130 | } 131 | 132 | type AnilistID = number | null; 133 | type MalID = number | null; 134 | 135 | export async function getAnimeEpisodeSources( 136 | episodeId: string, 137 | server: AnimeServers, 138 | category: "sub" | "dub" | "raw" 139 | ): Promise< 140 | ScrapedAnimeEpisodesSources & { anilistID: AnilistID; malID: MalID } 141 | > { 142 | try { 143 | if (episodeId === "" || episodeId.indexOf("?ep=") === -1) { 144 | throw new HiAnimeError( 145 | "invalid anime episode id", 146 | getAnimeEpisodeSources.name, 147 | 400 148 | ); 149 | } 150 | if (category.trim() === "") { 151 | throw new HiAnimeError( 152 | "invalid anime episode category", 153 | getAnimeEpisodeSources.name, 154 | 400 155 | ); 156 | } 157 | 158 | let malID: MalID; 159 | let anilistID: AnilistID; 160 | const animeURL = new URL(episodeId?.split("?ep=")[0], SRC_BASE_URL) 161 | ?.href; 162 | 163 | const [episodeSrcData, animeSrc] = await Promise.all([ 164 | _getAnimeEpisodeSources(episodeId, server, category), 165 | axios.get(animeURL, { 166 | headers: { 167 | Referer: SRC_BASE_URL, 168 | "User-Agent": USER_AGENT_HEADER, 169 | "X-Requested-With": "XMLHttpRequest", 170 | }, 171 | }), 172 | ]); 173 | log.info(`EPISODE_SRC_DATA: ${JSON.stringify(episodeSrcData)}`); 174 | 175 | const $: CheerioAPI = load(animeSrc?.data); 176 | 177 | try { 178 | anilistID = Number( 179 | JSON.parse($("body")?.find("#syncData")?.text())?.anilist_id 180 | ); 181 | malID = Number( 182 | JSON.parse($("body")?.find("#syncData")?.text())?.mal_id 183 | ); 184 | } catch (err) { 185 | anilistID = null; 186 | malID = null; 187 | } 188 | 189 | return { 190 | ...episodeSrcData, 191 | anilistID, 192 | malID, 193 | }; 194 | } catch (err: any) { 195 | throw HiAnimeError.wrapError(err, getAnimeEpisodeSources.name); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/hianime/scrapers/animeEpisodes.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI } from "cheerio"; 2 | import { client } from "../../config/client.js"; 3 | import { HiAnimeError } from "../error.js"; 4 | import { SRC_BASE_URL, SRC_AJAX_URL } from "../../utils/index.js"; 5 | import type { ScrapedAnimeEpisodes } from "../types/scrapers/index.js"; 6 | 7 | /** 8 | * @param {string} animeId - unique anime id 9 | * @example 10 | * import { getAnimeEpisodes } from "aniwatch"; 11 | * 12 | * getAnimeEpisodes("attack-on-titan-112") 13 | * .then((data) => console.log(data)) 14 | * .catch((err) => console.error(err)); 15 | * 16 | */ 17 | export async function getAnimeEpisodes( 18 | animeId: string 19 | ): Promise { 20 | const res: ScrapedAnimeEpisodes = { 21 | totalEpisodes: 0, 22 | episodes: [], 23 | }; 24 | 25 | try { 26 | if (animeId.trim() === "" || animeId.indexOf("-") === -1) { 27 | throw new HiAnimeError( 28 | "invalid anime id", 29 | getAnimeEpisodes.name, 30 | 400 31 | ); 32 | } 33 | 34 | const episodesAjax = await client.get( 35 | `${SRC_AJAX_URL}/v2/episode/list/${animeId.split("-").pop()}`, 36 | { 37 | headers: { 38 | "X-Requested-With": "XMLHttpRequest", 39 | Referer: `${SRC_BASE_URL}/watch/${animeId}`, 40 | }, 41 | } 42 | ); 43 | 44 | const $: CheerioAPI = load(episodesAjax.data.html); 45 | 46 | res.totalEpisodes = Number( 47 | $(".detail-infor-content .ss-list a").length 48 | ); 49 | 50 | $(".detail-infor-content .ss-list a").each((_, el) => { 51 | res.episodes.push({ 52 | title: $(el)?.attr("title")?.trim() || null, 53 | episodeId: $(el)?.attr("href")?.split("/")?.pop() || null, 54 | number: Number($(el).attr("data-number")), 55 | isFiller: $(el).hasClass("ssl-item-filler"), 56 | }); 57 | }); 58 | 59 | return res; 60 | } catch (err: any) { 61 | throw HiAnimeError.wrapError(err, getAnimeEpisodes.name); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/hianime/scrapers/animeGenre.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 2 | import { client } from "../../config/client.js"; 3 | import { HiAnimeError } from "../error.js"; 4 | import { 5 | SRC_BASE_URL, 6 | extractAnimes, 7 | extractMostPopularAnimes, 8 | } from "../../utils/index.js"; 9 | import type { ScrapedGenreAnime } from "../types/scrapers/index.js"; 10 | 11 | export async function getGenreAnime( 12 | genreName: string, 13 | page: number 14 | ): Promise { 15 | const res: ScrapedGenreAnime = { 16 | // there's a typo with hianime where "martial" arts is "marial" arts 17 | genreName: 18 | genreName === "martial-arts" ? "marial-arts" : genreName.trim(), 19 | animes: [], 20 | genres: [], 21 | topAiringAnimes: [], 22 | totalPages: 1, 23 | hasNextPage: false, 24 | currentPage: (Number(page) || 0) < 1 ? 1 : Number(page), 25 | }; 26 | 27 | genreName = res.genreName; 28 | page = res.currentPage; 29 | 30 | try { 31 | if (genreName === "") { 32 | throw new HiAnimeError( 33 | "invalid genre name", 34 | getGenreAnime.name, 35 | 400 36 | ); 37 | } 38 | 39 | const genreUrl: URL = new URL( 40 | `/genre/${genreName}?page=${page}`, 41 | SRC_BASE_URL 42 | ); 43 | 44 | const mainPage = await client.get(genreUrl.href); 45 | const $: CheerioAPI = load(mainPage.data); 46 | 47 | const selector: SelectorType = 48 | "#main-content .tab-content .film_list-wrap .flw-item"; 49 | 50 | const genreNameSelector: SelectorType = 51 | "#main-content .block_area .block_area-header .cat-heading"; 52 | res.genreName = $(genreNameSelector)?.text()?.trim() ?? genreName; 53 | 54 | res.hasNextPage = 55 | $(".pagination > li").length > 0 56 | ? $(".pagination li.active").length > 0 57 | ? $(".pagination > li").last().hasClass("active") 58 | ? false 59 | : true 60 | : false 61 | : false; 62 | 63 | res.totalPages = 64 | Number( 65 | $('.pagination > .page-item a[title="Last"]') 66 | ?.attr("href") 67 | ?.split("=") 68 | .pop() ?? 69 | $('.pagination > .page-item a[title="Next"]') 70 | ?.attr("href") 71 | ?.split("=") 72 | .pop() ?? 73 | $(".pagination > .page-item.active a")?.text()?.trim() 74 | ) || 1; 75 | 76 | res.animes = extractAnimes($, selector, getGenreAnime.name); 77 | 78 | if (res.animes.length === 0 && !res.hasNextPage) { 79 | res.totalPages = 0; 80 | } 81 | 82 | const genreSelector: SelectorType = 83 | "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; 84 | $(genreSelector).each((_, el) => { 85 | res.genres.push(`${$(el).text().trim()}`); 86 | }); 87 | 88 | const topAiringSelector: SelectorType = 89 | "#main-sidebar .block_area.block_area_sidebar.block_area-realtime .anif-block-ul ul li"; 90 | res.topAiringAnimes = extractMostPopularAnimes( 91 | $, 92 | topAiringSelector, 93 | getGenreAnime.name 94 | ); 95 | 96 | return res; 97 | } catch (err: any) { 98 | throw HiAnimeError.wrapError(err, getGenreAnime.name); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/hianime/scrapers/animeProducer.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 2 | import { client } from "../../config/client.js"; 3 | import { HiAnimeError } from "../error.js"; 4 | import { 5 | SRC_BASE_URL, 6 | extractMostPopularAnimes, 7 | extractAnimes, 8 | extractTop10Animes, 9 | } from "../../utils/index.js"; 10 | import type { ScrapedProducerAnime } from "../types/scrapers/index.js"; 11 | 12 | export async function getProducerAnimes( 13 | producerName: string, 14 | page: number 15 | ): Promise { 16 | const res: ScrapedProducerAnime = { 17 | producerName, 18 | animes: [], 19 | top10Animes: { 20 | today: [], 21 | week: [], 22 | month: [], 23 | }, 24 | topAiringAnimes: [], 25 | totalPages: 0, 26 | hasNextPage: false, 27 | currentPage: (Number(page) || 0) < 1 ? 1 : Number(page), 28 | }; 29 | 30 | try { 31 | if (producerName.trim() === "") { 32 | throw new HiAnimeError( 33 | "invalid producer name", 34 | getProducerAnimes.name, 35 | 400 36 | ); 37 | } 38 | page = res.currentPage; 39 | 40 | const producerUrl: URL = new URL( 41 | `/producer/${producerName}?page=${page}`, 42 | SRC_BASE_URL 43 | ); 44 | 45 | const mainPage = await client.get(producerUrl.href); 46 | 47 | const $: CheerioAPI = load(mainPage.data); 48 | 49 | const animeSelector: SelectorType = 50 | "#main-content .tab-content .film_list-wrap .flw-item"; 51 | 52 | res.hasNextPage = 53 | $(".pagination > li").length > 0 54 | ? $(".pagination li.active").length > 0 55 | ? $(".pagination > li").last().hasClass("active") 56 | ? false 57 | : true 58 | : false 59 | : false; 60 | 61 | res.totalPages = 62 | Number( 63 | $('.pagination > .page-item a[title="Last"]') 64 | ?.attr("href") 65 | ?.split("=") 66 | .pop() ?? 67 | $('.pagination > .page-item a[title="Next"]') 68 | ?.attr("href") 69 | ?.split("=") 70 | .pop() ?? 71 | $(".pagination > .page-item.active a")?.text()?.trim() 72 | ) || 1; 73 | 74 | res.animes = extractAnimes($, animeSelector, getProducerAnimes.name); 75 | 76 | if (res.animes.length === 0 && !res.hasNextPage) { 77 | res.totalPages = 0; 78 | } 79 | 80 | const producerNameSelector: SelectorType = 81 | "#main-content .block_area .block_area-header .cat-heading"; 82 | res.producerName = 83 | $(producerNameSelector)?.text()?.trim() ?? producerName; 84 | 85 | const top10AnimeSelector: SelectorType = 86 | '#main-sidebar .block_area-realtime [id^="top-viewed-"]'; 87 | 88 | $(top10AnimeSelector).each((_, el) => { 89 | const period = $(el).attr("id")?.split("-")?.pop()?.trim(); 90 | 91 | if (period === "day") { 92 | res.top10Animes.today = extractTop10Animes( 93 | $, 94 | period, 95 | getProducerAnimes.name 96 | ); 97 | return; 98 | } 99 | if (period === "week") { 100 | res.top10Animes.week = extractTop10Animes( 101 | $, 102 | period, 103 | getProducerAnimes.name 104 | ); 105 | return; 106 | } 107 | if (period === "month") { 108 | res.top10Animes.month = extractTop10Animes( 109 | $, 110 | period, 111 | getProducerAnimes.name 112 | ); 113 | } 114 | }); 115 | 116 | const topAiringSelector: SelectorType = 117 | "#main-sidebar .block_area_sidebar:nth-child(2) .block_area-content .anif-block-ul ul li"; 118 | res.topAiringAnimes = extractMostPopularAnimes( 119 | $, 120 | topAiringSelector, 121 | getProducerAnimes.name 122 | ); 123 | 124 | return res; 125 | } catch (err: any) { 126 | throw HiAnimeError.wrapError(err, getProducerAnimes.name); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/hianime/scrapers/animeQtip.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 2 | import { HiAnimeError } from "../error.js"; 3 | import { client } from "../../config/client.js"; 4 | import { SRC_AJAX_URL, SRC_HOME_URL } from "../../utils/index.js"; 5 | import type { ScrapedAnimeQtipInfo } from "../types/scrapers/index.js"; 6 | 7 | export async function getAnimeQtipInfo( 8 | animeId: string 9 | ): Promise { 10 | const res: ScrapedAnimeQtipInfo = { 11 | anime: { 12 | id: animeId.trim(), 13 | name: null, 14 | malscore: null, 15 | quality: null, 16 | episodes: { 17 | sub: null, 18 | dub: null, 19 | }, 20 | type: null, 21 | description: null, 22 | 23 | jname: null, 24 | synonyms: null, 25 | aired: null, 26 | status: null, 27 | genres: [], 28 | }, 29 | }; 30 | 31 | try { 32 | animeId = String(res.anime.id); 33 | const id = animeId.split("-").pop(); 34 | if (animeId === "" || animeId.indexOf("-") === -1 || !id) { 35 | throw new HiAnimeError( 36 | "invalid anime id", 37 | getAnimeQtipInfo.name, 38 | 400 39 | ); 40 | } 41 | 42 | const mainPage = await client.get(`${SRC_AJAX_URL}/movie/qtip/${id}`, { 43 | headers: { 44 | Referer: SRC_HOME_URL, 45 | "X-Requested-With": "XMLHttpRequest", 46 | }, 47 | }); 48 | const $: CheerioAPI = load(mainPage.data); 49 | const selector: SelectorType = ".pre-qtip-content"; 50 | 51 | res.anime.id = 52 | $(selector) 53 | ?.find(".pre-qtip-button a.btn-play") 54 | ?.attr("href") 55 | ?.trim() 56 | ?.split("/") 57 | ?.pop() || null; 58 | res.anime.name = 59 | $(selector)?.find(".pre-qtip-title")?.text()?.trim() || null; 60 | res.anime.malscore = 61 | $(selector) 62 | ?.find(".pre-qtip-detail") 63 | ?.children() 64 | ?.first() 65 | ?.text() 66 | ?.trim() || null; 67 | res.anime.quality = 68 | $(selector)?.find(".tick .tick-quality")?.text()?.trim() || null; 69 | res.anime.type = 70 | $(selector)?.find(".badge.badge-quality")?.text()?.trim() || null; 71 | 72 | res.anime.episodes.sub = 73 | Number($(selector)?.find(".tick .tick-sub")?.text()?.trim()) || 74 | null; 75 | res.anime.episodes.dub = 76 | Number($(selector)?.find(".tick .tick-dub")?.text()?.trim()) || 77 | null; 78 | 79 | res.anime.description = 80 | $(selector)?.find(".pre-qtip-description")?.text()?.trim() || null; 81 | 82 | $(`${selector} .pre-qtip-line`).each((_, el) => { 83 | const key = $(el) 84 | .find(".stick") 85 | .text() 86 | .trim() 87 | .slice(0, -1) 88 | .toLowerCase(); 89 | const value = 90 | key !== "genres" 91 | ? $(el)?.find(".stick-text")?.text()?.trim() || null 92 | : $(el) 93 | ?.text() 94 | ?.trim() 95 | ?.slice(key.length + 1); 96 | 97 | switch (key) { 98 | case "japanese": 99 | res.anime.jname = value; 100 | break; 101 | case "synonyms": 102 | res.anime.synonyms = value; 103 | break; 104 | case "aired": 105 | res.anime.aired = value; 106 | break; 107 | case "status": 108 | res.anime.status = value; 109 | break; 110 | case "genres": 111 | res.anime.genres = 112 | value?.split(",")?.map((i) => i?.trim()) || []; 113 | break; 114 | } 115 | }); 116 | 117 | return res; 118 | } catch (err: any) { 119 | throw HiAnimeError.wrapError(err, getAnimeQtipInfo.name); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/hianime/scrapers/animeSearch.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 2 | import { client } from "../../config/client.js"; 3 | import { HiAnimeError } from "../error.js"; 4 | import { 5 | SRC_SEARCH_URL, 6 | extractAnimes, 7 | getSearchFilterValue, 8 | extractMostPopularAnimes, 9 | getSearchDateFilterValue, 10 | } from "../../utils/index.js"; 11 | import type { ScrapedAnimeSearchResult } from "../types/scrapers/index.js"; 12 | import type { SearchFilters, FilterKeys } from "../types/animeSearch.js"; 13 | 14 | const searchFilters: Record = { 15 | filter: true, 16 | type: true, 17 | status: true, 18 | rated: true, 19 | score: true, 20 | season: true, 21 | language: true, 22 | start_date: true, 23 | end_date: true, 24 | sort: true, 25 | genres: true, 26 | } as const; 27 | 28 | async function _getAnimeSearchResults( 29 | q: string, 30 | page: number = 1, 31 | filters: SearchFilters 32 | ): Promise { 33 | try { 34 | const res: ScrapedAnimeSearchResult = { 35 | animes: [], 36 | mostPopularAnimes: [], 37 | searchQuery: q, 38 | searchFilters: filters, 39 | totalPages: 0, 40 | hasNextPage: false, 41 | currentPage: (Number(page) || 0) < 1 ? 1 : Number(page), 42 | }; 43 | 44 | const url = new URL(SRC_SEARCH_URL); 45 | url.searchParams.set("keyword", q); 46 | url.searchParams.set("page", `${res.currentPage}`); 47 | url.searchParams.set("sort", "default"); 48 | 49 | for (const key in filters) { 50 | if (key.includes("_date")) { 51 | const dates = getSearchDateFilterValue( 52 | key === "start_date", 53 | filters[key as keyof SearchFilters] || "" 54 | ); 55 | if (!dates) continue; 56 | 57 | dates.map((dateParam) => { 58 | const [key, val] = dateParam.split("="); 59 | url.searchParams.set(key, val); 60 | }); 61 | continue; 62 | } 63 | 64 | const filterVal = getSearchFilterValue( 65 | key as FilterKeys, 66 | filters[key as keyof SearchFilters] || "" 67 | ); 68 | filterVal && url.searchParams.set(key, filterVal); 69 | } 70 | 71 | const mainPage = await client.get(url.href); 72 | 73 | const $: CheerioAPI = load(mainPage.data); 74 | 75 | const selector: SelectorType = 76 | "#main-content .tab-content .film_list-wrap .flw-item"; 77 | 78 | res.hasNextPage = 79 | $(".pagination > li").length > 0 80 | ? $(".pagination li.active").length > 0 81 | ? $(".pagination > li").last().hasClass("active") 82 | ? false 83 | : true 84 | : false 85 | : false; 86 | 87 | res.totalPages = 88 | Number( 89 | $('.pagination > .page-item a[title="Last"]') 90 | ?.attr("href") 91 | ?.split("=") 92 | .pop() ?? 93 | $('.pagination > .page-item a[title="Next"]') 94 | ?.attr("href") 95 | ?.split("=") 96 | .pop() ?? 97 | $(".pagination > .page-item.active a")?.text()?.trim() 98 | ) || 1; 99 | 100 | res.animes = extractAnimes($, selector, getAnimeSearchResults.name); 101 | 102 | if (res.animes.length === 0 && !res.hasNextPage) { 103 | res.totalPages = 0; 104 | } 105 | 106 | const mostPopularSelector: SelectorType = 107 | "#main-sidebar .block_area.block_area_sidebar.block_area-realtime .anif-block-ul ul li"; 108 | res.mostPopularAnimes = extractMostPopularAnimes( 109 | $, 110 | mostPopularSelector, 111 | getAnimeSearchResults.name 112 | ); 113 | 114 | return res; 115 | } catch (err: any) { 116 | throw HiAnimeError.wrapError(err, getAnimeSearchResults.name); 117 | } 118 | } 119 | 120 | export async function getAnimeSearchResults( 121 | q: string, 122 | page: number, 123 | filters: SearchFilters 124 | ): Promise { 125 | try { 126 | q = q.trim() ? decodeURIComponent(q.trim()) : ""; 127 | if (q.trim() === "") { 128 | throw new HiAnimeError( 129 | "invalid search query", 130 | getAnimeSearchResults.name, 131 | 400 132 | ); 133 | } 134 | page = page < 1 ? 1 : page; 135 | 136 | const parsedFilters: SearchFilters = {}; 137 | for (const key in filters) { 138 | if (searchFilters[key]) { 139 | parsedFilters[key as keyof SearchFilters] = 140 | filters[key as keyof SearchFilters]; 141 | } 142 | } 143 | 144 | return _getAnimeSearchResults(q, page, parsedFilters); 145 | } catch (err: any) { 146 | throw HiAnimeError.wrapError(err, getAnimeSearchResults.name); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/hianime/scrapers/animeSearchSuggestion.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 2 | import { client } from "../../config/client.js"; 3 | import { HiAnimeError } from "../error.js"; 4 | import { SRC_HOME_URL, SRC_AJAX_URL } from "../../utils/index.js"; 5 | import type { ScrapedAnimeSearchSuggestion } from "../types/scrapers/index.js"; 6 | 7 | export async function getAnimeSearchSuggestion( 8 | q: string 9 | ): Promise { 10 | try { 11 | const res: ScrapedAnimeSearchSuggestion = { 12 | suggestions: [], 13 | }; 14 | 15 | q = q.trim() ? decodeURIComponent(q.trim()) : ""; 16 | if (q.trim() === "") { 17 | throw new HiAnimeError( 18 | "invalid search query", 19 | getAnimeSearchSuggestion.name, 20 | 400 21 | ); 22 | } 23 | 24 | const { data } = await client.get( 25 | `${SRC_AJAX_URL}/search/suggest?keyword=${encodeURIComponent(q)}`, 26 | { 27 | headers: { 28 | Accept: "*/*", 29 | Pragma: "no-cache", 30 | Referer: SRC_HOME_URL, 31 | "X-Requested-With": "XMLHttpRequest", 32 | }, 33 | } 34 | ); 35 | 36 | const $: CheerioAPI = load(data.html); 37 | const selector: SelectorType = ".nav-item:has(.film-poster)"; 38 | 39 | if ($(selector).length < 1) return res; 40 | 41 | $(selector).each((_, el) => { 42 | const id = $(el).attr("href")?.split("?")[0].includes("javascript") 43 | ? null 44 | : $(el).attr("href")?.split("?")[0]?.slice(1) || null; 45 | 46 | res.suggestions.push({ 47 | id, 48 | name: 49 | $(el).find(".srp-detail .film-name")?.text()?.trim() || 50 | null, 51 | jname: 52 | $(el) 53 | .find(".srp-detail .film-name") 54 | ?.attr("data-jname") 55 | ?.trim() || 56 | $(el).find(".srp-detail .alias-name")?.text()?.trim() || 57 | null, 58 | poster: 59 | $(el) 60 | .find(".film-poster .film-poster-img") 61 | ?.attr("data-src") 62 | ?.trim() || null, 63 | moreInfo: [ 64 | ...$(el) 65 | .find(".film-infor") 66 | .contents() 67 | .map((_, el) => $(el).text().trim()), 68 | ].filter((i) => i), 69 | }); 70 | }); 71 | 72 | return res; 73 | } catch (err: any) { 74 | throw HiAnimeError.wrapError(err, getAnimeSearchSuggestion.name); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/hianime/scrapers/episodeServers.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 2 | import { client } from "../../config/client.js"; 3 | import { HiAnimeError } from "../error.js"; 4 | import { SRC_BASE_URL, SRC_AJAX_URL } from "../../utils/index.js"; 5 | import type { ScrapedEpisodeServers } from "../types/scrapers/index.js"; 6 | 7 | export async function getEpisodeServers( 8 | episodeId: string 9 | ): Promise { 10 | const res: ScrapedEpisodeServers = { 11 | sub: [], 12 | dub: [], 13 | raw: [], 14 | episodeId, 15 | episodeNo: 0, 16 | }; 17 | 18 | try { 19 | if (episodeId.trim() === "" || episodeId.indexOf("?ep=") === -1) { 20 | throw new HiAnimeError( 21 | "invalid anime episode id", 22 | getEpisodeServers.name, 23 | 400 24 | ); 25 | } 26 | 27 | const epId = episodeId.split("?ep=")[1]; 28 | 29 | const { data } = await client.get( 30 | `${SRC_AJAX_URL}/v2/episode/servers?episodeId=${epId}`, 31 | { 32 | headers: { 33 | "X-Requested-With": "XMLHttpRequest", 34 | Referer: new URL(`/watch/${episodeId}`, SRC_BASE_URL).href, 35 | }, 36 | } 37 | ); 38 | 39 | const $: CheerioAPI = load(data.html); 40 | 41 | const epNoSelector: SelectorType = ".server-notice strong"; 42 | res.episodeNo = Number($(epNoSelector).text().split(" ").pop()) || 0; 43 | 44 | $(`.ps_-block.ps_-block-sub.servers-sub .ps__-list .server-item`).each( 45 | (_, el) => { 46 | res.sub.push({ 47 | serverName: $(el).find("a").text().toLowerCase().trim(), 48 | serverId: 49 | Number($(el)?.attr("data-server-id")?.trim()) || null, 50 | }); 51 | } 52 | ); 53 | 54 | $(`.ps_-block.ps_-block-sub.servers-dub .ps__-list .server-item`).each( 55 | (_, el) => { 56 | res.dub.push({ 57 | serverName: $(el).find("a").text().toLowerCase().trim(), 58 | serverId: 59 | Number($(el)?.attr("data-server-id")?.trim()) || null, 60 | }); 61 | } 62 | ); 63 | 64 | $(`.ps_-block.ps_-block-sub.servers-raw .ps__-list .server-item`).each( 65 | (_, el) => { 66 | res.raw.push({ 67 | serverName: $(el).find("a").text().toLowerCase().trim(), 68 | serverId: 69 | Number($(el)?.attr("data-server-id")?.trim()) || null, 70 | }); 71 | } 72 | ); 73 | 74 | return res; 75 | } catch (err: any) { 76 | throw HiAnimeError.wrapError(err, getEpisodeServers.name); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/hianime/scrapers/estimatedSchedule.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 2 | import type { 3 | ScrapedEstimatedSchedule, 4 | ScrapedNextEpisodeSchedule, 5 | } from "../types/scrapers/index.js"; 6 | import { client } from "../../config/client.js"; 7 | import { HiAnimeError } from "../error.js"; 8 | import { SRC_HOME_URL, SRC_AJAX_URL, SRC_BASE_URL } from "../../utils/index.js"; 9 | 10 | export async function getEstimatedSchedule( 11 | date: string, 12 | tzOffset: number = -330 13 | ): Promise { 14 | const res: ScrapedEstimatedSchedule = { 15 | scheduledAnimes: [], 16 | }; 17 | try { 18 | date = date?.trim(); 19 | if (date === "" || /^\d{4}-\d{2}-\d{2}$/.test(date) === false) { 20 | throw new HiAnimeError( 21 | "invalid date format", 22 | getEstimatedSchedule.name, 23 | 400 24 | ); 25 | } 26 | 27 | if (tzOffset && (typeof tzOffset !== "number" || isNaN(tzOffset))) { 28 | throw new HiAnimeError( 29 | "invalid timezone offset", 30 | getEstimatedSchedule.name, 31 | 400 32 | ); 33 | } 34 | 35 | const estScheduleURL = 36 | `${SRC_AJAX_URL}/schedule/list?tzOffset=${tzOffset}&date=${date}` as const; 37 | const mainPage = await client.get(estScheduleURL, { 38 | headers: { 39 | Accept: "*/*", 40 | Referer: SRC_HOME_URL, 41 | "X-Requested-With": "XMLHttpRequest", 42 | }, 43 | }); 44 | const $: CheerioAPI = load(mainPage?.data?.html); 45 | const selector: SelectorType = "li"; 46 | if ($(selector)?.text()?.trim()?.includes("No data to display")) { 47 | return res; 48 | } 49 | $(selector).each((_, el) => { 50 | const airingTimestamp = new Date( 51 | `${date}T${$(el)?.find("a .time")?.text()?.trim()}:00` 52 | ).getTime(); 53 | res.scheduledAnimes.push({ 54 | id: $(el)?.find("a")?.attr("href")?.slice(1)?.trim() || null, 55 | time: $(el)?.find("a .time")?.text()?.trim() || null, 56 | name: 57 | $(el)?.find("a .film-name.dynamic-name")?.text()?.trim() || 58 | null, 59 | jname: 60 | $(el) 61 | ?.find("a .film-name.dynamic-name") 62 | ?.attr("data-jname") 63 | ?.trim() || null, 64 | airingTimestamp, 65 | secondsUntilAiring: Math.floor( 66 | (airingTimestamp - Date.now()) / 1000 67 | ), 68 | episode: Number( 69 | $(el).find("a .fd-play button").text().trim().split(" ")[1] 70 | ), 71 | }); 72 | }); 73 | return res; 74 | } catch (err: any) { 75 | throw HiAnimeError.wrapError(err, getEstimatedSchedule.name); 76 | } 77 | } 78 | 79 | export async function getNextEpisodeSchedule( 80 | animeId: string 81 | ): Promise { 82 | const res: ScrapedNextEpisodeSchedule = { 83 | airingISOTimestamp: null, 84 | airingTimestamp: null, 85 | secondsUntilAiring: null, 86 | }; 87 | try { 88 | animeId = animeId?.trim(); 89 | if (!animeId || animeId.indexOf("-") === -1) { 90 | throw new HiAnimeError( 91 | "invalid anime id", 92 | getNextEpisodeSchedule.name, 93 | 400 94 | ); 95 | } 96 | 97 | const animeUrl = `${SRC_BASE_URL}/watch/${animeId}` as const; 98 | const mainPage = await client.get(animeUrl, { 99 | headers: { 100 | Accept: "*/*", 101 | Referer: SRC_HOME_URL, 102 | }, 103 | }); 104 | 105 | const $: CheerioAPI = load(mainPage.data); 106 | const selector: SelectorType = 107 | ".schedule-alert > .alert.small > span:last"; 108 | 109 | const timestamp = String( 110 | $(selector).attr("data-value")?.trim() || null 111 | ); 112 | const schedule = new Date(timestamp); 113 | if (isNaN(schedule.getTime())) return res; 114 | 115 | res.airingISOTimestamp = schedule.toISOString(); 116 | res.airingTimestamp = schedule.getTime(); 117 | res.secondsUntilAiring = Math.floor( 118 | (res.airingTimestamp - Date.now()) / 1000 119 | ); 120 | 121 | return res; 122 | } catch (err: any) { 123 | throw HiAnimeError.wrapError(err, getNextEpisodeSchedule.name); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/hianime/scrapers/homePage.ts: -------------------------------------------------------------------------------- 1 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 2 | import { client } from "../../config/client.js"; 3 | import { HiAnimeError } from "../error.js"; 4 | import { 5 | SRC_HOME_URL, 6 | extractTop10Animes, 7 | extractAnimes, 8 | extractMostPopularAnimes, 9 | } from "../../utils/index.js"; 10 | import type { ScrapedHomePage } from "../types/scrapers/index.js"; 11 | 12 | /** 13 | * @example 14 | * import { getHomePage } from "aniwatch"; 15 | * 16 | * getHomePage() 17 | * .then((data) => console.log(data)) 18 | * .catch((err) => console.error(err)); 19 | * 20 | */ 21 | export async function getHomePage(): Promise { 22 | const res: ScrapedHomePage = { 23 | spotlightAnimes: [], 24 | trendingAnimes: [], 25 | latestEpisodeAnimes: [], 26 | topUpcomingAnimes: [], 27 | top10Animes: { 28 | today: [], 29 | week: [], 30 | month: [], 31 | }, 32 | topAiringAnimes: [], 33 | mostPopularAnimes: [], 34 | mostFavoriteAnimes: [], 35 | latestCompletedAnimes: [], 36 | genres: [], 37 | }; 38 | 39 | try { 40 | const mainPage = await client.get(SRC_HOME_URL as string); 41 | 42 | const $: CheerioAPI = load(mainPage.data); 43 | 44 | const spotlightSelector: SelectorType = 45 | "#slider .swiper-wrapper .swiper-slide"; 46 | 47 | $(spotlightSelector).each((_, el) => { 48 | const otherInfo = $(el) 49 | .find(".deslide-item-content .sc-detail .scd-item") 50 | .map((_, el) => $(el).text().trim()) 51 | .get() 52 | .slice(0, -1); 53 | 54 | res.spotlightAnimes.push({ 55 | rank: 56 | Number( 57 | $(el) 58 | .find(".deslide-item-content .desi-sub-text") 59 | ?.text() 60 | .trim() 61 | .split(" ")[0] 62 | .slice(1) 63 | ) || null, 64 | id: 65 | $(el) 66 | .find(".deslide-item-content .desi-buttons a") 67 | ?.last() 68 | ?.attr("href") 69 | ?.slice(1) 70 | ?.trim() || null, 71 | name: $(el) 72 | .find(".deslide-item-content .desi-head-title.dynamic-name") 73 | ?.text() 74 | .trim(), 75 | description: 76 | $(el) 77 | .find(".deslide-item-content .desi-description") 78 | ?.text() 79 | ?.split("[") 80 | ?.shift() 81 | ?.trim() || null, 82 | poster: 83 | $(el) 84 | .find( 85 | ".deslide-cover .deslide-cover-img .film-poster-img" 86 | ) 87 | ?.attr("data-src") 88 | ?.trim() || null, 89 | jname: 90 | $(el) 91 | .find( 92 | ".deslide-item-content .desi-head-title.dynamic-name" 93 | ) 94 | ?.attr("data-jname") 95 | ?.trim() || null, 96 | episodes: { 97 | sub: 98 | Number( 99 | $(el) 100 | .find( 101 | ".deslide-item-content .sc-detail .scd-item .tick-item.tick-sub" 102 | ) 103 | ?.text() 104 | ?.trim() 105 | ) || null, 106 | dub: 107 | Number( 108 | $(el) 109 | .find( 110 | ".deslide-item-content .sc-detail .scd-item .tick-item.tick-dub" 111 | ) 112 | ?.text() 113 | ?.trim() 114 | ) || null, 115 | }, 116 | type: otherInfo?.[0] || null, 117 | otherInfo, 118 | }); 119 | }); 120 | 121 | const trendingSelector: SelectorType = 122 | "#trending-home .swiper-wrapper .swiper-slide"; 123 | 124 | $(trendingSelector).each((_, el) => { 125 | res.trendingAnimes.push({ 126 | rank: parseInt( 127 | $(el) 128 | .find(".item .number") 129 | ?.children() 130 | ?.first() 131 | ?.text() 132 | ?.trim() 133 | ), 134 | id: 135 | $(el) 136 | .find(".item .film-poster") 137 | ?.attr("href") 138 | ?.slice(1) 139 | ?.trim() || null, 140 | name: $(el) 141 | .find(".item .number .film-title.dynamic-name") 142 | ?.text() 143 | ?.trim(), 144 | jname: 145 | $(el) 146 | .find(".item .number .film-title.dynamic-name") 147 | ?.attr("data-jname") 148 | ?.trim() || null, 149 | poster: 150 | $(el) 151 | .find(".item .film-poster .film-poster-img") 152 | ?.attr("data-src") 153 | ?.trim() || null, 154 | }); 155 | }); 156 | 157 | const latestEpisodeSelector: SelectorType = 158 | "#main-content .block_area_home:nth-of-type(1) .tab-content .film_list-wrap .flw-item"; 159 | res.latestEpisodeAnimes = extractAnimes( 160 | $, 161 | latestEpisodeSelector, 162 | getHomePage.name 163 | ); 164 | 165 | const topUpcomingSelector: SelectorType = 166 | "#main-content .block_area_home:nth-of-type(3) .tab-content .film_list-wrap .flw-item"; 167 | res.topUpcomingAnimes = extractAnimes( 168 | $, 169 | topUpcomingSelector, 170 | getHomePage.name 171 | ); 172 | 173 | const genreSelector: SelectorType = 174 | "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; 175 | $(genreSelector).each((_, el) => { 176 | res.genres.push(`${$(el).text().trim()}`); 177 | }); 178 | 179 | const mostViewedSelector: SelectorType = 180 | '#main-sidebar .block_area-realtime [id^="top-viewed-"]'; 181 | $(mostViewedSelector).each((_, el) => { 182 | const period = $(el).attr("id")?.split("-")?.pop()?.trim(); 183 | 184 | if (period === "day") { 185 | res.top10Animes.today = extractTop10Animes( 186 | $, 187 | period, 188 | getHomePage.name 189 | ); 190 | return; 191 | } 192 | if (period === "week") { 193 | res.top10Animes.week = extractTop10Animes( 194 | $, 195 | period, 196 | getHomePage.name 197 | ); 198 | return; 199 | } 200 | if (period === "month") { 201 | res.top10Animes.month = extractTop10Animes( 202 | $, 203 | period, 204 | getHomePage.name 205 | ); 206 | } 207 | }); 208 | 209 | res.topAiringAnimes = extractMostPopularAnimes( 210 | $, 211 | "#anime-featured .row div:nth-of-type(1) .anif-block-ul ul li", 212 | getHomePage.name 213 | ); 214 | res.mostPopularAnimes = extractMostPopularAnimes( 215 | $, 216 | "#anime-featured .row div:nth-of-type(2) .anif-block-ul ul li", 217 | getHomePage.name 218 | ); 219 | res.mostFavoriteAnimes = extractMostPopularAnimes( 220 | $, 221 | "#anime-featured .row div:nth-of-type(3) .anif-block-ul ul li", 222 | getHomePage.name 223 | ); 224 | res.latestCompletedAnimes = extractMostPopularAnimes( 225 | $, 226 | "#anime-featured .row div:nth-of-type(4) .anif-block-ul ul li", 227 | getHomePage.name 228 | ); 229 | 230 | return res; 231 | } catch (err: any) { 232 | throw HiAnimeError.wrapError(err, getHomePage.name); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/hianime/scrapers/index.ts: -------------------------------------------------------------------------------- 1 | import { getHomePage } from "./homePage.js"; 2 | import { getAZList } from "./animeAZList.js"; 3 | import { getGenreAnime } from "./animeGenre.js"; 4 | import { getAnimeQtipInfo } from "./animeQtip.js"; 5 | import { getAnimeEpisodes } from "./animeEpisodes.js"; 6 | import { getAnimeCategory } from "./animeCategory.js"; 7 | import { getProducerAnimes } from "./animeProducer.js"; 8 | import { getEpisodeServers } from "./episodeServers.js"; 9 | import { getAnimeAboutInfo } from "./animeAboutInfo.js"; 10 | import { getAnimeSearchResults } from "./animeSearch.js"; 11 | import { getAnimeEpisodeSources } from "./animeEpisodeSrcs.js"; 12 | import { getAnimeSearchSuggestion } from "./animeSearchSuggestion.js"; 13 | import { 14 | getEstimatedSchedule, 15 | getNextEpisodeSchedule, 16 | } from "./estimatedSchedule.js"; 17 | 18 | export { 19 | getAZList, 20 | getHomePage, 21 | getGenreAnime, 22 | getAnimeQtipInfo, 23 | getAnimeEpisodes, 24 | getAnimeCategory, 25 | getEpisodeServers, 26 | getProducerAnimes, 27 | getAnimeAboutInfo, 28 | getEstimatedSchedule, 29 | getAnimeSearchResults, 30 | getNextEpisodeSchedule, 31 | getAnimeEpisodeSources, 32 | getAnimeSearchSuggestion, 33 | }; 34 | -------------------------------------------------------------------------------- /src/hianime/types/anime.ts: -------------------------------------------------------------------------------- 1 | import type { AZ_LIST_SORT_OPTIONS } from "../../utils/constants.js"; 2 | 3 | export type Anime = { 4 | id: string | null; 5 | name: string | null; 6 | jname: string | null; 7 | poster: string | null; 8 | duration: string | null; 9 | type: string | null; 10 | rating: string | null; 11 | episodes: { 12 | sub: number | null; 13 | dub: number | null; 14 | }; 15 | }; 16 | 17 | type CommonAnimeProps = "id" | "name" | "poster"; 18 | 19 | export type Top10Anime = Pick & { 20 | rank: number | null; 21 | jname: string | null; 22 | }; 23 | 24 | export type Top10AnimeTimePeriod = "day" | "week" | "month"; 25 | 26 | export type MostPopularAnime = Pick< 27 | Anime, 28 | CommonAnimeProps | "episodes" | "type" 29 | > & { 30 | jname: string | null; 31 | }; 32 | 33 | export type SpotlightAnime = MostPopularAnime & 34 | Pick & { 35 | description: string | null; 36 | otherInfo: string[]; 37 | }; 38 | 39 | export type TrendingAnime = Pick & 40 | Pick; 41 | 42 | export type LatestEpisodeAnime = Anime; 43 | 44 | export type TopUpcomingAnime = Anime; 45 | 46 | export type TopAiringAnime = MostPopularAnime; 47 | export type MostFavoriteAnime = MostPopularAnime; 48 | export type LatestCompletedAnime = MostPopularAnime; 49 | 50 | export type AnimeGeneralAboutInfo = Pick & 51 | Pick & { 52 | anilistId: number | null; 53 | malId: number | null; 54 | stats: { 55 | quality: string | null; 56 | } & Pick; 57 | promotionalVideos: AnimePromotionalVideo[]; 58 | charactersVoiceActors: AnimeCharactersAndVoiceActors[]; 59 | }; 60 | 61 | export type RecommendedAnime = Anime; 62 | 63 | export type RelatedAnime = MostPopularAnime; 64 | 65 | export type Season = Pick & { 66 | isCurrent: boolean; 67 | title: string | null; 68 | }; 69 | 70 | export type AnimePromotionalVideo = { 71 | title: string | undefined; 72 | source: string | undefined; 73 | thumbnail: string | undefined; 74 | }; 75 | 76 | export type AnimeCharactersAndVoiceActors = { 77 | character: AnimeCharacter; 78 | voiceActor: AnimeCharacter; 79 | }; 80 | 81 | export type AnimeCharacter = { 82 | id: string; 83 | poster: string; 84 | name: string; 85 | cast: string; 86 | }; 87 | 88 | export type AnimeSearchSuggestion = Omit< 89 | MostPopularAnime, 90 | "episodes" | "type" 91 | > & { 92 | moreInfo: string[]; 93 | }; 94 | 95 | export type AnimeEpisode = Pick & { 96 | episodeId: string | null; 97 | number: number; 98 | isFiller: boolean; 99 | }; 100 | 101 | export type SubEpisode = { 102 | serverName: string; 103 | serverId: number | null; 104 | }; 105 | export type DubEpisode = SubEpisode; 106 | export type RawEpisode = SubEpisode; 107 | 108 | type ObjectToSumType = { 109 | [K in keyof Obj]: Obj[K] extends Readonly ? K : never; 110 | }[keyof Obj]; 111 | 112 | export type AZListSortOptions = ObjectToSumType; 113 | 114 | export type AnimeCategories = 115 | | "most-favorite" 116 | | "most-popular" 117 | | "subbed-anime" 118 | | "dubbed-anime" 119 | | "recently-updated" 120 | | "recently-added" 121 | | "top-upcoming" 122 | | "top-airing" 123 | | "movie" 124 | | "special" 125 | | "ova" 126 | | "ona" 127 | | "tv" 128 | | "completed"; 129 | 130 | export type AnimeServers = 131 | | "hd-1" 132 | | "hd-2" 133 | | "megacloud" 134 | | "streamsb" 135 | | "streamtape"; 136 | 137 | export enum Servers { 138 | VidStreaming = "hd-1", 139 | MegaCloud = "megacloud", 140 | StreamSB = "streamsb", 141 | StreamTape = "streamtape", 142 | VidCloud = "hd-2", 143 | AsianLoad = "asianload", 144 | GogoCDN = "gogocdn", 145 | MixDrop = "mixdrop", 146 | UpCloud = "upcloud", 147 | VizCloud = "vizcloud", 148 | MyCloud = "mycloud", 149 | Filemoon = "filemoon", 150 | } 151 | -------------------------------------------------------------------------------- /src/hianime/types/animeSearch.ts: -------------------------------------------------------------------------------- 1 | export type AnimeSearchQueryParams = { 2 | q?: string; 3 | page?: string; 4 | type?: string; 5 | status?: string; 6 | rated?: string; 7 | score?: string; 8 | season?: string; 9 | language?: string; 10 | start_date?: string; 11 | end_date?: string; 12 | sort?: string; 13 | genres?: string; 14 | }; 15 | 16 | export type SearchFilters = Omit; 17 | 18 | export type FilterKeys = Partial< 19 | keyof Omit 20 | >; 21 | -------------------------------------------------------------------------------- /src/hianime/types/extractor.ts: -------------------------------------------------------------------------------- 1 | export type Video = { 2 | url: string; 3 | quality?: string; 4 | isM3U8?: boolean; 5 | size?: number; 6 | [x: string]: unknown; 7 | }; 8 | 9 | export type Subtitle = { 10 | id?: string; 11 | url: string; 12 | lang: string; 13 | }; 14 | 15 | export type Intro = { 16 | start: number; 17 | end: number; 18 | }; 19 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/animeAZList.ts: -------------------------------------------------------------------------------- 1 | import type { AZListSortOptions } from "../anime.js"; 2 | import type { 3 | ScrapedAnimeCategory, 4 | CommonAnimeScrapeTypes, 5 | } from "./animeCategory.js"; 6 | 7 | export type ScrapedAnimeAZList = Pick< 8 | ScrapedAnimeCategory, 9 | CommonAnimeScrapeTypes 10 | > & { 11 | sortOption: AZListSortOptions; 12 | }; 13 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/animeAboutInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Season, 3 | type RelatedAnime, 4 | type RecommendedAnime, 5 | type AnimeGeneralAboutInfo, 6 | } from "../anime.js"; 7 | import { type ScrapedAnimeSearchResult } from "./animeSearch.js"; 8 | 9 | export type ScrapedAnimeAboutInfo = Pick< 10 | ScrapedAnimeSearchResult, 11 | "mostPopularAnimes" 12 | > & { 13 | anime: { 14 | info: AnimeGeneralAboutInfo; 15 | moreInfo: Record; 16 | }; 17 | seasons: Season[]; 18 | relatedAnimes: RelatedAnime[]; 19 | recommendedAnimes: RecommendedAnime[]; 20 | }; 21 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/animeCategory.ts: -------------------------------------------------------------------------------- 1 | import type { Anime, Top10Anime } from "../anime.js"; 2 | 3 | export type ScrapedAnimeCategory = { 4 | animes: Anime[]; 5 | genres: string[]; 6 | top10Animes: { 7 | today: Top10Anime[]; 8 | week: Top10Anime[]; 9 | month: Top10Anime[]; 10 | }; 11 | category: string; 12 | totalPages: number; 13 | currentPage: number; 14 | hasNextPage: boolean; 15 | }; 16 | 17 | export type CommonAnimeScrapeTypes = 18 | | "animes" 19 | | "totalPages" 20 | | "hasNextPage" 21 | | "currentPage"; 22 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/animeEpisodeSrcs.ts: -------------------------------------------------------------------------------- 1 | import type { Intro, Subtitle, Video } from "../extractor.js"; 2 | 3 | export type ScrapedAnimeEpisodesSources = { 4 | headers?: { 5 | [k: string]: string; 6 | }; 7 | intro?: Intro; 8 | subtitles?: Subtitle[]; 9 | sources: Video[]; 10 | download?: string; 11 | embedURL?: string; 12 | }; 13 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/animeEpisodes.ts: -------------------------------------------------------------------------------- 1 | import { type AnimeEpisode } from "../anime.js"; 2 | 3 | export type ScrapedAnimeEpisodes = { 4 | totalEpisodes: number; 5 | episodes: AnimeEpisode[]; 6 | }; 7 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/animeGenre.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ScrapedAnimeCategory, 3 | CommonAnimeScrapeTypes, 4 | } from "./animeCategory.js"; 5 | import { type ScrapedHomePage } from "./homePage.js"; 6 | 7 | export type ScrapedGenreAnime = Pick< 8 | ScrapedAnimeCategory, 9 | CommonAnimeScrapeTypes | "genres" 10 | > & 11 | Pick & { 12 | genreName: string; 13 | }; 14 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/animeProducer.ts: -------------------------------------------------------------------------------- 1 | import type { ScrapedHomePage } from "./homePage.js"; 2 | import type { ScrapedAnimeCategory } from "./animeCategory.js"; 3 | 4 | export type ScrapedProducerAnime = Omit< 5 | ScrapedAnimeCategory, 6 | "genres" | "category" 7 | > & 8 | Pick & { 9 | producerName: string; 10 | }; 11 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/animeQtip.ts: -------------------------------------------------------------------------------- 1 | import type { Anime } from "../anime.js"; 2 | 3 | export type ScrapedAnimeQtipInfo = { 4 | anime: { 5 | quality: string | null; 6 | genres: string[]; 7 | aired: string | null; 8 | synonyms: string | null; 9 | status: string | null; 10 | malscore: string | null; 11 | description: string | null; 12 | } & Omit; 13 | }; 14 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/animeSearch.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ScrapedAnimeCategory, 3 | CommonAnimeScrapeTypes, 4 | } from "./animeCategory.js"; 5 | import type { MostPopularAnime } from "../anime.js"; 6 | import type { SearchFilters } from "../animeSearch.js"; 7 | 8 | export type ScrapedAnimeSearchResult = Pick< 9 | ScrapedAnimeCategory, 10 | CommonAnimeScrapeTypes 11 | > & { 12 | mostPopularAnimes: MostPopularAnime[]; 13 | searchQuery: string; 14 | searchFilters: SearchFilters; 15 | }; 16 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/animeSearchSuggestion.ts: -------------------------------------------------------------------------------- 1 | import type { AnimeSearchSuggestion } from "../anime.js"; 2 | 3 | export type ScrapedAnimeSearchSuggestion = { 4 | suggestions: AnimeSearchSuggestion[]; 5 | }; 6 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/episodeServers.ts: -------------------------------------------------------------------------------- 1 | import type { SubEpisode, DubEpisode, RawEpisode } from "../anime.js"; 2 | 3 | export type ScrapedEpisodeServers = { 4 | sub: SubEpisode[]; 5 | dub: DubEpisode[]; 6 | raw: RawEpisode[]; 7 | episodeNo: number; 8 | episodeId: string; 9 | }; 10 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/estimatedSchedule.ts: -------------------------------------------------------------------------------- 1 | type EstimatedSchedule = { 2 | id: string | null; 3 | time: string | null; 4 | name: string | null; 5 | jname: string | null; 6 | airingTimestamp: number; 7 | secondsUntilAiring: number; 8 | episode: number; 9 | }; 10 | 11 | export type ScrapedEstimatedSchedule = { 12 | scheduledAnimes: EstimatedSchedule[]; 13 | }; 14 | 15 | export type ScrapedNextEpisodeSchedule = { 16 | airingISOTimestamp: string | null; 17 | airingTimestamp: number | null; 18 | secondsUntilAiring: number | null; 19 | }; 20 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/homePage.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | TrendingAnime, 3 | SpotlightAnime, 4 | TopAiringAnime, 5 | TopUpcomingAnime, 6 | LatestEpisodeAnime, 7 | MostFavoriteAnime, 8 | MostPopularAnime, 9 | LatestCompletedAnime, 10 | } from "../anime.js"; 11 | import type { ScrapedAnimeCategory } from "./animeCategory.js"; 12 | 13 | export type ScrapedHomePage = Pick< 14 | ScrapedAnimeCategory, 15 | "genres" | "top10Animes" 16 | > & { 17 | spotlightAnimes: SpotlightAnime[]; 18 | trendingAnimes: TrendingAnime[]; 19 | latestEpisodeAnimes: LatestEpisodeAnime[]; 20 | topUpcomingAnimes: TopUpcomingAnime[]; 21 | topAiringAnimes: TopAiringAnime[]; 22 | mostPopularAnimes: MostPopularAnime[]; 23 | mostFavoriteAnimes: MostFavoriteAnime[]; 24 | latestCompletedAnimes: LatestCompletedAnime[]; 25 | }; 26 | -------------------------------------------------------------------------------- /src/hianime/types/scrapers/index.ts: -------------------------------------------------------------------------------- 1 | import type { ScrapedHomePage } from "./homePage.js"; 2 | import type { ScrapedGenreAnime } from "./animeGenre.js"; 3 | import type { ScrapedAnimeAZList } from "./animeAZList.js"; 4 | import type { ScrapedAnimeQtipInfo } from "./animeQtip.js"; 5 | import type { ScrapedAnimeEpisodes } from "./animeEpisodes.js"; 6 | import type { ScrapedAnimeCategory } from "./animeCategory.js"; 7 | import type { ScrapedProducerAnime } from "./animeProducer.js"; 8 | import type { ScrapedEpisodeServers } from "./episodeServers.js"; 9 | import type { ScrapedAnimeAboutInfo } from "./animeAboutInfo.js"; 10 | import type { ScrapedAnimeSearchResult } from "./animeSearch.js"; 11 | import type { ScrapedAnimeEpisodesSources } from "./animeEpisodeSrcs.js"; 12 | import type { ScrapedAnimeSearchSuggestion } from "./animeSearchSuggestion.js"; 13 | import type { 14 | ScrapedEstimatedSchedule, 15 | ScrapedNextEpisodeSchedule, 16 | } from "./estimatedSchedule.js"; 17 | 18 | export type { 19 | ScrapedHomePage, 20 | ScrapedGenreAnime, 21 | ScrapedAnimeAZList, 22 | ScrapedAnimeQtipInfo, 23 | ScrapedAnimeEpisodes, 24 | ScrapedProducerAnime, 25 | ScrapedAnimeCategory, 26 | ScrapedEpisodeServers, 27 | ScrapedAnimeAboutInfo, 28 | ScrapedAnimeSearchResult, 29 | ScrapedEstimatedSchedule, 30 | ScrapedNextEpisodeSchedule, 31 | ScrapedAnimeEpisodesSources, 32 | ScrapedAnimeSearchSuggestion, 33 | }; 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // hianime class export 2 | export * as HiAnime from "./hianime/hianime.js"; 3 | 4 | // hianime error 5 | export { HiAnimeError } from "./hianime/error.js"; 6 | 7 | export type { AniwatchError } from "./config/error.js"; 8 | 9 | // helpful constant exports 10 | // export { 11 | // typeIdMap, 12 | // sortIdMap, 13 | // ratedIdMap, 14 | // scoreIdMap, 15 | // genresIdMap, 16 | // seasonIdMap, 17 | // statusIdMap, 18 | // languageIdMap, 19 | // } from "./utils/constants.js"; 20 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const ACCEPT_ENCODING_HEADER = "gzip, deflate, br" as const; 2 | export const USER_AGENT_HEADER = 3 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" as const; 4 | export const ACCEPT_HEADER = 5 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" as const; 6 | 7 | // previously zoro.to -> aniwatch.to -> aniwatchtv.to -> hianimez.to 8 | const DOMAIN = "hianimez.to" as const; 9 | 10 | export const SRC_BASE_URL = `https://${DOMAIN}` as const; 11 | export const SRC_AJAX_URL = `${SRC_BASE_URL}/ajax` as const; 12 | export const SRC_HOME_URL = `${SRC_BASE_URL}/home` as const; 13 | export const SRC_SEARCH_URL = `${SRC_BASE_URL}/search` as const; 14 | 15 | type SearchPageFilters = { 16 | GENRES_ID_MAP: Record; 17 | TYPE_ID_MAP: Record; 18 | STATUS_ID_MAP: Record; 19 | RATED_ID_MAP: Record; 20 | SCORE_ID_MAP: Record; 21 | SEASON_ID_MAP: Record; 22 | LANGUAGE_ID_MAP: Record; 23 | SORT_ID_MAP: Record; 24 | }; 25 | 26 | export const SEARCH_PAGE_FILTERS: SearchPageFilters = { 27 | GENRES_ID_MAP: { 28 | action: 1, 29 | adventure: 2, 30 | cars: 3, 31 | comedy: 4, 32 | dementia: 5, 33 | demons: 6, 34 | drama: 8, 35 | ecchi: 9, 36 | fantasy: 10, 37 | game: 11, 38 | harem: 35, 39 | historical: 13, 40 | horror: 14, 41 | isekai: 44, 42 | josei: 43, 43 | kids: 15, 44 | magic: 16, 45 | "martial-arts": 17, 46 | mecha: 18, 47 | military: 38, 48 | music: 19, 49 | mystery: 7, 50 | parody: 20, 51 | police: 39, 52 | psychological: 40, 53 | romance: 22, 54 | samurai: 21, 55 | school: 23, 56 | "sci-fi": 24, 57 | seinen: 42, 58 | shoujo: 25, 59 | "shoujo-ai": 26, 60 | shounen: 27, 61 | "shounen-ai": 28, 62 | "slice-of-life": 36, 63 | space: 29, 64 | sports: 30, 65 | "super-power": 31, 66 | supernatural: 37, 67 | thriller: 41, 68 | vampire: 32, 69 | }, 70 | 71 | TYPE_ID_MAP: { 72 | all: 0, 73 | movie: 1, 74 | tv: 2, 75 | ova: 3, 76 | ona: 4, 77 | special: 5, 78 | music: 6, 79 | }, 80 | 81 | STATUS_ID_MAP: { 82 | all: 0, 83 | "finished-airing": 1, 84 | "currently-airing": 2, 85 | "not-yet-aired": 3, 86 | }, 87 | 88 | RATED_ID_MAP: { 89 | all: 0, 90 | g: 1, 91 | pg: 2, 92 | "pg-13": 3, 93 | r: 4, 94 | "r+": 5, 95 | rx: 6, 96 | }, 97 | 98 | SCORE_ID_MAP: { 99 | all: 0, 100 | appalling: 1, 101 | horrible: 2, 102 | "very-bad": 3, 103 | bad: 4, 104 | average: 5, 105 | fine: 6, 106 | good: 7, 107 | "very-good": 8, 108 | great: 9, 109 | masterpiece: 10, 110 | }, 111 | 112 | SEASON_ID_MAP: { 113 | all: 0, 114 | spring: 1, 115 | summer: 2, 116 | fall: 3, 117 | winter: 4, 118 | }, 119 | 120 | LANGUAGE_ID_MAP: { 121 | all: 0, 122 | sub: 1, 123 | dub: 2, 124 | "sub-&-dub": 3, 125 | }, 126 | 127 | SORT_ID_MAP: { 128 | default: "default", 129 | "recently-added": "recently_added", 130 | "recently-updated": "recently_updated", 131 | score: "score", 132 | "name-a-z": "name_az", 133 | "released-date": "released_date", 134 | "most-watched": "most_watched", 135 | }, 136 | } as const; 137 | 138 | export const AZ_LIST_SORT_OPTIONS = { 139 | all: true, 140 | other: true, 141 | "0-9": true, 142 | a: true, 143 | b: true, 144 | c: true, 145 | d: true, 146 | e: true, 147 | f: true, 148 | g: true, 149 | h: true, 150 | i: true, 151 | j: true, 152 | k: true, 153 | l: true, 154 | m: true, 155 | n: true, 156 | o: true, 157 | p: true, 158 | q: true, 159 | r: true, 160 | s: true, 161 | t: true, 162 | u: true, 163 | v: true, 164 | w: true, 165 | x: true, 166 | y: true, 167 | z: true, 168 | } as const; 169 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./methods.js"; 2 | export * from "./constants.js"; 3 | -------------------------------------------------------------------------------- /src/utils/methods.ts: -------------------------------------------------------------------------------- 1 | import { HiAnimeError } from "../hianime/error.js"; 2 | import type { 3 | Anime, 4 | Top10Anime, 5 | MostPopularAnime, 6 | Top10AnimeTimePeriod, 7 | } from "../hianime/types/anime.js"; 8 | import { SEARCH_PAGE_FILTERS } from "./constants.js"; 9 | import type { CheerioAPI, SelectorType } from "cheerio"; 10 | import type { FilterKeys } from "../hianime/types/animeSearch.js"; 11 | 12 | export const extractAnimes = ( 13 | $: CheerioAPI, 14 | selector: SelectorType, 15 | scraperName: string 16 | ): Anime[] => { 17 | try { 18 | const animes: Anime[] = []; 19 | 20 | $(selector).each((_, el) => { 21 | const animeId = 22 | $(el) 23 | .find(".film-detail .film-name .dynamic-name") 24 | ?.attr("href") 25 | ?.slice(1) 26 | .split("?ref=search")[0] || null; 27 | 28 | animes.push({ 29 | id: animeId, 30 | name: $(el) 31 | .find(".film-detail .film-name .dynamic-name") 32 | ?.text() 33 | ?.trim(), 34 | jname: 35 | $(el) 36 | .find(".film-detail .film-name .dynamic-name") 37 | ?.attr("data-jname") 38 | ?.trim() || null, 39 | poster: 40 | $(el) 41 | .find(".film-poster .film-poster-img") 42 | ?.attr("data-src") 43 | ?.trim() || null, 44 | duration: $(el) 45 | .find(".film-detail .fd-infor .fdi-item.fdi-duration") 46 | ?.text() 47 | ?.trim(), 48 | type: $(el) 49 | .find(".film-detail .fd-infor .fdi-item:nth-of-type(1)") 50 | ?.text() 51 | ?.trim(), 52 | rating: 53 | $(el).find(".film-poster .tick-rate")?.text()?.trim() || 54 | null, 55 | episodes: { 56 | sub: 57 | Number( 58 | $(el) 59 | .find(".film-poster .tick-sub") 60 | ?.text() 61 | ?.trim() 62 | .split(" ") 63 | .pop() 64 | ) || null, 65 | dub: 66 | Number( 67 | $(el) 68 | .find(".film-poster .tick-dub") 69 | ?.text() 70 | ?.trim() 71 | .split(" ") 72 | .pop() 73 | ) || null, 74 | }, 75 | }); 76 | }); 77 | 78 | return animes; 79 | } catch (err: any) { 80 | throw HiAnimeError.wrapError(err, scraperName); 81 | } 82 | }; 83 | 84 | export const extractTop10Animes = ( 85 | $: CheerioAPI, 86 | period: Top10AnimeTimePeriod, 87 | scraperName: string 88 | ): Top10Anime[] => { 89 | try { 90 | const animes: Top10Anime[] = []; 91 | const selector = `#top-viewed-${period} ul li`; 92 | 93 | $(selector).each((_, el) => { 94 | animes.push({ 95 | id: 96 | $(el) 97 | .find(".film-detail .dynamic-name") 98 | ?.attr("href") 99 | ?.slice(1) 100 | .trim() || null, 101 | rank: 102 | Number($(el).find(".film-number span")?.text()?.trim()) || 103 | null, 104 | name: 105 | $(el).find(".film-detail .dynamic-name")?.text()?.trim() || 106 | null, 107 | jname: 108 | $(el) 109 | .find(".film-detail .dynamic-name") 110 | ?.attr("data-jname") 111 | ?.trim() || null, 112 | poster: 113 | $(el) 114 | .find(".film-poster .film-poster-img") 115 | ?.attr("data-src") 116 | ?.trim() || null, 117 | episodes: { 118 | sub: 119 | Number( 120 | $(el) 121 | .find( 122 | ".film-detail .fd-infor .tick-item.tick-sub" 123 | ) 124 | ?.text() 125 | ?.trim() 126 | ) || null, 127 | dub: 128 | Number( 129 | $(el) 130 | .find( 131 | ".film-detail .fd-infor .tick-item.tick-dub" 132 | ) 133 | ?.text() 134 | ?.trim() 135 | ) || null, 136 | }, 137 | }); 138 | }); 139 | 140 | return animes; 141 | } catch (err: any) { 142 | throw HiAnimeError.wrapError(err, scraperName); 143 | } 144 | }; 145 | 146 | export const extractMostPopularAnimes = ( 147 | $: CheerioAPI, 148 | selector: SelectorType, 149 | scraperName: string 150 | ): MostPopularAnime[] => { 151 | try { 152 | const animes: MostPopularAnime[] = []; 153 | 154 | $(selector).each((_, el) => { 155 | animes.push({ 156 | id: 157 | $(el) 158 | .find(".film-detail .dynamic-name") 159 | ?.attr("href") 160 | ?.slice(1) 161 | .trim() || null, 162 | name: 163 | $(el).find(".film-detail .dynamic-name")?.text()?.trim() || 164 | null, 165 | jname: 166 | $(el) 167 | .find(".film-detail .film-name .dynamic-name") 168 | .attr("data-jname") 169 | ?.trim() || null, 170 | poster: 171 | $(el) 172 | .find(".film-poster .film-poster-img") 173 | ?.attr("data-src") 174 | ?.trim() || null, 175 | episodes: { 176 | sub: 177 | Number( 178 | $(el) 179 | ?.find(".fd-infor .tick .tick-sub") 180 | ?.text() 181 | ?.trim() 182 | ) || null, 183 | dub: 184 | Number( 185 | $(el) 186 | ?.find(".fd-infor .tick .tick-dub") 187 | ?.text() 188 | ?.trim() 189 | ) || null, 190 | }, 191 | type: 192 | $(el) 193 | ?.find(".fd-infor .tick") 194 | ?.text() 195 | ?.trim() 196 | ?.replace(/[\s\n]+/g, " ") 197 | ?.split(" ") 198 | ?.pop() || null, 199 | }); 200 | }); 201 | 202 | return animes; 203 | } catch (err: any) { 204 | throw HiAnimeError.wrapError(err, scraperName); 205 | } 206 | }; 207 | 208 | export function retrieveServerId( 209 | $: CheerioAPI, 210 | index: number, 211 | category: "sub" | "dub" | "raw" 212 | ) { 213 | return ( 214 | $( 215 | `.ps_-block.ps_-block-sub.servers-${category} > .ps__-list .server-item` 216 | ) 217 | ?.map((_, el) => 218 | $(el).attr("data-server-id") == `${index}` ? $(el) : null 219 | ) 220 | ?.get()[0] 221 | ?.attr("data-id") || null 222 | ); 223 | } 224 | 225 | function getGenresFilterVal(genreNames: string[]): string | undefined { 226 | if (genreNames.length < 1) { 227 | return undefined; 228 | } 229 | return genreNames 230 | .map((name) => SEARCH_PAGE_FILTERS["GENRES_ID_MAP"][name]) 231 | .join(","); 232 | } 233 | 234 | export function getSearchFilterValue( 235 | key: FilterKeys, 236 | rawValue: string 237 | ): string | undefined { 238 | rawValue = rawValue.trim(); 239 | if (!rawValue) return undefined; 240 | 241 | switch (key) { 242 | case "genres": { 243 | return getGenresFilterVal(rawValue.split(",")); 244 | } 245 | case "type": { 246 | const val = SEARCH_PAGE_FILTERS["TYPE_ID_MAP"][rawValue] ?? 0; 247 | return val === 0 ? undefined : `${val}`; 248 | } 249 | case "status": { 250 | const val = SEARCH_PAGE_FILTERS["STATUS_ID_MAP"][rawValue] ?? 0; 251 | return val === 0 ? undefined : `${val}`; 252 | } 253 | case "rated": { 254 | const val = SEARCH_PAGE_FILTERS["RATED_ID_MAP"][rawValue] ?? 0; 255 | return val === 0 ? undefined : `${val}`; 256 | } 257 | case "score": { 258 | const val = SEARCH_PAGE_FILTERS["SCORE_ID_MAP"][rawValue] ?? 0; 259 | return val === 0 ? undefined : `${val}`; 260 | } 261 | case "season": { 262 | const val = SEARCH_PAGE_FILTERS["SEASON_ID_MAP"][rawValue] ?? 0; 263 | return val === 0 ? undefined : `${val}`; 264 | } 265 | case "language": { 266 | const val = SEARCH_PAGE_FILTERS["LANGUAGE_ID_MAP"][rawValue] ?? 0; 267 | return val === 0 ? undefined : `${val}`; 268 | } 269 | case "sort": { 270 | return SEARCH_PAGE_FILTERS["SORT_ID_MAP"][rawValue] ?? undefined; 271 | } 272 | default: 273 | return undefined; 274 | } 275 | } 276 | 277 | // this fn tackles both start_date and end_date 278 | export function getSearchDateFilterValue( 279 | isStartDate: boolean, 280 | rawValue: string 281 | ): string[] | undefined { 282 | rawValue = rawValue.trim(); 283 | if (!rawValue) return undefined; 284 | 285 | const dateRegex = /^\d{4}-([0-9]|1[0-2])-([0-9]|[12][0-9]|3[01])$/; 286 | const dateCategory = isStartDate ? "s" : "e"; 287 | const [year, month, date] = rawValue.split("-"); 288 | 289 | if (!dateRegex.test(rawValue)) { 290 | return undefined; 291 | } 292 | 293 | // sample return -> [sy=2023, sm=10, sd=11] 294 | return [ 295 | Number(year) > 0 ? `${dateCategory}y=${year}` : "", 296 | Number(month) > 0 ? `${dateCategory}m=${month}` : "", 297 | Number(date) > 0 ? `${dateCategory}d=${date}` : "", 298 | ].filter((d) => Boolean(d)); 299 | } 300 | 301 | export function substringAfter(str: string, toFind: string) { 302 | const index = str.indexOf(toFind); 303 | return index == -1 ? "" : str.substring(index + toFind.length); 304 | } 305 | 306 | export function substringBefore(str: string, toFind: string) { 307 | const index = str.indexOf(toFind); 308 | return index == -1 ? "" : str.substring(0, index); 309 | } 310 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "noImplicitAny": true, 5 | "esModuleInterop": true, 6 | "strictNullChecks": true, 7 | "target": "ES2022", 8 | "module": "NodeNext", 9 | "moduleResolution": "NodeNext", 10 | 11 | "noEmit": true, 12 | "allowJs": true, 13 | "declaration": true, 14 | "skipLibCheck": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "verbatimModuleSyntax": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | 21 | "outDir": "dist", 22 | "rootDir": "./", 23 | "sourceMap": true, 24 | // "removeComments": true, 25 | "strictFunctionTypes": true, 26 | "forceConsistentCasingInFileNames": true 27 | }, 28 | "include": ["src"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | format: ["esm"], 5 | entry: ["./src/index.ts"], 6 | dts: true, 7 | shims: true, 8 | clean: true, 9 | splitting: true, 10 | // minify: true, 11 | // minifySyntax: true, 12 | // minifyIdentifiers: true, 13 | // minifyWhitespace: true, 14 | globalName: "aniwatch", 15 | skipNodeModulesBundle: true, 16 | }); 17 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | name: "aniwatch", 6 | environment: "node", 7 | testTimeout: 15000, 8 | reporters: "default", 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------