diff --git a/consumet.ts b/consumet.ts deleted file mode 160000 index 2bcd9287..00000000 --- a/consumet.ts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2bcd9287dc1471ed081bc23333e7629779924e0e diff --git a/consumet.ts/.commitlintrc.json b/consumet.ts/.commitlintrc.json new file mode 100644 index 00000000..ecf30b6e --- /dev/null +++ b/consumet.ts/.commitlintrc.json @@ -0,0 +1,23 @@ +{ + "extends": ["@commitlint/config-angular"], + "rules": { + "scope-case": [0], + "type-enum": [ + 2, + "always", [ + "chore", + "ci", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test", + "types", + "workflow", + "wip" + ] + ] + } +} \ No newline at end of file diff --git a/consumet.ts/.editorconfig b/consumet.ts/.editorconfig new file mode 100644 index 00000000..3fef2711 --- /dev/null +++ b/consumet.ts/.editorconfig @@ -0,0 +1,7 @@ +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true \ No newline at end of file diff --git a/consumet.ts/.gitattributes b/consumet.ts/.gitattributes new file mode 100644 index 00000000..89deaff2 --- /dev/null +++ b/consumet.ts/.gitattributes @@ -0,0 +1,5 @@ +# Set the default behavior, in case people don't have core.autocrlf set +* text=auto + +# Require Unix line endings +* text eol=lf \ No newline at end of file diff --git a/consumet.ts/.github/CODEOWNERS b/consumet.ts/.github/CODEOWNERS new file mode 100644 index 00000000..b623ae62 --- /dev/null +++ b/consumet.ts/.github/CODEOWNERS @@ -0,0 +1 @@ +* @riimuru @prince-ao \ No newline at end of file diff --git a/consumet.ts/.github/ISSUE_TEMPLATE/bug-report.yml b/consumet.ts/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..fef471a3 --- /dev/null +++ b/consumet.ts/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,57 @@ +name: 🐞 Bug report +description: Create a report to help us improve +labels: [bug] +body: + - type: input + id: describe-the-bug + attributes: + label: Describe the bug + description: | + A clear and concise description of what the bug is. + placeholder: | + Example: "This provider is not working..." + validations: + required: true + + - type: textarea + id: reproduce-steps + attributes: + label: Steps to reproduce + description: Provide an example of the issue. + placeholder: | + Example: + 1. First step + 2. Second step + 3. Issue here + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + placeholder: | + Example: + "This should happen..." + validations: + required: true + + - type: textarea + id: actual-behavior + attributes: + label: Actual behavior + placeholder: | + Example: + "This happened instead..." + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: | + Add any other context about the problem here. + placeholder: | + Example: + "Also ..." diff --git a/consumet.ts/.github/ISSUE_TEMPLATE/config.yml b/consumet.ts/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..a5bdec6c --- /dev/null +++ b/consumet.ts/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +contact_links: + - name: 🙋 Question + url: https://discord.gg/qTPfvMxzNH + about: Ask your question or suggestion in Discord server + + - name: 🍔 Providers list + url: https://consumet.org/extensions/list/ + about: A list of the available providers + + - name: 🌐 Rest-API reference + url: https://docs.consumet.org + about: A reference of the Consumet Rest-API documentation diff --git a/consumet.ts/.github/ISSUE_TEMPLATE/feature-request.md b/consumet.ts/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 00000000..325272d6 --- /dev/null +++ b/consumet.ts/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,19 @@ +--- +name: 🆕 Feature request +about: Suggest an idea for this project +title: '' +labels: ['enhancement'] +assignees: [] +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/consumet.ts/.github/ISSUE_TEMPLATE/provider-request.yml b/consumet.ts/.github/ISSUE_TEMPLATE/provider-request.yml new file mode 100644 index 00000000..feff2577 --- /dev/null +++ b/consumet.ts/.github/ISSUE_TEMPLATE/provider-request.yml @@ -0,0 +1,54 @@ +name: 🍕 Provider request +description: Request a new source to be added +labels: [provider request] +body: + - type: input + id: source-name + attributes: + label: Source name + placeholder: | + Example: "9anime" + validations: + required: true + + - type: input + id: source-link + attributes: + label: Source link + placeholder: | + Example: "https://9anime.to/" + validations: + required: true + + - type: input + id: source-language + attributes: + label: Language + placeholder: | + Example: "English" + validations: + required: true + + - type: dropdown + id: source-type + attributes: + label: Source type + options: + - Anime + - Books + - Comics + - Manga + - Movie/TV + - Light Novels + - Other + validations: + required: true + + - type: textarea + id: additional-info + attributes: + label: Additional info + description: | + Please provide any additional information that may be helpful to the provider. + placeholder: | + Example: "Anime is also supported by this Movie/TV source." diff --git a/consumet.ts/.github/PULL_REQUEST_TEMPLATE.md b/consumet.ts/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..981a98f7 --- /dev/null +++ b/consumet.ts/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ + + +**What kind of change does this PR introduce?** + + + +**Did you add tests for your changes?** + +**If relevant, did you update the documentation?** + +**Summary** + + + + +**Other information** diff --git a/consumet.ts/.github/dependabot.yml b/consumet.ts/.github/dependabot.yml new file mode 100644 index 00000000..428277c2 --- /dev/null +++ b/consumet.ts/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: daily + open-pull-requests-limit: 3 + allow: + - dependency-type: "production" + ignore: + - dependency-name: "axios" + versions: ["1.x"] + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 2 diff --git a/consumet.ts/.github/stale.yml b/consumet.ts/.github/stale.yml new file mode 100644 index 00000000..a236c8a2 --- /dev/null +++ b/consumet.ts/.github/stale.yml @@ -0,0 +1,22 @@ +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: > + We are closing this issue. If the issue still persists in the latest version of + consumet.ts, please reopen the issue and update the description. We will try our + best to accomodate it! +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 30 +# Issues with these labels will never be considered stale +exemptLabels: + - provider request + - enhancement + - help wanted +# Comment to post when marking an issue as stale. +markComment: > + We're marking this issue as wontfix because it has not had recent activity. It will be closed if no further activity occurs + within the next 30 days. Thank you for your contributions. +# Only mark issues. +only: issues +# Label to use when marking an issue as stale +staleLabel: wontfix diff --git a/consumet.ts/.github/workflows/codeql-analysis.yml b/consumet.ts/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..3143db86 --- /dev/null +++ b/consumet.ts/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["master"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["master"] + schedule: + - cron: "40 21 * * 4" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["typeScript"] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/consumet.ts/.github/workflows/nodejs-ci.yml b/consumet.ts/.github/workflows/nodejs-ci.yml new file mode 100644 index 00000000..0e73bab4 --- /dev/null +++ b/consumet.ts/.github/workflows/nodejs-ci.yml @@ -0,0 +1,26 @@ +name: Node.js CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: ["14.x"] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4.0.2 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies & build + run: | + yarn install --ignore-engines + yarn build diff --git a/consumet.ts/.github/workflows/npm-publish.yml b/consumet.ts/.github/workflows/npm-publish.yml new file mode 100644 index 00000000..9e2a2fac --- /dev/null +++ b/consumet.ts/.github/workflows/npm-publish.yml @@ -0,0 +1,34 @@ +name: Node.js Package + +on: + release: + types: [created] + workflow_dispatch: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4.0.2 + with: + node-version: 16 + - run: npm install + - run: npm run build + # - run: npm test + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4.0.2 + with: + node-version: 16 + registry-url: https://registry.npmjs.org/ + - run: npm install + - run: npm run build + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.npm_token}} diff --git a/consumet.ts/.github/workflows/push-build.yml b/consumet.ts/.github/workflows/push-build.yml new file mode 100644 index 00000000..bfee510c --- /dev/null +++ b/consumet.ts/.github/workflows/push-build.yml @@ -0,0 +1,46 @@ +name: Compile TS + +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Checkout PR + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh pr checkout ${{ github.event.pull_request.number }} + + - name: Install dependencies + run: yarn install + + - name: Compile TS + run: yarn build + + - name: Setup Git + run: | + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --global user.name "Rapid Stream Bot" + git config --global credential.helper store + echo "https://${{ secrets.USERNAME }}:${{ secrets.TOKEN }}@github.com" > ~/.git-credentials + shell: bash + + - name: Check Diff + id: check_diff + run: | + git diff --exit-code + if [ $? -eq 0 ] + then + echo "push_changes=false" >> $GITHUB_OUTPUT + else + echo "push_changes=true" >> $GITHUB_OUTPUT + fi + shell: bash + + - name: Push Changes + if: steps.check_diff.outputs.push_changes == 'true' + run: | + git add . + git commit -am "Compile TS" + git push diff --git a/consumet.ts/.github/workflows/remove-npm-package.yml b/consumet.ts/.github/workflows/remove-npm-package.yml new file mode 100644 index 00000000..2e8ae394 --- /dev/null +++ b/consumet.ts/.github/workflows/remove-npm-package.yml @@ -0,0 +1,22 @@ +name: Remove Npm Package + +on: + workflow_dispatch: + inputs: + version: + description: "Remove Version" + required: true + +jobs: + publish-npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4.0.2 + with: + node-version: 16 + registry-url: https://registry.npmjs.org/ + - run: npm unpublish @consumet/extensions@$VERSION + env: + NODE_AUTH_TOKEN: ${{ secrets.npm_token }} + VERSION: ${{ inputs.version }} diff --git a/consumet.ts/.gitignore b/consumet.ts/.gitignore new file mode 100644 index 00000000..8fcc7791 --- /dev/null +++ b/consumet.ts/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +*.tgz +yarn-error.log +.vscode +.idea +.DS_Store +.env +package-lock.json +yarn.lock +test.* \ No newline at end of file diff --git a/consumet.ts/.husky/pre-commit b/consumet.ts/.husky/pre-commit new file mode 100644 index 00000000..6cc1fdcb --- /dev/null +++ b/consumet.ts/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn build && git add dist diff --git a/consumet.ts/.npmignore b/consumet.ts/.npmignore new file mode 100644 index 00000000..7637f894 --- /dev/null +++ b/consumet.ts/.npmignore @@ -0,0 +1,3 @@ +node_modules +.github +yarn.lock \ No newline at end of file diff --git a/consumet.ts/.prettierignore b/consumet.ts/.prettierignore new file mode 100644 index 00000000..a58cb6c9 --- /dev/null +++ b/consumet.ts/.prettierignore @@ -0,0 +1,15 @@ +node_modules +*.js +*.d.ts +*.md +*.json +*.lock +*.yml +*.yaml +Dockerfile +Dockerfile.* +.eslintrc +*.html +/examples +/docs +jest.config.ts \ No newline at end of file diff --git a/consumet.ts/.prettierrc.yml b/consumet.ts/.prettierrc.yml new file mode 100644 index 00000000..2b34f4c7 --- /dev/null +++ b/consumet.ts/.prettierrc.yml @@ -0,0 +1,10 @@ +parser: 'typescript' +useTabs: false +tabWidth: 2 +singleQuote: true +printWidth: 110 +bracketSpacing: true +semi: true +endOfLine: 'lf' +proseWrap: never +arrowParens: avoid diff --git a/consumet.ts/CHANGELOG.md b/consumet.ts/CHANGELOG.md new file mode 100644 index 00000000..7239b9f3 --- /dev/null +++ b/consumet.ts/CHANGELOG.md @@ -0,0 +1,649 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). + +## [v1.4.16](https://github.com/consumet/consumet.ts/compare/v1.4.16...v1.4.16) + +### Commits + +- add fmovies to providers-list [`9ab35e2`](https://github.com/consumet/consumet.ts/commit/9ab35e2ddf4f31d1a413555b735989fd23247fa6) + +## [v1.4.16](https://github.com/consumet/consumet.ts/compare/v1.4.15...v1.4.16) - 2023-03-13 + +### Merged + +- Added Fmovies to index.ts [`#306`](https://github.com/consumet/consumet.ts/pull/306) +- Fixed 9anime's search [`#304`](https://github.com/consumet/consumet.ts/pull/304) +- chore(build) yarn build [`#303`](https://github.com/consumet/consumet.ts/pull/303) +- change tld to .gr [`#302`](https://github.com/consumet/consumet.ts/pull/302) +- Added Fmovies [`#300`](https://github.com/consumet/consumet.ts/pull/300) +- chore(build) yarn build [`#299`](https://github.com/consumet/consumet.ts/pull/299) +- Fixed vizcloud [`#298`](https://github.com/consumet/consumet.ts/pull/298) +- Fix 9anime search [`#294`](https://github.com/consumet/consumet.ts/pull/294) +- Build [`#291`](https://github.com/consumet/consumet.ts/pull/291) +- 9anime fix [`#290`](https://github.com/consumet/consumet.ts/pull/290) + +## [v1.4.15](https://github.com/consumet/consumet.ts/compare/v1.4.14...v1.4.15) - 2023-03-04 + +### Merged + +- Fix unused imports for ComicK [`#288`](https://github.com/consumet/consumet.ts/pull/288) +- Added ComicK [`#287`](https://github.com/consumet/consumet.ts/pull/287) +- Fix Vizcloud [`#286`](https://github.com/consumet/consumet.ts/pull/286) +- Qualities for Vizcloud [`#283`](https://github.com/consumet/consumet.ts/pull/283) +- fix: scraping sub/dub info now works for nineanime [`#279`](https://github.com/consumet/consumet.ts/pull/279) +- fixed spelling error of subOrDub (was suborsub) [`#277`](https://github.com/consumet/consumet.ts/pull/277) +- fix base-parser causing some routes to fail [`#276`](https://github.com/consumet/consumet.ts/pull/276) +- Added API keys [`#275`](https://github.com/consumet/consumet.ts/pull/275) +- chore(marin): remove logging 🙏 [`#274`](https://github.com/consumet/consumet.ts/pull/274) + +### Commits + +- change license [`ed074ab`](https://github.com/consumet/consumet.ts/commit/ed074ab6a1708f26f21f3ae502939912c75a32d8) +- chore(yarn) yarn build [`3224942`](https://github.com/consumet/consumet.ts/commit/3224942ef952858a234502d06fd903154c8c9b34) +- Delete LICENSE [`92ef734`](https://github.com/consumet/consumet.ts/commit/92ef734650347e22944dcb6d920c9f8ebcc38523) + +## [v1.4.14](https://github.com/consumet/consumet.ts/compare/v1.4.13...v1.4.14) - 2023-02-26 + +### Merged + +- v1.4.14 [`#273`](https://github.com/consumet/consumet.ts/pull/273) +- proxy config to 9anime [`#272`](https://github.com/consumet/consumet.ts/pull/272) + +## [v1.4.13](https://github.com/consumet/consumet.ts/compare/v1.4.12...v1.4.13) - 2023-02-25 + +### Merged + +- v1.4.13 [`#271`](https://github.com/consumet/consumet.ts/pull/271) +- Added intros and fixed some bugs (9anime) [`#270`](https://github.com/consumet/consumet.ts/pull/270) +- chore(build) yarn build [`#268`](https://github.com/consumet/consumet.ts/pull/268) +- Fixed 9anime [`#267`](https://github.com/consumet/consumet.ts/pull/267) + +### Commits + +- refactor 9anime [`aa011fd`](https://github.com/consumet/consumet.ts/commit/aa011fde891fd3e88a37b6799739f12a7b79cd3c) + +## [v1.4.12](https://github.com/consumet/consumet.ts/compare/v1.4.10...v1.4.12) - 2023-02-23 + +### Merged + +- v1.4.12 [`#266`](https://github.com/consumet/consumet.ts/pull/266) +- fixes "working code" [`#265`](https://github.com/consumet/consumet.ts/pull/265) +- v1.4.11 [`#264`](https://github.com/consumet/consumet.ts/pull/264) +- 9anime [`#263`](https://github.com/consumet/consumet.ts/pull/263) + +### Commits + +- refactor 9anime [`ed817a3`](https://github.com/consumet/consumet.ts/commit/ed817a3f007b9a97917c01acf6d9b61ff757fbe1) +- feat (base parser): rotate proxies if given an array [`ca0dfdf`](https://github.com/consumet/consumet.ts/commit/ca0dfdfc9a55364032a7c46e6c750e301078d340) + +## [v1.4.10](https://github.com/consumet/consumet.ts/compare/v1.4.9...v1.4.10) - 2023-02-16 + +### Merged + +- Ver [`#261`](https://github.com/consumet/consumet.ts/pull/261) +- fixed marin [`#260`](https://github.com/consumet/consumet.ts/pull/260) +- Update dependabot.yml [`#259`](https://github.com/consumet/consumet.ts/pull/259) + +## [v1.4.9](https://github.com/consumet/consumet.ts/compare/v1.4.8...v1.4.9) - 2023-02-14 + +### Merged + +- added ops and eds to mal [`#258`](https://github.com/consumet/consumet.ts/pull/258) +- chore(dist): update build [`#256`](https://github.com/consumet/consumet.ts/pull/256) +- chore(prettier): fix prettier config [`#255`](https://github.com/consumet/consumet.ts/pull/255) +- fix(marin): fix in case anime doesn't has synonyms [`#254`](https://github.com/consumet/consumet.ts/pull/254) +- Apply fixes from CodeFactor [`#251`](https://github.com/consumet/consumet.ts/pull/251) +- feat(anilist) add colour to functions [`#253`](https://github.com/consumet/consumet.ts/pull/253) + +## [v1.4.8](https://github.com/consumet/consumet.ts/compare/v1.4.7...v1.4.8) - 2023-02-11 + +### Merged + +- New ver [`#250`](https://github.com/consumet/consumet.ts/pull/250) + +## [v1.4.7](https://github.com/consumet/consumet.ts/compare/v1.4.6...v1.4.7) - 2023-02-11 + +### Merged + +- A NEW NEW NEW NEW VERSION [`#249`](https://github.com/consumet/consumet.ts/pull/249) +- Update package.json [`#248`](https://github.com/consumet/consumet.ts/pull/248) + +## [v1.4.6](https://github.com/consumet/consumet.ts/compare/v1.4.5...v1.4.6) - 2023-02-11 + +### Merged + +- New ver [`#247`](https://github.com/consumet/consumet.ts/pull/247) + +## [v1.4.5](https://github.com/consumet/consumet.ts/compare/v1.4.3...v1.4.5) - 2023-02-11 + +### Merged + +- New ver [`#246`](https://github.com/consumet/consumet.ts/pull/246) +- Update package.json [`#245`](https://github.com/consumet/consumet.ts/pull/245) + +## [v1.4.3](https://github.com/consumet/consumet.ts/compare/v1.4.2...v1.4.3) - 2023-02-11 + +### Merged + +- Marin [`#244`](https://github.com/consumet/consumet.ts/pull/244) +- Marin [`#242`](https://github.com/consumet/consumet.ts/pull/242) + +## [v1.4.2](https://github.com/consumet/consumet.ts/compare/v1.4.1...v1.4.2) - 2023-02-10 + +### Merged + +- feat (zoro): added `totalPages` count [`#240`](https://github.com/consumet/consumet.ts/pull/240) +- Fixes [`#241`](https://github.com/consumet/consumet.ts/pull/241) +- fix(zoro): hasNextPage returning true, when it's false [`#237`](https://github.com/consumet/consumet.ts/pull/237) +- Update streamsb [`#236`](https://github.com/consumet/consumet.ts/pull/236) + +### Commits + +- chore: bump version `1.4.1` -> `1.4.2` [`038456c`](https://github.com/consumet/consumet.ts/commit/038456c26b1bc28b1d743ce1ea98505b21c57988) + +## [v1.4.1](https://github.com/consumet/consumet.ts/compare/v1.4.0...v1.4.1) - 2023-02-05 + +### Merged + +- v1.4.1 [`#234`](https://github.com/consumet/consumet.ts/pull/234) +- feat(anilist) add currentEpisodeCount [`#233`](https://github.com/consumet/consumet.ts/pull/233) +- Zoro fix [`#232`](https://github.com/consumet/consumet.ts/pull/232) + +### Commits + +- chore: add dist [`fd84b65`](https://github.com/consumet/consumet.ts/commit/fd84b651be12d8063c9986adc017755a84d36fd5) +- comment unnecessary code [`edf683d`](https://github.com/consumet/consumet.ts/commit/edf683dc0eb45451285946caa01dd15b2bc0cabb) + +## [v1.4.0](https://github.com/consumet/consumet.ts/compare/v1.3.8...v1.4.0) - 2023-02-03 + +### Merged + +- v1.4.0 [`#231`](https://github.com/consumet/consumet.ts/pull/231) +- v1.3.8 [`#229`](https://github.com/consumet/consumet.ts/pull/229) +- Mal popularity [`#228`](https://github.com/consumet/consumet.ts/pull/228) + +## [v1.3.8](https://github.com/consumet/consumet.ts/compare/v1.3.7...v1.3.8) - 2023-02-03 + +### Merged + +- Mal popularity [`#227`](https://github.com/consumet/consumet.ts/pull/227) +- added popularity to mal [`#226`](https://github.com/consumet/consumet.ts/pull/226) + +## [v1.3.7](https://github.com/consumet/consumet.ts/compare/v1.3.6...v1.3.7) - 2023-02-02 + +### Merged + +- v1.3.7 [`#225`](https://github.com/consumet/consumet.ts/pull/225) +- fixed base parser/cors config and fixed animepahe test [`#224`](https://github.com/consumet/consumet.ts/pull/224) + +## [v1.3.6](https://github.com/consumet/consumet.ts/compare/v1.3.5...v1.3.6) - 2023-01-30 + +### Merged + +- v1.3.6 [`#218`](https://github.com/consumet/consumet.ts/pull/218) +- fix wrong cors proxy in mangasee123 [`#214`](https://github.com/consumet/consumet.ts/pull/214) +- fix kwik extractor wrong cors url [`#212`](https://github.com/consumet/consumet.ts/pull/212) +- fix mal is undefined [`#211`](https://github.com/consumet/consumet.ts/pull/211) + +### Commits + +- feat (gogoanime): add a method to get anime id from episode id(#216) [`2eb4908`](https://github.com/consumet/consumet.ts/commit/2eb49080bbfe40b790bb24cc57f15fbfcbb502b3) + +## [v1.3.5](https://github.com/consumet/consumet.ts/compare/v1.3.4...v1.3.5) - 2023-01-28 + +### Merged + +- chore (gogoanime): Stop sending errors to the console [`#210`](https://github.com/consumet/consumet.ts/pull/210) +- Add proxy change method [`#205`](https://github.com/consumet/consumet.ts/pull/205) +- feat: add MAL ID to zoro anime info [`#193`](https://github.com/consumet/consumet.ts/pull/193) +- 👺 bye, kamyroll! optimised tests slightly. [`#207`](https://github.com/consumet/consumet.ts/pull/207) + +### Commits + +- chore: bump version to v1.3.5 [`4c2590a`](https://github.com/consumet/consumet.ts/commit/4c2590a509190450763e21ce7b51222ed0cadec8) +- Update cors proxy urls [`6e507da`](https://github.com/consumet/consumet.ts/commit/6e507daed9bfd3f8d41f37c6053fe385e57e8c8d) +- Update README.md [`6c1bce3`](https://github.com/consumet/consumet.ts/commit/6c1bce3319e545631eb45a912f1cbbaae7e6432d) + +## [v1.3.4](https://github.com/consumet/consumet.ts/compare/v1.3.3...v1.3.4) - 2023-01-23 + +### Merged + +- Bump to v1.3.4 [`#204`](https://github.com/consumet/consumet.ts/pull/204) +- Write tests for animepahe [`#203`](https://github.com/consumet/consumet.ts/pull/203) +- fix streamsb and kamyroll logo [`#202`](https://github.com/consumet/consumet.ts/pull/202) +- fix gogohd.pro being defunct [`#199`](https://github.com/consumet/consumet.ts/pull/199) +- Update nodejs-ci.yml [`#201`](https://github.com/consumet/consumet.ts/pull/201) +- Update nodejs-ci.yml [`#200`](https://github.com/consumet/consumet.ts/pull/200) +- subtitles now returns when using enime, if available [`#198`](https://github.com/consumet/consumet.ts/pull/198) + +## [v1.3.3](https://github.com/consumet/consumet.ts/compare/v1.3.1...v1.3.3) - 2023-01-23 + +### Merged + +- Bump to 1.3.3 [`#197`](https://github.com/consumet/consumet.ts/pull/197) +- Add workflow to remove npm version [`#196`](https://github.com/consumet/consumet.ts/pull/196) +- Fixed minor issues with some providers [`#195`](https://github.com/consumet/consumet.ts/pull/195) +- ignore axios in dependabot [`#192`](https://github.com/consumet/consumet.ts/pull/192) +- add workflow dispatch for releases [`#190`](https://github.com/consumet/consumet.ts/pull/190) + +## [v1.3.1](https://github.com/consumet/consumet.ts/compare/v1.3.0...v1.3.1) - 2023-01-17 + +### Merged + +- Undo version to 1.3.1 [`#189`](https://github.com/consumet/consumet.ts/pull/189) +- Update npm-publish.yml [`#186`](https://github.com/consumet/consumet.ts/pull/186) +- version bump [`#185`](https://github.com/consumet/consumet.ts/pull/185) +- 👋 Tenshi, さよなら! [`#184`](https://github.com/consumet/consumet.ts/pull/184) +- Kamyroll and Crunchyroll as Providers [`#182`](https://github.com/consumet/consumet.ts/pull/182) +- fix(Tenshi): sources returning empty array [`#180`](https://github.com/consumet/consumet.ts/pull/180) +- fix kamyroll [`#179`](https://github.com/consumet/consumet.ts/pull/179) +- fix typo [`#178`](https://github.com/consumet/consumet.ts/pull/178) +- feat(Tenshi) add Tenshi as a provider [`#177`](https://github.com/consumet/consumet.ts/pull/177) +- fix gogocdn undefined issue [`#175`](https://github.com/consumet/consumet.ts/pull/175) +- fix(MAL, mangasee123): mangasee123 duplicate items & MAL findAnimeSlug [`#174`](https://github.com/consumet/consumet.ts/pull/174) +- fixe non-correct logos for website [`#173`](https://github.com/consumet/consumet.ts/pull/173) +- add isReleased to tvShows seasons [`#170`](https://github.com/consumet/consumet.ts/pull/170) +- Use non-cached pages to get the keys [`#169`](https://github.com/consumet/consumet.ts/pull/169) +- Change default Gogo Server to VidStreaming [`#167`](https://github.com/consumet/consumet.ts/pull/167) +- Add more info to TMDB [`#166`](https://github.com/consumet/consumet.ts/pull/166) +- Fix anilist episodes mappings [`#164`](https://github.com/consumet/consumet.ts/pull/164) +- Updated rapidclown key URL [`#163`](https://github.com/consumet/consumet.ts/pull/163) +- feat(tmdb) add episode Id to movie info [`#162`](https://github.com/consumet/consumet.ts/pull/162) +- fix(TMDB): fix page and add other pagination values [`#161`](https://github.com/consumet/consumet.ts/pull/161) + +## [v1.3.0](https://github.com/consumet/consumet.ts/compare/v1.2.12...v1.3.0) - 2022-12-26 + +### Merged + +- feat(dramacool) add dramacool provider [`#158`](https://github.com/consumet/consumet.ts/pull/158) +- Add TMDB Provider [`#154`](https://github.com/consumet/consumet.ts/pull/154) +- feat(flixhq): add recommendations and cover image to the info method [`#155`](https://github.com/consumet/consumet.ts/pull/155) +- 👋 AniMixPlay, さよなら! [`#153`](https://github.com/consumet/consumet.ts/pull/153) +- Refactor BaseParser [`#148`](https://github.com/consumet/consumet.ts/pull/148) +- feat(flixhq): add trending and fix recent [`#147`](https://github.com/consumet/consumet.ts/pull/147) +- positive look-behind issue [iOS/Expo] [`#145`](https://github.com/consumet/consumet.ts/pull/145) +- fix(mixdrop): positive look-behind issue [iOS/Expo] [`#144`](https://github.com/consumet/consumet.ts/pull/144) +- feat(MangaReader): Add MangaReader provider [`#143`](https://github.com/consumet/consumet.ts/pull/143) +- feat(MangaPill): Add MangaPill provider [`#142`](https://github.com/consumet/consumet.ts/pull/142) +- refactor(mangadex): reverse chapter order [`#141`](https://github.com/consumet/consumet.ts/pull/141) +- chore(yarn): yarn build & version change [`#140`](https://github.com/consumet/consumet.ts/pull/140) + +### Commits + +- chore: bump version 1.2.13-rc -> 1.3.0 [`c3497c8`](https://github.com/consumet/consumet.ts/commit/c3497c83b28d7379f76180556bf10ea5a3ffb73c) + +## [v1.2.12](https://github.com/consumet/consumet.ts/compare/v1.2.12-rc...v1.2.12) - 2022-12-14 + +## [v1.2.12-rc](https://github.com/consumet/consumet.ts/compare/v1.2.11...v1.2.12-rc) - 2022-12-14 + +### Merged + +- chore(yarn): yarn build & version change [`#140`](https://github.com/consumet/consumet.ts/pull/140) +- bump package version [`#138`](https://github.com/consumet/consumet.ts/pull/138) +- Added Common Methods to docs [`#135`](https://github.com/consumet/consumet.ts/pull/135) +- apply codefactor fixes [`#133`](https://github.com/consumet/consumet.ts/pull/133) +- fixed mal stuff [`#132`](https://github.com/consumet/consumet.ts/pull/132) +- fix(MAL): Aad null safety [`#131`](https://github.com/consumet/consumet.ts/pull/131) +- feat(gogoanime): Add download url to episode sources [`#129`](https://github.com/consumet/consumet.ts/pull/129) +- Apply fixes from CodeFactor [`#128`](https://github.com/consumet/consumet.ts/pull/128) +- feat(gogoanime): Add Genres [`#127`](https://github.com/consumet/consumet.ts/pull/127) + +### Commits + +- Update README.md [`b1d2d12`](https://github.com/consumet/consumet.ts/commit/b1d2d122907c60a898b95d70cad5712399ed60b7) + +## [v1.2.11](https://github.com/consumet/consumet.ts/compare/v1.2.10...v1.2.11) - 2022-12-03 + +### Merged + +- removed useless hasDub [`#125`](https://github.com/consumet/consumet.ts/pull/125) + +### Commits + +- removed useless has [`ea6fbbd`](https://github.com/consumet/consumet.ts/commit/ea6fbbddeb823b129255cd75350e7e89ebab0b6c) + +## [v1.2.10](https://github.com/consumet/consumet.ts/compare/v1.2.9...v1.2.10) - 2022-12-01 + +### Merged + +- Fix StreamSB headers [`#123`](https://github.com/consumet/consumet.ts/pull/123) +- StreamSB Fix [`#122`](https://github.com/consumet/consumet.ts/pull/122) + +### Commits + +- feat(anilist): Add air date for episodes [`bf44977`](https://github.com/consumet/consumet.ts/commit/bf449776e35eb404f3bdacc0ca32c86effb2fa01) +- update build [`f5a950a`](https://github.com/consumet/consumet.ts/commit/f5a950a769c6715e2f9779dcf2252a31034b6749) +- feat(anilist): add external provider mappings [`dbe5c98`](https://github.com/consumet/consumet.ts/commit/dbe5c98f4d28ea484a061450f191aa8f9103a8a9) + +## [v1.2.9](https://github.com/consumet/consumet.ts/compare/v1.2.8...v1.2.9) - 2022-11-27 + +### Merged + +- Update package-lock.json [`#120`](https://github.com/consumet/consumet.ts/pull/120) +- MAL and Tenshi [`#119`](https://github.com/consumet/consumet.ts/pull/119) +- Update index.ts [`#118`](https://github.com/consumet/consumet.ts/pull/118) +- fix(mal): fixed search math [`#116`](https://github.com/consumet/consumet.ts/pull/116) +- MAL [`#115`](https://github.com/consumet/consumet.ts/pull/115) +- Update README.md [`#112`](https://github.com/consumet/consumet.ts/pull/112) +- feat(viewAsian): add viewAsian provider [`#110`](https://github.com/consumet/consumet.ts/pull/110) + +### Commits + +- update build [`01a4afd`](https://github.com/consumet/consumet.ts/commit/01a4afd1c3ed13dde7d780ce2f18824e277393a7) +- remove ws lib [`288456b`](https://github.com/consumet/consumet.ts/commit/288456bdd39cd624d17cb08bd945db6d13723ac5) +- mal and tenshi [`723b634`](https://github.com/consumet/consumet.ts/commit/723b6343e7714ba846bbeb6590fc146926dd076e) + +## [v1.2.8](https://github.com/consumet/consumet.ts/compare/v1.2.7...v1.2.8) - 2022-11-13 + +### Merged + +- Improvements [`#109`](https://github.com/consumet/consumet.ts/pull/109) +- fixed mangadex page numbers [`#108`](https://github.com/consumet/consumet.ts/pull/108) +- R.I.P. Z-Library 💀 [`#105`](https://github.com/consumet/consumet.ts/pull/105) + +### Commits + +- bump version [`946593c`](https://github.com/consumet/consumet.ts/commit/946593ce53b1b85b67087a25d2b8da608124b98d) + +## [v1.2.7](https://github.com/consumet/consumet.ts/compare/v1.2.6...v1.2.7) - 2022-11-09 + +### Merged + +- Dub mapping Improvements [`#104`](https://github.com/consumet/consumet.ts/pull/104) +- feat(animepahe): fixed by adding cors proxy [`#102`](https://github.com/consumet/consumet.ts/pull/102) + +### Commits + +- added proxy to mangasee [`c73d763`](https://github.com/consumet/consumet.ts/commit/c73d7638fae368a0b77e0fcf97b867feb122e48a) + +## [v1.2.6](https://github.com/consumet/consumet.ts/compare/v1.2.5...v1.2.6) - 2022-11-05 + +### Merged + +- feat: bug fixes [`#99`](https://github.com/consumet/consumet.ts/pull/99) +- chore(deps): bump actions/setup-node from 3.5.0 to 3.5.1 [`#88`](https://github.com/consumet/consumet.ts/pull/88) +- feat(anilist): added VA language to characters* [`#97`](https://github.com/consumet/consumet.ts/pull/97) +- feature (anilist): added voice actor language to characters [`#95`](https://github.com/consumet/consumet.ts/pull/95) + +### Commits + +- added language to anilist info [`d3cec3b`](https://github.com/consumet/consumet.ts/commit/d3cec3be3f0012c93cf145c731e9eff0b779ad99) +- feat(mangapark): added mangapark provider, tests & docs (#100 🎉) [`29b8e02`](https://github.com/consumet/consumet.ts/commit/29b8e023f084d3b23065705c0e535d4aa8799be4) +- fix bilibili on anilist route [`df4b006`](https://github.com/consumet/consumet.ts/commit/df4b006ee7e0a6380b6021ebc5fccf9ee2dfd22a) + +## [v1.2.5](https://github.com/consumet/consumet.ts/compare/v1.2.4...v1.2.5) - 2022-11-01 + +### Commits + +- Fix bilibili [`64542ab`](https://github.com/consumet/consumet.ts/commit/64542abdc013db2dc46bd11d3bbec2ba59dc9598) + +## [v1.2.4](https://github.com/consumet/consumet.ts/compare/v1.2.3...v1.2.4) - 2022-11-01 + +### Merged + +- feat(mangadex): add cover image [`#94`](https://github.com/consumet/consumet.ts/pull/94) + +### Commits + +- Add Bilibili [`5d3623e`](https://github.com/consumet/consumet.ts/commit/5d3623e7ee00283fdecb8ec3a13085412332f433) + +## [v1.2.3](https://github.com/consumet/consumet.ts/compare/v1.2.2...v1.2.3) - 2022-10-26 + +### Commits + +- fix crunchyroll on anilist [`5047e6f`](https://github.com/consumet/consumet.ts/commit/5047e6f11cae053e13ef87a66feab3b6c85173e3) + +## [v1.2.2](https://github.com/consumet/consumet.ts/compare/v1.2.1...v1.2.2) - 2022-10-24 + +### Merged + +- Add support for zoro dubs [`#93`](https://github.com/consumet/consumet.ts/pull/93) + +### Commits + +- fix rapidcloud [`0b28835`](https://github.com/consumet/consumet.ts/commit/0b28835349075442ac9244e5debdd3015085f418) + +## [v1.2.1](https://github.com/consumet/consumet.ts/compare/v1.2.0...v1.2.1) - 2022-10-21 + +### Commits + +- export crunchyroll [`dc6fb27`](https://github.com/consumet/consumet.ts/commit/dc6fb274bbd2c5a5bd51331f437f74cd55add822) +- export crunchyroll [`d42ac49`](https://github.com/consumet/consumet.ts/commit/d42ac499bf5b557577e014e66e6ab27c7ee70501) + +## [v1.2.0](https://github.com/consumet/consumet.ts/compare/v1.1.9...v1.2.0) - 2022-10-21 + +### Merged + +- fix mangadex typo [`#92`](https://github.com/consumet/consumet.ts/pull/92) + +### Commits + +- bump version 1.1.9 -> 1.2.0 [`b81cda1`](https://github.com/consumet/consumet.ts/commit/b81cda1e6dae54a3a386a6fb3119d4d781faa66d) +- add crunchyroll [`3b12a7c`](https://github.com/consumet/consumet.ts/commit/3b12a7c5d4481fc2c724dc7ca03f83ad7f6b647c) + +## [v1.1.9](https://github.com/consumet/consumet.ts/compare/v1.1.8...v1.1.9) - 2022-10-18 + +### Merged + +- Fixes examples in docs [`#89`](https://github.com/consumet/consumet.ts/pull/89) + +### Commits + +- Fix undefined ids on anilist recent releases(#91) [`e78731e`](https://github.com/consumet/consumet.ts/commit/e78731ef4cbd2f99b84c9738f5b5dcf27dea69ff) + +## [v1.1.8](https://github.com/consumet/consumet.ts/compare/v1.1.7...v1.1.8) - 2022-10-15 + +### Commits + +- fix gogo source crashing on mp4 [`a2ccabe`](https://github.com/consumet/consumet.ts/commit/a2ccabe04d4a0f453725b6c3a90dca745b8fc5bb) + +## [v1.1.7](https://github.com/consumet/consumet.ts/compare/v1.1.6...v1.1.7) - 2022-10-13 + +### Commits + +- cleanup [`e75975c`](https://github.com/consumet/consumet.ts/commit/e75975c35c38b141398db11f532d7bdc814e12c6) +- bump fix [`f52756d`](https://github.com/consumet/consumet.ts/commit/f52756defc7ad02b1ba1771a7f2252b61928c9de) + +## [v1.1.6](https://github.com/consumet/consumet.ts/compare/v1.1.5...v1.1.6) - 2022-10-13 + +### Merged + +- Add proxy & feat gogo sources resolutions on enime [`#87`](https://github.com/consumet/consumet.ts/pull/87) +- fix: dub episodes returning a sub id despite not having dub episodes [`#84`](https://github.com/consumet/consumet.ts/pull/84) + +### Commits + +- fix some ids not working [`8f1f474`](https://github.com/consumet/consumet.ts/commit/8f1f474ecb81df0e641e2e43cd2fd5c31be1cfe0) +- fix conflicts [`a5dfefc`](https://github.com/consumet/consumet.ts/commit/a5dfefc390a71821e878de7f92535c73a9443d41) +- update dist [`9637e27`](https://github.com/consumet/consumet.ts/commit/9637e270d86e6705dd25f682b98392262f7523bb) + +## [v1.1.5](https://github.com/consumet/consumet.ts/compare/v1.1.4...v1.1.5) - 2022-10-05 + +### Commits + +- flixhq hotfix [`92831a2`](https://github.com/consumet/consumet.ts/commit/92831a2bd3cc85d4b14192bedbb1588eb531cb82) + +## [v1.1.4](https://github.com/consumet/consumet.ts/compare/v1.1.3...v1.1.4) - 2022-10-04 + +### Commits + +- refactor consumet-app.herokuapp.com -> api.consumet.org [`a0006b4`](https://github.com/consumet/consumet.ts/commit/a0006b4bcdc65ece0a2529439b25bd9e6459f64e) +- chore: bump version 1.1.3 -> 1.1.4 [`59e4df0`](https://github.com/consumet/consumet.ts/commit/59e4df0dd10d0db1e6aff7a3895dbbaec7c09880) + +## [v1.1.3](https://github.com/consumet/consumet.ts/compare/v1.1.2...v1.1.3) - 2022-10-01 + +### Merged + +- chore: bump version from `1.1.2` -> `1.1.3` [`#80`](https://github.com/consumet/consumet.ts/pull/80) +- feat GogoCDN: parse m3u8 file to extract resolutions [`#79`](https://github.com/consumet/consumet.ts/pull/79) + +## [v1.1.2](https://github.com/consumet/consumet.ts/compare/v1.1.1...v1.1.2) - 2022-10-01 + +### Merged + +- chore(deps): bump actions/setup-node from 3.4.1 to 3.5.0 [`#77`](https://github.com/consumet/consumet.ts/pull/77) + +### Commits + +- fix 9anime & flixhq [`0f20fcf`](https://github.com/consumet/consumet.ts/commit/0f20fcfbe47aa0de3077f948a0c6826cfdb7609f) +- chore: bump version from `1.1.1` -> `1.1.2` [`e39307a`](https://github.com/consumet/consumet.ts/commit/e39307a4c4deeb37f1465af23794bf418fbc1d07) +- Update README.md [`4746c9a`](https://github.com/consumet/consumet.ts/commit/4746c9a6bdc5a4997458bb7cc2b7ed24912d7c4a) + +## [v1.1.1](https://github.com/consumet/consumet.ts/compare/v1.1.0...v1.1.1) - 2022-09-26 + +### Merged + +- chore(deps): bump ws from 8.8.1 to 8.9.0 [`#76`](https://github.com/consumet/consumet.ts/pull/76) +- add missing fields to info and data & change character voice actor sorting [`#74`](https://github.com/consumet/consumet.ts/pull/74) +- not sure what i build, but i did it [`#73`](https://github.com/consumet/consumet.ts/pull/73) +- Fix zoro [`#72`](https://github.com/consumet/consumet.ts/pull/72) + +### Commits + +- feat(anilist): add manga mapping [`337028b`](https://github.com/consumet/consumet.ts/commit/337028bd1e9c463d0150a2227b9ecd03dba1e4a9) +- feat(anilist): add season to advanced search & fix airing schedule [`7ff0077`](https://github.com/consumet/consumet.ts/commit/7ff0077b1ac68b32ba5e1258bc29d40d8f0c8f75) +- fix: vidcloud & vizcloud + 9anime [`858c1ad`](https://github.com/consumet/consumet.ts/commit/858c1adb42938067ce8228f38e59a617ab0b6ccf) + +## [v1.1.0](https://github.com/consumet/consumet.ts/compare/v1.0.10...v1.1.0) - 2022-08-27 + +### Merged + +- Add Anime News Network provider [`#70`](https://github.com/consumet/consumet.ts/pull/70) +- Added total Episodes to anilist [`#69`](https://github.com/consumet/consumet.ts/pull/69) +- Added color field to fetchAnimeInfo [`#67`](https://github.com/consumet/consumet.ts/pull/67) + +### Commits + +- feat(anilist): added filtering by status & year [`6aacad1`](https://github.com/consumet/consumet.ts/commit/6aacad1a9fb61e52d121245cae33fcc4382f15e6) +- feat(anilist): added seperate methods from anime info and episodes [`c7fc35f`](https://github.com/consumet/consumet.ts/commit/c7fc35f3ef3790a38d3d4a9a542811607ffda9ab) +- feat(anilist): added enime 2020 anime & characterInfo method [`d2ba9d5`](https://github.com/consumet/consumet.ts/commit/d2ba9d5c5260d9d03b1b3f3ecf133ff705196a06) + +## [v1.0.10](https://github.com/consumet/consumet.ts/compare/v1.0.9...v1.0.10) - 2022-08-18 + +### Merged + +- (feat) new provider: Mangasee123 [`#62`](https://github.com/consumet/consumet.ts/pull/62) +- feat(anilist): CountryOrigin filtering on airing schedule [`#60`](https://github.com/consumet/consumet.ts/pull/60) +- chore: build dist [`#59`](https://github.com/consumet/consumet.ts/pull/59) +- feat(anilist): Added country to airingschedule [`#58`](https://github.com/consumet/consumet.ts/pull/58) +- Genres Functionality [`#53`](https://github.com/consumet/consumet.ts/pull/53) +- Added AnimeFox [`#52`](https://github.com/consumet/consumet.ts/pull/52) +- feat (anilist): added airing schedule & nextAiringEpisode property to Anime Info [`#49`](https://github.com/consumet/consumet.ts/pull/49) +- Added AniMixPlay [`#42`](https://github.com/consumet/consumet.ts/pull/42) +- fix (anilist): fix episodes number bug [`#46`](https://github.com/consumet/consumet.ts/pull/46) +- feat (zoro): added fetch recently updated anime [`#45`](https://github.com/consumet/consumet.ts/pull/45) +- fix Mangadex docs' typo [`#43`](https://github.com/consumet/consumet.ts/pull/43) +- feat (anilist): added fetch popular anime [`#41`](https://github.com/consumet/consumet.ts/pull/41) +- feat (anilist): added recommendations [`#40`](https://github.com/consumet/consumet.ts/pull/40) + +### Commits + +- fix(anilist): airing schedule(#61) [`629da2e`](https://github.com/consumet/consumet.ts/commit/629da2ee66972bd44f260009a0761798aa15d131) +- fix(9anime): fixed keys & added prettier [`10e8edd`](https://github.com/consumet/consumet.ts/commit/10e8edd587179f2071d775278d47056a902490ea) +- feat(anime): added enime [`435bd42`](https://github.com/consumet/consumet.ts/commit/435bd426d5c7e0db678ef3eb9894e73d9ebc17b5) + +## [v1.0.9](https://github.com/consumet/consumet.ts/compare/v1.0.8...v1.0.9) - 2022-08-04 + +### Commits + +- feat (anilist): added fetch trending anime method [`a3227ee`](https://github.com/consumet/consumet.ts/commit/a3227ee01cc7afafa336276e61e7d113d8141eec) +- chore: new dist [`0197e73`](https://github.com/consumet/consumet.ts/commit/0197e7305d2ecf016aa9c10e783c33bc963640ac) +- Fixed 9anime [`3fdbf1c`](https://github.com/consumet/consumet.ts/commit/3fdbf1c46bd45c526b2afb325148dba80f4bf704) + +## [v1.0.8](https://github.com/consumet/consumet.ts/compare/v1.0.7...v1.0.8) - 2022-07-31 + +### Merged + +- feat (anime): 9anime experimental [`#35`](https://github.com/consumet/consumet.ts/pull/35) +- build(deps): bump actions/setup-node from `2.2.0` -> `3.4.1` [`#34`](https://github.com/consumet/consumet.ts/pull/34) + +### Commits + +- fix workflow [`0a1d3b4`](https://github.com/consumet/consumet.ts/commit/0a1d3b498af5776a0213d921a405509ba0c10b92) +- chore: bump patch version `1.0.6` -> `1.0.7` [`85f77aa`](https://github.com/consumet/consumet.ts/commit/85f77aa7ce7dbd979fadf7f6690df0866268a421) +- feat (anilist): added `cover` property [`09cc457`](https://github.com/consumet/consumet.ts/commit/09cc45714b7c88b9cb9cdb0260a30295f700ca60) + +## [v1.0.7](https://github.com/consumet/consumet.ts/compare/v1.0.6...v1.0.7) - 2022-07-25 + +### Merged + +- chore (dist): zoro.to [`#32`](https://github.com/consumet/consumet.ts/pull/32) +- feat (anime): added zoro.to [`#31`](https://github.com/consumet/consumet.ts/pull/31) +- build (dist)* [`#30`](https://github.com/consumet/consumet.ts/pull/30) +- feat: MangaKakalot [`#25`](https://github.com/consumet/consumet.ts/pull/25) +- fix (mangahere): file clean up [`#29`](https://github.com/consumet/consumet.ts/pull/29) +- fix: handle copyright issue [`#28`](https://github.com/consumet/consumet.ts/pull/28) + +### Commits + +- chore [skip ci]: cleanup [`c7ec5e2`](https://github.com/consumet/consumet.ts/commit/c7ec5e2681d19f1300155dbb68825c71c2a2ab50) +- feat (types): added intro [`3180634`](https://github.com/consumet/consumet.ts/commit/31806345b8f5824cef68e264399eb04198ee7067) +- fix (mangakakalot): dist [`1a6b7dd`](https://github.com/consumet/consumet.ts/commit/1a6b7dd6ff63b7910cc59ff91f74189a851b7f43) + +## [v1.0.6](https://github.com/consumet/consumet.ts/compare/v1.0.5...v1.0.6) - 2022-07-20 + +### Merged + +- FEAT: changed comic result [`#27`](https://github.com/consumet/consumet.ts/pull/27) +- FIX: increased speed of libgen scrapper [`#24`](https://github.com/consumet/consumet.ts/pull/24) +- feat: MangaHere [`#23`](https://github.com/consumet/consumet.ts/pull/23) + +### Commits + +- feat (anilist): added studios to object response [`d821cfa`](https://github.com/consumet/consumet.ts/commit/d821cfa4717f07dd17f9dae432ddd6c836e3882f) +- build: new dist [`626506a`](https://github.com/consumet/consumet.ts/commit/626506ad80f1c7514293e6a17117a6c3d0d0c663) +- feat [skip ci]: added CODEOWNDERS [`683864a`](https://github.com/consumet/consumet.ts/commit/683864affeeea6be0ed72c7c3e06e16e88a689d5) + +## [v1.0.5](https://github.com/consumet/consumet.ts/compare/v1.0.4...v1.0.5) - 2022-07-17 + +### Merged + +- chore: bump patch version `1.0.4` -> `1.0.5` [`#22`](https://github.com/consumet/consumet.ts/pull/22) +- FIX: fixed getComcis page [`#21`](https://github.com/consumet/consumet.ts/pull/21) + +### Commits + +- chore: bump dist build [`73d2f27`](https://github.com/consumet/consumet.ts/commit/73d2f27e4a4294f35e334680cc6d79d0c0e5143b) +- refactor [skip ci]: `consumet extensions` -> `consumet.ts` [`e53078e`](https://github.com/consumet/consumet.ts/commit/e53078ef6d364f0812879256f0f65be719c4295a) +- refactor (issue_template): new dropbox [`0e5f282`](https://github.com/consumet/consumet.ts/commit/0e5f282506c4895bd98757fe5dc15a99605f26fe) + +## [v1.0.4](https://github.com/consumet/consumet.ts/compare/v1.0.3...v1.0.4) - 2022-07-13 + +### Merged + +- feat: new meta provider [`#14`](https://github.com/consumet/consumet.ts/pull/14) + +### Commits + +- temp commit [`c18f14f`](https://github.com/consumet/consumet.ts/commit/c18f14fbb7559cc348388f3b3f65e6d09747ce4f) +- chore: bump patch version `1.0.3` -> `1.0.4` [`0dcb825`](https://github.com/consumet/consumet.ts/commit/0dcb825072eb3e72714a923be8c7bb2a000eec0f) + +## [v1.0.3](https://github.com/consumet/consumet.ts/compare/v1.0.2...v1.0.3) - 2022-07-10 + +### Commits + +- chore: bump patch version `1.0.2` -> `1.0.3` [`e3ca4b3`](https://github.com/consumet/consumet.ts/commit/e3ca4b34b2768310883de84e51ae65920b392860) +- feat(anime): added animepahe provider [`19fd07d`](https://github.com/consumet/consumet.ts/commit/19fd07d3a7912743448504dd0e6767d7407afc44) +- feat(anime): added animepahe provider [`8bcdbd1`](https://github.com/consumet/consumet.ts/commit/8bcdbd17c491cbe264112f49ae4e518b6f48f38c) + +## [v1.0.2](https://github.com/consumet/consumet.ts/compare/v1.0.1...v1.0.2) - 2022-07-06 + +### Commits + +- FIX: added a package-lock.json [`6e1e2e2`](https://github.com/consumet/consumet.ts/commit/6e1e2e276c229504b83cd79b0301e4d7d2521859) +- build(deps-dev): bump ts-jest from 28.0.4 to 28.0.5 [`d9dc1b1`](https://github.com/consumet/consumet.ts/commit/d9dc1b1eee719ed5a201907da23e57cd8e1c8471) +- build(deps-dev): bump typescript from 4.6.4 to 4.7.4 [`790158c`](https://github.com/consumet/consumet.ts/commit/790158c1fb3b87759a92df53d3b1c2c616208b3b) + +## v1.0.1 - 2022-06-22 + +### Commits + +- started zlibrary provider [`c33c055`](https://github.com/consumet/consumet.ts/commit/c33c055b3a2065e75f4571003e0267e7c0d240a5) +- [skip ci] fixed providers-list [`8cee088`](https://github.com/consumet/consumet.ts/commit/8cee088102e31867f1084c726d74b6738f393b24) +- feat: documentation cleanup [`94e1ef9`](https://github.com/consumet/consumet.ts/commit/94e1ef9ac37e5dbeacd572152581cb9ad4ce03b2) diff --git a/consumet.ts/CODE_OF_CONDUCT.md b/consumet.ts/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..de2d1971 --- /dev/null +++ b/consumet.ts/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders 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, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +consumet.org@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +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. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/consumet.ts/CONTRIBUTING.md b/consumet.ts/CONTRIBUTING.md new file mode 100644 index 00000000..d9494de0 --- /dev/null +++ b/consumet.ts/CONTRIBUTING.md @@ -0,0 +1,167 @@ +

Contributing

+ +This guide is for the people who are interested in contributing to consumet.ts. It is not a complete guide yet, but it should help you get started. If you have any questions or any suggestions, please open a [issue](https://github.com/consumet/extensions/issues/new?assignees=&labels=Bug&template=bug-report.yml) or join the [discord server](https://discord.gg/qTPfvMxzNH). + +See our [informal contributing guide](./docs/guides/contributing.md) for more details on contributing to this project. + +

Table of Contents

+ +- [Prerequisites](#prerequisites) + - [Cloning the repository](#cloning-the-repository) + - [Project structure](#project-structure) +- [Writing a provider](#writing-a-provider) + - [Setting up the provider](#setting-up-the-provider) +- [Updaing codebase](#updaing-codebase) + - [Updating documentation](#updating-documentation) + - [Fixing a provider](#fixing-a-provider) +- [Commit message](#commit-message) + + +## Prerequisites +To contribute to Consumet code, you need to know the following: + - [Nodejs](https://nodejs.org/) + - [TypeScript](https://www.typescriptlang.org/) + - Web scraping + - [Cheerio](https://cheerio.js.org/) + - [Axios](https://axios-http.com/docs/example) + - [Css Selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) + - [DevTools](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools) + +### Cloning the repository +1. [Fork the repository](https://github.com/consumet/consumet.ts/fork) +2. Clone your fork to your local machine using the following command **(make sure to change `` to your GitHub username)**: +```sh +git clone https://github.com//consumet-api.git +``` +3. Create a new branch: +```sh +git checkout -b +``` + +### Project structure +I believe that project structure is needed to make it simple to contribute to consumet.ts. + +***\*** is the category of the provider. For example, `anime` or `book`, `etc`.\ +***\*** is the name of the provider. For example, `libgen` or `gogoanime`, `etc`. (must be in camel case) + +```sh +> tree +docs/ +├── guides/ +| ├── ... +| ├── anime.md +| ├── getting-started.md +│ └── contributing.md (informal guide) +├── providers/ +│ └── .md (provider documentation) +├── README.md +src/ +├── index.ts +|── models +├── providers +│ ├── +│ │ ├── index.ts +│ │ └── .ts +│ └── +└── utils +``` + +## Writing a provider +Each provider is a class that extends abstract class. For example, `Libgen` provider extends `BooksParser` class, and `Gogoanime` extends `AnimeParser`. the parser abstract classes can be found in the `src/models/` folder as follows: +```sh +src/models/anime-parser.ts # AnimeParser +src/models/book-parser.ts # BookParser +src/models/lightnovel-parser.ts # LightNovelParser +src/models/comic-parser.ts # ComicParser +src/models/manga-parser.ts # MangaParser +src/models/movie-parser.ts # MovieParser +``` +You are welcome to add anything to the abstract class that you believe will be beneficial. + +
+ + visualization of the abstract classes hierarchy + + + ```mermaid + classDiagram + Proxy <|-- BaseProvider + BaseProvider <|-- BaseParser + BaseProvider : +String name + BaseProvider : +String baseUrl + BaseProvider: +toString() + BaseParser <|-- AnimeParser + BaseParser <|-- BookParser + BaseParser <|-- MangaParser + BaseParser <|-- LightNovelParser + BaseParser <|-- ComicParser + BaseParser <|-- MovieParser + class Proxy{ + ProxyConfig + } + class BaseParser{ + +search(String query) + } + class AnimeParser{ + +fetchAnimeInfo(String animeId) + +fetchEpisodeSources(String episodeId) + +fetchEpisodeServers(String episodeId) + } + class MovieParser{ + +fetchMediaInfo(String mediaId) + +fetchEpisodeSources(String episodeId) + +fetchEpisodeServers(String episodeId) + } + class BookParser{ + empty + } + class MangaParser{ + +fetchMangaInfo(String mangaId) + +fetchChapterPages(String chapterId) + } + class ComicParser{ + empty + } + class LightNovelParser{ + +fetchLighNovelInfo(String lightNovelId) + +fetchChapterContent(String chapterId) + } + ``` +
+ + +#### Setting up the provider +1. Create a new file in the `src/providers//.ts` folder. +2. Import the abstract class from the `src/models/-parser.ts` file. for example: if you are writing an anime provider, you would need to implement the abstract class `AnimeParser`, which is defined in the `src/models/anime-parser.ts` file. +3. Start writing your provider code. +4. Add the provider to the `src/providers//index.ts` file. + + +## Updaing codebase +### Updating documentation +1. Update the documentation. +2. [Commit the changes](#commit-message). + +### Fixing a provider +1. Update the provider code. +2. [Commit the changes](#commit-message). + +## Commit message +When you've made changes to one or more files, you have to *commit* that file. You also need a +*message* for that *commit*. + +You should read [these](https://www.freecodecamp.org/news/writing-good-commit-messages-a-practical-guide/) guidelines, or that summarized: + +- Short and detailed +- Prefix one of these commit types: + - `feat:` A feature, possibly improving something already existing + - `fix:` A fix, for example of a bug + - `refactor:` Refactoring a specific section of the codebase + - `test:` Everything related to testing + - `docs:` Everything related to documentation + - `chore:` Code maintenance + +Examples: + - `feat: Speed up parsing with new technique` + - `fix: Fix 9anime search` + - `refactor: Reformat code at 9anime.ts` diff --git a/consumet.ts/LICENSE b/consumet.ts/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/consumet.ts/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/consumet.ts/README.md b/consumet.ts/README.md new file mode 100644 index 00000000..def61e33 --- /dev/null +++ b/consumet.ts/README.md @@ -0,0 +1,120 @@ +

+ +

consumet.ts

+ +consumet.ts is a Node library which provides high-level APIs to get information about several entertainment mediums like books, movies, comics, anime, manga, etc. + +

+ + npm (scoped) + + + npm (scoped) + + + Prs are welcome + + + Discord + + + GitHub + +

+ +

Table of Contents

+ +- [Quick Start](#quick-start) + - [Installation](#installation) + - [Usage](#usage) +- [Documentation](#documentation) +- [Ecosystem](#ecosystem) +- [Provider Request](#provider-request) +- [Contributing](#contributing) +- [Support](#support) +- [Contributors ✨](#contributors-) + - [Credits](#credits) +- [License](#license) + +## Quick Start + +### Installation + +To use consumet.ts in your project, run: +```bash +yarn add @consumet/extensions +# or "npm i @consumet/extensions" +``` + +### Usage + +**Example** - searching for a book using the libgen provider. +```ts +import { BOOKS } from "@consumet/extensions" + +// Create a new instance of the Libgen provider +const books = new BOOKS.Libgen(); +// Search for a book. In this case, "Pride and Prejudice" +const data = books.search('pride and prejudice').then(data => { + // print results + console.log(data) +}) +``` + +**Example** - searching for anime using the gogoanime provider. +```ts +import { ANIME } from "@consumet/extensions" + +// Create a new instance of the Gogoanime provider +const gogoanime = new ANIME.Gogoanime(); +// Search for an anime. In this case, "One Piece" +const results = gogoanime.search("One Piece").then(data => { + // print results + console.log(data); +}) +``` + +Do you want to know more? Head to the [`Getting Started`](https://github.com/consumet/consumet.ts/tree/master/docs/guides/getting-started.md). + +## Documentation +- [`Getting Started`](./docs/guides/getting-started.md) +- [`Guides`](https://github.com/consumet/consumet.ts/tree/master/docs) +- [`Anime`](./docs/guides/anime.md) +- [`Manga`](./docs/guides/manga.md) +- [`Books`](./docs/guides/books.md) +- [`Movies`](./docs/guides/movies.md) +- [`Light Novels`](./docs/guides/light-novels.md) +- [`Comics`](./docs/guides/comics.md) +- [`Meta`](./docs/guides/meta.md) +- [`News`](./docs/guides/news.md) + +## Ecosystem +- [Rest-API Reference](https://docs.consumet.org/) - public rest api documentation +- [Examples](https://github.com/consumet/consumet.ts/tree/master/examples) - examples of using consumet.ts. +- [Provider Status](https://github.com/consumet/providers-status/blob/main/README.md) - A list of providers and their status. +- [Changelog](https://github.com/consumet/consumet.ts/blob/master/CHANGELOG.md) - See the latest changes. +- [Discord Server](https://discord.gg/qTPfvMxzNH) - Join our discord server and chat with the maintainers. + +## Provider Request +Make a new [issue](https://github.com/consumet/consumet.ts/issues/new?assignees=&labels=provider+request&template=provider-request.yml) with the name of the provider on the title, as well as a link to the provider in the body paragraph. + +## Contributing +Check out [contributing guide](https://github.com/consumet/consumet.ts/blob/master/CONTRIBUTING.md) to get an overview of consumet.ts development. + +## Support +You can contact the maintainers of consumet.ts via [email](mailto:consumet.org@gmail.com), or [join the discord server](https://discord.gg/qTPfvMxzNH) (Recommended). + + + + + +## Contributors ✨ +Thanks to the following people for keeping this project alive and thriving. + +[![](https://contrib.rocks/image?repo=consumet/consumet.ts)](https://github.com/consumet/consumet.ts/graphs/contributors) + +### Credits +- [Anify API](https://github.com/Eltik/Anify) - Used as a caching layer for the meta/anilist provider to speed up responses. + +## License +Licensed under [MIT](./LICENSE). diff --git a/consumet.ts/docs/README.md b/consumet.ts/docs/README.md new file mode 100644 index 00000000..9c483312 --- /dev/null +++ b/consumet.ts/docs/README.md @@ -0,0 +1,14 @@ +

consumet.ts

+

Table of Contents

+ +- [Getting Started](./guides/getting-started.md): Introduction tutorial for consumet.ts. This is where beginners should start. +- [Anime](./guides/anime.md): How to use anime providers. +- [Manga](./guides/manga.md): How to use manga providers. +- [Books](./guides/books.md): How to use book providers. +- [Movie](./guides/movies.md): How to use movie providers. +- [Light Novels](./guides/light-novels.md): How to use light novel providers. +- [Comics](./guides/comics.md): How to use comic providers. +- [News](./guides/news.md): How to use news providers. +- [Meta](./guides/meta.md): How to use meta providers. +- [Benchmarks](https://github.com/consumet/providers-status#readme): Real-time benchmarking of providers. +- [Contributing](./guides/contributing.md): Details about how to contribute to consumet.ts. diff --git a/consumet.ts/docs/guides/anime.md b/consumet.ts/docs/guides/anime.md new file mode 100644 index 00000000..747f47a1 --- /dev/null +++ b/consumet.ts/docs/guides/anime.md @@ -0,0 +1,46 @@ +

consumet.ts

+ +

ANIME

+ +By using `ANIME` category you can interact with the anime providers. And get access to the anime providers methods. Which allows you to search for anime, get the anime information, get the anime episodes with streaming links. + +```ts +// ESM +import { ANIME } from '@consumet/extensions'; + +// is the name of the provider you want to use. list of the proivders is below. +const animeProvider = ANIME.(); +``` + +## Common Methods + +``languages`` - string, the language of the current provider, return language code, example: ``languages: 'en'`` + +``isNSFW`` - bool, ``true`` if the provider provides NSFW content. + +``isWorking`` - bool, a bool to identify the state of the current provider, ``true`` if the provider is working, ``false`` otherwise. + +``isDubAvailableSeparately`` - bool, ``true`` if the provider provides dubbed content. + +``name`` - string, the name of the current provider, example: ``name: 'Crunchyroll'`` + +``baseUrl`` - string, url to the base URL of the current provider + +``logo`` - string, url to the logo image of the current provider + +``classPath`` - string, + + +## Anime Providers List +This list is in alphabetical order. (except the sub bullet points) + +- [9anime](../providers/9anime.md) +- [Anify](../providers/anify.md) +- [AnimeFox](../providers/animefox.md) +- [AnimePahe](../providers/animepahe.md) +- [AnimeSaturn](../providers/animesaturn.md) +- [AnimeUnity](../providers/animeunity.md) +- [Gogoanime](../providers/gogoanime.md) +- [Zoro.to](../providers/zoro.md) + +

(back to table of contents)

diff --git a/consumet.ts/docs/guides/books.md b/consumet.ts/docs/guides/books.md new file mode 100644 index 00000000..fd5f001f --- /dev/null +++ b/consumet.ts/docs/guides/books.md @@ -0,0 +1,20 @@ +

consumet.ts

+ +

BOOKS

+ +By using `BOOKS` category you can interact with the book providers. And have access to the book providers methods. Which allows you to search for books, get the book information, get the book pdf/epub links. + +```ts +// ESM +import { BOOKS } from '@consumet/extensions'; + +// is the name of the provider you want to use. list of the proivders is below. +const bookProvider = BOOKS.(); +``` + +## Book Providers List +This list is in alphabetical order. (except the sub bullet points) + +- [Libgen](../providers/libgen.md) + +

(back to table of contents)

\ No newline at end of file diff --git a/consumet.ts/docs/guides/comics.md b/consumet.ts/docs/guides/comics.md new file mode 100644 index 00000000..9891fda6 --- /dev/null +++ b/consumet.ts/docs/guides/comics.md @@ -0,0 +1,20 @@ +

consumet.ts

+ +

COMICS

+ +By using `COMICS` category you can interact with the book providers. And have access to the comic providers methods. Which allows you to search for comics, get the comic information, get the comic chapters with images to read. + +```ts +// ESM +import { COMICS } from '@consumet/extensions'; + +// is the name of the provider you want to use. list of the proivders is below. +const comicProvider = COMICS.(); +``` + +## Comic Providers List +This list is in alphabetical order. (except the sub bullet points) + +- [GetComics](#todo) + +

(back to table of contents)

\ No newline at end of file diff --git a/consumet.ts/docs/guides/contributing.md b/consumet.ts/docs/guides/contributing.md new file mode 100644 index 00000000..bf4368a8 --- /dev/null +++ b/consumet.ts/docs/guides/contributing.md @@ -0,0 +1,35 @@ +

Contributing to consumet.ts

+Thank you for your interest in contributing to consumet.ts. We appreciate whatever form of contribution you are willing to make. There is no such thing as a little contribution. + +
+
+ +> ### Note: +> This is an informal guide. Please go over the formal [CONTRIBUTING Document](../../CONTRIBUTING.md) for more information. + +## Table of Contents +- [Table of Contents](#table-of-contents) +- [Types Of Contributions We're Looking For](#types-of-contributions-were-looking-for) +- [Ground Rules & Expectations](#ground-rules--expectations) +- [How To Contribute](#how-to-contribute) + +## Types Of Contributions We're Looking For + +In short, we welcome any type of contribution you are willing to provide. No +contribution is too small. We gladly accept contributions such as: + +- Documentation improvements: from small typo corrections to major document reworks +- Helping others by answering questions in pull requests +- Fixing [known bugs](https://github.com/consumet/extensions/issues?q=is%3Aissue+is%3Aopen+label%3ABug) + +## Ground Rules & Expectations +Before we begin, here are a few things we anticipate from you (and that you should expect from others): + +* Be respectful and thoughtful in your conversations around this project. Each person has their own views and opinions about the project. Try to listen to each other and reach an agreement or compromise. + +## How To Contribute +If you'd like to contribute, start by searching through the [issues](https://github.com/consumet/extensions/issues) and [pull requests](https://github.com/consumet/extensions/pulls) to see whether someone else has raised a similar idea or question. + +If you don't see your idea listed, and you think it fits into the goals of this guide, do one of the following: +* **If your contribution is minor,** such as a typo fix or new provider, open a pull request. +* **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. \ No newline at end of file diff --git a/consumet.ts/docs/guides/getting-started.md b/consumet.ts/docs/guides/getting-started.md new file mode 100644 index 00000000..095a3917 --- /dev/null +++ b/consumet.ts/docs/guides/getting-started.md @@ -0,0 +1,81 @@ +

consumet.ts

+ +## Getting Started + +Hello! Thank you for checking out consumet.ts! + +This document aims to be a gentle introduction to the library and its usage. + +Let's start! + +### Installation +Install with npm: +```sh +npm i @consumet/extensions +``` +Install with yarn: +```sh +yarn add @consumet/extensions +``` + +### Usage + +**Example** - searching for a book using the libgen provider. +```ts +// ESM +import { BOOKS } from "@consumet/extensions" +// CommonJS +const { BOOKS } = require("@consumet/extensions"); + +const main = async () => { + // Create a new instance of the Libgen provider + const books = new BOOKS.Libgen(); + // Search for a book. In this case, "Pride and Prejudice" + const results = await books.search('pride and prejudice'); + // Print the results + console.log(results); + // Get the first book info + const firstBook = results[0]; + const bookInfo = await books.scrapePage(firstBook.link); + // Print the info + console.log(bookInfo); +}; + +main(); +``` +*see also [BOOKS documentation](./books.md#books) for more information.*\ +**Example** - searching for anime using the gogoanime provider. +```ts +// ESM +import { ANIME } from "@consumet/extensions" +// CommonJS +const { ANIME } = require("@consumet/extensions"); + +const main = async () => { + // Create a new instance of the Gogoanime provider + const gogoanime = new ANIME.Gogoanime(); + // Search for a anime. In this case, "One Piece" + const results = await gogoanime.search("One Piece"); + // Print the results + console.log(results); + // Get the first anime info + const firstAnime = results.results[0]; + const animeInfo = await gogoanime.fetchAnimeInfo(firstAnime.id); + // Print the info + console.log(animeInfo); + // get the first episode stream link. By default, it chooses goload server. + const episodes = await gogoanime.fetchEpisodeSources(animeInfo.episodes[0].id); + // get the available streaming servers for the first episode + const streamingServers = await gogoanime.fetchEpisodeServers(animeInfo.episodes[0].id); +} +``` +*see also [ANIME documentation](./anime.md#anime) for more information.*\ +Awesome, that was easy. + +if you want to use different providers, you can check the providers list [here](https://consumet.org/extensions/list/) or in [json format](https://github.com/consumet/providers-status/blob/main/providers-list.json). + +if you have any questions, please join the [discord server](https://discord.gg/qTPfvMxzNH) or open an [issue](https://github.com/consumet/extensions/issues). + +

(back to table of contents)

+ + diff --git a/consumet.ts/docs/guides/light-novels.md b/consumet.ts/docs/guides/light-novels.md new file mode 100644 index 00000000..9ad218f6 --- /dev/null +++ b/consumet.ts/docs/guides/light-novels.md @@ -0,0 +1,23 @@ +

consumet.ts

+ +

LIGHT_NOVELS

+ +By using `LIGHT_NOVELS` category you can interact with the light novel providers. And have access to the light novel providers methods. Which allows you to search for light novels, get the light novel information, get the light novel chapters with text content to read. + +```ts +// ESM +import { LIGHT_NOVELS } from '@consumet/extensions'; + +// is the name of the provider you want to use. list of the proivders is below. +const lightnovelProvider = LIGHT_NOVELS.(); +``` + + +## Light Novels Providers List +This list is in alphabetical order. (except the sub bullet points) + +- [NovelUpdates](../providers/novelupdates.md) +- [ReadLightNovels](../providers/readlightnovels.md) + + +

(back to table of contents)

\ No newline at end of file diff --git a/consumet.ts/docs/guides/manga.md b/consumet.ts/docs/guides/manga.md new file mode 100644 index 00000000..7e7c6c51 --- /dev/null +++ b/consumet.ts/docs/guides/manga.md @@ -0,0 +1,43 @@ +

consumet.ts

+ +

MANGA

+ +By using `MANGA` category you can interact with the manga providers. And have access to the manga providers methods. Which allows you to search for manga, get the manga information, get the manga chapters with images to read. + +```ts +// ESM +import { MANGA } from '@consumet/extensions'; + +// is the name of the provider you want to use. list of the proivders is below. +const mangaProvider = MANGA.(); +``` + +## Common Methods + +``languages`` - string, the language of the current provider, return language code, example: ``languages: 'en'`` + +``isNSFW`` - bool, ``true`` if the provider providers NSFW content. + +``isWorking`` - bool, a bool to identify the state of the current provider, ``true`` if the provider is working, ``false`` otherwise. + +``name`` - string, the name of the current provider, example: ``name: 'Crunchyroll'`` + +``baseUrl`` - string, url to the base URL of the current provider + +``logo`` - string, url to the logo image of the current provider + +``classPath`` - string, + + +## Manga Providers List +This list is in alphabetical order. (except the sub bullet points) + +- [MangaDex](../providers/mangadex.md) +- [MangaHere](../providers/mangahere.md) +- [MangaKakalot](../providers/mangakakalot.md) +- [Mangasee123](../providers/mangasee123.md) +- [MangaHost](../providers/mangahost.md) +- [BRManga](../providers/brmanga.md) + + +

(back to table of contents)

diff --git a/consumet.ts/docs/guides/meta.md b/consumet.ts/docs/guides/meta.md new file mode 100644 index 00000000..50867541 --- /dev/null +++ b/consumet.ts/docs/guides/meta.md @@ -0,0 +1,41 @@ +

consumet.ts

+ +

META

+ +By using `META` category you can interact with the custom providers. And get access to the meta providers methods. + +```ts +// ESM +import { META } from '@consumet/extensions'; + +// is the name of the provider you want to use. list of the proivders is below. +const metaProvider = META.(); +``` + +## Common Methods + +provider Anilist { + +``languages`` - string, the language of the current provider, return language code, example: ``languages: 'en'`` + +``isNSFW`` - bool, ``true`` if the provider providers NSFW content. + +``isWorking`` - bool, a bool to identify the state of the current provider, ``true`` if the provider is working, ``false`` otherwise. + +``isDubAvailableSeparately`` - bool, ``true`` if the provider providers dubbed content. + +``name`` - string, the name of the current provider, example: ``name: 'Anilist'`` + +``baseUrl`` - string, url to the base URL of the current provider + +``logo`` - string, url to the logo image of the current provider + +``classPath`` - string, + + +## Meta Providers List +This list is in alphabetical order. (except the sub bullet points) + +- [Anilist](../providers/anilist.md) + +

(back to table of contents)

diff --git a/consumet.ts/docs/guides/movies.md b/consumet.ts/docs/guides/movies.md new file mode 100644 index 00000000..b100e348 --- /dev/null +++ b/consumet.ts/docs/guides/movies.md @@ -0,0 +1,42 @@ +

consumet.ts

+ +

MOVIES

+ +By using `MOVIES` category you can interact with the movie providers. And have access to the movie providers methods. Which allows you to search for movies and shows, get the movie/tv series information, get the movie/tv series episodes with streaming links. + +```ts +// ESM +import { MOVIES } from '@consumet/extensions'; + +// is the name of the provider you want to use. list of the proivders is below. +const movieProvider = new MOVIES.(); +``` + +## Common Methods + +``languages`` - string, the language of the current provider, return language code, example: ``languages: 'en'`` + +``isNSFW`` - bool, ``true`` if the provider providers NSFW content. + +``isWorking`` - bool, a bool to identify the state of the current provider, ``true`` if the provider is working, ``false`` otherwise. + +``name`` - string, the name of the current provider, example: ``name: 'FlixHQ'`` + +``baseUrl`` - string, url to the base URL of the current provider + +``logo`` - string, url to the logo image of the current provider + +``classPath`` - string, + +``supportedTypes`` - object, A ``Set`` of supported types by the provider, to check if a type is supported use ``supportedTypes.has()``. + + +## Movies Providers List +This list is in alphabetical order. (except the sub bullet points) + +- [Goku](../providers/goku.md) +- [MovieHdWatch](../providers/moviehdwatch.md) +- [FlixHQ](../providers/flixhq.md) +- [ViewAsian](../providers/viewAsian.md) + +

(back to table of contents)

diff --git a/consumet.ts/docs/guides/news.md b/consumet.ts/docs/guides/news.md new file mode 100644 index 00000000..4b57ad6f --- /dev/null +++ b/consumet.ts/docs/guides/news.md @@ -0,0 +1,20 @@ +

consumet.ts

+ +

NEWS

+ +By using `NEWS` category you can interact with the news providers. And get access to the news providers methods. + +```ts +// ESM +import { NEWS } from '@consumet/extensions'; + +// is the name of the provider you want to use. list of the proivders is below. +const newsProvider = NEWS.(); +``` + +## Anime Providers List +This list is in alphabetical order. (except the sub bullet points) + +- [ANN (Anime News Network)](../providers/ann.md) + +

(back to table of contents)

\ No newline at end of file diff --git a/consumet.ts/docs/providers/9anime.md b/consumet.ts/docs/providers/9anime.md new file mode 100644 index 00000000..b81cb90e --- /dev/null +++ b/consumet.ts/docs/providers/9anime.md @@ -0,0 +1,179 @@ +

9Anime

+ +>Note: This provider has a special way of initializing +```ts +const nineanime = await ANIME.NineAnime.create(); +``` + +

Methods

+ +- [search](#search) +- [fetchAnimeInfo](#fetchanimeinfo) +- [fetchEpisodeSources](#fetchepisodesources) +- [fetchEpisodeServers](#fetchepisodeservers) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `ojisan`*) | + +```ts +nineanime.search("ojisan").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: false, + results: [ + { + id: 'uncle-from-another-world.oj9q8', + title: 'UNCLE FROM ANOTHER WORLD', + url: 'https://9anime.to/watch/uncle-from-another-world.oj9q8', + image: 'https://static.bunnycdn.ru/i/cache/images/1/1e/1e014e4ca206a486abef62cf0795c919.jpg', + subOrSub: 'sub', + type: 'TV' + }, + { + id: 'ojisan-and-marshmallow.4qo', + title: 'Ojisan and Marshmallow', + url: 'https://9anime.to/watch/ojisan-and-marshmallow.4qo', + image: 'https://static.bunnycdn.ru/i/cache/images/2018/04/7794c3d41b0cd0d2c521b034fcca6b23.jpg', + subOrSub: 'sub', + type: 'TV' + }, + {...} + ... + ] +} +``` + +### fetchAnimeInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------------------------------------------------------- | +| id | `string` | takes anime id as a parameter. (*anime id can be found in the anime search results or anime info object*) | + + +```ts +nineanime.fetchAnimeInfo("uncle-from-another-world.oj9q8").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ +output: +```js +{ + id: 'uncle-from-another-world.oj9q8', + title: 'UNCLE FROM ANOTHER WORLD', + url: 'https://9anime.id/watch/uncle-from-another-world.oj9q8', + jpTitle: 'Isekai Ojisan', + genres: [ 'Adventure', 'Comedy', 'Fantasy', 'Isekai' ], + image: 'https://static.bunnycdn.ru/i/cache/images/1/1e/1e014e4ca206a486abef62cf0795c919.jpg', + description: "Seventeen years ago, Takafumi's uncle fell into a coma, but now he's back like a man...", + type: 'TV', + studios: [ { id: 'atelierpontdarc', title: 'AtelierPontdarc' } ], + releaseDate: 'Jul 06, 2022', + status: 'Ongoing', + score: 7.95, + premiered: 'Summer 2022', + duration: '24 min', + views: 316267, + otherNames: [ 'Isekai Ojisan', 'UNCLE FROM ANOTHER WORLD' ], + totalEpisodes: 4, + episodes: [ + { + id: '155250', + number: 1, + title: 'I`m Finally Back from the Fantasy World of Granbahamal After 17 Long Years!', + isFiller: false, + url: 'https://9anime.id/ajax/server/list/155250?vrf=TYRythk8' + }, + { + id: '155251', + number: 2, + title: '"Guardian Heroes" Shoulda Been Number One!', + isFiller: false, + url: 'https://9anime.id/ajax/server/list/155251?vrf=TYRythk9' + }, + {...} + ... + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the anime info object*) | + + +In this example, we're getting the sources for the first episode of Overlord IV. +```ts +nineanime.fetchEpisodeSources("155250").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of episode sources. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ +output: +```js +lol jk. it doesnt work yet :). +``` + +### fetchEpisodeServers + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------------------------- | +| episodeId | `string` | take an episode id or url as a parameter. (*episode id or episode url can be found in the anime info object*) | + +```ts +nineanime.fetchEpisodeServers("155250").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of episode servers. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L54-L57)*)\ +output: +```js +[ + { + name: 'vidstream', + url: 'https://9anime.id/ajax/server/1080419?vrf=TYFtBg99w' + }, + { + name: 'mycloud', + url: 'https://9anime.id/ajax/server/1080418?vrf=TYFtBg99g' + }, + { + name: 'filemoon', + url: 'https://9anime.id/ajax/server/1219176?vrf=TYN2vR07%2BA' + }, + { + name: 'streamtape', + url: 'https://9anime.id/ajax/server/1080423?vrf=TYFtBg%2BQ' + }, + { + name: 'mp4upload', + url: 'https://9anime.id/ajax/server/1080422?vrf=TYFtBg%2BA' + } +] +``` + +

(back to anime providers list)

diff --git a/consumet.ts/docs/providers/anify.md b/consumet.ts/docs/providers/anify.md new file mode 100644 index 00000000..2e89df05 --- /dev/null +++ b/consumet.ts/docs/providers/anify.md @@ -0,0 +1,179 @@ +

Anify

+ +```ts +const anify = new ANIME.Anify(); +``` + +

Methods

+ +- [search](#search) +- [fetchAnimeInfo](#fetchanimeinfo) +- [fetchEpisodeSources](#fetchepisodesources) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| query | `string` | query to search for. (*In this case, We're searching for `Overlord IV`*) | +| page | `number` | page number to search for. | +| perPage | `number` | number of results per page. | + +```ts +anify.search("Overlord IV").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: true, + results: [ + { + id: '133844', + anilistId: 133844, + malId: 48895, + title: 'Overlord IV', + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx133844-E32FjKZ0XxEs.jpg', + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/133844-uIaUmh5aJX3M.jpg', + releaseDate: 2022, + description: 'The fourth season of Overlord.', + genres: [ 'Action', 'Fantasy', 'Adventure' ], + rating: 8, + status: 'RELEASING', + mappings: [ + { + id: "/watch/overlord-iv.r77y", + providerId: "9anime", + providerType: "ANIME", + similarity: 1 + }, + { + id: "48895", + providerId: "mal", + providerType: "META", + similarity: 1 + } + ] + }, + {...}, + ... + ] +} +``` + +### fetchAnimeInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------------------------------------------------------- | +| id | `string` | takes anime id as a parameter. (*anime id can be found in the anime search results or anime info object*) | + +```ts +anify.fetchAnimeInfo("133844").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ +output: +```js +{ + id: '133844', + anilistId: 133844, + malId: 48895, + title: 'Overlord IV', + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx133844-E32FjKZ0XxEs.jpg', + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/133844-uIaUmh5aJX3M.jpg', + releaseDate: 2022, + description: 'The fourth season of Overlord.', + genres: [ 'Action', 'Fantasy', 'Adventure' ], + rating: 8, + status: 'RELEASING', + mappings: [ + { + id: "/watch/overlord-iv.r77y", + providerId: "9anime", + providerType: "ANIME", + similarity: 1 + }, + { + id: "48895", + providerId: "mal", + providerType: "META", + similarity: 1 + } + ], + episodes: [ + { + id: '/overlord-iv-episode-6', + number: 6, + title: 'The Impending Crisis', + isFiller: false, + description: null, + image: null, + rating: null + }, + { + id: '/overlord-iv-episode-5', + number: 5, + title: 'In Pursuit of the Land of Dwarves', + isFiller: false, + description: null, + image: null, + rating: null + }, + {...}, + ... + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the anime info object*) | +| episodeNumber | `number` | takes episode number as a parameter. (*episode number can be found in the anime info object*) | +| id | `string` | takes anime ID as a parameter. (*anime id can be found in the anime search results or anime info object*) | + + +In this example, we're getting the sources for the first episode of Overlord IV. +```ts +anify.fetchEpisodeSources("/overlord-iv-episode-5", 11, "133844").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of episode sources. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ +output: +```js +{ + headers: { + Referer: 'https://goload.io/streaming.php?id=MTkwMzEy&title=Overlord+IV+Episode+6' + }, + sources: [ + { + url: 'https://www050.vipanicdn.net/streamhls/42f4d05521ce0b276e0d779493c16837/ep.11.1697533391.360.m3u8', + quality: '360p' + } + ], + subtitles: [], + audio: [], + intro: { start: 0, end: 0 }, + outro: { start: 0, end: 0 } +} +``` + +Make sure to check the `headers` property of the returned object. It contains the referer header, which is needed to bypass the 403 error and allow you to stream the video without any issues. + +

(back to anime providers list)

diff --git a/consumet.ts/docs/providers/anilist.md b/consumet.ts/docs/providers/anilist.md new file mode 100644 index 00000000..1a4b2404 --- /dev/null +++ b/consumet.ts/docs/providers/anilist.md @@ -0,0 +1,434 @@ +

Anilist

+This is a custom provider that maps an anime provider (like gogoanime) to anilist and kitsu. + +`Anilist` class takes a [`AnimeParser`](https://github.com/consumet/extensions/blob/master/src/models/anime-parser.ts) object as a parameter **(optional)**. This object is used to parse the anime episodes from the provider, then mapped to anilist and kitsu. + +```ts +const anilist = new META.Anilist(); +``` + +

Methods

+ +- [search](#search) +- [fetchTrendingAnime](#fetchtrendinganime) +- [fetchPopularAnime](#fetchpopularanime) +- [fetchAnimeInfo](#fetchanimeinfo) +- [fetchAnimeGenres](#fetchanimegenres) +- [fetchAiringSchedule](#fetchairingschedule) +- [fetchEpisodeSources](#fetchepisodesources) + +### search + +

Parameters

+ +| Parameter | Type | Description | +| -------------------- | -------- | ----------------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `Classroom of the elite`*) | +| page (optional) | `number` | page number to search for. | +| perPage (optional) | `number` | number of results per page. **Default: 15** | + +```ts +anilist.search("Classroom of the elite").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: false, + results: [ + { + id: '98659', + title: { + romaji: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e', + english: 'Classroom of the Elite', + native: 'ようこそ実力至上主義の教室へ', + userPreferred: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e' + }, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b98659-sH5z5RfMuyMr.png', + rating: 77, + releasedDate: 2017 + }, + { + id: '145545', + title: { + romaji: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e 2nd Season', + english: 'Classroom of the Elite Season 2', + native: 'ようこそ実力至上主義の教室へ 2nd Season', + userPreferred: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e 2nd Season' + }, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145545-DGl3LVvFlnHi.png', + rating: 79, + releasedDate: 2022 + } + {...} + ... + ] +} +``` + +### fetchTrendingAnime + +

Parameters

+ +| Parameter | Type | Description | +| ------------------ | -------- | --------------------------- | +| page (optional) | `number` | page number to search for. | +| perPage (optional) | `number` | number of results per page. | + +```ts +anilist.fetchTrendingAnime().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```ts +{ + currentPage: 1, + hasNextPage: true, + results: [ + { + id: '153288', + malId: null, + title: { + romaji: 'Kaijuu 8-gou', + english: 'Kaiju No.8', + native: '怪獣8号', + userPreferred: 'Kaijuu 8-gou' + }, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153288-INFE21hHhAUD.jpg', + trailer: { + id: '-MaTda-Ws3Y', + site: 'youtube', + thumbnail: 'https://i.ytimg.com/vi/-MaTda-Ws3Y/hqdefault.jpg' + }, + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153288-INFE21hHhAUD.jpg', + rating: null, + releaseDate: null, + totalEpisodes: 0, + duration: null, + type: null + }, + { + id: '130592', + malId: 48413, + title: {...}, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx130592-LAUlhx15mxQu.jpg', + trailer: {...}, + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/130592-WPfrW1SR4dnY.jpg', + rating: 74, + releaseDate: 2022, + totalEpisodes: 12, + duration: 24, + score: 75, + type: 'TV' + }, + ] +} +``` + +### fetchPopularAnime + +

Parameters

+ +| Parameter | Type | Description | +| ------------------ | -------- | --------------------------- | +| page (optional) | `number` | page number to search for. | +| perPage (optional) | `number` | number of results per page. | + +```ts +anilist.fetchPopularAnime().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```ts +{ + currentPage: 1, + hasNextPage: true, + results: [ + { + id: '153288', + malId: null, + title: { + romaji: 'Kaijuu 8-gou', + english: 'Kaiju No.8', + native: '怪獣8号', + userPreferred: 'Kaijuu 8-gou' + }, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153288-INFE21hHhAUD.jpg', + trailer: { + id: '-MaTda-Ws3Y', + site: 'youtube', + thumbnail: 'https://i.ytimg.com/vi/-MaTda-Ws3Y/hqdefault.jpg' + }, + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx153288-INFE21hHhAUD.jpg', + rating: null, + releaseDate: null, + totalEpisodes: 0, + duration: null, + type: null + }, + { + id: '130592', + malId: 48413, + title: {...}, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx130592-LAUlhx15mxQu.jpg', + trailer: {...}, + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/130592-WPfrW1SR4dnY.jpg', + rating: 74, + releaseDate: 2022, + totalEpisodes: 12, + duration: 24, + score: 75, + type: 'TV' + }, + ] +} +``` + +### fetchAnimeInfo + +

Parameters

+ +| Parameter | Type | Description | +| -------------- | --------- | --------------------------------------------------------------------------------------------------------- | +| id | `string` | takes anime id as a parameter. (*anime id can be found in the anime search results or anime info object*) | +| dub (optional) | `boolean` | if true, will fetch dubbed anime. **Default: false** | + + +```ts +anilist.fetchAnimeInfo("98659").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ +output: +```js +{ + id: '98659', + title: { + romaji: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e', + english: 'Classroom of the Elite', + native: 'ようこそ実力至上主義の教室へ', + userPreferred: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e' + }, + malId: 35507, + trailer: { + id: 'gMZDGyihTyc', + site: 'youtube', + thumbnail: 'https://i.ytimg.com/vi/gMZDGyihTyc/hqdefault.jpg' + }, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b98659-sH5z5RfMuyMr.png', + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/111321-nnetF1qONAcE.jpg', + description: 'Koudo Ikusei Senior High School is a leading school with state-of-the-art facilities. The students there have the freedom to wear any hairstyle ...', + status: 'Completed', + releaseDate: 2017, + nextAiringEpisode:{ + airingTime: 2312312123, + timeUntilAiring: 12512355, + episode: 5, + } + rating: 77, + duration: 24, + genres: [ 'Drama', 'Psychological' ], + studios: [ 'Lerche' ], + subOrDub: 'sub', + recommendations: [ + { + id: 101921, + idMal: 37999, + title: { + romaji: 'Kaguya-sama wa Kokurasetai: Tensaitachi no Renai Zunousen', + english: 'Kaguya-sama: Love is War', + native: undefined, + userPreferred: undefined + }, + status: 'Completed', + episodes: 12, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx101921-VvdGQy1ZySYf.jpg', + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/101921-GgvvFhlNhzlF.jpg', + score: 83 + }, + {...} + ... + ], + episodes: [ + { + id: 'youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e-tv-episode-12', + title: 'What is evil? Whatever springs from weakness.', + image: 'https://media.kitsu.io/episodes/thumbnails/228542/original.jpg', + number: 12, + description: "Melancholy, unmotivated Ayanokoji Kiyotaka attends his first day at Tokyo Metropoiltan Advanced Nuturing High School, ...", + url: '...' + }, + {...} + ... + ] +} +``` + +### fetchAnimeGenres + +

Parameters

+ +| Parameter | Type | Description | +| ------------------ | -------- | --------------------------- | +| genres | `string[]` | a list containing the genres of the animes to fetch. | +| page (optional) | `number` | page number to search for. | +| perPage (optional) | `number` | number of results per page. | + + +```ts +anilist.fetchAnimeGenres(["Action", "Adventure"]) +.then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: + +```ts +{ + currentPage: 1, + hasNextPage: true, + results: [ + { + id: '1', + malId: 1, + title: {...}, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx1-CXtrrkMpJ8Zq.png', + trailer: {...}, + description: 'Enter a world in the distant future, where Bounty Hunters roam the solar system. Spike and Jet, bounty hunting partners, set out on journeys in an ever struggling effort to win bounty rewards to survive.

\n' + + 'While traveling, they meet up with other very interesting people. Could Faye, the beautiful and ridiculously poor gambler, Edward, the computer genius, and Ein, the engineered dog be a good addition to the group?', + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/1-T3PJUjFJyRwg.jpg', + rating: 86, + releaseDate: 1998, + totalEpisodes: 26, + duration: 24, + type: 'TV' + }, + {...} + ] +} +``` + +### fetchAiringSchedule + +

Parameters

+ +| Parameter | Type | Description | +| ---------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| page (optional) | `number` | page number to search for. | +| perPage (optional) | `number` | number of results per page. | +| weekStart | `number` | Filter by the time of airing. eg. if you set weekStart to this week's monday, and set weekEnd to next week's sunday, you will get all the airing anime in between these two dates. | +| weekEnd | `number` | Filter by the time of airing. | +| notYetAired (optional) | `boolean` | Filter to episodes that haven't yet aired. (default: false) | + + +```ts +anilist.fetchAiringSchedule(1 , 20, 1660047922, 1661832000, true).then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```ts + + { + currentPage: 1, + hasNextPage: true, + results: [ + { + id: '133844', + malId: 48895, + episode: 6, + airingAt: 1660050000, + title: { + romaji: 'SHINE POST', + english: 'SHINEPOST', + native: 'シャインポスト', + userPreferred: 'SHINE POST' + }, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx133844-E32FjKZ0XxEs.jpg', + description: 'The fourth season of Overlord.', + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/133844-uIaUmh5aJX3M.jpg', + rating: 80, + releaseDate: 2022, + type: 'TV' + }, + { + id: '146210', + malId: 51213, + episode: 6, + airingAt: 1660051800, + title: { + romaji: 'Jashin-chan Dropkick X', + english: 'Dropkick on My Devil!!! X', + native: '邪神ちゃんドロップキックX', + userPreferred: 'Jashin-chan Dropkick X' + }, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx146210-ZnIithxFLLHn.jpg', + description: 'Meet Alto, a hapless student at Royal Ortigia Magic Academy whose academic performance leaves much to be desired. Rather than take a more sensible approach to salvaging his grades in time for graduation', + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx146210-ZnIithxFLLHn.jpg', + rating: 69, + releaseDate: 2022, + type: 'TV' + }, + {...}, + ... + ] + } + +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the anime info object*) | + + +In this example, we're getting the sources for the first episode of classroom of the elite. + + +```ts +anilist.fetchEpisodeSources("youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e-tv-episode-12").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of episode sources. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ +output: +```js +{ + headers: { + Referer: 'https://goload.pro/streaming.php?id=MTAxMTU3&title=Youkoso+Jitsuryoku+Shijou+Shugi+no+Kyoushitsu+e+%28TV%29+Episode+12&typesub=SUB' + }, + sources: [ + { + url: 'https://manifest.prod.boltdns.net/manifest/v1/hls/v4/clear/6310475588001/d34ba94f-c1db-4b05-a0b2-34d5a40134b2/6s/master.m3u8?fastly_token=NjJjZjkxZGFfODlmNWQyMWU1ZDM1NzhlNWM1MGMyMTBkNjczMjY4YjQ5ZGMyMzEzMWI2YjgyZjVhNWRhMDU4YmI0NjFjMTY4Zg%3D%3D', + isM3U8: true + }, + { + url: 'https://www13.gogocdn.stream/hls/ba0b5d73fb1737d2e8007c65f347dae8/ep.12.1649784300.m3u8', + isM3U8: true + } + ] +} +``` + +Make sure to check the `headers` property of the returned object. It contains the referer header, which is needed to bypass the 403 error and allow you to stream the video without any issues. + +

(back to meta providers list)

diff --git a/consumet.ts/docs/providers/animefox.md b/consumet.ts/docs/providers/animefox.md new file mode 100644 index 00000000..a9686553 --- /dev/null +++ b/consumet.ts/docs/providers/animefox.md @@ -0,0 +1,187 @@ +

AnimeFox

+ +```ts +const animefox = new ANIME.AnimeFox(); +``` + +

Methods

+ +- [search](#search) +- [fetchAnimeInfo](#fetchanimeinfo) +- [fetchEpisodeSources](#fetchepisodesources) +- [fetchRecentEpisodes](#fetchrecentepisodes) + +### fetchRecentEpisodes + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| page (optional) | `number` | page number (default 1) | + +```ts +animefox.fetchRecentEpisodes().then(data => { + console.log(data); +}) +``` + + +```js +{ + currentPage: 1, + hasNextPage: true, + results: [ + { + id: 'kinsou-no-vermeil-gakeppuchi-majutsushi-wa-saikyou-no-yakusai-to-mahou-sekai-wo-tsukisusumu-episode-6', + image: 'https://cdn.animefox.tv/cover/kinsou-no-vermeil-gakeppuchi-majutsushi-wa-saikyou-no-yakusai-to-mahou-sekai-wo-tsukisusumu.png', + title: 'Kinsou no Vermeil: Gakeppuchi Majutsushi wa Saikyou no Yakusai to Mahou Sekai wo Tsukisusumu', + url: 'https://animefox.tv/watch/kinsou-no-vermeil-gakeppuchi-majutsushi-wa-saikyou-no-yakusai-to-mahou-sekai-wo-tsukisusumu-episode-6!', + episode: 6 + }, + { + id: 'overlord-iv-episode-6', + image: 'https://cdn.animefox.tv/cover/overlord-iv.png', + title: 'Overlord IV', + url: 'https://animefox.tv/watch/overlord-iv-episode-6!', + episode: 6 + }, + { + id: 'sekai-no-owari-ni-shiba-inu-to-episode-5', + image: 'https://cdn.animefox.tv/cover/sekai-no-owari-ni-shiba-inu-to.png', + title: 'Sekai no Owari ni Shiba Inu to', + url: 'https://animefox.tv/watch/sekai-no-owari-ni-shiba-inu-to-episode-5!', + episode: 5 + }, + {...}, + ... + ] +} +``` + +### search + + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| query | `string` | query to search for. (*In this case, We're searching for `Overlord IV`*) | + +```ts +animefox.search("Overlord IV").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: false, + results: [ + { + id: 'overlord-iv', + title: 'Overlord IV', + type: 'Overlord IV', + image: 'Summer 2022 ', + url: 'https://animefox.tv/anime/overlord-iv', + episode: 6 + }, + { + id: 'overlord-iv-dub', + title: 'Overlord IV (Dub)', + type: 'Overlord IV (Dub)', + image: 'TV Series', + url: 'https://animefox.tv/anime/overlord-iv-dub', + episode: 3 + }, + {...} + ... + ] +} +``` + +### fetchAnimeInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------------------------------------------------------- | +| id | `string` | takes anime id as a parameter. (*anime id can be found in the anime search results or anime info object*) | + + +```ts +animefox.fetchAnimeInfo("overlord-iv").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ +output: +```js +{ + id: 'overlord-iv', + title: 'Overlord IV', + image: 'https://cdn.animefox.tv/cover/overlord-iv.png', + description: 'Fourth season of Overlord.', + type: 'Summer 2022', + releaseYear: '2022', + status: 'Ongoing', + totalEpisodes: 6, + url: 'https://animefox.tv/overlord-iv', + episodes: [ + { + id: 'overlord-iv-episode-1', + number: 1, + title: 'Overlord IV Episode 1', + url: 'https://animefox.tv/watch/overlord-iv-episode-1' + }, + { + id: 'overlord-iv-episode-2', + number: 2, + title: 'Overlord IV Episode 2', + url: 'https://animefox.tv/watch/overlord-iv-episode-2' + }, + {...}, + ... + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the anime info object*) | + + +In this example, we're getting the sources for the first episode of Overlord IV. +```ts +animefox.fetchEpisodeSources("overlord-iv").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of episode sources. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ +output: +```js +{ + sources: [ + { + url: 'https://wwwx20.gogocdn.stream/videos/hls/NbM2m1QH_oxhhOUt6gLkSg/1660076576/188769/ca09dc1ce88568467994ea8e756c4493/ep.1.1657688625.m3u8', + isM3U8: true + }, + { + url: 'https://wwwx20.gogocdn.stream/videos/hls/NbM2m1QH_oxhhOUt6gLkSg/1660076576/188769/ca09dc1ce88568467994ea8e756c4493/ep.1.1657688625.m3u8', + isM3U8: true + } + ] +} +``` + +Make sure to check the `headers` property of the returned object. It contains the referer header, which might be needed to bypass the 403 error and allow you to stream the video without any issues. + +

(back to anime providers list)

diff --git a/consumet.ts/docs/providers/animepahe.md b/consumet.ts/docs/providers/animepahe.md new file mode 100644 index 00000000..2d2772c2 --- /dev/null +++ b/consumet.ts/docs/providers/animepahe.md @@ -0,0 +1,147 @@ +

AnimePahe

+ +```ts +const animepahe = new ANIME.AnimePahe(); +``` + +

Methods

+ +- [search](#search) +- [fetchAnimeInfo](#fetchanimeinfo) +- [fetchEpisodeSources](#fetchepisodesources) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| query | `string` | query to search for. (*In this case, We're searching for `Overlord IV`*) | + +```ts +animepahe.search("Overlord IV").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + results: [ + { + id: 'adb84358-8fec-fe80-1dc5-ad6218421dc1', + title: 'Overlord IV', + image: 'https://i.animepahe.com/posters/cb77e1e2a76b985a7c9d9b90a497fee65d89fa9c41d0e9e6fab4608d10313ddf.jpg', + rating: 8.3, + releaseDate: 2022, + type: 'TV' + }, + { + id: 'a0d776d3-48d2-5487-971d-f5d8dada5c42', + title: 'Overlord', + image: 'https://i.animepahe.com/posters/e78bf21dfd4e382dbc985501edb0f57bda7d5305b87863fe8991a5e658c9c1a8.jpg', + rating: 7.92, + releaseDate: 2015, + type: 'TV' + }, + {...} + ... + ] +} +``` + +### fetchAnimeInfo + +

Parameters

+ +| Parameter | Type | Description | +| ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| id | `string` | takes anime id as a parameter. (*anime id can be found in the anime search results or anime info object*) | +| episodePage (optional) | `number` | takes episode page number as a parameter. default: -1 to get all episodes at once (*episodePages can be found in the anime info object*) | + + +```ts +animepahe.fetchAnimeInfo("adb84358-8fec-fe80-1dc5-ad6218421dc1").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ +output: +```js +{ + id: 'adb84358-8fec-fe80-1dc5-ad6218421dc1', + title: 'Overlord IV', + image: 'https://i.animepahe.com/posters/cb77e1e2a76b985a7c9d9b90a497fee65d89fa9c41d0e9e6fab4608d10313ddf.jpg', + cover: 'https://i.animepahe.com/covers/cover_default3.jpg', + description: 'Fourth season of Overlord.', + genres: [ 'Action', 'Fantasy', 'Supernatural' ], + status: 'Ongoing', + type: 'TV', + releaseDate: 'Jul 05, 2022', + aired: 'Jul 05, 2022 to ?', + studios: [ 'Madhouse' ], + totalEpisodes: NaN, // NaN means that the anime is ongoing. + episodes: [ + { + id: 'c673b4d6cedf5e4cd1900d30d61ee2130e23a74e58f4401a85f21a4e95c94f73', + number: 1, + title: '', + image: 'https://i.animepahe.com/snapshots/8b3499c66e59e4c266485b54b78ad8469a520d9957dbe5a117f8d0934a93817a.jpg', + duration: '00:23:40' + } + ], + episodePages: 1 // 1 means that there is only one page of episodes. +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the anime info object*) | + + +In this example, we're getting the sources for the first episode of Overlord IV. +```ts +animepahe.fetchEpisodeSources("c673b4d6cedf5e4cd1900d30d61ee2130e23a74e58f4401a85f21a4e95c94f73").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of episode sources. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ +output: +```js +{ + headers: { Referer: 'https://kwik.cx/' }, + sources: [ + { + url: 'https://na-191.files.nextcdn.org/hls/01/b49063a1225cf4350deb46d79b42a7572e323274d1c9d63f3b067cc4df09986a/uwu.m3u8', + isM3U8: true, + quality: '360', + size: 44617958 + }, + { + url: 'https://na-191.files.nextcdn.org/hls/01/c32da1b1975a5106dcee7e7182219f9b4dbef836fb782d7939003a8cde8f057f/uwu.m3u8', + isM3U8: true, + quality: '720', + size: 78630133 + }, + { + url: 'https://na-191.files.nextcdn.org/hls/01/b85d4450908232aa32b71bc67c80e8aedcc4f32a282e5df9ad82e4662786e9d8/uwu.m3u8', + isM3U8: true, + quality: '1080', + size: 118025148 + } + ] +} +``` + +Make sure to check the `headers` property of the returned object. It contains the referer header, which is needed to bypass the 403 error and allow you to stream the video without any issues. + +

(back to anime providers list)

diff --git a/consumet.ts/docs/providers/animesaturn.md b/consumet.ts/docs/providers/animesaturn.md new file mode 100644 index 00000000..a5ed3d2b --- /dev/null +++ b/consumet.ts/docs/providers/animesaturn.md @@ -0,0 +1,151 @@ +

Anime Saturn

+ +```ts +const animesaturn = new ANIME.AnimeSaturn(); +``` + +

Methods

+ +- [search](#search) +- [fetchAnimeInfo](#fetchanimeinfo) +- [fetchEpisodeSources](#fetchepisodesources) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| query | `string` | query to search for. (*In this case, We're searching for `Tokyo Revengers`*) | + +```ts +animesaturn.search("Tokyo Revengers").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + hasNextPage: false, + results: [ + { + id: 'Tokyo-Revengers-aaaaaa', + title: 'Tokyo Revengers', + image: 'https://cdn.animesaturn.tv/static/images/copertine/4af2d1048aeb86aeb9b585f3619275601626143497_full.jpg', + url: 'https://www.animesaturn.tv/anime/Tokyo-Revengers-aaaaaa' + }, + { + id: 'Tokyo-Revengers-ITA-aa', + title: 'Tokyo Revengers (ITA)', + image: 'https://cdn.animesaturn.tv/static/images/copertine/4af2d1048aeb86aeb9b585f3619275601626143497_full.jpg', + url: 'https://www.animesaturn.tv/anime/Tokyo-Revengers-ITA-aa' + }, + { + id: 'Tokyo-Revengers-Seiya-Kessen-hen-aa', + title: 'Tokyo Revengers: Seiya Kessen-hen', + image: 'https://cdn.animesaturn.tv/static/images/copertine/26084_1_1.png', + url: 'https://www.animesaturn.tv/anime/Tokyo-Revengers-Seiya-Kessen-hen-aa' + } + ] +} +``` + +### fetchAnimeInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------------------------------------------------------- | +| id | `string` | takes anime id as a parameter. (*anime id can be found in the anime search results or anime info object*) | + + +```ts +animesaturn.fetchAnimeInfo("Tokyo-Revengers-aaaaaa").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ +output: +```js +{ + id: 'Tokyo-Revengers-aaaaaa', + title: 'Tokyo Revengers Sub ITA', + genres: [ 'Azione', 'Drammatico', 'Scolastico', 'Shounen', 'Soprannaturale' ], + image: 'https://cdn.animesaturn.tv/static/images/locandine/Zkczy.png', + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/120120-oRfDsJjrpoQ4.jpg', + description: '', + episodes: [ + { number: 1, id: 'Tokyo-Revengers-ep-1' }, + { number: 2, id: 'Tokyo-Revengers-ep-2' }, + { number: 3, id: 'Tokyo-Revengers-ep-3' }, + { number: 4, id: 'Tokyo-Revengers-ep-4' }, + { number: 5, id: 'Tokyo-Revengers-ep-5' }, + { number: 6, id: 'Tokyo-Revengers-ep-6' }, + { number: 7, id: 'Tokyo-Revengers-ep-7' }, + { number: 8, id: 'Tokyo-Revengers-ep-8' }, + { number: 9, id: 'Tokyo-Revengers-ep-9' }, + { number: 10, id: 'Tokyo-Revengers-ep-10' }, + { number: 11, id: 'Tokyo-Revengers-ep-11' }, + { number: 12, id: 'Tokyo-Revengers-ep-12' }, + { number: 13, id: 'Tokyo-Revengers-ep-13' }, + { number: 14, id: 'Tokyo-Revengers-ep-14' }, + { number: 15, id: 'Tokyo-Revengers-ep-15' }, + { number: 16, id: 'Tokyo-Revengers-ep-16' }, + { number: 17, id: 'Tokyo-Revengers-ep-17' }, + { number: 18, id: 'Tokyo-Revengers-ep-18' }, + { number: 19, id: 'Tokyo-Revengers-ep-19' }, + { number: 20, id: 'Tokyo-Revengers-ep-20' }, + { number: 21, id: 'Tokyo-Revengers-ep-21' }, + { number: 22, id: 'Tokyo-Revengers-ep-22' }, + { number: 23, id: 'Tokyo-Revengers-ep-23' }, + { number: 24, id: 'Tokyo-Revengers-aszb-ep-24' } + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the anime info object*) | + + +In this example, we're getting the sources for the first episode of Tokyo Revengers. +```ts +animesaturn.fetchEpisodeSources("Tokyo-Revengers-ep-1").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of episode sources (the second sources url is recommended as more stable). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ +output: +```js +{ + headers: {}, + subtitles: [ + { + url: 'https://www.saturnspeed75.org/DDL/ANIME/TokyoRevengers/01/subtitles.vtt', + lang: 'Spanish' + } + ], + sources: [ + { + url: 'https://www.saturnspeed75.org/DDL/ANIME/TokyoRevengers/01/playlist.m3u8', + isM3U8: true + }, + { + url: 'https://streamtape.com/get_video?id=0ZamyRrDpBFbX01&expires=1694799037&ip=DxWsE0InDS9X&token=1Ut-RohY4oCE', + isM3U8: false + } + ] +} +``` + +

(back to anime providers list)

diff --git a/consumet.ts/docs/providers/animeunity.md b/consumet.ts/docs/providers/animeunity.md new file mode 100644 index 00000000..e819d7c4 --- /dev/null +++ b/consumet.ts/docs/providers/animeunity.md @@ -0,0 +1,152 @@ +

AnimeUnity

+ +```ts +const animeunity = new ANIME.AnimeUnity(); +``` + +

Methods

+ +- [search](#search) +- [fetchAnimeInfo](#fetchanimeinfo) +- [fetchEpisodeSources](#fetchepisodesources) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| query | `string` | query to search for. (*In this case, We're searching for `Jujutsu Kaisen 2`*) | + +```ts +animeunity.search("Demon Slayer: Kimetsu no Yaiba Hashira Training Arc").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + hasNextPage: false, + results: [ + { + id: '5167-demon-slayer-kimetsu-no-yaiba-hashira-training-arc', + title: 'Kimetsu no Yaiba: Hashira Geiko-hen', + url: 'https://www.animeunity.to/anime/5167-demon-slayer-kimetsu-no-yaiba-hashira-training-arc', + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx166240-bGHsLoWmJmiL.png', + cover: 'https://s4.anilist.co/file/anilistcdn/media/anime/banner/166240-YdxoEhrfwNk0.jpg', + subOrDub: 'sub' + } + {...}, + ... + ] +} +``` + +### fetchAnimeInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------------------------------------------------------- | +| id | `string` | takes anime id as a parameter. (*anime id can be found in the anime search results or anime info object*) | +| page? | `number` | takes page number as a parameter | + +Why page number? AnimeUnity provides only 120 episodes at a time, how to use: +- page: 1, you'll get episodes info from 1 to 120; +- page: 4, you'll get episodes info from 361 to 480. + +If no page number is passed, the first page will be fetched. + +```ts +animesaturn.fetchAnimeInfo("5167-demon-slayer-kimetsu-no-yaiba-hashira-training-arc", 1).then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: false, + totalPages: 1, + id: '5167-demon-slayer-kimetsu-no-yaiba-hashira-training-arc', + title: 'Demon Slayer: Kimetsu no Yaiba Hashira Training Arc', + url: 'https://www.animeunity.to/anime/5167-demon-slayer-kimetsu-no-yaiba-hashira-training-arc', + alID: '166240', + genres: [ + 'Action', + 'Adventure', + 'Drama', + 'Fantasy', + 'Historical', + 'Shounen', + 'Supernatural' + ], + totalEpisodes: 1, + image: 'https://img.animeunity.to/anime/bx166240-bGHsLoWmJmiL.png', + cover: 'https://img.animeunity.to/anime/166240-YdxoEhrfwNk0.jpg', + description: "Adattamento animato dell'arco Hashira Training", + episodes: [ + { + id: '5167-demon-slayer-kimetsu-no-yaiba-hashira-training-arc/80480', + number: 1, + url: 'https://www.animeunity.to/anime/5167-demon-slayer-kimetsu-no-yaiba-hashira-training-arc/80480' + }, + {...}, + ... + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the anime info object*) | + + +In this example, we're getting the sources for the first episode of Demon Slayer: Kimetsu no Yaiba Hashira Training Arc. +```ts +animesaturn.fetchEpisodeSources("5167-demon-slayer-kimetsu-no-yaiba-hashira-training-arc/80480").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of episode sources. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ +output: +```js +{ + sources: [ + { + url: 'https://vixcloud.co/playlist/226038?type=video&rendition=480p&token=3PBuZDfjsMTHY94nq6fjkg&expires=1721216219&edge=au-u1-01', + quality: '480p', + isM3U8: true + }, + { + url: 'https://vixcloud.co/playlist/226038?type=video&rendition=720p&token=9gqvFqv8EznuX3U6RuISZg&expires=1721216219&edge=au-u1-01', + quality: '720p', + isM3U8: true + }, + { + url: 'https://vixcloud.co/playlist/226038?type=video&rendition=1080p&token=zCuz2Jg81JGq5Dokyvw8zg&expires=1721216219&edge=au-u1-01', + quality: '1080p', + isM3U8: true + }, + { + url: 'https://vixcloud.co/playlist/226038?token=dc6d3b04327aa3f0c21d53b444d4d0cb&referer=&expires=1721216219&h=1', + quality: 'default', + isM3U8: true + } + ], + download: 'https://au-d1-01.scws-content.net/download/22/f/3a/f3ad66d6-262b-45e6-ba26-5225fdac18e4/1080p.mp4?token=q4oELFDSbkxeo6zh84zoIQ&expires=1716118619&filename=KimetsunoYaiba%3AHashiraGeiko-hen_Ep_01_SUB_ITA.mp4' +} +``` + +

(back to anime providers list)

diff --git a/consumet.ts/docs/providers/ann.md b/consumet.ts/docs/providers/ann.md new file mode 100644 index 00000000..dffd005a --- /dev/null +++ b/consumet.ts/docs/providers/ann.md @@ -0,0 +1,94 @@ +

Anime News Network

+ +```ts +const ann = new NEWS.ANN(); +``` + +

Methods

+ +- [fetchNewsFeeds](#fetchnewsfeeds) + - [Getting the news info for one of the feeds](#getting-the-news-info-for-one-of-the-feeds) +- [fetchNewsInfo](#fetchnewsinfo) + +### fetchNewsFeeds + +

Parameters

+ +| Parameter | Type | Description | +| ---------------- | ------------------------------------------------------------------------------------------- | --------------------------- | +| topic (optional) | [`Topics`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L348-361) | topic for getting the feed. | + +```ts +ann.fetchNewsFeeds().then(console.log) +``` +returns a promise which resolves into an array of the NewsFeed class. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/providers/news/animenewsnetwork.ts#L5-13)*)\ +output: +```js +[ + { + title: "Anime Films Airing on Indian TV: August 28-September 3", + id: "2022-08-27/anime-films-airing-on-indian-tv-august-28-september-3/.189058", + uploadedAt: "Aug 27, 12:34", + topics: [ + "anime" + ], + preview: { + intro: "Hungama TV airs Shin-chan film, Super Hungama airs Pokémon films", + full: "Editor's note: The titles and air times on this page will be updated as television channels make new announcements and update their schedules throughout the week. The third-party TV guide listings app \"What's On India: TV Guide App\" is currently listing that the following anime films will be airing in India this week: Sunday, August 2..." + }, + thumbnail: "https://www.animenewsnetwork.com/thumbnails/cover400x200/encyc/A16735-2734914642.1424057051.jpg", + url: "https://www.animenewsnetwork.com/news/2022-08-27/anime-films-airing-on-indian-tv-august-28-september-3/.189058" + }, + { + title: "Netflix India Lists Drifting Home Anime Film, Cyberpunk: Edgerunners Anime", + id: "2022-08-27/netflix-india-lists-drifting-home-anime-film-cyberpunk-edgerunners-anime/.189007", + uploadedAt: "Aug 27, 06:00", + topics: [ + "anime" + ], + preview: { + intro: "Drifting Home releases on September 16; Cyberpunk: Edgerunners yet to get release date", + full: "Netflix is listing Studio Colorido's new full-length anime film Drifting Home (Ame o Tsugeru Hyōryū Danchi) for release in India on September 16. It is also listing Cyberpunk: Edgerunners, the upcoming anime series by Studio Trigger based on CD Projekt Red's Cyberpunk 2077 game, for release in India without a con..." + }, + thumbnail: "https://www.animenewsnetwork.com/thumbnails/cover400x200/cms/news.5/184987/drifting-home-kv-2.jpeg", + url: "https://www.animenewsnetwork.com/news/2022-08-27/netflix-india-lists-drifting-home-anime-film-cyberpunk-edgerunners-anime/.189007" + }, + {...}, + ... +] +``` + +#### Getting the news info for one of the feeds + +```ts +ann.fetchNewsFeeds().then((res) => { + res[0].getInfo().then(console.log) +}) +``` + +### fetchNewsInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------ | +| id | `string` | id of the news.(*news id can be found in the url of the news, it is next to the "/news/"*) | + +```ts +ann.fetchNewsInfo("2022-08-26/higurashi-no-naku-koro-ni-rei-oni-okoshi-hen-manga-ends/.188996" /* --> https://www.animenewsnetwork.com/news/2022-08-26/higurashi-no-naku-koro-ni-rei-oni-okoshi-hen-manga-ends/.188996*/ ).then(console.log) +``` +returns a promise which resolves into a news info object. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L286-L291)*)\ +output: +```js +{ + id: "2022-08-26/higurashi-no-naku-koro-ni-rei-oni-okoshi-hen-manga-ends/.188996", + title: "Higurashi no Naku Koro ni Rei: Oni Okoshi-hen Manga Ends", + uploadedAt: "2022-08-27 00:30 IST", + intro: "Manga launched in November", + description: "Square Enix's Gangan Online manga app published the final chapter of Kei Natsumi's Higurashi no Naku Koro ni Rei: Oni Okoshi-hen manga on Wednesday.\n\nThe manga is one of two new manga ...", + thumbnail: "https://animenewsnetwork.com/thumbnails/max400x400/cms/news.5/188996/oniokoshi.jpg", + url: "https://www.animenewsnetwork.com/news/2022-08-26/higurashi-no-naku-koro-ni-rei-oni-okoshi-hen-manga-ends/.188996" +} +``` + +

(back to news providers list)

\ No newline at end of file diff --git a/consumet.ts/docs/providers/brmangas.md b/consumet.ts/docs/providers/brmangas.md new file mode 100644 index 00000000..3cf1bb19 --- /dev/null +++ b/consumet.ts/docs/providers/brmangas.md @@ -0,0 +1,122 @@ +

BRMangas 🇧🇷

+ +```ts +const brmangas = new MANGA.BRMangas(); +``` + +

Methods

+ +- [search](#search) +- [fetchMangaInfo](#fetchmangainfo) +- [fetchChapterPages](#fetchchapterpages) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. +> +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `punpun`*) | + +```ts +brmangas.search("punpun").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + results: [ + { + id: 'berserk-online', + title: 'Berserk', + image: 'https://cdn.plaquiz.xyz/uploads/b/berserk/berserk.jpg', + headerForImage: { Referer: 'https://brmangas.net' } + } + ] +} +``` + +### fetchMangaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------------- | +| mangaId | `string` | manga id.(*manga id can be found in the manga search results*) | + +```ts +brmangas.fetchMangaInfo("berserk-online").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an manga info object (including the chapters). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L120)*)\ +output: +```js +{ + id: 'berserk-online', + title: 'Berserk', + altTitles: [], + description: 'Gatts é um sobrevivente que vaga pelo mundo à procura de respostas. Antigo membro do ext “Bando dos Falcões”, um grupo mercenário de cavaleiros e guerreiros liderado por Griffith e Caska, Gatts se adentra na história que ganha corpo e emerge sob um ponto de vista totalmente imprevisível, a medida que os acontecimentos vão se completando. É uma obra dedicada à eterna luta do Catolicismo contra Paganismo….', + headerForImage: { Referer: 'https://www.brmangas.net' }, + image: 'https://cdn.plaquiz.xyz/uploads/b/berserk/berserk.jpg', + genres: [ + 'Ação', 'Aventura', + 'Demônios', 'Drama', + 'Fantasia', 'Horror', + 'Mangás', 'Militar', + 'Psicológico', 'Seinen', + 'Sobrenatural' + ], + status: 'Unknown', + views: null, + authors: [ 'Miura, Kentarou' ], + chapters: [ + { + id: 'berserk-16-online', + title: 'Capítulo -16', + views: null, + releasedDate: null + }, + {...} + ] + } +``` +Note: The `headerForImage` property might be useful when getting the image to display. + +### fetchChapterPages + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------- | +| chapterId | `string` | chapter id.(*chapter id can be found in the manga info*) | + +```ts +brmangas.fetchChapterPages("berserk-16-online").then(data => { + console.log(data); +}) +``` +returns an array of pages. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L122-L126)*)\ +output: +```js +[ + { + img: 'https://cdn.plaquiz.xyz/uploads/b/berserk/-16/1.jpg', + page: 0, + title: 'Page 1', + headerForImage: { Referer: 'https://www.brmangas.net' } + }, + { + img: 'https://cdn.plaquiz.xyz/uploads/b/berserk/-16/2.jpg', + page: 1, + title: 'Page 2', + headerForImage: { Referer: 'https://www.brmangas.net' } + }, + {...} +] +``` + +

(back to manga providers list)

diff --git a/consumet.ts/docs/providers/flixhq.md b/consumet.ts/docs/providers/flixhq.md new file mode 100644 index 00000000..7d72d821 --- /dev/null +++ b/consumet.ts/docs/providers/flixhq.md @@ -0,0 +1,456 @@ +

FlixHQ

+ +```ts +const flixhq = new MOVIES.FlixHQ(); +``` + +

Methods

+ +- [search](#search) +- [fetchMediaInfo](#fetchmediainfo) +- [fetchEpisodeSources](#fetchepisodesources) +- [fetchEpisodeServers](#fetchepisodeservers) +- [fetchRecentMovies](#fetchrecentmovies) +- [fetchRecentTvShows](#fetchrecenttvshows) +- [fetchTrendingMovies](#fetchtrendingmovies) +- [fetchTrendingTvShows](#fetchtrendingtvshows) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| query | `string` | query to search for. (*In this case, We're searching for `Vincenzo`*) P.S: `vincenzo` is a really good korean drama i highly recommend it. | +| page (optional) | `number` | page number (default: 1) | + +```ts +flixhq.search("Vincenzo").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies/tv series. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L233-L241)*)\ +output: +```js +{ + currentPage: 1, // current page + hasNextPage: false, // if there is a next page + results: [ + { + id: 'tv/watch-vincenzo-67955', // media id + title: 'Vincenzo', + url: 'https://flixhq.to/tv/watch-vincenzo-67955', // media url + image: 'https://img.flixhq.to/xxrz/250x400/379/79/6b/796b32989cf1308b9e0619524af5b022/796b32989cf1308b9e0619524af5b022.jpg', + type: 'TV Series' + } + {...}, + ... + ] +} +``` + +### fetchMediaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- | +| mediaId | `string` | takes media id or url as a parameter. (*media id or url can be found in the media search results as shown on the above method*) | + +```ts +flixhq.fetchMediaInfo("tv/watch-vincenzo-67955").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L243-L254)*)\ +output: +```js +{ + id: 'tv/watch-vincenzo-67955', // media id + title: 'Vincenzo', + url: 'https://flixhq.to/tv/watch-vincenzo-67955', // media url + image: 'https://img.flixhq.to/xxrz/250x400/379/79/6b/796b32989cf1308b9e0619524af5b022/796b32989cf1308b9e0619524af5b022.jpg', + description: '\n' + + ' At age of 8, Park Joo-Hyung went to Italy after he was adopted. He is now an adult and has the name of Vincenzo Cassano. ...\n' + + ' ', + type: 'TV Series', + releaseDate: '2021-02-20', + genres: [ 'Action', 'Adventure', '...' ], + casts: [ + 'Kwak Dong-yeon', + 'Kim Yeo-jin', + ... + ], + tags: [ + 'Watch Vincenzo Online Free,', + 'Vincenzo Online Free,', + ... + ], + production: 'Studio Dragon', + duration: '60 min', + rating: 8.4, + episodes: [ + { + id: '1167571', + title: 'Eps 1: Episode #1.1', + number: 1, + season: 1, // the number of episodes resets to 1 every season + url: 'https://flixhq.to/ajax/v2/episode/servers/1167571' + }, + {...}, + ... + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| ----------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the media info object*) | +| mediaId | `string` | takes media id as a parameter. (*media id can be found in the media info object*) | +| server (optional) | [`StreamingServers`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L144-L157) | takes server enum as a parameter. *default: [`StreamingServers.VidCloud`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L150)* | + + +```ts +flixhq.fetchEpisodeSources("1167571", "tv/watch-vincenzo-67955").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of episode sources and subtitles. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L300-L306)*)\ +output: +```js +{ + headers: { Referer: 'https://rabbitstream.net/embed-4/gBOHFKQ0sOxE?z=' }, + sources: [ + { + url: 'https://b-g-ca-5.feetcdn.com:2223/v2-hls-playback/01b3e0bf48e643923f849702a32bd97a5c4360797759b0838c8f34597271ed8bf541e616b85a255a1320417863fe198040e65edb91d55f65c2f187d38c159aac95365664aa55f6121e784c83e8719033f811224effd0aefb9b88c77caf71b2d8943454dee7f505d5e1aae5f70dea1472a541a7c283a37782ea8253b156aad0f83701ef208196d2a5b75a864b6d6e3a2d454e55ea1885f3d5df798053a843cc223d6e41ecb1af3f6d6a07fc72a41bce18/playlist.m3u8', + isM3U8: true + } + ], + subtitles: [ + { + url: 'https://cc.1clickcdn.ru/26/7f/267fbca84e18437aa7c7df80179b0751/ara-3.vtt', + lang: 'Arabic - Arabic' + }, + { + url: 'https://cc.1clickcdn.ru/26/7f/267fbca84e18437aa7c7df80179b0751/chi-4.vtt', + lang: 'Chinese - Chinese Simplified' + }, + {...} + ... + ] +} +``` + +### fetchEpisodeServers + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------------------------- | +| episodeId | `string` | take an episode id or url as a parameter. (*episode id or episode url can be found in the media info object*) | +| mediaId | `string` | takes media id as a parameter. (*media id can be found in the media info object*) | + +```ts +flixhq.fetchEpisodeServers('1167571', 'tv/watch-vincenzo-67955').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of episode servers. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L118)*)\ +output: +```js +[ + { + name: 'upcloud', + url: 'https://flixhq.to/watch-tv/watch-vincenzo-67955.4829542' + }, + { + name: 'vidcloud', + url: 'https://flixhq.to/watch-tv/watch-vincenzo-67955.4087001' + }, + { + name: 'streamlare', + url: 'https://flixhq.to/watch-tv/watch-vincenzo-67955.7041439' + }, + { + name: 'voe', + url: 'https://flixhq.to/watch-tv/watch-vincenzo-67955.7823107' + }, + {...}, + ... +] +``` + +

(back to movie providers list)

+ +### fetchRecentMovies + +```ts +flixhq.fetchRecentMovies().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ + { + id: 'movie/watch-violent-night-91333', + title: 'Violent Night', + url: 'https://flixhq.to/movie/watch-violent-night-91333', + image: 'https://img.flixhq.to/xxrz/250x400/379/cc/ff/ccff5242232b96b36ed22f0a0dda8234/ccff5242232b96b36ed22f0a0dda8234.jpg', + releaseDate: '2022', + duration: '112m', + type: 'Movie' + }, + { + id: 'movie/watch-holiday-heritage-91552', + title: 'Holiday Heritage', + url: 'https://flixhq.to/movie/watch-holiday-heritage-91552', + image: 'https://img.flixhq.to/xxrz/250x400/379/b9/4c/b94c9ef8b80fe5d71e9e4750602d086c/b94c9ef8b80fe5d71e9e4750602d086c.jpg', + releaseDate: '2022', + duration: '84m', + type: 'Movie' + }, + { + id: 'movie/watch-high-heat-91549', + title: 'High Heat', + url: 'https://flixhq.to/movie/watch-high-heat-91549', + image: 'https://img.flixhq.to/xxrz/250x400/379/4e/56/4e56d050f6d2578f1495dbf348e0becf/4e56d050f6d2578f1495dbf348e0becf.jpg', + releaseDate: '2022', + duration: '84m', + type: 'Movie' + }, + {...}, +] +``` + + +### fetchRecentTvShows + +```ts +flixhq.fetchRecentTvShows().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of tv shows. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ + { + id: 'tv/watch-yellowstone-38684', + title: 'Yellowstone', + url: 'https://flixhq.to/tv/watch-yellowstone-38684', + image: 'https://img.flixhq.to/xxrz/250x400/379/86/ba/86bacd45c63959587ef16c92927fe8eb/86bacd45c63959587ef16c92927fe8eb.jpg', + season: 'SS 5', + latestEpisode: 'EPS 7', + type: 'TV Series' + }, + { + id: 'tv/watch-his-dark-materials-34639', + title: 'His Dark Materials', + url: 'https://flixhq.to/tv/watch-his-dark-materials-34639', + image: 'https://img.flixhq.to/xxrz/250x400/379/0e/41/0e41301e8f1152499dcf51253b64a29f/0e41301e8f1152499dcf51253b64a29f.jpg', + season: 'SS 3', + latestEpisode: 'EPS 8', + type: 'TV Series' + }, + { + id: 'tv/watch-dangerous-liaisons-89965', + title: 'Dangerous Liaisons', + url: 'https://flixhq.to/tv/watch-dangerous-liaisons-89965', + image: 'https://img.flixhq.to/xxrz/250x400/379/fc/5b/fc5ba1c5d4445eb29d0b002f2c8425db/fc5ba1c5d4445eb29d0b002f2c8425db.jpg', + season: 'SS 1', + latestEpisode: 'EPS 7', + type: 'TV Series' + }, + {...}, +] +``` + + +### fetchTrendingMovies + +```ts +flixhq.fetchTrendingMovies().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ + { + id: 'movie/watch-avatar-the-way-of-water-79936', + title: 'Avatar: The Way of Water', + url: 'https://flixhq.to/movie/watch-avatar-the-way-of-water-79936', + image: 'https://img.flixhq.to/xxrz/250x400/379/1e/c6/1ec694a9d587d509ec7a9be815aacfac/1ec694a9d587d509ec7a9be815aacfac.jpg', + releaseDate: '2022', + duration: '192m', + type: 'Movie' + }, + { + id: 'movie/watch-the-banshees-of-inisherin-91351', + title: 'The Banshees of Inisherin', + url: 'https://flixhq.to/movie/watch-the-banshees-of-inisherin-91351', + image: 'https://img.flixhq.to/xxrz/250x400/379/6e/d2/6ed2e9486552bf0bda5dd3be8db0baec/6ed2e9486552bf0bda5dd3be8db0baec.jpg', + releaseDate: '2022', + duration: '114m', + type: 'Movie' + }, + { + id: 'movie/watch-avatar-19690', + title: 'Avatar', + url: 'https://flixhq.to/movie/watch-avatar-19690', + image: 'https://img.flixhq.to/xxrz/250x400/379/9d/0f/9d0fe6f16f205e483df14817753c1b0d/9d0fe6f16f205e483df14817753c1b0d.jpg', + releaseDate: '2009', + duration: '162m', + type: 'Movie' + }, + {...}, +] +``` + + +### fetchTrendingTvShows + +```ts +flixhq.fetchTrendingTvShows().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of tv shows. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ +{ + id: 'tv/watch-1923-91522', + title: '1923', + url: 'https://flixhq.to/tv/watch-1923-91522', + image: 'https://img.flixhq.to/xxrz/250x400/379/96/f3/96f3c8dfd9583a855473e2e9039c8bda/96f3c8dfd9583a855473e2e9039c8bda.jpg', + season: 'SS 1', + latestEpisode: 'EPS 1', + type: 'TV Series' + }, + { + id: 'tv/watch-the-recruit-91507', + title: 'The Recruit', + url: 'https://flixhq.to/tv/watch-the-recruit-91507', + image: 'https://img.flixhq.to/xxrz/250x400/379/0e/bd/0ebd5fe83f5a5f7055089d3390727e1c/0ebd5fe83f5a5f7055089d3390727e1c.jpg', + season: 'SS 1', + latestEpisode: 'EPS 8', + type: 'TV Series' + }, + { + id: 'tv/watch-wednesday-90553', + title: 'Wednesday', + url: 'https://flixhq.to/tv/watch-wednesday-90553', + image: 'https://img.flixhq.to/xxrz/250x400/379/9b/70/9b70e344f895fd9ed9cbac46d95b21a2/9b70e344f895fd9ed9cbac46d95b21a2.jpg', + season: 'SS 1', + latestEpisode: 'EPS 8', + type: 'TV Series' + }, + {...}, +] +``` + +### fetchByCountry + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ----------------------------------------------------------------------- | +| country | `string` | param to filter by country. (*In this case, We're filtering by `KR`*) | +| page (optional) | `number` | page number (default: 1) | + +```ts +flixhq.fetchByCountry('KR').then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies/tv series. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L233-L241)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: true, + results: [ + { + id: 'tv/watch-wedding-impossible-106609', + title: 'Wedding Impossible', + url: 'https://flixhq.to/tv/watch-wedding-impossible-106609', + image: 'https://img.flixhq.to/xxrz/250x400/379/d1/8c/d18c569318ce319a57ba681c69b01d73/d18c569318ce319a57ba681c69b01d73.jpg', + season: 'SS 1', + latestEpisode: 'EPS 1', + type: 'TV Series' + }, + { + id: 'tv/watch-a-killer-paradox-106036', + title: 'A Killer Paradox', + url: 'https://flixhq.to/tv/watch-a-killer-paradox-106036', + image: 'https://img.flixhq.to/xxrz/250x400/379/89/a0/89a0eede251cff2c9acf24fe64e2fe01/89a0eede251cff2c9acf24fe64e2fe01.jpg', + season: 'SS 1', + latestEpisode: 'EPS 8', + type: 'TV Series' + }, + {...} + ] +} +``` + +### fetchByGenre + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ---------------------------------------------------------------------- | +| genre | `string` | param to filter by genre. (*In this case, We're filtering by `drama`*) | +| page (optional) | `number` | page number (default: 1) | + +```ts +flixhq.fetchByGenre('drama').then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies/tv series. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L233-L241)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: true, + results: [ + { + id: 'movie/watch-no-new-friends-105202', + title: 'No New Friends', + url: 'https://flixhq.to/movie/watch-no-new-friends-105202', + image: 'https://img.flixhq.to/xxrz/250x400/379/16/30/16304d1c6302e6b078f6b74d5ff58347/16304d1c6302e6b078f6b74d5ff58347.jpg', + releaseDate: '2024', + seasons: undefined, + type: 'Movie' + }, + { + id: 'tv/watch-shogun-106618', + title: 'Shōgun', + url: 'https://flixhq.to/tv/watch-shogun-106618', + image: 'https://img.flixhq.to/xxrz/250x400/379/a7/fc/a7fca6a36c98856de5e71d120a16e521/a7fca6a36c98856de5e71d120a16e521.jpg', + releaseDate: undefined, + seasons: 1, + type: 'TV Series' + }, + {...} + ] +} +``` diff --git a/consumet.ts/docs/providers/gogoanime.md b/consumet.ts/docs/providers/gogoanime.md new file mode 100644 index 00000000..ee906076 --- /dev/null +++ b/consumet.ts/docs/providers/gogoanime.md @@ -0,0 +1,288 @@ +

Gogoanime

+ +```ts +const gogoanime = new ANIME.Gogoanime(); +``` + +

Methods

+ +- [search](#search) +- [fetchRecentEpisodes](#fetchrecentepisodes) +- [fetchTopAiring](#fetchtopairing) +- [fetchAnimeList](#fetchanimelist) +- [fetchAnimeInfo](#fetchanimeinfo) +- [fetchEpisodeSources](#fetchepisodesources) +- [fetchEpisodeServers](#fetchepisodeservers) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ---------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `One Piece`*) | +| page (optional) | `number` | page number (default: 1) | + +```ts +gogoanime.search("One Piece").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, // current page + hasNextPage: true, // if there is a next page + results: [ + { + id: 'one-piece', // anime id + title: 'One Piece', + url: 'https://gogoanime.gg//category/one-piece', // anime url + image: 'https://gogocdn.net/images/anime/One-piece.jpg', + releaseDate: 'Released: 1999', + subOrDub: 'sub' + }, + { + id: 'toriko-dub', + title: 'Toriko (Dub)', + url: 'https://gogoanime.gg//category/toriko-dub', + image: 'https://gogocdn.net/cover/toriko-dub.png', + releaseDate: 'Released: 2011', + subOrDub: 'dub' + }, + {...}, + ... + ] +} +``` + +### fetchRecentEpisodes + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| page (optional) | `number` | page number (default: 1) | +| type (optional) | `string` | type of anime (default: `1`). `1`: Japanese with subtitles, `2`: english/dub with no subtitles, `3`: chinese with english subtitles | + +```ts +gogoanime.fetchRecentEpisodes().then(data => { + console.log(data); +}) +``` + +output: +```js +{ + currentPage: 1, // current page + hasNextPage: true, // if there is a next page + results: [ + { + id: 'hellsing', + episodeId: 'hellsing-episode-13', + episodeNumber: 13, + title: 'Hellsing', + image: 'https://gogocdn.net/images/anime/H/hellsing.jpg', + url: 'https://gogoanime.gg//hellsing-episode-13' + }, + {...} + ... + ] +} +``` + +### fetchTopAiring + +return top airing anime list. + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ------------------------ | +| page (optional) | `number` | page number (default: 1) | + +```ts +gogoanime.fetchTopAiring().then(data => { + console.log(data); +}) +``` + +output: +```js +{ + currentPage: 1, + hasNextPage: true, + results: [ + { + id: 'ore-dake-level-up-na-ken', + title: 'Ore dake Level Up na Ken', + image: 'https://gogocdn.net/cover/ore-dake-level-up-na-ken-1708917521.png', + url: 'https://gogoanime3.co/category/ore-dake-level-up-na-ken', + genres: [ 'Action', 'Adventure', 'Fantasy' ], + episodeId: 'ore-dake-level-up-na-ken-episode-9', + episodeNumber: 9 + } + {...} + ... + ] +} +``` + +### fetchAnimeList + +return gogo anime list. + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ------------------------ | +| page (optional) | `number` | page number (default: 1) | + +```ts +gogoanime.fetchAnimeList().then(data => { + console.log(data); +}) +``` + +output: +```js +{ + currentPage: 1, + hasNextPage: true, + results: [ + { + id: 'hackgu-returner', + title: '.Hack//G.U. Returner', + image: 'https://gogocdn.net/images/anime/5745.jpg', + url: 'https://gogoanime3.co/category/hackgu-returner', + genres: [ 'Adventure', 'Drama', 'Game', 'Harem', 'Martial Arts', 'Seinen' ], + releaseDate: 'Released: 2007' + } + {...} + ... + ] +} +``` + +### fetchAnimeInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------------------------------------------------- | +| animeUrl | `string` | takes anime url or id as a parameter. (*anime id or url can be found in the anime search results*) | + +```ts +gogoanime.fetchAnimeInfo("one-piece").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ +output: +```js +{ + id: 'one-piece', + title: 'One Piece', + url: 'https://gogoanime.gg/category/one-piece', + genres: [ + 'Action', + 'Adventure', + '...' + ], + totalEpisodes: 1022, + image: 'https://gogocdn.net/images/anime/One-piece.jpg', + releaseDate: '1999', + description: 'One Piece is a story about Monkey D. Luffy, who wants to become a sea-robber. In a world mystical...', + subOrDub: 'sub', + type: 'TV Series', + status: 'Ongoing', + otherName: '', + episodes: [ + { + id: 'one-piece-episode-1022', + number: 1022, + url: 'https://gogoanime.gg//one-piece-episode-1022' + }, + { + id: 'one-piece-episode-1021', + number: 1021, + url: 'https://gogoanime.gg//one-piece-episode-1021' + }, + {...}, + ... + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| ----------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the anime info object*) | +| server (optional) | [`StreamingServers`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L76-L82) | takes server enum as a parameter. *default: [`StreamingServers.GogoCDN`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L76-L82)* | + + +```ts +gogoanime.fetchEpisodeSources("one-piece-episode-1022").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of episode sources. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ +output: +```js +{ + headers: { + Referer: 'https://goload.pro/streaming.php?id=MTg4MTgx&title=One+Piece+Episode+1022&typesub=SUB' + }, + sources: [ + { + url: 'https://manifest.prod.boltdns.net/manifest/v1/hls/v4/clear/6310593120001/6b17f612-a8e1-4fac-82ca-384537746607/6s/master.m3u8?fastly_token=NjJiNTU3Y2ZfZjdkZTc0MDYxODAwYTJkNTEzMGNiOTZhYjllNTA4MGVhNGFmZDNkMzNmZTQ2ZDdhNjc2MWI0NDU1YmRjYjcwZA%3D%3D', + isM3U8: true + }, + { + url: 'https://www07.gogocdn.stream/hls/0b594d900f47daabc194844092384914/ep.1022.1655606306.m3u8', + isM3U8: true + } + ] +} +``` + +### fetchEpisodeServers + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------------------------- | +| episodeId | `string` | take an episode id or url as a parameter. (*episode id or episode url can be found in the anime info object*) | + +```ts +gogoanime.fetchEpisodeServers("one-piece-episode-1022").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of episode servers. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L54-L57)*)\ +output: +```js +[ + { + name: 'Vidstreaming', + url: 'https://goload.pro/streaming.php?id=MTg4MTgx&title=One+Piece+Episode+1022&typesub=SUB' + }, + { + name: 'Gogo server', + url: 'https://goload.pro/embedplus?id=MTg4MTgx&token=Ii6QxAl2Y3IHtOerPM6n7Q&expires=1656041793' + }, + { name: 'Streamsb', url: 'https://ssbstream.net/e/a7xk4se5f1w9' }, + {...}, + ... +] +``` + +

(back to anime providers list)

diff --git a/consumet.ts/docs/providers/goku.md b/consumet.ts/docs/providers/goku.md new file mode 100644 index 00000000..a5d688ed --- /dev/null +++ b/consumet.ts/docs/providers/goku.md @@ -0,0 +1,371 @@ +

Goku

+ +```ts +const goku = new MOVIES.Goku(); +``` + +

Methods

+ +- [search](#search) +- [fetchMediaInfo](#fetchmediainfo) +- [fetchEpisodeSources](#fetchepisodesources) +- [fetchEpisodeServers](#fetchepisodeservers) +- [fetchRecentMovies](#fetchrecentmovies) +- [fetchRecentTvShows](#fetchrecenttvshows) +- [fetchTrendingMovies](#fetchtrendingmovies) +- [fetchTrendingTvShows](#fetchtrendingtvshows) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| query | `string` | query to search for. (*In this case, We're searching for `Batman`*) | +| page (optional) | `number` | page number (default: 1) | + +```ts +goku.search("Batman").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies/tv series. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L233-L241)*)\ +output: +```js +{ + currentPage: 1, // current page + hasNextPage: true, // if there is a next page + results: [ + { + id: 'watch-movie/watch-batman-13647', + title: 'Batman', + url: 'https://goku.sx/watch-movie/watch-batman-13647', + image: 'https://img.goku.sx/xxrz/250x400/576/7d/df/7ddf28de1b0053327ad6ff1c974894e8/7ddf28de1b0053327ad6ff1c974894e8.jpg', + releaseDate: '1966', + rating: 6.5, + type: 'TV Series' + } + {...}, + ... + ] +} +``` + +### fetchMediaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- | +| mediaId | `string` | takes media id or url as a parameter. (*media id or url can be found in the media search results as shown on the above method*) | + +```ts +goku.fetchMediaInfo("watch-movie/watch-batman-begins-19636").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L243-L254)*)\ +output: +```js +{ + id: 'watch-movie/watch-batman-begins-19636', + title: 'Batman Begins', + url: 'https://goku.sx/watch-movie/watch-batman-begins-19636', + image: 'https://img.goku.sx/xxrz/250x400/576/15/33/1533eaa2c80dbc5ea003c7cc4f6669ff/1533eaa2c80dbc5ea003c7cc4f6669ff.jpg', + description: 'Billionaire Bruce Wayne is driven by tragedy to expose and defeat the corruption that haunts his hometown of Gotham City. Because he is unable to work within the system, he establishes a new identity as The Batman, a symbol of fear for the criminal underworld. ', + type: 'Movie', + genres: [ 'Action', 'Crime', 'Drama' ], + casts: [ + 'Tom Wilkinson', + 'Vincent Wong', + 'Morgan Freeman', + 'Katie Holmes', + 'Ken Watanabe' + ], + production: 'DC Comics,Legendary Entertainment,DC Entertainment,Syncopy,Patalex III Productions Limited,Warner Bros. Pictures', + duration: '140 m', + episodes: [ + { + id: '1064170', + title: 'Batman Begins', + url: 'https://goku.sx/watch-movie/watch-batman-begins-19636/1064170' + } + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| ----------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the media info object*) | +| mediaId | `string` | takes media id as a parameter. (*media id can be found in the media info object*) | +| server (optional) | [`StreamingServers`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L144-L157) | takes server enum as a parameter. *default: [`StreamingServers.VidCloud`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L150)* | + + +```ts +goku.fetchEpisodeSources('1064170', 'watch-movie/watch-batman-begins-19636').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of episode sources and subtitles. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L300-L306)*)\ +output: +```js +{ + headers: { Referer: 'https://dokicloud.one/embed-4/K6ki8JPP1SkJ?autoPlay=0' }, + sources: [ + { + url: 'https://eno.dokicloud.one/_v10/2af246bae4e0d217720d27dba17e4a0f4e8f550c693738169f21cf8cc72f2198e352e26e99193ebd3dce37463b7c7fcb891ee7fda32f63c11374de4c1b5bf6c7dbfc2851e0463aafed7be7e5e481ececc3b638fc02a47e512968fd9a297ae7344582c2dde5e2b37efab41bbcafbc11b60de37d5e6fc50ce45f70ce940bf10f16bb7fcfb0a02f3bdbdd69999e636a61f4/1080/index.m3u8', + quality: '1080', + isM3U8: true + }, + { + url: 'https://eno.dokicloud.one/_v10/2af246bae4e0d217720d27dba17e4a0f4e8f550c693738169f21cf8cc72f2198e352e26e99193ebd3dce37463b7c7fcb891ee7fda32f63c11374de4c1b5bf6c7dbfc2851e0463aafed7be7e5e481ececc3b638fc02a47e512968fd9a297ae7344582c2dde5e2b37efab41bbcafbc11b60de37d5e6fc50ce45f70ce940bf10f16bb7fcfb0a02f3bdbdd69999e636a61f4/720/index.m3u8', + quality: '720', + isM3U8: true + }, + { + url: 'https://eno.dokicloud.one/_v10/2af246bae4e0d217720d27dba17e4a0f4e8f550c693738169f21cf8cc72f2198e352e26e99193ebd3dce37463b7c7fcb891ee7fda32f63c11374de4c1b5bf6c7dbfc2851e0463aafed7be7e5e481ececc3b638fc02a47e512968fd9a297ae7344582c2dde5e2b37efab41bbcafbc11b60de37d5e6fc50ce45f70ce940bf10f16bb7fcfb0a02f3bdbdd69999e636a61f4/360/index.m3u8', + quality: '360', + isM3U8: true + }, + { + url: 'https://eno.dokicloud.one/_v10/2af246bae4e0d217720d27dba17e4a0f4e8f550c693738169f21cf8cc72f2198e352e26e99193ebd3dce37463b7c7fcb891ee7fda32f63c11374de4c1b5bf6c7dbfc2851e0463aafed7be7e5e481ececc3b638fc02a47e512968fd9a297ae7344582c2dde5e2b37efab41bbcafbc11b60de37d5e6fc50ce45f70ce940bf10f16bb7fcfb0a02f3bdbdd69999e636a61f4/playlist.m3u8', + isM3U8: true, + quality: 'auto' + } + ], + subtitles: [ + { + url: 'https://cc.2cdns.com/19/09/19094c8682ed23d7d4ebf16ed2272164/19094c8682ed23d7d4ebf16ed2272164.vtt', + lang: 'English' + }, + { + url: 'https://cc.2cdns.com/12/1b/121bb7314feb9661f55597aa441d7551/121bb7314feb9661f55597aa441d7551.vtt', + lang: 'Spanish' + } + ] +} +``` + +### fetchEpisodeServers + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------------------------- | +| episodeId | `string` | take an episode id or url as a parameter. (*episode id or episode url can be found in the media info object*) | +| mediaId | `string` | takes media id as a parameter. (*media id can be found in the media info object*) | + +```ts +goku.fetchEpisodeServers('1064170', 'watch-movie/watch-batman-begins-19636').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of episode servers. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L118)*)\ +output: +```js +[ + { + name: 'UpCloud', + url: 'https://dokicloud.one/embed-4/K6ki8JPP1SkJ?autoPlay=0' + }, + { + name: 'Vidcloud', + url: 'https://rabbitstream.net/embed-4/eq17GDB0o3mj?autoPlay=0' + }, + { + name: 'Upstream', + url: 'https://upstream.to/embed-ncw8o5bt6ie5.html' + }, + { + name: 'MixDrop', + url: 'https://mixdrop.co/e/kn9l3gelc3d4med' + } + {...}, + ... +] +``` + +

(back to movie providers list)

+ +### fetchRecentMovies + +```ts +goku.fetchRecentMovies().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ + { + id: 'watch-movie/watch-the-wedding-contract-97651', + title: 'The Wedding Contract', + url: 'https://goku.sx/watch-movie/watch-the-wedding-contract-97651', + image: 'https://img.goku.sx/xxrz/250x400/576/33/10/33107bd51d8b311170c90b6f300fa362/33107bd51d8b311170c90b6f300fa362.jpg', + releaseDate: '2023', + duration: '84min', + type: 'Movie' + }, + { + id: 'watch-movie/watch-the-nudels-of-nudeland-97648', + title: 'The Nudels of Nudeland', + url: 'https://goku.sx/watch-movie/watch-the-nudels-of-nudeland-97648', + image: 'https://img.goku.sx/xxrz/250x400/576/f2/89/f289a6f11fac3f1633bac1d6c172d54d/f289a6f11fac3f1633bac1d6c172d54d.jpg', + releaseDate: '2022', + duration: '95min', + type: 'Movie' + }, + { + id: 'watch-movie/watch-the-machine-97645', + title: 'The Machine', + url: 'https://goku.sx/watch-movie/watch-the-machine-97645', + image: 'https://img.goku.sx/xxrz/250x400/576/eb/8d/eb8ddc18d6b098be9f04203c2d3d0a6b/eb8ddc18d6b098be9f04203c2d3d0a6b.jpg', + releaseDate: '2023', + duration: '112min', + type: 'Movie' + }, + {...}, +] +``` + + +### fetchRecentTvShows + +```ts +goku.fetchRecentTvShows().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of tv shows. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ + { + id: 'watch-series/watch-deadloch-97072', + title: 'Deadloch', + url: 'https://goku.sx/watch-series/watch-deadloch-97072', + image: 'https://img.goku.sx/xxrz/250x400/576/9f/85/9f8594271eb6540e32b7fbda24747c6e/9f8594271eb6540e32b7fbda24747c6e.jpg', + season: '1', + latestEpisode: '6', + type: 'TV Series' + }, + { + id: 'watch-series/watch-clone-high-96937', + title: 'Clone High', + url: 'https://goku.sx/watch-series/watch-clone-high-96937', + image: 'https://img.goku.sx/xxrz/250x400/576/ad/c5/adc55790c8c88d5538210f7558fec960/adc55790c8c88d5538210f7558fec960.jpg', + season: '1', + latestEpisode: '10', + type: 'TV Series' + }, + { + id: 'watch-series/watch-and-just-like-that-75286', + title: 'And Just Like That…', + url: 'https://goku.sx/watch-series/watch-and-just-like-that-75286', + image: 'https://img.goku.sx/xxrz/250x400/576/b8/e2/b8e20a6264e28cf1133413f63425297d/b8e20a6264e28cf1133413f63425297d.jpg', + season: '2', + latestEpisode: '2', + type: 'TV Series' + }, + {...}, +] +``` + + +### fetchTrendingMovies + +```ts +goku.fetchTrendingMovies().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ + { + id: 'watch-movie/watch-extraction-2-97549', + title: 'Extraction 2', + url: 'https://goku.sx/watch-movie/watch-extraction-2-97549', + image: 'https://img.goku.sx/xxrz/250x400/576/9c/d5/9cd56c00c2b79598f7fba8ba33b2128d/9cd56c00c2b79598f7fba8ba33b2128d.jpg', + releaseDate: '2023', + duration: '123min', + type: 'Movie' + }, + { + id: 'watch-movie/watch-the-flash-97519', + title: 'The Flash', + url: 'https://goku.sx/watch-movie/watch-the-flash-97519', + image: 'https://img.goku.sx/xxrz/250x400/576/f7/97/f7975e92348f8055ee359ea5218d1aa5/f7975e92348f8055ee359ea5218d1aa5.jpg', + releaseDate: '2023', + duration: '144min', + type: 'Movie' + }, + { + id: 'watch-movie/watch-fast-and-furious-10-8846', + title: 'Fast X', + url: 'https://goku.sx/watch-movie/watch-fast-and-furious-10-8846', + image: 'https://img.goku.sx/xxrz/250x400/576/a9/9b/a99ba7cd6b251e75c6723da994bc02b4/a99ba7cd6b251e75c6723da994bc02b4.jpg', + releaseDate: '2023', + duration: '142min', + type: 'Movie' + }, + {...}, +] +``` + + +### fetchTrendingTvShows + +```ts +goku.fetchTrendingTvShows().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of tv shows. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ + { + id: 'watch-series/watch-secret-invasion-88246', + title: 'Secret Invasion', + url: 'https://goku.sx/watch-series/watch-secret-invasion-88246', + image: 'https://img.goku.sx/xxrz/250x400/576/84/21/84218c778e006f43777e1f8fe18a2560/84218c778e006f43777e1f8fe18a2560.jpg', + season: '1', + latestEpisode: '1', + type: 'TV Series' + }, + { + id: 'watch-series/watch-black-mirror-39396', + title: 'Black Mirror', + url: 'https://goku.sx/watch-series/watch-black-mirror-39396', + image: 'https://img.goku.sx/xxrz/250x400/576/d6/9d/d69d87285ef143fab74322227616bb04/d69d87285ef143fab74322227616bb04.jpg', + season: '6', + latestEpisode: '5', + type: 'TV Series' + }, + { + id: 'watch-series/watch-demon-slayer-kimetsu-no-yaiba-42177', + title: 'Demon Slayer: Kimetsu no Yaiba', + url: 'https://goku.sx/watch-series/watch-demon-slayer-kimetsu-no-yaiba-42177', + image: 'https://img.goku.sx/xxrz/250x400/576/d7/38/d7380c0e22b5493e8f2257c539d8a6fa/d7380c0e22b5493e8f2257c539d8a6fa.jpg', + season: '3', + latestEpisode: '11', + type: 'TV Series' + }, + {...}, +] +``` diff --git a/consumet.ts/docs/providers/libgen.md b/consumet.ts/docs/providers/libgen.md new file mode 100644 index 00000000..5199b523 --- /dev/null +++ b/consumet.ts/docs/providers/libgen.md @@ -0,0 +1,43 @@ +# Libgen + +## Methods + +```ts +/** + * Scrapes a libgen search page by book query + * + * @param {string} query - the name of the book + * @param {number} [page=1] - maximum number of results + * @returns {Promise} +*/ +Libgen.search("One Houndred Years of Solitude", 30); + +/** + * scrapes a ligen book page by book page url + * + * @param {string} bookUrl - ligen book page url + * @returns {Promise} +*/ +Libgen.scrapeBook( + "http://libgen.rs/book/index.php?md5=262BFA73B8090B6AA3DBD2FBCDC4B91D" +); +``` + +## Variables + +```ts +/** + * @type {string} +*/ +Libgen.name; // the name of the provider. +/** + * @type {boolean} +*/ +Libgen.isNSFW; // if NSFW +/** + * @type {boolean} +*/ +Libgen.isWorking; // if provider is working +``` + +

(back to book providers list)

\ No newline at end of file diff --git a/consumet.ts/docs/providers/mangadex.md b/consumet.ts/docs/providers/mangadex.md new file mode 100644 index 00000000..0ef211f3 --- /dev/null +++ b/consumet.ts/docs/providers/mangadex.md @@ -0,0 +1,303 @@ +

Mangadex

+ +```ts +const mangadex = new MANGA.MangaDex(); +``` + +

Methods

+ +- [search](#search) +- [fetchMangaInfo](#fetchmangainfo) +- [fetchChapterPages](#fetchchapterpages) +- [fetchPopular](#fetchpopular) +- [fetchRecentlyAdded](#fetchRecentlyAdded) +- [fetchLatestUpdates](#fetchLatestUpdates) +- [fetchRandom](#fetchrandom) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. +> +

Parameters

+ +| Parameter | Type | Description | +| ---------------- | -------- | ---------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `Tomodachi Gamee`*) | +| page (optional) | `number` | page number (default: 1) | +| limit (optional) | `number` | limit of results (default: 20) | + +```ts +mangadex.search("Tomodachi Game").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + currentPage: 1, + results: [ + { + id: 'b35f67b6-bfb9-4cbd-86f0-621f37e6cb41', // manga id + title: 'Tomodachi Game', + altTitles: [ + { en: 'Friends Games' }, + { ja: 'トモダチゲーム' }, + {...}, + ... + ], + description: "Katagiri Yuichi believes that friends are more important than money, but he also knows the hardships of not having enough funds. He works hard to save up in ...", + status: 'ongoing', + releaseDate: 2013, + contentRating: 'suggestive', + lastVolume: null, + lastChapter: null + }, + {...} + ... + ] +} +``` + +### fetchMangaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------------- | +| mangaId | `string` | manga id.(*manga id can be found in the manga search results*) | + +```ts +managdex.fetchMangaInfo("b35f67b6-bfb9-4cbd-86f0-621f37e6cb41").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an manga info object (including the chapters). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L120)*)\ +output: +```js +{ + id: 'b35f67b6-bfb9-4cbd-86f0-621f37e6cb41', + title: 'Tomodachi Game', + altTitles: [ + { en: 'Friends Games' }, + { ja: 'トモダチゲーム' }, + {...}, + ... + ], + description: { + en: "Katagiri Yuichi believes that friends are more important than money, but he also knows the hardships ...', + pl: 'Dziękujemy za wpłatę dwudziestu milionów jenów! W ten sposób dołączyliście do jedynej w swoim rodzaju gry przyjaciół! Witajcie...', + ... + }, + genres: [ 'Psychological', 'Drama', '...' ], + themes: [ 'Survival' ], + status: 'Ongoing', + releaseDate: 2013, + chapters: [ + { + id: 'a79255c8-21b5-4a8c-a586-48469fa87020', + title: 'Accomplice', + pages: 35 + }, + { + id: '7633dee8-cd6d-4b6d-9335-1aec7646833e', + title: "The Game's Origins", + pages: 37 + }, + {...} + ... + ] +} +``` + +### fetchChapterPages + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------- | +| chapterId | `string` | chapter id.(*chapter id can be found in the manga info*) | + +```ts +mangadex.fetchChapterPages("a79255c8-21b5-4a8c-a586-48469fa87020").then(data => { + console.log(data); +}) +``` +returns an array of pages. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L122-L126)*)\ +output: +```js +[ + { + img: 'https://uploads.mangadex.org/data/67823e99a5e1b53bb44761c5bdcc7f33/1-6d943848bde48cdc712585fa45d97bbbe5a0432c8ecdfa4e673d53ea6fb8fb28.png', + page: 1 + }, + { + img: 'https://uploads.mangadex.org/data/67823e99a5e1b53bb44761c5bdcc7f33/2-060d75ddda24ef3d0848b5517572c8dc3ff0a5fe44f90798f7c71a4f7ce23fd9.png', + page: 2 + }, + {...} + ... +] +``` + +### fetchPopular + +

Parameters

+ +| Parameter | Type | Description | +| ---------------- | -------- | ---------------------------------------------------------------------------- | +| page (optional) | `number` | page number (default: 1) | +| limit (optional) | `number` | limit of results (default: 20) | + +```ts +mangadex.fetchPopular().then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + currentPage: 1, + results: [ + { + id: '32d76d19-8a05-4db0-9fc2-e0b0648fe9d0', + title: 'Solo Leveling', + altTitles: [ + { ko: '나 혼자만 레벨업' }, + { en: 'Only I Level up' }, + {...}, + ... + ], + description: 'Als sich vor zehn Jahren das „Gate“ öffnete und unsere Welt sich mit der von Monstern verband, erhielten einige normale Menschen die Macht...', + status: 'completed', + releaseDate: 2018, + contentRating: 'safe', + lastVolume: '3', + lastChapter: '200' + }, + {...} + ... + ] +} +``` + +### fetchRecentlyAdded + +

Parameters

+ +| Parameter | Type | Description | +| ---------------- | -------- | ---------------------------------------------------------------------------- | +| page (optional) | `number` | page number (default: 1) | +| limit (optional) | `number` | limit of results (default: 20) | + +```ts +mangadex.fetchRecentlyAdded().then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + currentPage: 1, + results: [ + { + id: '39480d0b-339d-4669-be8e-01ca0041fca4', + title: 'Mamono no kuchibiru', + altTitles: [ { en: "Devil's Lips" }, { ja: '魔物のくちびる' } ], + description: 'At first, it was a small "favour".Kibakura had instantly fallen in love with, Aikyou, his junior one grade below him.They had first met at last year...', + status: 'completed', + releaseDate: 2021, + contentRating: 'erotica', + lastVolume: '', + lastChapter: '1' +}, + {...} + ... + ] +} +``` + +### fetchLatestUpdates + +

Parameters

+ +| Parameter | Type | Description | +| ---------------- | -------- | ---------------------------------------------------------------------------- | +| page (optional) | `number` | page number (default: 1) | +| limit (optional) | `number` | limit of results (default: 20) | + +```ts +mangadex.fetchLatestUpdates().then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + currentPage: 1, + results: [ + { + id: 'e93849b2-939f-40f3-91b4-79b96133052e', + title: 'Medusa Dorei o Katta', + altTitles: [ + { ja: 'メドゥーサ奴隷を買った' }, + { en: 'I Bought a Medusa Slave' }, + {...}, + ... + ], + description: undefined, + status: 'ongoing', + releaseDate: 2023, + contentRating: 'suggestive', + lastVolume: '', + lastChapter: '' + }, + {...} + ... + ] +} +``` + +### fetchRandom + +

Parameters

+ +| Parameter | Type | Description | +| ---------------- | -------- | ---------------------------------------------------------------------------- | + + +```ts +mangadex.fetchRandom().then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + results: [ + { + id: '151bca3e-db98-4ad2-8d8d-239943b91437', + title: 'You Shou Yan', + altTitles: [ + { zh: '有兽焉' }, + { en: 'Fabulous Beasts' }, + {...}, + ... + ], + description: 'In this realm mythological creatures roam, descendant of the nine heavens they call home. Sibuxiang, a mythical deer-man couch potato, is kicked out of heaven and assigned to...', + status: 'ongoing', + releaseDate: 2017, + contentRating: 'safe', + lastVolume: null, + lastChapter: null + }, + {...} + ... + ] +} +``` + +

(back to manga providers list)

diff --git a/consumet.ts/docs/providers/mangahere.md b/consumet.ts/docs/providers/mangahere.md new file mode 100644 index 00000000..7c2ee835 --- /dev/null +++ b/consumet.ts/docs/providers/mangahere.md @@ -0,0 +1,135 @@ +

MangaHere

+ +```ts +const mangahere = new MANGA.MangaHere(); +``` + +

Methods

+ +- [search](#search) +- [fetchMangaInfo](#fetchmangainfo) +- [fetchChapterPages](#fetchchapterpages) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. +> +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `Tomodachi Gamee`*) | + +```ts +mangahere.search("Tomodachi Game").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: true, + results: [ + { + id: 'tomodachi_game', + title: 'Tomodachi Game', + image: 'http://fmcdn.mangahere.com/store/manga/15338/cover.jpg?token=18f21960258f216e0920191b8fe78c0b691e88b6&ttl=1658167200&v=1657454312', + description: 'Katagiri Yuichi believes that friends are more important than money, but he also knows the hardships of not ha...', + status: 'Ongoing' + }, + { + id: 'tomodachi', + title: 'Tomodachi', + image: 'http://fmcdn.mangahere.com/store/manga/1653/cover.jpg?token=ec848c72fcd6b3596f16d42c1ead656755ed47c6&ttl=1658167200&v=1272884354', + description: 'After being overseas for five years, 16-year-old Yamato comes back to Japan to find that her geeky best friend...', + status: 'Completed' + }, + {...} + ... + ] +} +``` + +### fetchMangaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------------- | +| mangaId | `string` | manga id.(*manga id can be found in the manga search results*) | + +```ts +mangahere.fetchMangaInfo("tomodachi_game").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an manga info object (including the chapters). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L120)*)\ +output: +```js +{ + id: 'tomodachi_game', + title: 'Tomodachi Game', + description: 'Katagiri Yuichi believes that friends are more important than money, but he also knows the hardships of not having enough funds. He works hard to save up in order to go on the high school trip, because he has promised his four ...', + headers: { Referer: 'http://www.mangahere.cc/' }, + image: 'http://fmcdn.mangahere.com/store/manga/15338/cover.jpg?token=18f21960258f216e0920191b8fe78c0b691e88b6&ttl=1658167200&v=1657454312', + genres: [ 'Mystery', 'Drama', 'Shounen', 'Psychological', 'Ecchi' ], + status: 'Ongoing', + rating: 4.84, + authors: [ 'YAMAGUCHI Mikoto' ], + chapters: [ + { + id: 'tomodachi_game/c102', + title: 'Ch.102', + releasedDate: 'Jul 10,2022 ' + }, + {...} + ... + ] +} +``` + +### fetchChapterPages + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------- | +| chapterId | `string` | chapter id.(*chapter id can be found in the manga info*) | + +```ts +mangahere.fetchChapterPages("tomodachi_game/c102").then(data => { + console.log(data); +}) +``` +returns an array of pages. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L122-L126)*)\ +output: +```js +[ + { + page: 0, + img: 'https://zjcdn.mangahere.org/store/manga/15338/102.0/compressed/h001.jp', + headers: { + Referer: 'http://www.mangahere.cc/manga/tomodachi_game/c102/1.html' + } + }, + { + page: 1, + img: 'https://zjcdn.mangahere.org/store/manga/15338/102.0/compressed/h002.jp', + headers: { + Referer: 'http://www.mangahere.cc/manga/tomodachi_game/c102/1.html' + } + }, + { + page: 2, + img: 'https://zjcdn.mangahere.org/store/manga/15338/102.0/compressed/h003.jp', + headers: { + Referer: 'http://www.mangahere.cc/manga/tomodachi_game/c102/1.html' + } + }, + {...} + ... +] +``` + +

(back to manga providers list)

diff --git a/consumet.ts/docs/providers/mangahost.md b/consumet.ts/docs/providers/mangahost.md new file mode 100644 index 00000000..9bdf14ba --- /dev/null +++ b/consumet.ts/docs/providers/mangahost.md @@ -0,0 +1,124 @@ +

MangaHost 🇧🇷

+ +```ts +const mangahost = new MANGA.MangaHost(); +``` + +

Methods

+ +- [search](#search) +- [fetchMangaInfo](#fetchmangainfo) +- [fetchChapterPages](#fetchchapterpages) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. +> +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `punpun`*) | + +```ts +mangahost.search("punpun").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + results: [ + { + id: 'oyasumi-punpun-mh34076', + title: 'Oyasumi Punpun', + image: 'https://img-host.filestatic3.xyz/mangas_files/oyasumi-punpun/image_oyasumi-punpun_xmedium.jpg', + headerForImage: { Referer: 'https://mangahosted.com' } + } + ] +} +``` + +### fetchMangaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------------- | +| mangaId | `string` | manga id.(*manga id can be found in the manga search results*) | + +```ts +mangahost.fetchMangaInfo("oyasumi-punpun-mh34076").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an manga info object (including the chapters). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L120)*)\ +output: +```js +{ + id: 'oyasumi-punpun-mh34076', + title: 'Oyasumi Punpun', + altTitles: 'おやすみプンプンCompleto', + description: 'Punpun é uma criança como todas as outras. Alegre e hiperativo, ele passa por muitos conflitos em sua vida, assim como qualquer outro ser humano. Essa é a história sobre a vida de Punpun, superando seus obstáculos e as adversidades que o mundo lhe traz.', + headerForImage: { Referer: 'https://mangahosted.com' }, + image: 'https://img-host.filestatic3.xyz/mangas_files/oyasumi-punpun/image_oyasumi-punpun_full.jpg', + genres: [ + 'adulto', + 'comedia', + 'drama', + 'escolar', + 'psicologico', + 'seinen', + 'slice of life' + ], + status: 'Completed', + views: 55498, + authors: [ 'Asano Inio' ], + chapters: [ + { + id: '147', + title: 'Capítulo #147 - Oyasumi Punpun', + views: null, + releasedDate: '' + }, + {...} + ] + } +``` +Note: The `headerForImage` property might be useful when getting the image to display. + +### fetchChapterPages + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------- | +| mangaId | `string` | manga id.(*chapter id is the same one from the fetchMangaInfo function*) | +| chapterId | `string` | chapter id.(*chapter id can be found in the manga info*) | + +```ts +mangahost.fetchChapterPages("oyasumi-punpun-mh34076/1").then(data => { + console.log(data); +}) +``` +returns an array of pages. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L122-L126)*)\ +output: +```js +[ + { + img: 'https://img-host.filestatic3.xyz/mangas_files/oyasumi-punpun/1/00.png', + page: 0, + title: 'Page 1', + headerForImage: { Referer: 'https://mangahosted.com' } + }, + { + img: 'https://img-host.filestatic3.xyz/mangas_files/oyasumi-punpun/1/01-02.jpg', + page: 1, + title: 'Page 2', + headerForImage: { Referer: 'https://mangahosted.com' } + }, + {...} +] +``` + +

(back to manga providers list)

diff --git a/consumet.ts/docs/providers/mangakakalot.md b/consumet.ts/docs/providers/mangakakalot.md new file mode 100644 index 00000000..02ab0f62 --- /dev/null +++ b/consumet.ts/docs/providers/mangakakalot.md @@ -0,0 +1,127 @@ +

MangaKakalot

+ +```ts +const mangakakalot = new MANGA.MangaKakalot(); +``` + +

Methods

+ +- [search](#search) +- [fetchMangaInfo](#fetchmangainfo) +- [fetchChapterPages](#fetchchapterpages) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. +> +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `Tomodachi Game`*) | + +```ts +mangakakalot.search("Tomodachi Game").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + results: [ + { + id: 'manga-kr954974', + title: 'Tomodachi Game', + image: 'https://avt.mkklcdnv6temp.com/24/h/3-1583468630.jpg', + headerForImage: { Referer: 'https://mangakakalot.com' } + }, + { + id: 'read-nf3ar158504885573', + title: 'Hanging Out With A Gamer Girl', + image: 'https://avt.mkklcdnv6temp.com/38/v/19-1583500595.jpg', + headerForImage: { Referer: 'https://mangakakalot.com' } + } + {...} + ... + ] +} +``` + +### fetchMangaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------------- | +| mangaId | `string` | manga id.(*manga id can be found in the manga search results*) | + +```ts +mangakakalot.fetchMangaInfo("manga-kr954974").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an manga info object (including the chapters). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L120)*)\ +output: +```js +{ + id: 'manga-kr954974', + title: 'Tomodachi Game', + altTitles: [ + 'トモダチゲーム (Japanese)', + ' 朋友游戏 (Chinese)', + '...' + ], + description: `Katagiri Yuichi believes that friends are more important than money, but he also knows the hardships of not having enough funds....`, + headerForImage: { Referer: 'https://readmanganato.com' }, + image: 'https://avt.mkklcdnv6temp.com/24/h/3-1583468630.jpg', + genres: [ 'Drama', 'Mystery', 'Psychological', 'Seinen' ], + status: 'Ongoing', + views: 20837606, + authors: [ 'Yamaguchi Mikoto' ], + chapters: [ + { + id: 'manga-kr954974/chapter-102$$READMANGANATO', + title: 'Chapter 102', + views: 36721, + releasedDate: 'Jul 10,2022 22:07' + }, + {...} + ] +} +``` +Note: The `headerForImage` property might be useful when getting the image to display. + +### fetchChapterPages + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------- | +| chapterId | `string` | chapter id.(*chapter id can be found in the manga info*) | + +```ts +mangakakalot.fetchChapterPages("manga-kr954974/chapter-102$$READMANGANATO").then(data => { + console.log(data); +}) +``` +returns an array of pages. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L122-L126)*)\ +output: +```js +[ + { + img: 'https://v17.mkklcdnv6tempv5.com/img/tab_17/00/36/17/kr954974/chapter_102/1-o.jpg', + page: 0, + title: 'Tomodachi Game Chapter 102 page 1', + headerForImage: { Referer: 'https://mangakakalot.com' } + }, + { + img: 'https://v17.mkklcdnv6tempv5.com/img/tab_17/00/36/17/kr954974/chapter_102/2-o.jpg', + page: 1, + title: 'Tomodachi Game Chapter 102 page 2', + headerForImage: { Referer: 'https://mangakakalot.com' } + }, + {...} +] +``` + +

(back to manga providers list)

diff --git a/consumet.ts/docs/providers/mangapark.md b/consumet.ts/docs/providers/mangapark.md new file mode 100644 index 00000000..a8477598 --- /dev/null +++ b/consumet.ts/docs/providers/mangapark.md @@ -0,0 +1,117 @@ +

Mangapark

+ +```ts +const mangapark = new MANGA.Mangapark(); +``` + +

Methods

+ +- [search](#search) +- [fetchMangaInfo](#fetchmangainfo) +- [fetchChapterPages](#fetchchapterpages) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class, meaning it is available across most categories. +> +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, we're searching for `Demon Slayer`*) | + +```ts +mangapark.search('Demon Slayer').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + results: [ + { + id: 'kimetsu-no-yaiba-gotouge-koyoharu', + title: 'Kimetsu no Yaiba', + image: 'https://xfs-208.mpcdn.net/thumb/W300/ampi/4aa/4aa22fd3ad34407a393f7b6913d2aa2b8f8ffb16_200_313_42953.jpg?acc=HWnoBrwaLc4Zr8oqnuye6A&exp=1667746330}' + }, + { + id: 'demon-slayer', + title: 'Demon Slayer', + image: 'https://xfs-202.mpcdn.net/thumb/W300/ampi/d53/d53c34517f4f01a432671daf6b40ddf286d1eb3f_420_560_93000.jpg?acc=-aM_ezD9ZjavQljf-5oKfA&exp=1667746330}' + }, + {...}, + ] +} +``` + +### fetchMangaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------------- | +| mangaId | `string` | manga id (*can be found in the manga search results*) | + +```ts +mangapark.fetchMangaInfo('kimetsu-no-yaiba-gotouge-koyoharu').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an manga info object (including the chapters). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L120)*)\ +output: +```js +{ + id: 'kimetsu-no-yaiba-gotouge-koyoharu', + title: 'Kimetsu no Yaiba Manga', + image: 'https://xfs-205.mpcdn.net/thumb/W600/ampi/4aa/4aa22fd3ad34407a393f7b6913d2aa2b8f8ffb16_200_313_42953.jpg?acc=rE6O-EEv2KdiP10eToF_JA&exp=1667748279', + description: 'Tanjiro is the eldest son in a family that has lost its father. Tanjiro visits another town one day to sell charcoal but ends up staying the night at someone else’s house instead of going home because of a rumor about a demon that stalks a nearby mountain at night. When he goes home the next day, tragedy is waiting for him.', + chapters: [ + { + id: 'kimetsu-no-yaiba-gotouge-koyoharu/i2458253', + title: 'ch.205: Lives That Make the Years Shine', + releaseDate: '2 years ago' + }, + {...}, + ] +} +``` +Note: The `headerForImage` property might be useful when getting the image to display. + +### fetchChapterPages + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------- | +| chapterId | `string` | chapter id (*can be found in the manga info*) | + +```ts +mangapark.fetchChapterPages('kimetsu-no-yaiba-gotouge-koyoharu/i2325814').then(data => { + console.log(data); +}) +``` +returns an array of pages. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L122-L126)*)\ +output: +```js +[ + { + page: 1, + img: 'https://xfs-227.mpcdn.net/comic/00005/images/bd/f1/bdf140d00acd17ce7f9a45f9b4ac148e332495b6_225748_800_1168.jpg?acc=pnJI5cyhhLQiJe85kXeDrg&exp=1667748434' + }, + { + page: 2, + img: 'https://xfs-223.mpcdn.net/comic/00005/images/91/17/911786e51e670d10422d65e1d82d5344fb0a314a_170091_800_1168.jpg?acc=_vqLK38I_5bYy7fnNewm9A&exp=1667748434' + }, + { + page: 3, + img: 'https://xfs-211.mpcdn.net/comic/00005/images/00/1d/001d537355ed17050395285a2b503f88ef481781_182747_1200_876.jpg?acc=iMYaYUDBkqfYRODG4y2QKg&exp=1667748434' + }, + { + page: 4, + img: 'https://xfs-202.mpcdn.net/comic/00005/images/c8/d3/c8d3610e09dd47552601187395c93f3e8f200137_102838_800_800.jpg?acc=svY_E6ZWyiBoiuhP7-fSHA&exp=1667748434' + }, + {...}, +] +``` + +

(Back to Providers List)

diff --git a/consumet.ts/docs/providers/mangapill.md b/consumet.ts/docs/providers/mangapill.md new file mode 100644 index 00000000..82dc040a --- /dev/null +++ b/consumet.ts/docs/providers/mangapill.md @@ -0,0 +1,126 @@ +

MangaPill

+ +```ts +const mangaPill = new MANGA.MangaPill(); +``` + +

Methods

+ +- [search](#search) +- [fetchMangaInfo](#fetchmangainfo) +- [fetchChapterPages](#fetchchapterpages) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class, meaning it is available across most categories. +> +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, we're searching for `one piece`*) | + +```ts +mangaPill.search('one piece').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + results: [ + { + id: '2/one-piece', + title: 'One Piece', + image: 'https://cdn.readdetectiveconan.com/file/mangapill/i/2.jpeg' + }, + { + id: '3258/one-piece-digital-colored-comics', + title: 'One Piece - Digital Colored Comics', + image: 'https://cdn.readdetectiveconan.com/file/mangapill/i/3258.jpeg' + }, + {...}, + ] +} +``` + +### fetchMangaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------------- | +| mangaId | `string` | manga id (*can be found in the manga search results*) | + +```ts +mangaPill.fetchMangaInfo('2/one-piece').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an manga info object (including the chapters). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L120)*)\ +output: +```js +{ + id: '3258/one-piece-digital-colored-comics', + title: 'One Piece - Digital Colored Comics', + description: 'As a child, Monkey D. Luffy dreamed of becoming the King of the Pirates. But his life changed when he accidentally gained the power to stretch like rubber...at the cost of never being able to swim again! Now Luffy, with the help of a motley collection of nakama, is setting off in search of "One Piece," said to be the greatest treasure in the world...', + releaseDate: '1997', + genres: [ + 'Action', + 'Adventure', + 'Comedy', + 'Drama', + 'Fantasy', + 'Shounen', + 'Supernatural' + ], + chapters: [ + { + id: '3258-11004000/one-piece-digital-colored-comics-chapter-1004', + title: 'Chapter 1004', + chapter: '1004' + }, + {...}, + ] +} +``` +Note: The `headerForImage` property might be useful when getting the image to display. + +### fetchChapterPages + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------- | +| chapterId | `string` | chapter id (*can be found in the manga info*) | + +```ts +mangaPill.fetchChapterPages('3258-11004000/one-piece-digital-colored-comics-chapter-1004').then(data => { + console.log(data); +}) +``` +returns an array of pages. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L122-L126)*)\ +output: +```js +[ + { + img: 'https://cdn.readdetectiveconan.com/file/mangap/3258/11004000/1.png', + page: 1 + }, + { + img: 'https://cdn.readdetectiveconan.com/file/mangap/3258/11004000/2.png', + page: 2 + }, + { + img: 'https://cdn.readdetectiveconan.com/file/mangap/3258/11004000/3.png', + page: 3 + }, + { + img: 'https://cdn.readdetectiveconan.com/file/mangap/3258/11004000/4.png', + page: 4 + }, + {...}, +] +``` + +

(Back to Providers List)

diff --git a/consumet.ts/docs/providers/mangareader.md b/consumet.ts/docs/providers/mangareader.md new file mode 100644 index 00000000..c8366f17 --- /dev/null +++ b/consumet.ts/docs/providers/mangareader.md @@ -0,0 +1,127 @@ +

MangaReader

+ +```ts + const mangaReader = new MANGA.MangaReader(); +``` + +

Methods

+ +- [search](#search) +- [fetchMangaInfo](#fetchmangainfo) +- [fetchChapterPages](#fetchchapterpages) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class, meaning it is available across most categories. +> +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, we're searching for `one piece`*) | + +```ts +mangaPill.search('one piece').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + results: [ + { + id: 'one-piece-colored-edition-55493', + title: 'One Piece (Colored Edition)', + image: 'https://img.mreadercdn.com/_m/300x400/100/58/59/5859e56db55fb29a12696a926419e815/5859e56db55fb29a12696a926419e815.jpg', + genres: [ 'Action', 'Adventure', 'Comedy' ] + }, + { + id: 'one-piece-3', + title: 'One Piece', + image: 'https://img.mreadercdn.com/_m/300x400/100/62/16/6216bad614899d8dc66cf8b2cb8047d9/6216bad614899d8dc66cf8b2cb8047d9.jpg', + genres: [ 'Action', 'Adventure', 'Comedy' ] + } + {...}, + ] +} +``` + +### fetchMangaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------------- | +| mangaId | `string` | manga id (*can be found in the manga search results*) | + +```ts +mangaPill.fetchMangaInfo('one-piece-colored-edition-55493').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an manga info object (including the chapters). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L120)*)\ +output: +```js +{ + id: 'one-piece-colored-edition-55493', + title: 'One Piece (Colored Edition)', + image: 'https://img.mreadercdn.com/_m/300x400/100/58/59/5859e56db55fb29a12696a926419e815/5859e56db55fb29a12696a926419e815.jpg', + description: 'Gol D. Roger, a man referred to as the "Pirate King," is set to be executed by the World Government. But just before his demise, he confirms the existence of a great treasure, One Piece, located somewhere within the vast ocean known as the Grand Line. Announcing that One Piece can be claimed by anyone worthy enough to reach it, the Pirate King is executed and the Great Age of Pirates begins. Twenty-two years later, a young man by the name of Monkey D. Luffy is ready to embark on his own adventure, searching for One Piece and striving to become the new Pirate King. Armed with just a straw hat, a small boat, and an elastic body, he sets out on a fantastic journey to gather his own crew and a worthy ship that will take them across the Grand Line to claim the greatest status on the high seas. [Written by MAL Rewrite]', + genres: [ + 'Action', + 'Adventure', + 'Comedy', + 'Fantasy', + 'Shounen', + 'Super Power' + ], + chapters: [ + { + id: 'one-piece-colored-edition-55493/en/chapter-1004', + title: 'Chapter 1004: MILLET DUMPLINGS', + chapter: '1004' + }, + {...}, + ] +} +``` +Note: The `headerForImage` property might be useful when getting the image to display. + +### fetchChapterPages + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------- | +| chapterId | `string` | chapter id (*can be found in the manga info*) | + +```ts +mangaPill.fetchChapterPages('one-piece-colored-edition-55493/en/chapter-1004').then(data => { + console.log(data); +}) +``` +returns an array of pages. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L122-L126)*)\ +output: +```js +[ + { + img: 'https://c-1.mreadercdn.com/_v2/1/0dcb8f9eaacfd940603bd75c7c152919c72e45517dcfb1087df215e3be94206cfdf45f64815888ea0749af4c0ae5636fabea0abab8c2e938ab3ad7367e9bfa52/e0/18/e018cc272ab186f6107b577862f3b8a2/e018cc272ab186f6107b577862f3b8a2.jpg?t=515363393022bbd440b0b7d9918f291a&ttl=1908547557', + page: 1 + }, + { + img: 'https://c-1.mreadercdn.com/_v2/0/0dcb8f9eaacfd940603bd75c7c152919c72e45517dcfb1087df215e3be94206cfdf45f64815888ea0749af4c0ae5636fabea0abab8c2e938ab3ad7367e9bfa52/61/2f/612f6ffee2881ecab50edc61b517efe7/612f6ffee2881ecab50edc61b517efe7.jpg?t=515363393022bbd440b0b7d9918f291a&ttl=1908547557', + page: 2 + }, + { + img: 'https://c-1.mreadercdn.com/_v2/0/0dcb8f9eaacfd940603bd75c7c152919c72e45517dcfb1087df215e3be94206cfdf45f64815888ea0749af4c0ae5636fabea0abab8c2e938ab3ad7367e9bfa52/22/bd/22bd0c8a5050b3145ad9116cb0b2aca9/22bd0c8a5050b3145ad9116cb0b2aca9.jpg?t=515363393022bbd440b0b7d9918f291a&ttl=1908547557', + page: 3 + }, + { + img: 'https://c-1.mreadercdn.com/_v2/1/0dcb8f9eaacfd940603bd75c7c152919c72e45517dcfb1087df215e3be94206cfdf45f64815888ea0749af4c0ae5636fabea0abab8c2e938ab3ad7367e9bfa52/c9/00/c900ff8a5ee537e019bf0caedf74a627/c900ff8a5ee537e019bf0caedf74a627.jpg?t=515363393022bbd440b0b7d9918f291a&ttl=1908547557', + page: 4 + }, + {...}, +] +``` + +

(Back to Providers List)

diff --git a/consumet.ts/docs/providers/mangasee123.md b/consumet.ts/docs/providers/mangasee123.md new file mode 100644 index 00000000..24e8380c --- /dev/null +++ b/consumet.ts/docs/providers/mangasee123.md @@ -0,0 +1,124 @@ +

Mangasee123

+ +```ts +const mangasee123 = new MANGA.Mangasee123(); +``` + +

Methods

+ +- [search](#search) +- [fetchMangaInfo](#fetchmangainfo) +- [fetchChapterPages](#fetchchapterpages) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class, meaning it is available across most categories. +> +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, we're searching for `Call of the Night`*) | + +```ts +mangasee123.search('Call of the Night').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ +output: +```js +{ + results: [ + { + id: 'Yofukashi-no-Uta', + title: 'Call of the Night', + altTitles: ['Yofukashi no Uta'], + image: 'https://temp.compsci88.com/cover/Yofukashi-no-Uta.jpg', + headerForImage: { Referer: 'https://mangasee123.com' } + }, + {...}, + ] +} +``` + +### fetchMangaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------------- | +| mangaId | `string` | manga id (*can be found in the manga search results*) | + +```ts +mangasee123.fetchMangaInfo('Yofukashi-no-Uta').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an manga info object (including the chapters). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L120)*)\ +output: +```js +{ + id: 'Yofukashi-no-Uta', + title: 'Call of the Night', + altTitles: [ 'Yofukashi no Uta' ], + genres: [ + 'Comedy', + 'Psychological', + 'Romance', + 'Shounen', + 'Slice of Life', + 'Supernatural' + ], + image: 'https://temp.compsci88.com/cover/Yofukashi-no-Uta.jpg', + headerForImage: { Referer: 'https://mangasee123.com' }, + description: 'Unable to sleep or find true satisfaction in his daily life, Yamori Kou begins wandering the night streets. He encounters a strange girl named Nanakusa Nazuna who offers to help soothe his insomnia by sleeping beside him, but it is not merely a one-way exchange...', + chapters: [ + { id: 'Yofukashi-no-Uta-chapter-137', title: 'null' }, + {...}, + ] +} +``` +Note: The `headerForImage` property might be useful when getting the image to display. + +### fetchChapterPages + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------- | +| chapterId | `string` | chapter id (*can be found in the manga info*) | + +```ts +mangasee123.fetchChapterPages('Yofukashi-no-Uta-chapter-1').then(data => { + console.log(data); +}) +``` +returns an array of pages. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L122-L126)*)\ +output: +```js +[ + { + page: 1, + img: 'https://official-ongoing-1.ivalice.us/manga/Yofukashi-no-Uta/0001-001.png', + headerForImage: { Referer: 'https://mangasee123.com' } + }, + { + page: 2, + img: 'https://official-ongoing-1.ivalice.us/manga/Yofukashi-no-Uta/0001-002.png', + headerForImage: { Referer: 'https://mangasee123.com' } + }, + { + page: 3, + img: 'https://official-ongoing-1.ivalice.us/manga/Yofukashi-no-Uta/0001-003.png', + headerForImage: { Referer: 'https://mangasee123.com' } + }, + { + page: 4, + img: 'https://official-ongoing-1.ivalice.us/manga/Yofukashi-no-Uta/0001-004.png', + headerForImage: { Referer: 'https://mangasee123.com' } + }, + {...}, +] +``` + +

(Back to Providers List)

diff --git a/consumet.ts/docs/providers/moviehdwatch.md b/consumet.ts/docs/providers/moviehdwatch.md new file mode 100644 index 00000000..026e1457 --- /dev/null +++ b/consumet.ts/docs/providers/moviehdwatch.md @@ -0,0 +1,436 @@ +

MovieHdWatch

+ +```ts +const moviesHd = new MOVIES.MovieHdWatch(); +``` + +

Methods

+ +- [search](#search) +- [fetchMediaInfo](#fetchmediainfo) +- [fetchEpisodeSources](#fetchepisodesources) +- [fetchEpisodeServers](#fetchepisodeservers) +- [fetchRecentMovies](#fetchrecentmovies) +- [fetchRecentTvShows](#fetchrecenttvshows) +- [fetchTrendingMovies](#fetchtrendingmovies) +- [fetchTrendingTvShows](#fetchtrendingtvshows) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| query | `string` | query to search for. (*In this case, We're searching for `Batman`*) | +| page (optional) | `number` | page number (default: 1) | + +```ts +moviesHd.search("Batman").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies/tv series. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L233-L241)*)\ +output: +```js +{ + currentPage: 1, // current page + hasNextPage: true, // if there is a next page + results: [ + { + id: 'tv/watch-batman-online-39276', + title: 'Batman', + url: 'https://movieshd.watch/tv/watch-batman-online-39276', + image: 'https://img.movieshd.watch/xxrz/250x400/391/fb/f9/fbf9562059527ed2075e3e61bf7439c6/fbf9562059527ed2075e3e61bf7439c6.jpg', + releaseDate: undefined, + seasons: 3, + duration: undefined, + type: 'TV Series' + }, + { + id: 'movie/watch-batman-online-13647', + title: 'Batman', + url: 'https://movieshd.watch/movie/watch-batman-online-13647', + image: 'https://img.movieshd.watch/xxrz/250x400/391/7d/df/7ddf28de1b0053327ad6ff1c974894e8/7ddf28de1b0053327ad6ff1c974894e8.jpg', + releaseDate: '1966', + seasons: undefined, + duration: '105m', + type: 'Movie' + }, + { + id: 'movie/watch-batman-online-18073', + title: 'Batman', + url: 'https://movieshd.watch/movie/watch-batman-online-18073', + image: 'https://img.movieshd.watch/xxrz/250x400/391/d9/bc/d9bc77bc0c00049fbaba0896b51d361f/d9bc77bc0c00049fbaba0896b51d361f.jpg', + releaseDate: '1989', + seasons: undefined, + duration: '126m', + type: 'Movie' + }, + {...}, + ... + ] +} +``` + +### fetchMediaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- | +| mediaId | `string` | takes media id or url as a parameter. (*media id or url can be found in the media search results as shown on the above method*) | + +```ts +moviesHd.fetchMediaInfo('movie/watch-the-batman-online-16076').then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L243-L254)*)\ +output: +```js +{ + id: 'movie/watch-the-batman-online-16076', + title: 'The Batman', + url: 'https://movieshd.watch/movie/watch-the-batman-online-16076', + cover: 'https://img.movieshd.watch/xxrz/1200x600/391/34/98/3498b36949518ca118b1ebe321dbd7ca/3498b36949518ca118b1ebe321dbd7ca.jpg', + image: 'https://img.movieshd.watch/xxrz/250x400/391/21/2d/212d2d95b9d515504a4de227d49a769f/212d2d95b9d515504a4de227d49a769f.jpg', + description: "A point-of-view driven noir tale with heavy focus on Batman's detective work. A stand-alone story with no connection to the DCEU.", + type: 'Movie', + releaseDate: '2022-03-01', + genres: [ 'Drama', 'Action', 'Crime', 'Mystery', 'Fantasy', 'Thriller' ], + casts: [ + 'Robert Pattinson', + 'Vanessa Kirby', + 'Jeffrey Wright', + 'Jonah Hill', + 'Peter Sarsgaard' + ], + production: 'DC Entertainment,Branded Entertainment/Batfilm Productions,Atlas Entertainment,Cruel & Unusual Films,Warner Bros. Pictures,6th & Idaho Productions,Mad Ghost Productions,DC Comics,DC Films,Dylan Clark Productions', + country: [ 'United States of America' ], + duration: '176min', + rating: 7.9, + recommendations: [ + { + id: 'movie/watch-through-my-window-across-the-sea-online-97675', + title: 'Through My Window: Across the Sea', + image: 'https://img.movieshd.watch/xxrz/250x400/391/fd/fa/fdfaee0cf2c0321390292d5d2f60c9b4/fdfaee0cf2c0321390292d5d2f60c9b4.jpg', + releaseDate: '2023', + seasons: undefined, + duration: '110m', + type: 'Movie' + }, + { + id: 'tv/watch-the-walking-dead-dead-city-online-97540', + title: 'The Walking Dead: Dead City', + image: 'https://img.movieshd.watch/xxrz/250x400/391/96/2f/962fd0158f7f708e16fc62f8b763a276/962fd0158f7f708e16fc62f8b763a276.jpg', + releaseDate: undefined, + seasons: 1, + duration: undefined, + type: 'TV Series' + }, + {...}, + ... + ], + episodes: [ + { + id: '16076', + title: 'The Batman', + url: 'https://movieshd.watch/ajax/movie/episodes/16076' + } + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| ----------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the media info object*) | +| mediaId | `string` | takes media id as a parameter. (*media id can be found in the media info object*) | +| server (optional) | [`StreamingServers`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L144-L157) | takes server enum as a parameter. *default: [`StreamingServers.VidCloud`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L150)* | + + +```ts +moviesHd.fetchEpisodeSources('16076', 'movie/watch-the-batman-online-16076').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of episode sources and subtitles. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L300-L306)*)\ +output: +```js +{ + headers: { Referer: 'https://dokicloud.one/embed-4/3F3nysmdRDMF?z=' }, + sources: [ + { + url: 'https://eno.dokicloud.one/_v10/fd5de830b89416820504ffef6b23be58878b11bc91d26f99a884f7d4c0dc7c4c500b6ce5d53054d705a74628a3b34208a95bf0d5663142027d6284e4ce2424b9a8cbe9241fb0054f352fcf4d797b2af0fec364a840a38d0d1d3a340c564ad89bb1fecb219076d813667da0ad13266f8a589df412b39bcc03c7c07dc5bfe401c2601ce19dd9530fac08c20fc89104a5d0/1080/index.m3u8', + quality: '1080', + isM3U8: true + }, + { + url: 'https://eno.dokicloud.one/_v10/fd5de830b89416820504ffef6b23be58878b11bc91d26f99a884f7d4c0dc7c4c500b6ce5d53054d705a74628a3b34208a95bf0d5663142027d6284e4ce2424b9a8cbe9241fb0054f352fcf4d797b2af0fec364a840a38d0d1d3a340c564ad89bb1fecb219076d813667da0ad13266f8a589df412b39bcc03c7c07dc5bfe401c2601ce19dd9530fac08c20fc89104a5d0/720/index.m3u8', + quality: '720', + isM3U8: true + }, + { + url: 'https://eno.dokicloud.one/_v10/fd5de830b89416820504ffef6b23be58878b11bc91d26f99a884f7d4c0dc7c4c500b6ce5d53054d705a74628a3b34208a95bf0d5663142027d6284e4ce2424b9a8cbe9241fb0054f352fcf4d797b2af0fec364a840a38d0d1d3a340c564ad89bb1fecb219076d813667da0ad13266f8a589df412b39bcc03c7c07dc5bfe401c2601ce19dd9530fac08c20fc89104a5d0/360/index.m3u8', + quality: '360', + isM3U8: true + }, + { + url: 'https://eno.dokicloud.one/_v10/fd5de830b89416820504ffef6b23be58878b11bc91d26f99a884f7d4c0dc7c4c500b6ce5d53054d705a74628a3b34208a95bf0d5663142027d6284e4ce2424b9a8cbe9241fb0054f352fcf4d797b2af0fec364a840a38d0d1d3a340c564ad89bb1fecb219076d813667da0ad13266f8a589df412b39bcc03c7c07dc5bfe401c2601ce19dd9530fac08c20fc89104a5d0/playlist.m3u8', + isM3U8: true, + quality: 'auto' + } + ], + subtitles: [ + { + url: 'https://cc.2cdns.com/85/ca/85ca0405b2fc0f1f3edacf13e84a9277/85ca0405b2fc0f1f3edacf13e84a9277.vtt', + lang: 'Arabic' + }, + { + url: 'https://cc.2cdns.com/9c/19/9c19c8fceb977034e8ef86bba8ec161e/9c19c8fceb977034e8ef86bba8ec161e.vtt', + lang: 'Danish' + }, + { + url: 'https://cc.2cdns.com/c7/7f/c77fc58f1848b61b665e7de01f298223/eng-2.vtt', + lang: 'English' + }, + { + url: 'https://cc.2cdns.com/a2/73/a2737a6c19b70eb7be88f852eb3f2b8a/a2737a6c19b70eb7be88f852eb3f2b8a.vtt', + lang: 'Finnish' + }, + { + url: 'https://cc.2cdns.com/b2/18/b2180b98383a2ad8a3d3297af5ee9e7f/b2180b98383a2ad8a3d3297af5ee9e7f.vtt', + lang: 'Indonesian' + }, + { + url: 'https://cc.2cdns.com/8f/4b/8f4bf106dc5aca724af13820acde367c/8f4bf106dc5aca724af13820acde367c.vtt', + lang: 'Norwegian' + }, + { + url: 'https://cc.2cdns.com/37/3f/373fd3656dfbccd23746b325f0fdf917/373fd3656dfbccd23746b325f0fdf917.vtt', + lang: 'Portuguese' + }, + { + url: 'https://cc.2cdns.com/b7/e5/b7e5765026f7031a883d19b9b919613a/b7e5765026f7031a883d19b9b919613a.vtt', + lang: 'Spanish' + } + ] +} +``` + +### fetchEpisodeServers + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------------------------- | +| episodeId | `string` | take an episode id or url as a parameter. (*episode id or episode url can be found in the media info object*) | +| mediaId | `string` | takes media id as a parameter. (*media id can be found in the media info object*) | + +```ts +moviesHd.fetchEpisodeServers('16076', 'movie/watch-the-batman-online-16076').then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of episode servers. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L118)*)\ +output: +```js +[ + { name: 'MixDrop', url: 'https://mixdrop.co/e/7r1l3erphjrn0o' }, + { name: 'DoodStream', url: 'https://dood.watch/e/6xi1hr51ghlb' }, + { + name: 'Vidcloud', + url: 'https://rabbitstream.net/embed-4/T2SqGLFECVhb?z=' + }, + { + name: 'UpCloud', + url: 'https://dokicloud.one/embed-4/3F3nysmdRDMF?z=' + } + {...}, + ... +] +``` + +

(back to movie providers list)

+ +### fetchRecentMovies + +```ts +moviesHd.fetchRecentMovies().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ + { + id: 'movie/watch-worlds-best-online-97678', + title: "World's Best", + url: 'https://movieshd.watch/movie/watch-worlds-best-online-97678', + image: 'https://img.movieshd.watch/xxrz/250x400/391/39/a6/39a6f63d6aa29ed36b292e029d3d38f0/39a6f63d6aa29ed36b292e029d3d38f0.jpg', + releaseDate: '2023', + duration: '101m', + type: 'Movie' + }, + { + id: 'movie/watch-through-my-window-across-the-sea-online-97675', + title: 'Through My Window: Across the Sea', + url: 'https://movieshd.watch/movie/watch-through-my-window-across-the-sea-online-97675', + image: 'https://img.movieshd.watch/xxrz/250x400/391/fd/fa/fdfaee0cf2c0321390292d5d2f60c9b4/fdfaee0cf2c0321390292d5d2f60c9b4.jpg', + releaseDate: '2023', + duration: '110m', + type: 'Movie' + }, + { + id: 'movie/watch-the-perfect-find-online-97669', + title: 'The Perfect Find', + url: 'https://movieshd.watch/movie/watch-the-perfect-find-online-97669', + image: 'https://img.movieshd.watch/xxrz/250x400/391/c9/a7/c9a780f7d7cd1eb8a72c3e4ee5880426/c9a780f7d7cd1eb8a72c3e4ee5880426.jpg', + releaseDate: '2023', + duration: '99m', + type: 'Movie' + }, + {...}, +] +``` + + +### fetchRecentTvShows + +```ts +moviesHd.fetchRecentTvShows().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of tv shows. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ + { + id: 'tv/watch-deadloch-online-97072', + title: 'Deadloch', + url: 'https://movieshd.watch/tv/watch-deadloch-online-97072', + image: 'https://img.movieshd.watch/xxrz/250x400/391/9f/85/9f8594271eb6540e32b7fbda24747c6e/9f8594271eb6540e32b7fbda24747c6e.jpg', + season: 1, + latestEpisode: 6, + type: 'TV Series' + }, + { + id: 'tv/watch-clone-high-online-96937', + title: 'Clone High', + url: 'https://movieshd.watch/tv/watch-clone-high-online-96937', + image: 'https://img.movieshd.watch/xxrz/250x400/391/ad/c5/adc55790c8c88d5538210f7558fec960/adc55790c8c88d5538210f7558fec960.jpg', + season: 1, + latestEpisode: 10, + type: 'TV Series' + }, + { + id: 'tv/watch-and-just-like-that-online-75286', + title: 'And Just Like That…', + url: 'https://movieshd.watch/tv/watch-and-just-like-that-online-75286', + image: 'https://img.movieshd.watch/xxrz/250x400/391/b8/e2/b8e20a6264e28cf1133413f63425297d/b8e20a6264e28cf1133413f63425297d.jpg', + season: 2, + latestEpisode: 2, + type: 'TV Series' + }, + {...}, +] +``` + + +### fetchTrendingMovies + +```ts +moviesHd.fetchTrendingMovies().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ + { + id: 'movie/watch-through-my-window-across-the-sea-online-97675', + title: 'Through My Window: Across the Sea', + url: 'https://movieshd.watch/movie/watch-through-my-window-across-the-sea-online-97675', + image: 'https://img.movieshd.watch/xxrz/250x400/391/fd/fa/fdfaee0cf2c0321390292d5d2f60c9b4/fdfaee0cf2c0321390292d5d2f60c9b4.jpg', + releaseDate: '2023', + duration: '110m', + type: 'Movie' + }, + { + id: 'movie/watch-extraction-2-online-97549', + title: 'Extraction 2', + url: 'https://movieshd.watch/movie/watch-extraction-2-online-97549', + image: 'https://img.movieshd.watch/xxrz/250x400/391/9c/d5/9cd56c00c2b79598f7fba8ba33b2128d/9cd56c00c2b79598f7fba8ba33b2128d.jpg', + releaseDate: '2023', + duration: '123m', + type: 'Movie' + }, + { + id: 'movie/watch-the-perfect-find-online-97669', + title: 'The Perfect Find', + url: 'https://movieshd.watch/movie/watch-the-perfect-find-online-97669', + image: 'https://img.movieshd.watch/xxrz/250x400/391/c9/a7/c9a780f7d7cd1eb8a72c3e4ee5880426/c9a780f7d7cd1eb8a72c3e4ee5880426.jpg', + releaseDate: '2023', + duration: '99m', + type: 'Movie' + }, + {...}, +] +``` + + +### fetchTrendingTvShows + +```ts +moviesHd.fetchTrendingTvShows().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of tv shows. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +[ + { + id: 'tv/watch-secret-invasion-online-88246', + title: 'Secret Invasion', + url: 'https://movieshd.watch/tv/watch-secret-invasion-online-88246', + image: 'https://img.movieshd.watch/xxrz/250x400/391/84/21/84218c778e006f43777e1f8fe18a2560/84218c778e006f43777e1f8fe18a2560.jpg', + season: 1, + latestEpisode: 1, + type: 'TV Series' + }, + { + id: 'tv/watch-skull-island-online-97690', + title: 'Skull Island', + url: 'https://movieshd.watch/tv/watch-skull-island-online-97690', + image: 'https://img.movieshd.watch/xxrz/250x400/391/83/c1/83c19806235b273f762c328a49d4d91d/83c19806235b273f762c328a49d4d91d.jpg', + season: 1, + latestEpisode: 8, + type: 'TV Series' + }, + { + id: 'tv/watch-black-mirror-online-39396', + title: 'Black Mirror', + url: 'https://movieshd.watch/tv/watch-black-mirror-online-39396', + image: 'https://img.movieshd.watch/xxrz/250x400/391/d6/9d/d69d87285ef143fab74322227616bb04/d69d87285ef143fab74322227616bb04.jpg', + season: 6, + latestEpisode: 5, + type: 'TV Series' + }, + {...}, +] +``` diff --git a/consumet.ts/docs/providers/novelupdates.md b/consumet.ts/docs/providers/novelupdates.md new file mode 100644 index 00000000..9c76ae20 --- /dev/null +++ b/consumet.ts/docs/providers/novelupdates.md @@ -0,0 +1,109 @@ +

NovelUpdates

+ +```ts +const novelupdates = new LIGHT_NOVELS.NovelUpdates(); +``` + +

Methods

+ +- [search](#search) +- [fetchLightNovelInfo](#fetchlightnovelinfo) +- [fetchChapterContent](#fetchchaptercontent) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ----------------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `Classrrom of the Elite`*) | + +```ts +novelupdates.search("Clasroom of the Elite").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of light novels. (* Promise>*)\ +output: +```js +{ + results: [ + { + id: 'youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e', // the light novel id + title: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e', + url: 'https://www-novelupdates-com.translate.goog/series/youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e/?_x_tr_sl=ja&_x_tr_tl=en&_x_tr_hl=en-US', + image: 'https://cdn.novelupdates.com/imgmid/series_10266.jpg' + }, + {...} + ... + ] +} +``` + +### fetchLightNovelInfo + +

Parameters

+ +| Parameter | Type | Description | +| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------ | +| lightNovelUrl | `string` | id or url of the light novel. (*light novel id or url can be found in the light novel search results*) | +| chapterPage (optional) | `number` | chapter page number (*default: -1 meaning will fetch all chapters*) | + +```ts +novelupdates.fetchLightNovelInfo("youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an light novel info object (including the chapters or volumes). (*Promise\*)\ +output: +```js +{ + id: 'youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e', + title: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e', + url: 'https://www.novelupdates.com/series/youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e', + image: 'https://cdn.novelupdates.com/images/2017/02/cover00219.jpeg', + author: 'Kinugasa Shougo衣笠彰梧', + genres: [ + 'Drama', + 'Psychological', + '...' + ], + rating: 9, + views: NaN, + description: 'Kōdo Ikusei Senior High School, a leading prestigious school with state-of-the-art facilities where nearly...', + status: 'Completed', + chapters: [ + { + id: '6659442', + title: 'v17...', + url: 'https://www.novelupdates.com/extnu/6659442' + }, + {...} + ... +``` + +### fetchChapterContent + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------- | +| chapterId | `string` | chapter id. (*chapter id can be found in the light novel info object*) | + +```ts +readlightnovels.fetchChapterContent("5692421").then(data => { + console.log(data); +}) +``` +returns a content object. (*Promise\*)\ +output: +```js +{ + text: '\n' + + 'It’s a bit sudden,...', + html: '

It’s a bit sudden, but listen seriously to the question I’m about to ask and think about...' +} +``` + +

(back to light novels providers list)

diff --git a/consumet.ts/docs/providers/readlightnovels.md b/consumet.ts/docs/providers/readlightnovels.md new file mode 100644 index 00000000..2e2d0ce5 --- /dev/null +++ b/consumet.ts/docs/providers/readlightnovels.md @@ -0,0 +1,110 @@ +

ReadLightNovels

+ +```ts +const readlightnovels = new LIGHT_NOVELS.ReadLightNovels(); +``` + +

Methods

+ +- [search](#search) +- [fetchLightNovelInfo](#fetchlightnovelinfo) +- [fetchChapterContent](#fetchchaptercontent) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ----------------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `Classrrom of the Elite`*) | + +```ts +readlightnovels.search("Classrrom of the Elite").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of light novels. (* Promise>*)\ +output: +```js +{ + results: [ + { + id: 'youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e', // the light novel id + title: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e Novel (Classroom of the Elite Novel)', + url: 'https://readlightnovels.net/youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e.html', + image: 'https://readlightnovels.net/wp-content/uploads/2020/01/youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e.jpg' + }, + {...} + ... + ] +} +``` + +### fetchLightNovelInfo + +

Parameters

+ +| Parameter | Type | Description | +| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------ | +| lightNovelUrl | `string` | id or url of the light novel. (*light novel id or url can be found in the light novel search results*) | +| chapterPage (optional) | `number` | chapter page number (*default: -1 meaning will fetch all chapters*) | + +```ts +readlightnovels.fetchLightNovelInfo("youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an light novel info object (including the chapters or volumes). (*Promise\*)\ +output: +```js +{ + id: 'youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e', + title: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e Novel (Classroom of the Elite Novel)', + url: 'https://readlightnovels.net/youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e.html', + image: 'https://readlightnovels.net/wp-content/uploads/2020/01/youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e.jpg', + author: 'Kinugasa Shougo衣笠彰梧', + genres: [ + 'Drama', + 'Harem', + '...' + ], + rating: 8.6, + views: 651729, + description: 'Kōdo Ikusei Senior High School, a leading prestigious school with state-of-the-art facilities where nearly...', + status: 'Ongoing', + pages: 13, + chapters: [ + { + id: 'youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e/volume-1-prologue-the-structure-of-japanese-society', + title: 'Volume 1, Prologue: The structure of Japanese society', + url: 'https://readlightnovels.net/youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e/volume-1-prologue-the-structure-of-japanese-society.html' + }, + {...} + ... +``` + +### fetchChapterContent + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------- | +| chapterId | `string` | chapter id. (*chapter id can be found in the light novel info object*) | + +```ts +readlightnovels.fetchChapterContent("youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e/volume-1-prologue-the-structure-of-japanese-society").then(data => { + console.log(data); +}) +``` +returns a content object. (*Promise\*)\ +output: +```js +{ + text: '\n' + + 'It’s a bit sudden,...', + html: '

It’s a bit sudden, but listen seriously to the question I’m about to ask and think about...' +} +``` + +

(back to light novels providers list)

diff --git a/consumet.ts/docs/providers/tmdb.md b/consumet.ts/docs/providers/tmdb.md new file mode 100644 index 00000000..5c31aa63 --- /dev/null +++ b/consumet.ts/docs/providers/tmdb.md @@ -0,0 +1,289 @@ +

TMDB

+This is a custom provider that maps an movie provider (like flixhq) to TMDB. + +`TMDB` class takes a [`MovieParser`](https://github.com/consumet/extensions/blob/master/src/models/movie-parser.ts) object as a parameter **(optional)**. This object is used to parse the anime episodes from the provider, then mapped to TMDB. + +```ts +const tmdb = new META.TMDB(); +``` + +

Methods

+ +- [fetchTrending](#fetchtrending) +- [search](#search) +- [fetchMediaInfo](#fetchmediainfo) +- [fetchEpisodeSources](#fetchepisodesources) + +### fetchTrending + +

Parameters

+ +| Parameter | Type | Description | +| --------------------- | -------- | ----------------------------------------------------------------------------------- | +| type | `string` | type of trending option we want('movie', 'tv series', 'people' or 'all') | +| timePeriod (optional) | `string` | the duration of trending we want ('day' or 'week') | +| page (optional) | `number` | page number to search for. | + +```ts +tmdb.fetchTrending("the flash").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/consumet.ts/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +{ + currentPage: 1, + results: [ + { + id: 848326, + title: 'Rebel Moon - Part One: A Child of Fire', + image: 'https://image.tmdb.org/t/p/original/ui4DrH1cKk2vkHshcUcGt2lKxCm.jpg', + type: 'Movie', + rating: 6.457, + releaseDate: '2023' + }, + { + id: 572802, + title: 'Aquaman and the Lost Kingdom', + image: 'https://image.tmdb.org/t/p/original/8xV47NDrjdZDpkVcCFqkdHa3T0C.jpg', + type: 'Movie', + rating: 6.551, + releaseDate: '2023' + }, + { + id: 930564, + title: 'Saltburn', + image: 'https://image.tmdb.org/t/p/original/qjhahNLSZ705B5JP92YMEYPocPz.jpg', + type: 'Movie', + rating: 7.2, + releaseDate: '2023' + }, + {...} + ... + ] +} +``` + +### search + +

Parameters

+ +| Parameter | Type | Description | +| -------------------- | -------- | ----------------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `Classroom of the elite`*) | +| page (optional) | `number` | page number to search for. | + +```ts +tmdb.search("the flash").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/consumet.ts/blob/master/src/models/types.ts#L328-L336)*)\ +output: +```js +{ + currentPage: 1, + results: [ + { + "id": 60735, + "title": "The Flash", + "image": "https://image.tmdb.org/t/p/original/lJA2RCMfsWoskqlQhXPSLFQGXEJ.jpg", + "type": "TV Series", + "rating": 7.807, + "releaseDate": "2014" + }, + { + "id": 236, + "title": "The Flash", + "image": "https://image.tmdb.org/t/p/original/fi1GEdCbyWRDHpyJcB25YYK7fh4.jpg", + "type": "TV Series", + "rating": 7.464, + "releaseDate": "1990" + }, + { + "id": 298618, + "title": "The Flash", + "image": "https://image.tmdb.org/t/p/original/oduJooXJya3u6wuA6FgljAFCEQp.jpg", + "type": "Movie", + "rating": 0, + "releaseDate": "2023" + }, + {...} + ... + ] +} +``` + + + +### fetchMediaInfo + +

Parameters

+ +| Parameter | Type | Description | +| -------------- | --------- | --------------------------------------------------------------------------------------------------------- | +| id | `string` | takes anime id as a parameter. (*anime id can be found in the anime search results or Movie info object*) | +| type | `string` | takes movie or tv as a parameter (*type can be found in the anime search results or Movie info object*) | + + +```ts +tmdb.fetchMediaInfo("60735", "tv").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an movie info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ +output: +```js +{ + "id": "tv/watch-the-flash-39535", + "title": "The Flash", + "image": "https://image.tmdb.org/t/p/original/lJA2RCMfsWoskqlQhXPSLFQGXEJ.jpg", + "cover": "https://image.tmdb.org/t/p/original/41yaWnIT8AjIHiULHtTbKNzZTjc.jpg", + "type": "TV Series", + "rating": 7.807, + "releaseDate": "2014-10-07", + "description": "After a particle accelerator causes a freak storm, CSI Investigator Barry Allen is struck by lightning and falls into a coma. Months later he awakens with the power of super speed, granting him the ability to move through Central City like an unseen guardian angel. Though initially excited by his newfound powers, Barry is shocked to discover he is not the only \"meta-human\" who was created in the wake of the accelerator explosion -- and not everyone is using their new powers for good. Barry partners with S.T.A.R. Labs and dedicates his life to protect the innocent. For now, only a few close friends and associates know that Barry is literally the fastest man alive, but it won't be long before the world learns what Barry Allen has become...The Flash.", + "genres": [ + "Drama", + "Sci-Fi & Fantasy" + ], + "duration": 44, + "totalEpisodes": 176, + "totalSeasons": 9, + "directors": [], + "writers": [], + "actors": [ + "Grant Gustin", + "Candice Patton", + "Danielle Panabaker", + "Danielle Nicolet", + "Kayla Compton", + "Brandon McKnight", + "Jon Cor" + ], + "trailer": { + "id": "Mx7xTF8fKz4", + "site": "YouTube", + "url": "https://www.youtube.com/watch?v=Mx7xTF8fKz4" + }, + "similar": [ + { + "id": 12971, + "title": "Dragon Ball Z", + "image": "https://image.tmdb.org/t/p/original/jB9l4mp0bzBgzE5y4tvBH6AMeMk.jpg", + "type": "TV Series", + "rating": 8.311, + "releaseDate": "1989-04-26" + }, + { + "id": 13023, + "title": "El Chapulín Colorado", + "image": "https://image.tmdb.org/t/p/original/qF8NDpVBSTDhdLlEjVAhNhfqB8K.jpg", + "type": "TV Series", + "rating": 7.932, + "releaseDate": "1973-04-11" + }, + {...} + ], + "seasons": [ + { + "season": 1, + "image": "https://image.tmdb.org/t/p/original/kHyXbcb2JGWIe1fyZa6PqBwlNJN.jpg", + "episodes": [ + { + "id": "2899", + "title": "Pilot", + "episode": 1, + "season": 1, + "releaseDate": "2014-10-07", + "overview": "Barry discovers his powers and puts them to the test, only when he finds its no longer a test but the real thing when he encounters a certain someone.", + "url": "https://flixhq.to/ajax/v2/episode/servers/2899", + "img": "https://image.tmdb.org/t/p/original/piyGyhwbqqyIxcyuZXYmDUWSylb.jpg" + }, + { + "id": "2900", + "title": "Fastest Man Alive", + "episode": 2, + "season": 1, + "releaseDate": "2014-10-14", + "overview": "Barry changes into the Flash when six gunmen storm a university event honoring a scientist, but his heroics don't match up to his expectations. Meanwhile, Iris becomes even more intrigued by the \"red streak.\"", + "url": "https://flixhq.to/ajax/v2/episode/servers/2900", + "img": "https://image.tmdb.org/t/p/original/mUgakZLNaMIjG63pz7VeJXJPMu4.jpg" + }, + {...} + ] + } + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------------------ | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the Movie info object*) | +| MediaId | `string` | takes media id as a parameter. (*media id can be found in the Movie info seasons episodes object*) | + + + +```ts +tmdb.fetchEpisodeSources("2899", "tv/watch-the-flash-39535").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of episode sources. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ +output: +```js +{ + "headers": { + "Referer": "https://dokicloud.one/embed-4/hkEpFgTBEN9G?z=" + }, + "sources": [ + { + "url": "https://t-eu-2.magnewscontent.org/_v9/f1f4fc0acbf8baef134d6ba2f0e42815c4f3e58a6648e8f2b046410b81510d90e399b927c3135c88b026299880c0ca317d1bb065d7ec76af49cb38620a075678f1e005f1336207700b67e48f4f466b546bce3cdd11ddd1775f99b45a46311887eb1a74d2403405bd85443785566b85ab8394f8191c72a97b3dd951a30bc02479/1080/index.m3u8", + "quality": "1080", + "isM3U8": true + }, + { + "url": "https://t-eu-2.magnewscontent.org/_v9/f1f4fc0acbf8baef134d6ba2f0e42815c4f3e58a6648e8f2b046410b81510d90e399b927c3135c88b026299880c0ca317d1bb065d7ec76af49cb38620a075678f1e005f1336207700b67e48f4f466b546bce3cdd11ddd1775f99b45a46311887eb1a74d2403405bd85443785566b85ab8394f8191c72a97b3dd951a30bc02479/720/index.m3u8", + "quality": "720", + "isM3U8": true + }, + { + "url": "https://t-eu-2.magnewscontent.org/_v9/f1f4fc0acbf8baef134d6ba2f0e42815c4f3e58a6648e8f2b046410b81510d90e399b927c3135c88b026299880c0ca317d1bb065d7ec76af49cb38620a075678f1e005f1336207700b67e48f4f466b546bce3cdd11ddd1775f99b45a46311887eb1a74d2403405bd85443785566b85ab8394f8191c72a97b3dd951a30bc02479/360/index.m3u8", + "quality": "360", + "isM3U8": true + }, + { + "url": "https://t-eu-2.magnewscontent.org/_v9/f1f4fc0acbf8baef134d6ba2f0e42815c4f3e58a6648e8f2b046410b81510d90e399b927c3135c88b026299880c0ca317d1bb065d7ec76af49cb38620a075678f1e005f1336207700b67e48f4f466b546bce3cdd11ddd1775f99b45a46311887eb1a74d2403405bd85443785566b85ab8394f8191c72a97b3dd951a30bc02479/playlist.m3u8", + "isM3U8": true, + "quality": "auto" + } + ], + "subtitles": [ + { + "url": "https://cc.2cdns.com/4a/02/4a027259c5bd865a75e756cc09f54cbb/4a027259c5bd865a75e756cc09f54cbb.vtt", + "lang": "English" + }, + { + "url": "https://cc.2cdns.com/48/66/4866edc69de4b4fd17c89b59efa726a5/4866edc69de4b4fd17c89b59efa726a5.vtt", + "lang": "Portuguese" + }, + { + "url": "https://prev.2cdns.com/_m_preview/15/1568bec9a3267e03fcca2a1fb86b3b59/thumbnails/sprite.vtt", + "lang": "Default (maybe)" + } + ] +} +``` + +Make sure to check the `headers` property of the returned object. It contains the referer header, which is needed to bypass the 403 error and allow you to stream the video without any issues. + +

(back to meta providers list)

diff --git a/consumet.ts/docs/providers/viewAsian.md b/consumet.ts/docs/providers/viewAsian.md new file mode 100644 index 00000000..506d469d --- /dev/null +++ b/consumet.ts/docs/providers/viewAsian.md @@ -0,0 +1,131 @@ +

ViewAsian

+ +```ts +const viewAsian = new MOVIES.ViewAsian(); +``` + +

Methods

+ +- [search](#search) +- [fetchMediaInfo](#fetchmediainfo) +- [fetchEpisodeSources](#fetchepisodesources) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| query | `string` | query to search for. (*In this case, We're searching for `Vincenzo`*) P.S: `vincenzo` is a really good korean drama i highly recommend it. | +| page (optional) | `number` | page number (default: 1) | + +```ts +viewAsian.search("Vincenzo").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of movies/tv series. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L233-L241)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: false, + results: [ + { + id: 'drama/vincenzo', + title: 'Vincenzo (2021)', + url: 'https://viewasian.co/drama/vincenzo', + image: 'https://imagecdn.me/cover/vincenzo.png', + releaseDate: undefined + } + ] +} +``` + +### fetchMediaInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- | +| mediaId | `string` | takes media id or url as a parameter. (*media id or url can be found in the media search results as shown on the above method*) | + +```ts +viewAsian.fetchMediaInfo("drama/vincenzo").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L243-L254)*)\ +output: +```js +{ + id: 'drama/vincenzo', + title: 'Vincenzo (2021)', + otherNames: [ '빈센조', 'Binsenjo' ], + description: 'At the age of 8, Park Ju Hyeong is adopted and sent off to Italy. Now an adult, he is known as Vincenzo Casano.\n' + + 'He is a Mafia lawyer and consigliere. ( Advisor and dispute reconciliation expert.) Warring factions within the Mafia force him to flee to South Korea. There he falls in love with Hong Cha Young. a lawyer who will do anything to win a case. Vincenzo manages to achieve some social justice there, and in his own way.\n' + + 'Jang Jun Woo is an intelligent and hardworking first-year law intern at the firm, who is polite and sincere. Despite his boyish charm and good looks, Jun Woo can come a cross as awkward and naive. Prone to making mistakes, he is often trouble at work.', + genre: [ + 'Comedy', 'Crime', + 'Drama', 'Law', + 'Mafia', 'Romance', + 'Suspense' + ], + director: 'Kim Hee Won [김희원]', + country: 'Korean', + releaseDate: '2021', + episodes: [ + { + id: '/watch/vincenzo/watching.html$episode$20', + title: 'Episode 20', + episode: '20', + url: 'https://viewasian.co/watch/vincenzo/watching.html?ep=20' + }, + {...}, + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| ----------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the media info object*) | +| mediaId | `string` | takes media id as a parameter. (*media id can be found in the media info object*) | +| server (optional) | [`StreamingServers`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L76-L82) | takes server enum as a parameter. *default: [`StreamingServers.AsianLoad`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L139-L152)* | + + +```ts +viewAsian.fetchEpisodeSources("/watch/vincenzo/watching.html$episode$20").then(data => { + console.log(data); +}) +``` +returns a promise which resolves into an array of episode sources and subtitles. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L295-L300)*)\ +output: +```js +{ + sources: [ + { + url: 'https://hlsx02.dracache.com/newvideos/newhls/sYIDXKabb521QT8JdzkPpg/1668712072/248711_194.35.232.200/db287e9dc37d8c5b67c2498e3ef07c5a/ep.20.v0.1657641277.m3u8', + isM3U8: true + }, + { + url: 'https://hlsx02.dracache.com/newvideos/newhls/sYIDXKabb521QT8JdzkPpg/1668712072/248711_194.35.232.200/db287e9dc37d8c5b67c2498e3ef07c5a/ep.20.v0.1657641277.m3u8', + isM3U8: true + } + ], + subtitles: [ + { + url: 'https://asiancdn.com/images/db287e9dc37d8c5b67c2498e3ef07c5a/20.vtt', + lang: 'Default (maybe)' + } + ] +} +``` +

(back to movie providers list)

\ No newline at end of file diff --git a/consumet.ts/docs/providers/zoro.md b/consumet.ts/docs/providers/zoro.md new file mode 100644 index 00000000..cd5e57b0 --- /dev/null +++ b/consumet.ts/docs/providers/zoro.md @@ -0,0 +1,802 @@ +

Zoro

+ +```ts +const zoro = new ANIME.Zoro(); +``` + +

Methods

+ +- [search](#search) +- [fetchAnimeInfo](#fetchanimeinfo) +- [fetchEpisodeSources](#fetchepisodesources) +- [fetchTopAiring](#fetchTopAiring) +- [fetchMostPopular](#fetchMostPopular) +- [fetchMostFavorite](#fetchMostFavorite) +- [fetchLatestCompleted](#fetchLatestCompleted) +- [fetchRecentlyUpdated](#fetchRecentlyUpdated) +- [fetchRecentlyAdded](#fetchRecentlyAdded) +- [fetchTopUpcoming](#fetchTopUpcoming) +- [fetchSchedule](#fetchSchedule) +- [fetchStudio](#fetchStudio) +- [fetchSpotlight](#fetchSpotlight) +- [fetchSearchSuggestions] (#fetchSearchSuggestions) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| query | `string` | query to search for. (*In this case, We're searching for `Spy x Family`*) | + +```ts +zoro.search("spy x family").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: true, + totalPages: 3, + results: [ + { + id: 'spy-x-family-17977', + title: 'Spy x Family', url: 'https://aniwatch.to/spy-x-family-17977?ref=search', + image: 'https://img.flawlessfiles.com/_r/300x400/100/88/bd/88bd17534dc4884f23027035d23d74e5/88bd17534dc4884f23027035d23d74e5.jpg', + type: 'TV', + duration: '24m', + japaneseTitle: 'Spy x Family', + nsfw: false, + sub: 12, + dub: 12, + episodes: 12 + }, + { + id: 'spy-x-family-part-2-18152', title: 'Spy x Family, Part 2', + url: 'https://aniwatch.to/spy-x-family-part-2-18152?ref=search', + image: 'https://img.flawlessfiles.com/_r/300x400/100/53/d2/53d283223e562b22a14023d8dc1e934d/53d283223e562b22a14023d8dc1e934d.jpg', + type: 'TV', + duration: '23m', japaneseTitle: 'Spy x Family Part 2', + nsfw: false, + sub: 13, + dub: 13, + episodes: 13 + }, + {...} + ... + ] +} +``` + +### fetchAnimeInfo + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------------------------------------------------------- | +| id | `string` | takes anime id as a parameter. (*anime id can be found in the anime search results or anime info object*) | + + +```ts +zoro.fetchAnimeInfo("overlord-iv-18075").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an anime info object (including the episodes and optionally MAL and Anilist ID ). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ +output: +```js +{ + id: 'overlord-iv-18075', + title: 'Overlord IV', + malID: 48895, + alID: 133844, + image: 'https://img.zorores.com/_r/300x400/100/ef/1d/ef1d1028cf6c177587805651b78282a6/ef1d1028cf6c177587805651b78282a6.jpg', + description: 'Fourth season of Overlord', + type: 'TV', + url: 'https://zoro.to/overlord-iv-18075', + totalEpisodes: 3, + episodes: [ + { + id: 'overlord-iv-18075$episode$92599', + number: 1, + title: 'Sorcerous Nation of Ainz Ooal Gown', + isFiller: false, + url: 'https://zoro.to/watch/overlord-iv-18075?ep=92599' + }, + { + id: 'overlord-iv-18075$episode$92769', + number: 2, + title: 'Re-Estize Kingdom', + isFiller: false, + url: 'https://zoro.to/watch/overlord-iv-18075?ep=92769' + }, + ] +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the anime info object*) | + + +In this example, we're getting the sources for the first episode of Overlord IV. +```ts +zoro.fetchEpisodeSources("overlord-iv-18075$episode$92599").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of episode sources. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ +output: +```js +{ + headers: { Referer: 'https://rapid-cloud.ru/embed-6/hMN2fYuGi1E2?z=' }, + intro: { + start: 0, + end: 100 + } + sources: [ + { + url: 'https://c-an-ca3.betterstream.cc:2223/v2-hls-playback/584bca0a36f1cfe0153bc80d79d62f9171c193441d424b2804000153234bb744f6eb7197bd91842408660ab8516c67f5ad565acd0d18e9b565c6abf2b5c0e55879ca70bef239d78711bf0845ddb6005baf5a5e957a17efc7bb6f1b4f3a87fb3723cfc56a1330960ec99ce338d86d49211bc6e8c2830d50842034ed99335c654529d2b0ca1e19045357a6b01876ae12ea313473387cb8c5272b37c7ba8a2bbc3b185c0cc72517ee0237ce673914ac3e54/index-f1-v1-a1.m3u8', + quality: '1080p', + isM3U8: true + }, + { + url: 'https://c-an-ca3.betterstream.cc:2223/v2-hls-playback/584bca0a36f1cfe0153bc80d79d62f9171c193441d424b2804000153234bb744f6eb7197bd91842408660ab8516c67f5ad565acd0d18e9b565c6abf2b5c0e55879ca70bef239d78711bf0845ddb6005baf5a5e957a17efc7bb6f1b4f3a87fb3723cfc56a1330960ec99ce338d86d49211bc6e8c2830d50842034ed99335c654529d2b0ca1e19045357a6b01876ae12ea313473387cb8c5272b37c7ba8a2bbc3b185c0cc72517ee0237ce673914ac3e54/index-f2-v1-a1.m3u8', + quality: '720p', + isM3U8: true + }, + { + url: 'https://c-an-ca3.betterstream.cc:2223/v2-hls-playback/584bca0a36f1cfe0153bc80d79d62f9171c193441d424b2804000153234bb744f6eb7197bd91842408660ab8516c67f5ad565acd0d18e9b565c6abf2b5c0e55879ca70bef239d78711bf0845ddb6005baf5a5e957a17efc7bb6f1b4f3a87fb3723cfc56a1330960ec99ce338d86d49211bc6e8c2830d50842034ed99335c654529d2b0ca1e19045357a6b01876ae12ea313473387cb8c5272b37c7ba8a2bbc3b185c0cc72517ee0237ce673914ac3e54/index-f3-v1-a1.m3u8', + quality: '360p', + isM3U8: true + } + ], + subtitles: [ + { + url: 'https://cc.zorores.com/5f/b4/5fb4481163961694ef0dc661a1bf51d7/eng-2.vtt', + lang: 'English' + }, + { + url: 'https://cc.zorores.com/5f/b4/5fb4481163961694ef0dc661a1bf51d7/por-3.vtt', + lang: 'Portuguese - Portuguese(Brazil)' + }, + { + url: 'https://cc.zorores.com/5f/b4/5fb4481163961694ef0dc661a1bf51d7/rus-5.vtt', + lang: 'Russian' + }, + { + url: 'https://cc.zorores.com/5f/b4/5fb4481163961694ef0dc661a1bf51d7/spa-4.vtt', + lang: 'Spanish - Spanish(Latin_America)' + }, + { + url: 'https://preview.zorores.com/53/531eb74affebbec2613a6ba0883754f3/thumbnails/sprite.vtt', + lang: 'Default (maybe)' + } + ] +} +``` + +### fetchTopAiring + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| page (optional) | `number` | page number (default 1) | + +```ts +zoro.fetchTopAiring().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ currentPage: 1, + hasNextPage: true, + totalPages: 9, + results: [ + { + id: 'one-piece-100', + title: 'One Piece', + url: 'https://aniwatch.to/one-piece-100', + image: 'https://img.flawlessfiles.com/_r/300x400/100/54/90/5490cb32786d4f7fef0f40d7266df532/5490cb32786d4f7fef0f40d7266df532.jpg', + type: 'TV', + duration: '24m', + japaneseTitle: 'One Piece', + nsfw: false, + sub: 1089, + dub: 1048, + episodes: 0 + }, + { + id: 'attack-on-titan-the-final-season-part-3-1839', + title: 'Attack on Titan: The Final Season Part 3', + url: 'https://aniwatch.to/attack-on-titan-the-final-season-part-3-18329', + image: 'https://img.flawlessfiles.com/_r/300x400/100/54/d3/54d3f59bcc7caf1539c701eb0a064ec9/54d3f59bcc7caf1539c701eb0a064ec9.png', + type: 'TV', + duration: '61m', + japaneseTitle: 'Shingeki no Kyojin: The Final Season - Kanketsu-hen', + nsfw: true, + sub: 2, + dub: 2, + episodes: 0 + }, + {...} + ... + ] +} +``` + +### fetchMostPopular + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| page (optional) | `number` | page number (default 1) | + +```ts +zoro.fetchMostPopular().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ currentPage: 1, + hasNextPage: true, + totalPages: 50, + results: [ + { + id: 'one-piece-100', + title: 'One Piece', + url: 'https://aniwatch.to/one-piece-100', + image: 'https://img.flawlessfiles.com/_r/300x400/100/54/90/5490cb32786d4f7fef0f40d7266df532/5490cb32786d4f7fef0f40d7266df532.jpg', + type: 'TV', + duration: '24m', + japaneseTitle: 'One Piece', + nsfw: false, + sub: 1089, + dub: 1048, + episodes: 0 + }, + { + id: 'naruto-shippuden-355', + title: 'Naruto: Shippuden', + url: 'https://aniwatch.to/naruto-shippuden-355', + image: 'https://img.flawlessfiles.com/_r/300x400/100/9c/bc/9cbcf87f54194742e7686119089478f8/9cbcf87f54194742e7686119089478f8.jpg', + type: 'TV', + duration: '23m', + japaneseTitle: 'Naruto: Shippuuden', + nsfw: false, + sub: 500, + dub: 500, + episodes: 500 + }, + {...} + ... + ] +} +``` + +### fetchMostFavorite + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| page (optional) | `number` | page number (default 1) | + +```ts +zoro.fetchMostFavorite().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: true, + totalPages: 166, + results: [ + { + id: 'one-piece-100', + title: 'One Piece', + url: 'https://aniwatch.to/one-piece-100', + image: 'https://img.flawlessfiles.com/_r/300x400/100/54/90/5490cb32786d4f7fef0f40d7266df532/5490cb32786d4f7fef0f40d7266df532.jpg', + type: 'TV', + duration: '24m', + japaneseTitle: 'One Piece', + nsfw: false, + sub: 1089, + dub: 1048, + episodes: 0 + }, + { + id: 'chainsaw-man-17406', + title: 'Chainsaw Man', + url: 'https://aniwatch.to/chainsaw-man-17406', + image: 'https://img.flawlessfiles.com/_r/300x400/100/b3/da/b3da1326e07269ddd8d73475c5dabf2c/b3da1326e07269ddd8d73475c5dabf2c.jpg', + type: 'TV', + duration: '24m', + japaneseTitle: 'Chainsaw Man', + nsfw: true, + sub: 12, + dub: 12, + episodes: 12 + }, + {...} + ... + ] +} +``` + +### fetchLatestCompleted + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| page (optional) | `number` | page number (default 1) | + +```ts +zoro.fetchLatestCompleted().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: true, + totalPages: 162, + results: [ + { + id: 'love-flops-18173', + title: 'Love Flops', + url: 'https://aniwatch.to/love-flops-18173', + image: 'https://img.flawlessfiles.com/_r/300x400/100/8c/08/8c08b4fd12e27ac4e1dc4e72af8e9568/8c08b4fd12e27ac4e1dc4e72af8e9568.jpg', + type: 'TV', + duration: '24m', + japaneseTitle: 'Renai Flops', + nsfw: true, + sub: 12, + dub: 7, + episodes: 12 + }, + { + id: 'nurarihyon-no-mago-gekitou-dai-futsal-taikai-nuragumi-w-cup-5796', + title: 'Nurarihyon no Mago: Gekitou Dai Futsal Taikai! Nuragumi W Cup!!', + url: 'https://aniwatch.to/nurarihyon-no-mago-gekitou-dai-futsal-taikai-nuragumi-w-cup-5796', + image: 'https://img.flawlessfiles.com/_r/300x400/100/bd/72/bd722d8e64272fb484a7f48e75eb9716/bd722d8e64272fb484a7f48e75eb9716.jpg', + type: 'Special', + duration: '13m', + japaneseTitle: 'Nurarihyon no Mago: Gekitou Dai Futsal Taikai! Nuragumi W Cup!!', + nsfw: false, + sub: 1, + dub: 0, + episodes: 0 + }, + {...} + ... + ] +} +``` + +### fetchRecentlyUpdated + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| page (optional) | `number` | page number (default 1) | + +```ts +zoro.fetchRecentlyUpdated().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: true, + totalPages: 166, + results: [ + { + id: 'pokemon-horizons-the-series-18397', + title: 'Pokémon Horizons: The Series', + url: 'https://aniwatch.to/pokemon-horizons-the-series-18397', + image: 'https://img.flawlessfiles.com/_r/300x400/100/4b/14/4b145f650126e400b69e783e3d6cdd2a/4b145f650126e400b69e783e3d6cdd2a.jpg', + type: 'TV', + duration: '24m', + japaneseTitle: 'Pokemon (2023)', + nsfw: false, + sub: 35, + dub: 0, + episodes: 0 + }, + { + id: 'bang-brave-bang-bravern-18733', + title: 'Bang Brave Bang Bravern', + url: 'https://aniwatch.to/bang-brave-bang-bravern-18733', + image: 'https://img.flawlessfiles.com/_r/300x400/100/ad/1d/ad1d79d4c929278f23b91f2e787e5a50/ad1d79d4c929278f23b91f2e787e5a50.jpg', + type: 'TV', + duration: '25m', + japaneseTitle: 'Yuuki Bakuhatsu Bang Bravern', + nsfw: false, + sub: 1, + dub: 0, + episodes: 0 + }, + {...} + ... + ] +} +``` + +### fetchRecentlyAdded + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| page (optional) | `number` | page number (default 1) | + +```ts +zoro.fetchRecentlyAdded().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: true, + totalPages: 162, + results: [ + { + id: 'love-flops-18173', + title: 'Love Flops', + url: 'https://aniwatch.to/love-flops-18173', + image: 'https://img.flawlessfiles.com/_r/300x400/100/8c/08/8c08b4fd12e27ac4e1dc4e72af8e9568/8c08b4fd12e27ac4e1dc4e72af8e9568.jpg', + type: 'TV', + duration: '24m', + japaneseTitle: 'Renai Flops', + nsfw: true, + sub: 12, + dub: 7, + episodes: 12 + }, + { + id: 'nurarihyon-no-mago-gekitou-dai-futsal-taikai-nuragumi-w-cup-5796', + title: 'Nurarihyon no Mago: Gekitou Dai Futsal Taikai! Nuragumi W Cup!!', + url: 'https://aniwatch.to/nurarihyon-no-mago-gekitou-dai-futsal-taikai-nuragumi-w-cup-5796', + image: 'https://img.flawlessfiles.com/_r/300x400/100/bd/72/bd722d8e64272fb484a7f48e75eb9716/bd722d8e64272fb484a7f48e75eb9716.jpg', + type: 'Special', + duration: '13m', + japaneseTitle: 'Nurarihyon no Mago: Gekitou Dai Futsal Taikai! Nuragumi W Cup!!', + nsfw: false, + sub: 1, + dub: 0, + episodes: 0 + }, + {...} + ... + ] +} +``` + +### fetchTopUpcoming + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| page (optional) | `number` | page number (default 1) | + +```ts +zoro.fetchTopUpcoming().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: true, + totalPages: 6, + results: [ + { + id: 'bucchigiri-18781', + title: 'Bucchigiri?!', + url: 'https://aniwatch.to/bucchigiri-18781', + image: 'https://img.flawlessfiles.com/_r/300x400/100/72/bf/72bfde46c44a200ff11d82049005d3c8/72bfde46c44a200ff11d82049005d3c8.jpg', + type: 'TV', + duration: 'Jan 13, 2024', + japaneseTitle: 'Bucchigiri?!', + nsfw: false, + sub: 0, + dub: 0, + episodes: 0 + }, + { + id: 'dead-dead-demons-dededede-destruction-18925', + title: 'Dead Dead Demons Dededede Destruction', + url: 'https://aniwatch.to/dead-dead-demons-dededede-destruction-18925', + image: 'https://img.flawlessfiles.com/_r/300x400/100/8d/11/8d112670f41684d97015004293a087dc/8d112670f41684d97015004293a087dc.jpg', + type: 'Movie', + duration: 'Mar 22, 2024', + japaneseTitle: 'Dead Dead Demons Dededede Destruction', + nsfw: false, + sub: 0, + dub: 0, + episodes: 0 + }, + {...} + ... + ] +} +``` +### fetchSchedule + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| date | `string` | Date in format 'YYYY-MM-DD'. Defaults to the current date. | + +```ts +zoro.fetchSchedule('2024-03-11').then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + results: [ + { + id: 'high-card-season-2-18820', + title: 'High Card Season 2', + japaneseTitle: 'High Card Season 2', + url: 'https://hianime.to/high-card-season-2-18820', + airingEpisode: 'Episode 10', + airingTime: '07:30' + }, + { + id: 'tsukimichi-moonlit-fantasy-season-2-18877', + title: 'Tsukimichi -Moonlit Fantasy- Season 2', + japaneseTitle: 'Tsuki ga Michibiku Isekai Douchuu 2nd Season', + url: 'https://hianime.to/tsukimichi-moonlit-fantasy-season-2-18877', + airingEpisode: 'Episode 10', + airingTime: '09:00' + }, + { + id: 'the-foolish-angel-dances-with-the-devil-18832', + title: 'The Foolish Angel Dances with the Devil', + japaneseTitle: 'Oroka na Tenshi wa Akuma to Odoru', + url: 'https://hianime.to/the-foolish-angel-dances-with-the-devil-18832', + airingEpisode: 'Episode 10', + airingTime: '10:30' + }, + { + id: 'synduality-noir-part-2-18754', + title: 'Synduality: Noir Part 2', + japaneseTitle: 'Synduality: Noir Part 2', + url: 'https://hianime.to/synduality-noir-part-2-18754', + airingEpisode: 'Episode 10', + airingTime: '10:30' + }, + { + id: 'tis-time-for-torture-princess-18778', + title: 'Tis Time for "Torture," Princess', + japaneseTitle: 'Himesama "Goumon" no Jikan desu', + url: 'https://hianime.to/tis-time-for-torture-princess-18778', + airingEpisode: 'Episode 10', + airingTime: '11:30' + }, + { + id: 'hokkaido-gals-are-super-adorable-18853', + title: 'Hokkaido Gals Are Super Adorable!', + japaneseTitle: 'Dosanko Gal wa Namara Menkoi', + url: 'https://hianime.to/hokkaido-gals-are-super-adorable-18853', + airingEpisode: 'Episode 10', + airingTime: '11:45' + } + ] +} +``` + +### fetchStudio + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| studio | `string` | studio id, e.g. "toei-animation" | +| page (optional) | `number` | page number (default 1) | + +```ts +zoro.fetchStudio('toei-animation').then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + currentPage: 1, + hasNextPage: true, + totalPages: 9, + results: [ + { + id: 'one-piece-100', + title: 'One Piece', + url: 'https://aniwatch.to/one-piece-100', + image: 'https://img.flawlessfiles.com/_r/300x400/100/54/90/5490cb32786d4f7fef0f40d7266df532/5490cb32786d4f7fef0f40d7266df532.jpg', + type: 'TV', + duration: '24m', + japaneseTitle: 'One Piece', + nsfw: false, + sub: 1089, + dub: 1048, + episodes: 0 + }, + { + id: 'attack-on-titan-the-final-season-part-3-1839', + title: 'Attack on Titan: The Final Season Part 3', + url: 'https://aniwatch.to/attack-on-titan-the-final-season-part-3-18329', + image: 'https://img.flawlessfiles.com/_r/300x400/100/54/d3/54d3f59bcc7caf1539c701eb0a064ec9/54d3f59bcc7caf1539c701eb0a064ec9.png', + type: 'TV', + duration: '61m', + japaneseTitle: 'Shingeki no Kyojin: The Final Season - Kanketsu-hen', + nsfw: true, + sub: 2, + dub: 2, + episodes: 0 + }, + {...} + ... + ] +} +``` + +### fetchSpotlight + +```ts +zoro.fetchSpotlight().then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + + results: [ + { + id: 'delicious-in-dungeon-18506', + title: 'Delicious in Dungeon', + japaneseTitle: 'Dungeon Meshi', + banner: 'https://cdn.noitatnemucod.net/thumbnail/1366x768/100/50affe2ea9a02c36d5a7c0532c1b7ef9.jpeg', + rank: 1, + url: 'https://hianime.to/delicious-in-dungeon-18506', + type: 'TV', + duration: '23m', + releaseDate: 'Jan 4, 2024', + quality: 'HD', + sub: 11, + dub: 10, + episodes: 0, + description: "After the Golden Kingdom is sunk underground by an insane magician, its king emerges, promising all of his treasure to any who defeat the magician, before crumbling to dust. Guilds are spurred on by this promise, traversing the labyrinthine dungeon in search of the magician. Laios, the leader of one such guild, encounters a dragon that wipes out his party and devours his sister Falin. Despite having lost the entirety of their supplies and belongings, Laios along with Marcille, an elven healer, and Chilchuck, a halfling thief, immediately reenter the dungeon, determined to save Falin. Time being of the essence, Laios suggests the taboo of eating the monsters of the dungeon as a means of gathering supplies. Upon the preparation of their first meal in the dungeon, they are stopped by an onlooking dwarf named Senshi. An enthusiast of monster cooking, he helps them prepare their monster ingredients for safe consumption. After learning of Laios' circumstances, Senshi expresses his desire to cook a dragon and joins their guild, thus beginning their food-filled foray into the dungeon together." + }, + {...} + ... + ] +} +``` +### fetchSearchSuggestions + +```ts +zoro.fetchSearchSuggestions("One Piece").then(data => { + console.log(data); +}) +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + results: [ + { + image: 'https://cdn.noitatnemucod.net/thumbnail/300x400/100/ff736656ba002e0dd51363c3d889d9ff.jpg', + id: 'one-piece-movie-1-3096', + title: 'One Piece Movie 1', + japaneseTitle: 'One Piece Movie 1', + aliasTitle: 'One Piece Movie 1', + releaseDate: 'Mar 4, 2000', + type: 'Movie', + duration: '50m', + url: 'https://hianime.to/one-piece-movie-1-3096' + }, + { + image: 'https://cdn.noitatnemucod.net/thumbnail/300x400/100/bcd84731a3eda4f4a306250769675065.jpg', + id: 'one-piece-100', + title: 'One Piece', + japaneseTitle: 'One Piece', + aliasTitle: 'One Piece', + releaseDate: 'Oct 20, 1999', + type: 'TV', + duration: '24m', + url: 'https://hianime.to/one-piece-100' + }, + { + image: 'https://cdn.noitatnemucod.net/thumbnail/300x400/100/a1e98b07e290cd9653b41a895342a377.jpg', + id: 'one-piece-film-red-18236', + title: 'One Piece Film: Red', + japaneseTitle: 'One Piece Film: Red', + aliasTitle: 'One Piece Film: Red', + releaseDate: 'Aug 6, 2022', + type: 'Movie', + duration: '1h 55m', + url: 'https://hianime.to/one-piece-film-red-18236' + }, + { + image: 'https://cdn.noitatnemucod.net/thumbnail/300x400/100/7156c377053c230cc42b66bbf7260325.jpg', + id: 'one-piece-the-movie-13-film-gold-550', + title: 'One Piece: The Movie 13 - Film: Gold', + japaneseTitle: 'One Piece Film: Gold', + aliasTitle: 'One Piece Film: Gold', + releaseDate: 'Jul 23, 2016', + type: 'Movie', + duration: '1h 30m', + url: 'https://hianime.to/one-piece-the-movie-13-film-gold-550' + }, + { + image: 'https://cdn.noitatnemucod.net/thumbnail/300x400/100/14f2be76eee4a497ad81a5039425ff06.jpg', + id: 'one-room-third-season-6959', + title: 'One Room Third Season', + japaneseTitle: 'One Room Third Season', + aliasTitle: 'One Room Third Season', + releaseDate: 'Oct 6, 2020', + type: 'TV', + duration: '4m', + url: 'https://hianime.to/one-room-third-season-6959' + } + ] +} +``` + + +Make sure to check the `headers` property of the returned object. It contains the referer header, which might be needed to bypass the 403 error and allow you to stream the video without any issues. + +

(back to anime providers list)

diff --git a/consumet.ts/examples/batman.ts b/consumet.ts/examples/batman.ts new file mode 100644 index 00000000..e1c740af --- /dev/null +++ b/consumet.ts/examples/batman.ts @@ -0,0 +1,13 @@ +import { COMICS } from ".."; + +const main = async () => { + const getComics = new COMICS.GetComics(); + + const { containers } = await getComics.search("Batman"); + + for (const v of containers) { + console.log(v.title); + } +}; + +main(); diff --git a/consumet.ts/examples/one-piece.ts b/consumet.ts/examples/one-piece.ts new file mode 100644 index 00000000..ebe80f1b --- /dev/null +++ b/consumet.ts/examples/one-piece.ts @@ -0,0 +1,12 @@ +import { ANIME } from ".."; + +const main = async () => { + // Create a new instance of the Gogoanime provider + const gogoanime = new ANIME.Gogoanime(); + // Search for an anime. In this case, "One Piece" + const results = await gogoanime.search("One Piece"); + // print the results + console.log(results); +}; + +main(); diff --git a/consumet.ts/examples/pride-and-prejudice.ts b/consumet.ts/examples/pride-and-prejudice.ts new file mode 100644 index 00000000..e027ddec --- /dev/null +++ b/consumet.ts/examples/pride-and-prejudice.ts @@ -0,0 +1,13 @@ +import { BOOKS } from ".."; + +const main = async () => { + const books = new BOOKS.Libgen(); + + const res = await books.search("pride and prejudice"); + + for (const v of res) { + console.log(v.title); + } +}; + +main(); diff --git a/consumet.ts/examples/saikyou-onmyouji-no-isekai-tenseiki.ts b/consumet.ts/examples/saikyou-onmyouji-no-isekai-tenseiki.ts new file mode 100644 index 00000000..a1b09b2a --- /dev/null +++ b/consumet.ts/examples/saikyou-onmyouji-no-isekai-tenseiki.ts @@ -0,0 +1,22 @@ +import { ANIME } from "../src"; + +const main = async () => { + // Create a new instance of the Gogoanime provider + const gogoanime = new ANIME.Gogoanime(); + const animeId = "saikyou-onmyouji-no-isekai-tenseiki"; + try { + // try to get anime info + const results = await gogoanime.fetchAnimeInfo(animeId); + console.log(results); + return results; + } catch { + // get new id and try again (default will be episode 1 i think) + const anime_id = await gogoanime.fetchAnimeIdFromEpisodeId( + animeId + "-episode-4" + ); + const results = await gogoanime.fetchAnimeInfo(anime_id); + console.log(results); + } +}; + +main(); diff --git a/consumet.ts/examples/solo-leveling.ts b/consumet.ts/examples/solo-leveling.ts new file mode 100644 index 00000000..37e0c0c6 --- /dev/null +++ b/consumet.ts/examples/solo-leveling.ts @@ -0,0 +1,18 @@ +import { MANGA } from ".."; + +const main = async () => { + const getManga = new MANGA.AsuraScans(); + + const resultsSearch = await getManga.search("hero returns"); + console.log(resultsSearch); + + const results = await getManga.fetchMangaInfo("1948049029-the-hero-returns"); + console.log(results); + + const resultsPage = await getManga.fetchChapterPages( + "4675140488-the-hero-returns-chapter-1" + ); + console.log(resultsPage); +}; + +main(); diff --git a/consumet.ts/jest.config.ts b/consumet.ts/jest.config.ts new file mode 100644 index 00000000..65d2a16d --- /dev/null +++ b/consumet.ts/jest.config.ts @@ -0,0 +1,6 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + testMatch: ["**/test/**/*.test.ts"], +}; diff --git a/consumet.ts/package.json b/consumet.ts/package.json new file mode 100644 index 00000000..9d7ad81d --- /dev/null +++ b/consumet.ts/package.json @@ -0,0 +1,97 @@ +{ + "name": "@consumet/extensions", + "version": "1.6.0", + "description": "Nodejs library that provides high-level APIs for obtaining information on various entertainment media such as books, movies, comic books, anime, manga, and so on.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "rimraf dist && tsc -p tsconfig.json", + "version": "auto-changelog -p && git add CHANGELOG.md", + "lint": "prettier --write .", + "prepare": "husky install", + "test": "jest", + "test:anime": "jest ./test/anime", + "test:books": "jest ./test/books/libgen.test.ts", + "test:comics": "jest ./test/comics", + "test:movies": "jest ./test/movies", + "test:manga": "jest ./test/manga", + "test:lightnovels": "jest ./test/light-novels", + "test:news": "jest ./test/news", + "test:meta": "jest ./test/meta" + }, + "pre-commit": [ + "lint", + "build" + ], + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/consumet/consumet.ts.git" + }, + "keywords": [ + "consumet", + "scraper", + "streaming", + "anime", + "books", + "comics", + "movies", + "manga", + "light-novels", + "news", + "meta" + ], + "authors": [ + "https://github.com/riimuru", + "https://github.com/prince-ao" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/consumet/consumet.ts/issues" + }, + "homepage": "https://github.com/consumet/consumet.ts#readme", + "dependencies": { + "ascii-url-encoder": "^1.2.0", + "axios": "^0.27.2", + "cheerio": "^1.0.0-rc.12", + "crypto-js": "^4.1.1", + "form-data": "^4.0.0" + }, + "devDependencies": { + "@commitlint/cli": "^17.6.3", + "@commitlint/config-angular": "^17.6.3", + "@types/crypto-js": "^4.1.1", + "@types/jest": "^29.5.1", + "@types/node": "^20.2.1", + "@types/ws": "^8.5.4", + "auto-changelog": "^2.4.0", + "cloudscraper": "^4.6.0", + "husky": "^8.0.3", + "is-ci": "^3.0.1", + "jest": "^29.5.0", + "pre-commit": "^1.2.2", + "prettier": "^2.8.8", + "request": "^2.88.2", + "rimraf": "^5.0.5", + "ts-jest": "^29.1.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + }, + "auto-changelog": { + "template": "keepachangelog", + "preset": "typescript-preset-eslint", + "output": "CHANGELOG.md", + "commitUrl": "https://github.com/consumet/consumet.ts/commit/{id}", + "compareUrl": "https://github.com/consumet/consumet.ts/compare/{from}...{to}", + "issueUrl": "https://github.com/consumet/consumet.ts/issues/{id}", + "mergeUrl": "https://github.com/consumet/consumet.ts/pull/{id}" + }, + "directories": { + "doc": "docs", + "example": "examples", + "test": "test", + "lib": "src" + } +} diff --git a/consumet.ts/src/extractors/asianload.ts b/consumet.ts/src/extractors/asianload.ts new file mode 100644 index 00000000..9ae46ceb --- /dev/null +++ b/consumet.ts/src/extractors/asianload.ts @@ -0,0 +1,85 @@ +import { CheerioAPI, load } from 'cheerio'; +import CryptoJS from 'crypto-js'; + +import { VideoExtractor, IVideo, ISubtitle } from '../models'; + +class AsianLoad extends VideoExtractor { + protected override serverName = 'asianload'; + protected override sources: IVideo[] = []; + + private readonly keys = { + key: CryptoJS.enc.Utf8.parse('93422192433952489752342908585752'), + iv: CryptoJS.enc.Utf8.parse('9262859232435825'), + }; + + override extract = async (videoUrl: URL): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> => { + const res = await this.client.get(videoUrl.href); + const $ = load(res.data); + + const encyptedParams = await this.generateEncryptedAjaxParams($, videoUrl.searchParams.get('id') ?? ''); + + const encryptedData = await this.client.get( + `${videoUrl.protocol}//${videoUrl.hostname}/encrypt-ajax.php?${encyptedParams}`, + { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + } + ); + + const decryptedData = await this.decryptAjaxData(encryptedData.data.data); + + if (!decryptedData.source) throw new Error('No source found. Try a different server.'); + + decryptedData.source.forEach((source: any) => { + this.sources.push({ + url: source.file, + isM3U8: source.file.includes('.m3u8'), + }); + }); + + decryptedData.source_bk.forEach((source: any) => { + this.sources.push({ + url: source.file, + isM3U8: source.file.includes('.m3u8'), + }); + }); + + const subtitles = decryptedData.track?.tracks?.map( + (track: any): ISubtitle => ({ + url: track.file, + lang: track.kind === 'thumbnails' ? 'Default (maybe)' : track.kind, + }) + ); + + return { + sources: this.sources, + subtitles: subtitles, + }; + }; + private generateEncryptedAjaxParams = async ($: CheerioAPI, id: string): Promise => { + const encryptedKey = CryptoJS.AES.encrypt(id, this.keys.key, { + iv: this.keys.iv, + }).toString(); + + const scriptValue = $("script[data-name='crypto']").data().value as string; + + const decryptedToken = CryptoJS.AES.decrypt(scriptValue, this.keys.key, { + iv: this.keys.iv, + }).toString(CryptoJS.enc.Utf8); + + return `id=${encryptedKey}&alias=${decryptedToken}`; + }; + + private decryptAjaxData = async (encryptedData: string): Promise => { + const decryptedData = CryptoJS.enc.Utf8.stringify( + CryptoJS.AES.decrypt(encryptedData, this.keys.key, { + iv: this.keys.iv, + }) + ); + + return JSON.parse(decryptedData); + }; +} + +export default AsianLoad; diff --git a/consumet.ts/src/extractors/bilibili.ts b/consumet.ts/src/extractors/bilibili.ts new file mode 100644 index 00000000..94b037e8 --- /dev/null +++ b/consumet.ts/src/extractors/bilibili.ts @@ -0,0 +1,82 @@ +import { convertDuration } from '../utils/utils'; +import { ISource, IVideo, VideoExtractor } from '../models'; + +class BilibiliExtractor extends VideoExtractor { + protected override serverName = 'Bilibili'; + protected override sources: IVideo[] = []; + + override async extract(episodeId: any): Promise { + this.sources.push({ + url: `https://api.consumet.org/utils/bilibili/playurl?episode_id=${episodeId}`, + isM3U8: false, + isDASH: true, + }); + return { + sources: this.sources, + }; + } + + toDash = (data: any) => { + const videos = data.video + .filter((video: any) => video.video_resource.url) + .map((video: any) => video.video_resource); + + const audios = data.audio_resource; + + const duration = convertDuration(data.duration); + + const dash = ` + + + + ${videos.map((video: any, index: any) => this.videoSegment(video, index)).join()} + + + ${audios.map((audio: any, index: any) => this.audioSegment(audio, videos.length + index)).join()} + + +`; + + return dash; + }; + + videoSegment = (video: any, index = 0) => { + const allUrls = [video.url, video.backup_url[0]]; + + const videoUrl = allUrls.find(url => url.includes('akamaized.net')); + + return ` + + ${videoUrl.replace(/&/g, '&')} + + + + + `; + }; + + audioSegment = (audio: any, index = 0) => { + const allUrls = [audio.url, audio.backup_url[0]]; + + const audioUrl = allUrls.find(url => url.includes('akamaized.net')); + + return ` + + ${audioUrl.replace(/&/g, '&')} + + + + + `; + }; +} + +export default BilibiliExtractor; diff --git a/consumet.ts/src/extractors/filemoon.ts b/consumet.ts/src/extractors/filemoon.ts new file mode 100644 index 00000000..7ef5bb77 --- /dev/null +++ b/consumet.ts/src/extractors/filemoon.ts @@ -0,0 +1,36 @@ +import { load } from 'cheerio'; + +import { VideoExtractor, IVideo, ISubtitle, Intro } from '../models'; +import { USER_AGENT } from '../utils'; +import { Console } from 'console'; + +/** + * work in progress + */ +class Filemoon extends VideoExtractor { + protected override serverName = 'Filemoon'; + protected override sources: IVideo[] = []; + + private readonly host = 'https://filemoon.sx'; + + override extract = async (videoUrl: URL): Promise => { + const options = { + headers: { + Referer: videoUrl.href, + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': USER_AGENT, + 'X-Requested-With': 'XMLHttpRequest', + }, + }; + + const { data } = await this.client.get(videoUrl.href); + + const s = data.substring(data.indexOf('eval(function') + 5, data.lastIndexOf(')))')); + try { + const newScript = 'function run(' + s.split('function(')[1] + '))'; + } catch (err) {} + return this.sources; + }; +} + +export default Filemoon; diff --git a/consumet.ts/src/extractors/gogocdn.ts b/consumet.ts/src/extractors/gogocdn.ts new file mode 100644 index 00000000..8203d3a1 --- /dev/null +++ b/consumet.ts/src/extractors/gogocdn.ts @@ -0,0 +1,143 @@ +import { CheerioAPI, load } from 'cheerio'; +import CryptoJS from 'crypto-js'; + +import { VideoExtractor, IVideo, ProxyConfig } from '../models'; +import { USER_AGENT } from '../utils'; + +class GogoCDN extends VideoExtractor { + protected override serverName = 'goload'; + protected override sources: IVideo[] = []; + + private readonly keys = { + key: CryptoJS.enc.Utf8.parse('37911490979715163134003223491201'), + secondKey: CryptoJS.enc.Utf8.parse('54674138327930866480207815084989'), + iv: CryptoJS.enc.Utf8.parse('3134003223491201'), + }; + + private referer: string = ''; + + override extract = async (videoUrl: URL): Promise => { + this.referer = videoUrl.href; + + const res = await this.client.get(videoUrl.href); + const $ = load(res.data); + + const encyptedParams = await this.generateEncryptedAjaxParams($, videoUrl.searchParams.get('id') ?? ''); + + const encryptedData = await this.client.get( + `${videoUrl.protocol}//${videoUrl.hostname}/encrypt-ajax.php?${encyptedParams}`, + { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + } + ); + + const decryptedData = await this.decryptAjaxData(encryptedData.data.data); + if (!decryptedData.source) throw new Error('No source found. Try a different server.'); + + if (decryptedData.source[0].file.includes('.m3u8')) { + const resResult = await this.client.get(decryptedData.source[0].file.toString()); + const resolutions = resResult.data.match(/(RESOLUTION=)(.*)(\s*?)(\s*.*)/g); + resolutions?.forEach((res: string) => { + const index = decryptedData.source[0].file.lastIndexOf('/'); + const quality = res.split('\n')[0].split('x')[1].split(',')[0]; + const url = decryptedData.source[0].file.slice(0, index); + this.sources.push({ + url: url + '/' + res.split('\n')[1], + isM3U8: (url + res.split('\n')[1]).includes('.m3u8'), + quality: quality + 'p', + }); + }); + + decryptedData.source.forEach((source: any) => { + this.sources.push({ + url: source.file, + isM3U8: source.file.includes('.m3u8'), + quality: 'default', + }); + }); + } else + decryptedData.source.forEach((source: any) => { + this.sources.push({ + url: source.file, + isM3U8: source.file.includes('.m3u8'), + quality: source.label.split(' ')[0] + 'p', + }); + }); + + decryptedData.source_bk.forEach((source: any) => { + this.sources.push({ + url: source.file, + isM3U8: source.file.includes('.m3u8'), + quality: 'backup', + }); + }); + + return this.sources; + }; + + private addSources = async (source: any) => { + if (source.file.includes('m3u8')) { + const m3u8Urls = await this.client + .get(source.file, { + headers: { + Referer: this.referer, + 'User-Agent': USER_AGENT, + }, + }) + .catch(() => null); + + const videoList = m3u8Urls?.data.split('#EXT-X-I-FRAME-STREAM-INF:'); + for (const video of videoList ?? []) { + if (!video.includes('m3u8')) continue; + + const url = video + .split('\n') + .find((line: any) => line.includes('URI=')) + .split('URI=')[1] + .replace(/"/g, ''); + + const quality = video.split('RESOLUTION=')[1].split(',')[0].split('x')[1]; + + this.sources.push({ + url: url, + quality: `${quality}p`, + isM3U8: true, + }); + } + + return; + } + this.sources.push({ + url: source.file, + isM3U8: source.file.includes('.m3u8'), + }); + }; + + private generateEncryptedAjaxParams = async ($: CheerioAPI, id: string): Promise => { + const encryptedKey = CryptoJS.AES.encrypt(id, this.keys.key, { + iv: this.keys.iv, + }); + + const scriptValue = $("script[data-name='episode']").attr('data-value') as string; + + const decryptedToken = CryptoJS.AES.decrypt(scriptValue, this.keys.key, { + iv: this.keys.iv, + }).toString(CryptoJS.enc.Utf8); + + return `id=${encryptedKey}&alias=${id}&${decryptedToken}`; + }; + + private decryptAjaxData = async (encryptedData: string): Promise => { + const decryptedData = CryptoJS.enc.Utf8.stringify( + CryptoJS.AES.decrypt(encryptedData, this.keys.secondKey, { + iv: this.keys.iv, + }) + ); + + return JSON.parse(decryptedData); + }; +} + +export default GogoCDN; diff --git a/consumet.ts/src/extractors/index.ts b/consumet.ts/src/extractors/index.ts new file mode 100644 index 00000000..5272809b --- /dev/null +++ b/consumet.ts/src/extractors/index.ts @@ -0,0 +1,43 @@ +import AsianLoad from './asianload'; +import BilibiliExtractor from './bilibili'; +import Filemoon from './filemoon'; +import GogoCDN from './gogocdn'; +import Kwik from './kwik'; +import MixDrop from './mixdrop'; +import Mp4Player from './mp4player'; +import Mp4Upload from './mp4upload'; +import RapidCloud from './rapidcloud'; +import MegaCloud from './megacloud'; +import SmashyStream from './smashystream'; +import StreamHub from './streamhub'; +import StreamLare from './streamlare'; +import StreamSB from './streamsb'; +import StreamTape from './streamtape'; +import StreamWish from './streamwish'; +import VidCloud from './vidcloud'; +import VidMoly from './vidmoly'; +import VizCloud from './vizcloud'; +import Voe from './voe'; + +export { + AsianLoad, + BilibiliExtractor, + Filemoon, + GogoCDN, + Kwik, + MixDrop, + Mp4Player, + Mp4Upload, + RapidCloud, + MegaCloud, + SmashyStream, + StreamHub, + StreamLare, + StreamSB, + StreamTape, + StreamWish, + VidCloud, + VidMoly, + VizCloud, + Voe, +}; diff --git a/consumet.ts/src/extractors/kwik.ts b/consumet.ts/src/extractors/kwik.ts new file mode 100644 index 00000000..485b795a --- /dev/null +++ b/consumet.ts/src/extractors/kwik.ts @@ -0,0 +1,30 @@ +import { VideoExtractor, IVideo } from '../models'; + +class Kwik extends VideoExtractor { + protected override serverName = 'kwik'; + protected override sources: IVideo[] = []; + + private readonly host = 'https://animepahe.com'; + + override extract = async (videoUrl: URL): Promise => { + try { + const { data } = await this.client.get(`${videoUrl.href}`, { + headers: { Referer: this.host }, + }); + + const source = eval(/(eval)(\(f.*?)(\n<\/script>)/s.exec(data)![2].replace('eval', '')).match( + /https.*?m3u8/ + ); + + this.sources.push({ + url: source[0], + isM3U8: source[0].includes('.m3u8'), + }); + + return this.sources; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} +export default Kwik; diff --git a/consumet.ts/src/extractors/megacloud.ts b/consumet.ts/src/extractors/megacloud.ts new file mode 100644 index 00000000..5f5139eb --- /dev/null +++ b/consumet.ts/src/extractors/megacloud.ts @@ -0,0 +1,230 @@ +import crypto from "crypto"; +import { IVideo, ISubtitle, Intro, VideoExtractor } from '../models'; + +const megacloud = { + script: "https://megacloud.tv/js/player/a/prod/e1-player.min.js?v=", + sources: "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=", +} as const; + +type tracks = { + file: string; + kind: string; + label?: string; + default?: boolean; +}; + +type unencrypSources = { + file: string; + type: string; +}; + +type apiFormat = { + sources: string | unencrypSources[]; + tracks: tracks[]; + encrypted: boolean; + intro: Intro; + outro: Intro; + server: number; +}; + +class MegaCloud extends VideoExtractor { + + protected override serverName = 'MegaCloud'; + protected override sources: IVideo[] = []; + + async extract(videoUrl: URL) { + try { + const result: { + sources: IVideo[]; + subtitles: ISubtitle[]; + intro?: Intro; + outro?: Intro + } = { + sources: [], + subtitles: [], + }; + + const videoId = videoUrl?.href?.split("/")?.pop()?.split("?")[0]; + const { data: srcsData } = await this.client.get( + megacloud.sources.concat(videoId || ""), + { + headers: { + Accept: "*/*", + "X-Requested-With": "XMLHttpRequest", + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + Referer: videoUrl.href, + }, + } + ); + if (!srcsData) { + throw new Error("Url may have an invalid video id"); + } + + const encryptedString = srcsData.sources; + if (srcsData.encrypted && Array.isArray(encryptedString)) { + result.intro = srcsData.intro; + result.outro = srcsData.outro; + result.subtitles = srcsData.tracks.map((s: any) => ({ + url: s.file, + lang: s.label ? s.label : 'Thumbnails', + })); + result.sources = encryptedString.map((s) => ({ + url: s.file, + type: s.type, + isM3U8: s.file.includes('.m3u8'), + })); + + return result; + } + + const { data } = await this.client.get( + megacloud.script.concat(Date.now().toString()) + ); + + const text = data; + if (!text) + throw new Error("Couldn't fetch script to decrypt resource"); + + const vars = this.extractVariables(text, "MEGACLOUD"); + + const { secret, encryptedSource } = this.getSecret( + encryptedString as string, + vars + ); + const decrypted = this.decrypt(encryptedSource, secret); + try { + const sources = JSON.parse(decrypted); + result.intro = srcsData.intro; + result.outro = srcsData.outro; + result.subtitles = srcsData.tracks.map((s: any) => ({ + url: s.file, + lang: s.label ? s.label : 'Thumbnails', + })); + result.sources = sources.map((s: any) => ({ + url: s.file, + type: s.type, + isM3U8: s.file.includes('.m3u8'), + })); + + return result; + } catch (error) { + throw new Error("Failed to decrypt resource"); + } + } catch (err) { + throw err; + } + } + + extractVariables(text: string, sourceName: string) { + let allvars; + if (sourceName !== "MEGACLOUD") { + allvars = + text + .match( + /const (?:\w{1,2}=(?:'.{0,50}?'|\w{1,2}\(.{0,20}?\)).{0,20}?,){7}.+?;/gm + ) + ?.at(-1) ?? ""; + } else { + allvars = + text + .match(/\w{1,2}=new URLSearchParams.+?;(?=function)/gm) + ?.at(1) ?? ""; + } + const vars = allvars + .slice(0, -1) + .split("=") + .slice(1) + .map((pair) => Number(pair.split(",").at(0))) + .filter((num) => num === 0 || num); + + return vars; + } + + getSecret(encryptedString: string, values: number[]) { + let secret = "", + encryptedSource = encryptedString, + totalInc = 0; + + for (let i = 0; i < values[0]!; i++) { + let start, inc; + switch (i) { + case 0: + (start = values[2]), (inc = values[1]); + break; + case 1: + (start = values[4]), (inc = values[3]); + break; + case 2: + (start = values[6]), (inc = values[5]); + break; + case 3: + (start = values[8]), (inc = values[7]); + break; + case 4: + (start = values[10]), (inc = values[9]); + break; + case 5: + (start = values[12]), (inc = values[11]); + break; + case 6: + (start = values[14]), (inc = values[13]); + break; + case 7: + (start = values[16]), (inc = values[15]); + break; + case 8: + (start = values[18]), (inc = values[17]); + } + const from = start! + totalInc, + to = from + inc!; + (secret += encryptedString.slice(from, to)), + (encryptedSource = encryptedSource.replace( + encryptedString.substring(from, to), + "" + )), + (totalInc += inc!); + } + + return { secret, encryptedSource }; + } + + decrypt(encrypted: string, keyOrSecret: string, maybe_iv?: string) { + let key; + let iv; + let contents; + if (maybe_iv) { + key = keyOrSecret; + iv = maybe_iv; + contents = encrypted; + } else { + const cypher = Buffer.from(encrypted, "base64"); + const salt = cypher.subarray(8, 16); + const password = Buffer.concat([ + Buffer.from(keyOrSecret, "binary"), + salt, + ]); + const md5Hashes = []; + let digest = password; + for (let i = 0; i < 3; i++) { + md5Hashes[i] = crypto.createHash("md5").update(digest).digest(); + digest = Buffer.concat([md5Hashes[i], password]); + } + key = Buffer.concat([md5Hashes[0], md5Hashes[1]]); + iv = md5Hashes[2]; + contents = cypher.subarray(16); + } + + const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); + const decrypted = + decipher.update( + contents as any, + typeof contents === "string" ? "base64" : undefined, + "utf8" + ) + decipher.final(); + + return decrypted; + } +} + +export default MegaCloud; diff --git a/consumet.ts/src/extractors/mixdrop.ts b/consumet.ts/src/extractors/mixdrop.ts new file mode 100644 index 00000000..6e166daa --- /dev/null +++ b/consumet.ts/src/extractors/mixdrop.ts @@ -0,0 +1,30 @@ +import { VideoExtractor, IVideo } from '../models'; + +class MixDrop extends VideoExtractor { + protected override serverName = 'MixDrop'; + protected override sources: IVideo[] = []; + + override extract = async (videoUrl: URL): Promise => { + try { + const { data } = await this.client.get(videoUrl.href); + + const formated = eval(/(eval)(\(f.*?)(\n<\/script>)/s.exec(data)![2].replace('eval', '')); + + const [poster, source] = formated + .match(/poster="([^"]+)"|wurl="([^"]+)"/g) + .map((x: string) => x.split(`="`)[1].replace(/"/g, '')) + .map((x: string) => (x.startsWith('http') ? x : `https:${x}`)); + + this.sources.push({ + url: source, + isM3U8: source.includes('.m3u8'), + poster: poster, + }); + + return this.sources; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} +export default MixDrop; diff --git a/consumet.ts/src/extractors/mp4player.ts b/consumet.ts/src/extractors/mp4player.ts new file mode 100644 index 00000000..e1dc6df0 --- /dev/null +++ b/consumet.ts/src/extractors/mp4player.ts @@ -0,0 +1,55 @@ +import { VideoExtractor, IVideo, ISubtitle } from '../models'; + +class Mp4Player extends VideoExtractor { + protected override serverName = 'mp4player'; + protected override sources: IVideo[] = []; + + private readonly domains = ['mp4player.site']; + + override extract = async (videoUrl: URL): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> => { + try { + const result: { sources: IVideo[]; subtitles: ISubtitle[] } = { + sources: [], + subtitles: [], + }; + + const response = await this.client.get(videoUrl.href); + + const data = response.data + .match(new RegExp('(?<=sniff\\()(.*)(?=\\))'))[0] + ?.replace(/\"/g, '') + ?.split(','); + + const link = `https://${videoUrl.host}/m3u8/${data[1]}/${data[2]}/master.txt?s=1&cache=${data[7]}`; + //const thumbnails = response.data.match(new RegExp('(?<=file":")(.*)(?=","kind)'))[0]?.replace(/\\/g, ''); + + const m3u8Content = await this.client.get(link, { + headers: { + accept: '*/*', + referer: videoUrl.href, + }, + }); + + if (m3u8Content.data.includes('EXTM3U')) { + const videoList = m3u8Content.data.split('#EXT-X-STREAM-INF:'); + for (const video of videoList ?? []) { + if (video.includes('BANDWIDTH')) { + const url = video.split('\n')[1]; + const quality = video.split('RESOLUTION=')[1].split('\n')[0].split('x')[1]; + + result.sources.push({ + url: url, + quality: `${quality}`, + isM3U8: url.includes('.m3u8'), + }); + } + } + } + + return result; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} +export default Mp4Player; diff --git a/consumet.ts/src/extractors/mp4upload.ts b/consumet.ts/src/extractors/mp4upload.ts new file mode 100644 index 00000000..d326f402 --- /dev/null +++ b/consumet.ts/src/extractors/mp4upload.ts @@ -0,0 +1,30 @@ +import { VideoExtractor, IVideo } from '../models'; + +class Mp4Upload extends VideoExtractor { + protected override serverName = 'mp4upload'; + protected override sources: IVideo[] = []; + + override extract = async (videoUrl: URL): Promise => { + try { + const { data } = await this.client.get(videoUrl.href); + + const playerSrc = data.match( + /(?<=player\.src\()\s*{\s*type:\s*"[^"]+",\s*src:\s*"([^"]+)"\s*}\s*(?=\);)/s + ); + const streamUrl = playerSrc[1]; + + if (!streamUrl) throw new Error('Stream url not found'); + + this.sources.push({ + quality: 'auto', + url: streamUrl, + isM3U8: streamUrl.includes('.m3u8'), + }); + + return this.sources; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} +export default Mp4Upload; diff --git a/consumet.ts/src/extractors/rapidcloud.ts b/consumet.ts/src/extractors/rapidcloud.ts new file mode 100644 index 00000000..6d4a0fb9 --- /dev/null +++ b/consumet.ts/src/extractors/rapidcloud.ts @@ -0,0 +1,203 @@ +import { load } from 'cheerio'; +import CryptoJS from 'crypto-js'; +import { substringAfter, substringBefore } from '../utils'; +import { VideoExtractor, IVideo, ISubtitle, Intro, ProxyConfig } from '../models'; + +class RapidCloud extends VideoExtractor { + protected override serverName = 'RapidCloud'; + protected override sources: IVideo[] = []; + + private readonly fallbackKey = 'c1d17096f2ca11b7'; + private readonly host = 'https://rapid-cloud.co'; + + override extract = async (videoUrl: URL): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> => { + const result: { sources: IVideo[]; subtitles: ISubtitle[]; intro?: Intro; outro?: Intro } = { + sources: [], + subtitles: [], + }; + try { + const id = videoUrl.href.split('/').pop()?.split('?')[0]; + const options = { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + }; + + let res = null; + + res = await this.client.get( + `https://${videoUrl.hostname}/embed-2/ajax/e-1/getSources?id=${id}`, + options + ); + + let { + data: { sources, tracks, intro, outro, encrypted }, + } = res; + + let decryptKey = await ( + await this.client.get('https://raw.githubusercontent.com/cinemaxhq/keys/e1/key') + ).data; + + decryptKey = substringBefore( + substringAfter(decryptKey, '"blob-code blob-code-inner js-file-line">'), + '' + ); + + if (!decryptKey) { + decryptKey = await ( + await this.client.get('https://raw.githubusercontent.com/cinemaxhq/keys/e1/key') + ).data; + } + + if (!decryptKey) decryptKey = this.fallbackKey; + + try { + if (encrypted) { + const sourcesArray = sources.split(''); + + let extractedKey = ''; + let currentIndex = 0; + for (const index of decryptKey) { + const start = index[0] + currentIndex; + const end = start + index[1]; + for (let i = start; i < end; i++) { + extractedKey += res.data.sources[i]; + sourcesArray[i] = ''; + } + currentIndex += index[1]; + } + + decryptKey = extractedKey; + sources = sourcesArray.join(''); + + const decrypt = CryptoJS.AES.decrypt(sources, decryptKey); + sources = JSON.parse(decrypt.toString(CryptoJS.enc.Utf8)); + } + } catch (err) { + throw new Error('Cannot decrypt sources. Perhaps the key is invalid.'); + } + this.sources = sources?.map((s: any) => ({ + url: s.file, + isM3U8: s.file.includes('.m3u8'), + })); + + result.sources.push(...this.sources); + + if (videoUrl.href.includes(new URL(this.host).host)) { + result.sources = []; + this.sources = []; + for (const source of sources) { + const { data } = await this.client.get(source.file, options); + const m3u8data = data + .split('\n') + .filter((line: string) => line.includes('.m3u8') && line.includes('RESOLUTION=')); + + const secondHalf = m3u8data.map((line: string) => + line.match(/RESOLUTION=.*,(C)|URI=.*/g)?.map((s: string) => s.split('=')[1]) + ); + + const TdArray = secondHalf.map((s: string[]) => { + const f1 = s[0].split(',C')[0]; + const f2 = s[1].replace(/"/g, ''); + + return [f1, f2]; + }); + for (const [f1, f2] of TdArray) { + this.sources.push({ + url: `${source.file?.split('master.m3u8')[0]}${f2.replace('iframes', 'index')}`, + quality: f1.split('x')[1] + 'p', + isM3U8: f2.includes('.m3u8'), + }); + } + result.sources.push(...this.sources); + } + } + + result.intro = intro?.end > 1 ? { start: intro.start, end: intro.end } : undefined; + result.outro = outro?.end > 1 ? { start: outro.start, end: outro.end } : undefined; + + result.sources.push({ + url: sources[0].file, + isM3U8: sources[0].file.includes('.m3u8'), + quality: 'auto', + }); + + result.subtitles = tracks + .map((s: any) => + s.file + ? { + url: s.file, + lang: s.label ? s.label : 'Thumbnails', + } + : null + ) + .filter((s: any) => s); + + return result; + } catch (err) { + throw err; + } + }; + + private captcha = async (url: string, key: string): Promise => { + const uri = new URL(url); + const domain = uri.protocol + '//' + uri.host; + + const { data } = await this.client.get(`https://www.google.com/recaptcha/api.js?render=${key}`, { + headers: { + Referer: domain, + }, + }); + + const v = data + ?.substring(data.indexOf('/releases/'), data.lastIndexOf('/recaptcha')) + .split('/releases/')[1]; + + //TODO: NEED to fix the co (domain) parameter to work with every domain + const anchor = `https://www.google.com/recaptcha/api2/anchor?ar=1&hl=en&size=invisible&cb=kr42069kr&k=${key}&co=aHR0cHM6Ly9yYXBpZC1jbG91ZC5ydTo0NDM.&v=${v}`; + const c = load((await this.client.get(anchor)).data)('#recaptcha-token').attr('value'); + + // currently its not returning proper response. not sure why + const res = await this.client.post( + `https://www.google.com/recaptcha/api2/reload?k=${key}`, + { + v: v, + k: key, + c: c, + co: 'aHR0cHM6Ly9yYXBpZC1jbG91ZC5ydTo0NDM.', + sa: '', + reason: 'q', + }, + { + headers: { + Referer: anchor, + }, + } + ); + + return res.data.substring(res.data.indexOf('rresp","'), res.data.lastIndexOf('",null')); + }; + + // private wss = async (): Promise => { + // let sId = ''; + + // const ws = new WebSocket('wss://ws1.rapid-cloud.ru/socket.io/?EIO=4&transport=websocket'); + + // ws.on('open', () => { + // ws.send('40'); + // }); + + // return await new Promise((resolve, reject) => { + // ws.on('message', (data: string) => { + // data = data.toString(); + // if (data?.startsWith('40')) { + // sId = JSON.parse(data.split('40')[1]).sid; + // ws.close(4969, "I'm a teapot"); + // resolve(sId); + // } + // }); + // }); + // }; +} + +export default RapidCloud; diff --git a/consumet.ts/src/extractors/smashystream.ts b/consumet.ts/src/extractors/smashystream.ts new file mode 100644 index 00000000..cd63eeaf --- /dev/null +++ b/consumet.ts/src/extractors/smashystream.ts @@ -0,0 +1,358 @@ +import crypto from 'crypto'; +import { VideoExtractor, IVideo, ISubtitle } from '../models'; +import { load } from 'cheerio'; + +// Copied form https://github.com/JorrinKievit/restreamer/blob/main/src/main/extractors/smashystream.ts/smashystream.ts +// Thanks Jorrin Kievit +class SmashyStream extends VideoExtractor { + protected override serverName = 'SmashyStream'; + protected override sources: IVideo[] = []; + + private readonly host = 'https://embed.smashystream.com'; + + override extract = async (videoUrl: URL): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> => { + try { + const result: { + source: string; + data: { sources: IVideo[] } & { subtitles: ISubtitle[] }; + }[] = []; + + const { data } = await this.client.get(videoUrl.href); + const $ = load(data); + + const sourceUrls = $('.dropdown-menu a[data-id]') + .map((_, el) => $(el).attr('data-id')) + .get() + .filter(it => it !== '_default'); + + await Promise.all( + sourceUrls.map(async sourceUrl => { + if (sourceUrl.includes('/ffix')) { + const data = await this.extractSmashyFfix(sourceUrl); + result.push({ + source: 'FFix', + data: data, + }); + } + + if (sourceUrl.includes('/watchx')) { + const data = await this.extractSmashyWatchX(sourceUrl); + result.push({ + source: 'WatchX', + data: data, + }); + } + + if (sourceUrl.includes('/nflim')) { + const data = await this.extractSmashyNFlim(sourceUrl); + result.push({ + source: 'NFilm', + data: data, + }); + } + + if (sourceUrl.includes('/fx')) { + const data = await this.extractSmashyFX(sourceUrl); + result.push({ + source: 'FX', + data: data, + }); + } + + if (sourceUrl.includes('/cf')) { + const data = await this.extractSmashyCF(sourceUrl); + result.push({ + source: 'CF', + data: data, + }); + } + + if (sourceUrl.includes('eemovie')) { + const data = await this.extractSmashyEEMovie(sourceUrl); + result.push({ + source: 'EEMovie', + data: data, + }); + } + + return undefined; + }) + ); + + return result.filter(a => a.source === 'FFix')[0].data; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + async extractSmashyFfix(url: string): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> { + try { + const result: { sources: IVideo[]; subtitles: ISubtitle[] } = { + sources: [], + subtitles: [], + }; + + const res = await this.client.get(url, { + headers: { + referer: url, + }, + }); + const config = JSON.parse(res.data.match(/var\s+config\s*=\s*({.*?});/)[1]); + + const files = config.file + .match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/g) + .map((entry: { match: (arg0: RegExp) => [string, string, string] }) => { + const [, quality, link] = entry.match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/); + return { quality, link: link.replace(',', '') }; + }); + + const vttArray = config.subtitle + .match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/g) + .map((entry: { match: (arg0: RegExp) => [string, string, string] }) => { + const [, language, link] = entry.match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/); + return { language, link: link.replace(',', '') }; + }); + + files.map((source: { link: string; quality: string }) => { + result.sources.push({ + url: source.link, + quality: source.quality, + isM3U8: source.link.includes('.m3u8'), + }); + }); + + vttArray.map((subtitle: { language: string; link: string }) => { + result.subtitles.push({ + url: subtitle.link, + lang: subtitle.language, + }); + }); + + return result; + } catch (err) { + throw new Error((err as Error).message); + } + } + + async extractSmashyWatchX(url: string): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> { + try { + const result: { sources: IVideo[]; subtitles: ISubtitle[] } = { + sources: [], + subtitles: [], + }; + + const key = '4VqE3#N7zt&HEP^a'; + + const res = await this.client.get(url, { + headers: { + referer: url, + }, + }); + + const regex = /MasterJS\s*=\s*'([^']*)'/; + const base64EncryptedData = regex.exec(res.data)![1]; + const base64DecryptedData = JSON.parse(Buffer.from(base64EncryptedData, 'base64').toString('utf8')); + + const derivedKey = crypto.pbkdf2Sync( + key, + Buffer.from(base64DecryptedData.salt, 'hex'), + base64DecryptedData.iterations, + 32, + 'sha512' + ); + const decipher = crypto.createDecipheriv( + 'aes-256-cbc', + derivedKey, + Buffer.from(base64DecryptedData.iv, 'hex') + ); + decipher.setEncoding('utf8'); + + let decrypted = decipher.update(base64DecryptedData.ciphertext, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + + const sources = JSON.parse(decrypted.match(/sources: ([^\]]*\])/)![1]); + const tracks = JSON.parse(decrypted.match(/tracks: ([^]*?\}\])/)![1]); + + const subtitles = tracks.filter( + (it: { file: string; label: string; kind: string }) => it.kind === 'captions' + ); + + sources.map((source: { file: string; label: string }) => { + result.sources.push({ + url: source.file, + quality: source.label, + isM3U8: source.file.includes('.m3u8'), + }); + }); + + subtitles.map((subtitle: { file: string; label: string }) => { + result.subtitles.push({ + url: subtitle.file, + lang: subtitle.label, + }); + }); + + return result; + } catch (err) { + throw new Error((err as Error).message); + } + } + + async extractSmashyNFlim(url: string): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> { + try { + const result: { sources: IVideo[]; subtitles: ISubtitle[] } = { + sources: [], + subtitles: [], + }; + + const res = await this.client.get(url, { + headers: { + referer: url, + }, + }); + const configData = res.data.match(/var\s+config\s*=\s*({.*?});/); + + const config = JSON.parse(configData?.length > 0 ? configData[1] : null); + + const files = config?.file + .match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/g) + .map((entry: { match: (arg0: RegExp) => [string, string, string] }) => { + const [, quality, link] = entry.match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/); + return { quality, link: link.replace(',', '') }; + }); + + const vttArray = config?.subtitle + .match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/g) + .map((entry: { match: (arg0: RegExp) => [string, string, string] }) => { + const [, language, link] = entry.match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/); + return { language, link: link.replace(',', '') }; + }); + + let validFiles = files; + + if (files) { + await Promise.all( + files?.map(async (source: { link: string; quality: string }) => { + await this.client + .head(source.link) + .then(res => console.log(res.status)) + .catch(err => { + if (err.response.status.status !== 200) { + validFiles = validFiles.filter( + (obj: { link: string; quality: string }) => obj.link !== source.link + ); + } + }); + }) + ); + } + + if (validFiles) { + validFiles?.map((source: { link: string; quality: string }) => { + result.sources.push({ + url: source.link, + quality: source.quality, + isM3U8: source.link.includes('.m3u8'), + }); + }); + + if (vttArray) { + vttArray?.map((subtitle: { language: string; link: string }) => { + result.subtitles.push({ + url: subtitle.link, + lang: subtitle.language, + }); + }); + } + } + + return result; + } catch (err) { + throw new Error((err as Error).message); + } + } + + async extractSmashyFX(url: string): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> { + try { + const result: { sources: IVideo[]; subtitles: ISubtitle[] } = { + sources: [], + subtitles: [], + }; + + const res = await this.client.get(url, { + headers: { + referer: url, + }, + }); + + const file = res.data.match(/file:\s*"([^"]+)"/)[1]; + + result.sources.push({ + url: file, + isM3U8: file.includes('.m3u8'), + }); + + return result; + } catch (err) { + throw new Error((err as Error).message); + } + } + + async extractSmashyCF(url: string): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> { + try { + const result: { sources: IVideo[]; subtitles: ISubtitle[] } = { + sources: [], + subtitles: [], + }; + + const res = await this.client.get(url, { + headers: { + referer: url, + }, + }); + + const file = res.data.match(/file:\s*"([^"]+)"/)[1]; + const fileRes = await this.client.head(file); + + if (fileRes.status !== 200 || fileRes.data.includes('404')) { + return result; + } else { + result.sources.push({ + url: file, + isM3U8: file.includes('.m3u8'), + }); + } + return result; + } catch (err) { + throw new Error((err as Error).message); + } + } + + async extractSmashyEEMovie(url: string): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> { + try { + const result: { sources: IVideo[]; subtitles: ISubtitle[] } = { + sources: [], + subtitles: [], + }; + + const res = await this.client.get(url, { + headers: { + referer: url, + }, + }); + + const file = res.data.match(/file:\s*"([^"]+)"/)[1]; + + result.sources.push({ + url: file, + isM3U8: file.includes('.m3u8'), + }); + + return result; + } catch (err) { + throw new Error((err as Error).message); + } + } +} + +export default SmashyStream; diff --git a/consumet.ts/src/extractors/streamhub.ts b/consumet.ts/src/extractors/streamhub.ts new file mode 100644 index 00000000..86a99568 --- /dev/null +++ b/consumet.ts/src/extractors/streamhub.ts @@ -0,0 +1,55 @@ +import { VideoExtractor, IVideo, ISubtitle } from '../models'; + +class StreamHub extends VideoExtractor { + protected override serverName = 'StreamHub'; + protected override sources: IVideo[] = []; + + override extract = async (videoUrl: URL): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> => { + try { + const result: { sources: IVideo[]; subtitles: ISubtitle[] } = { + sources: [], + subtitles: [], + }; + + const { data } = await this.client.get(videoUrl.href).catch(() => { + throw new Error('Video not found'); + }); + + const unpackedData = eval(/(eval)(\(f.*?)(\n<\/script>)/s.exec(data)![2].replace('eval', '')); + + const links = unpackedData.match(new RegExp('sources:\\[\\{src:"(.*?)"')) ?? []; + const m3u8Content = await this.client.get(links[1], { + headers: { + Referer: links[1], + }, + }); + + result.sources.push({ + quality: 'auto', + url: links[1], + isM3U8: links[1].includes('.m3u8'), + }); + + if (m3u8Content.data.includes('EXTM3U')) { + const videoList = m3u8Content.data.split('#EXT-X-STREAM-INF:'); + for (const video of videoList ?? []) { + if (!video.includes('m3u8')) continue; + + const url = video.split('\n')[1]; + const quality = video.split('RESOLUTION=')[1].split(',')[0].split('x')[1]; + + result.sources.push({ + url: url, + quality: `${quality}p`, + isM3U8: url.includes('.m3u8'), + }); + } + } + + return result; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} +export default StreamHub; diff --git a/consumet.ts/src/extractors/streamlare.ts b/consumet.ts/src/extractors/streamlare.ts new file mode 100644 index 00000000..e937bcf1 --- /dev/null +++ b/consumet.ts/src/extractors/streamlare.ts @@ -0,0 +1,66 @@ +import { load } from 'cheerio'; +import { IVideo, ISource } from '../models'; +import VideoExtractor from '../models/video-extractor'; + +class StreamLare extends VideoExtractor { + protected serverName: string = 'StreamLare'; + protected sources: IVideo[] = []; + + private readonly host = 'https://streamlare.com'; + + private readonly regex = new RegExp('/[ve]/([^?#&/]+)'); + + private readonly USER_AGENT = + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36'; + + async extract( + videoUrl: URL, + userAgent: string = this.USER_AGENT.toString(), + ...args: any + ): Promise { + const res = await this.client.get(videoUrl.href); + + const $ = load(res.data); + + const CSRF_TOKEN = $('head > meta:nth-child(3)').attr('content')?.toString(); + + const videoId = videoUrl.href.match(this.regex)![1]; + + if (videoId == undefined) { + throw new Error('Video id not matched!'); + } + + const POST = await this.client.post( + this.host + '/api/video/stream/get', + { + id: videoId, + }, + { + headers: { + 'User-Agent': userAgent, + }, + } + ); + + const POST_RES = POST.data; + + const result = { + headers: { + 'User-Agent': userAgent, + }, + status: POST_RES.status, + message: POST_RES.message, + type: POST_RES.type, + token: POST_RES.token, + sources: POST_RES.result, + }; + + if (POST_RES.status == 'error') { + throw new Error('Request Failed! Error: ' + POST_RES.message); + } + + return result; + } +} + +export default StreamLare; diff --git a/consumet.ts/src/extractors/streamsb.ts b/consumet.ts/src/extractors/streamsb.ts new file mode 100644 index 00000000..a5123971 --- /dev/null +++ b/consumet.ts/src/extractors/streamsb.ts @@ -0,0 +1,73 @@ +import { VideoExtractor, IVideo } from '../models'; +import { USER_AGENT } from '../utils'; + +class StreamSB extends VideoExtractor { + protected override serverName = 'streamsb'; + protected override sources: IVideo[] = []; + + private readonly host = 'https://streamsss.net/sources50'; + // TODO: update host2 + private readonly host2 = 'https://watchsb.com/sources50'; + + private PAYLOAD = (hex: string) => + `566d337678566f743674494a7c7c${hex}7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362`; + + override extract = async (videoUrl: URL, isAlt: boolean = false): Promise => { + let headers: any = { + watchsb: 'sbstream', + 'User-Agent': USER_AGENT, + Referer: videoUrl.href, + }; + let id = videoUrl.href.split('/e/').pop(); + if (id?.includes('html')) id = id.split('.html')[0]; + const bytes = new TextEncoder().encode(id); + + const res = await this.client + .get(`${isAlt ? this.host2 : this.host}/${this.PAYLOAD(Buffer.from(bytes).toString('hex'))}`, { + headers, + }) + .catch(() => null); + + if (!res?.data.stream_data) throw new Error('No source found. Try a different server.'); + + headers = { + 'User-Agent': USER_AGENT, + Referer: videoUrl.href.split('e/')[0], + }; + const m3u8Urls = await this.client.get(res.data.stream_data.file, { + headers, + }); + + const videoList = m3u8Urls.data.split('#EXT-X-STREAM-INF:'); + + for (const video of videoList ?? []) { + if (!video.includes('m3u8')) continue; + + const url = video.split('\n')[1]; + const quality = video.split('RESOLUTION=')[1].split(',')[0].split('x')[1]; + + this.sources.push({ + url: url, + quality: `${quality}p`, + isM3U8: true, + }); + } + + this.sources.push({ + quality: 'auto', + url: res.data.stream_data.file, + isM3U8: res.data.stream_data.file.includes('.m3u8'), + }); + + return this.sources; + }; + + private addSources = (source: any) => { + this.sources.push({ + url: source.file, + isM3U8: source.file.includes('.m3u8'), + }); + }; +} + +export default StreamSB; diff --git a/consumet.ts/src/extractors/streamtape.ts b/consumet.ts/src/extractors/streamtape.ts new file mode 100644 index 00000000..3115955d --- /dev/null +++ b/consumet.ts/src/extractors/streamtape.ts @@ -0,0 +1,37 @@ +import { load } from 'cheerio'; + +import { VideoExtractor, IVideo } from '../models'; + +class StreamTape extends VideoExtractor { + protected override serverName = 'StreamTape'; + protected override sources: IVideo[] = []; + + override extract = async (videoUrl: URL): Promise => { + try { + const { data } = await this.client.get(videoUrl.href).catch(() => { + throw new Error('Video not found'); + }); + + const $ = load(data); + + let [fh, sh] = $.html() + ?.match(/robotlink'\).innerHTML = (.*)'/)![1] + .split("+ ('"); + + sh = sh.substring(3); + fh = fh.replace(/\'/g, ''); + + const url = `https:${fh}${sh}`; + + this.sources.push({ + url: url, + isM3U8: url.includes('.m3u8'), + }); + + return this.sources; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} +export default StreamTape; diff --git a/consumet.ts/src/extractors/streamwish.ts b/consumet.ts/src/extractors/streamwish.ts new file mode 100644 index 00000000..7559aa75 --- /dev/null +++ b/consumet.ts/src/extractors/streamwish.ts @@ -0,0 +1,57 @@ +import { VideoExtractor, IVideo } from '../models'; +import { USER_AGENT } from '../utils'; +class StreamWish extends VideoExtractor { + protected override serverName = 'streamwish'; + protected override sources: IVideo[] = []; + + override extract = async (videoUrl: URL): Promise => { + try { + const options = { + headers: { + 'User-Agent': USER_AGENT, + }, + }; + const { data } = await this.client.get(videoUrl.href, options); + const links = data.match(/file:\s*"([^"]+)"/); + let lastLink = null; + links.forEach((link: string) => { + if(link.includes('file:"')){ + link = link.replace('file:"', '').replace(new RegExp('"', 'g'), ''); + } + this.sources.push({ + quality: lastLink! ? 'backup' : 'default', + url: link, + isM3U8: link.includes('.m3u8'), + }); + lastLink = link; + }); + + const m3u8Content = await this.client.get(links[1], { + headers: { + Referer: videoUrl.href, + }, + }); + + if (m3u8Content.data.includes('EXTM3U')) { + const videoList = m3u8Content.data.split('#EXT-X-STREAM-INF:'); + for (const video of videoList ?? []) { + if (!video.includes('m3u8')) continue; + + const url = links[1].split('master.m3u8')[0] + video.split('\n')[1]; + const quality = video.split('RESOLUTION=')[1].split(',')[0].split('x')[1]; + + this.sources.push({ + url: url, + quality: `${quality}`, + isM3U8: url.includes('.m3u8'), + }); + } + } + + return this.sources; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} +export default StreamWish; diff --git a/consumet.ts/src/extractors/vidcloud.ts b/consumet.ts/src/extractors/vidcloud.ts new file mode 100644 index 00000000..37ede8df --- /dev/null +++ b/consumet.ts/src/extractors/vidcloud.ts @@ -0,0 +1,95 @@ +import CryptoJS from 'crypto-js'; + +import { VideoExtractor, IVideo, ISubtitle, Intro } from '../models'; +import { USER_AGENT, isJson, substringAfter, substringBefore } from '../utils'; + +class VidCloud extends VideoExtractor { + protected override serverName = 'VidCloud'; + protected override sources: IVideo[] = []; + + private readonly host = 'https://dokicloud.one'; + private readonly host2 = 'https://rabbitstream.net'; + + override extract = async ( + videoUrl: URL, + isAlternative: boolean = false + ): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> => { + const result: { sources: IVideo[]; subtitles: ISubtitle[]; intro?: Intro } = { + sources: [], + subtitles: [], + }; + try { + const id = videoUrl.href.split('/').pop()?.split('?')[0]; + const options = { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + Referer: videoUrl.href, + 'User-Agent': USER_AGENT, + }, + }; + let res = undefined; + let sources = undefined; + + res = await this.client.get( + `${isAlternative ? this.host2 : this.host}/ajax/embed-4/getSources?id=${id}`, + options + ); + + if (!isJson(res.data.sources)) { + const keys = await (await this.client.get('https://raw.githubusercontent.com/eatmynerds/key/e4/key.txt')).data; + const keyString = btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(JSON.parse(JSON.stringify(keys)))))); + const decryptedVal = CryptoJS.AES.decrypt(res.data.sources, keyString).toString(CryptoJS.enc.Utf8); + sources = isJson(decryptedVal) ? JSON.parse(decryptedVal) : res.data.sources; + } + + this.sources = sources.map((s: any) => ({ + url: s.file, + isM3U8: s.file.includes('.m3u8'), + })); + + result.sources.push(...this.sources); + + result.sources = []; + this.sources = []; + + for (const source of sources) { + const { data } = await this.client.get(source.file, options); + const urls = data.split('\n').filter((line: string) => line.includes('.m3u8')) as string[]; + const qualities = data.split('\n').filter((line: string) => line.includes('RESOLUTION=')) as string[]; + + const TdArray = qualities.map((s, i) => { + const f1 = s.split('x')[1]; + const f2 = urls[i]; + + return [f1, f2]; + }); + + for (const [f1, f2] of TdArray) { + this.sources.push({ + url: f2, + quality: f1, + isM3U8: f2.includes('.m3u8'), + }); + } + result.sources.push(...this.sources); + } + + result.sources.push({ + url: sources[0].file, + isM3U8: sources[0].file.includes('.m3u8'), + quality: 'auto', + }); + + result.subtitles = res.data.tracks.map((s: any) => ({ + url: s.file, + lang: s.label ? s.label : 'Default (maybe)', + })); + + return result; + } catch (err) { + throw err; + } + }; +} + +export default VidCloud; diff --git a/consumet.ts/src/extractors/vidmoly.ts b/consumet.ts/src/extractors/vidmoly.ts new file mode 100644 index 00000000..086bd516 --- /dev/null +++ b/consumet.ts/src/extractors/vidmoly.ts @@ -0,0 +1,47 @@ +import { VideoExtractor, IVideo } from '../models'; + +class VidMoly extends VideoExtractor { + protected override serverName = 'vidmoly'; + protected override sources: IVideo[] = []; + + override extract = async (videoUrl: URL): Promise => { + try { + const { data } = await this.client.get(videoUrl.href); + + const links = data.match(/file:\s*"([^"]+)"/); + + const m3u8Content = await this.client.get(links[1], { + headers: { + Referer: videoUrl.href, + }, + }); + + this.sources.push({ + quality: 'auto', + url: links[1], + isM3U8: links[1].includes('.m3u8'), + }); + + if (m3u8Content.data.includes('EXTM3U')) { + const videoList = m3u8Content.data.split('#EXT-X-STREAM-INF:'); + for (const video of videoList ?? []) { + if (!video.includes('m3u8')) continue; + + const url = video.split('\n')[1]; + const quality = video.split('RESOLUTION=')[1].split(',')[0].split('x')[1]; + + this.sources.push({ + url: url, + quality: `${quality}`, + isM3U8: url.includes('.m3u8'), + }); + } + } + + return this.sources; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} +export default VidMoly; diff --git a/consumet.ts/src/extractors/vizcloud.ts b/consumet.ts/src/extractors/vizcloud.ts new file mode 100644 index 00000000..cde10457 --- /dev/null +++ b/consumet.ts/src/extractors/vizcloud.ts @@ -0,0 +1,66 @@ +import { VideoExtractor, IVideo, ISubtitle, Intro } from '../models'; + +class VizCloud extends VideoExtractor { + protected override serverName = 'VizCloud'; + protected override sources: IVideo[] = []; + + private readonly host = 'https://vidstream.pro'; + private keys: { + cipher: string; + encrypt: string; + main: string; + operations: Map; + post: string[]; + pre: string[]; + } = { + cipher: '', + encrypt: '', + main: '', + operations: new Map(), + pre: [], + post: [], + }; + + override extract = async (videoUrl: URL, vizCloudHelper: string, apiKey: string): Promise => { + const vizID: Array = videoUrl.href.split('/'); + let url; + if (!vizID.length) { + throw new Error('Video not found'); + } else { + url = `${vizCloudHelper}/vizcloud?query=${encodeURIComponent(vizID.pop() ?? '')}&apikey=${apiKey}`; + } + + const { data } = await this.client.get(url); + if (!data.data?.media) throw new Error('Video not found'); + + this.sources = [ + ...this.sources, + ...data.data.media.sources.map((source: any) => ({ + url: source.file, + quality: 'auto', + isM3U8: source.file?.includes('.m3u8'), + })), + ]; + + const main = this.sources[this.sources.length - 1].url; + const req = await this.client({ + method: 'get', + url: main, + headers: { referer: 'https://9anime.to' }, + }); + const resolutions = req.data.match(/(RESOLUTION=)(.*)(\s*?)(\s*.*)/g); + resolutions?.forEach((res: string) => { + const index = main.lastIndexOf('/'); + const quality = res.split('\n')[0].split('x')[1].split(',')[0]; + const url = main.slice(0, index); + this.sources.push({ + url: url + '/' + res.split('\n')[1], + isM3U8: (url + res.split('\n')[1]).includes('.m3u8'), + quality: quality + 'p', + }); + }); + return this.sources; + }; +} + +export default VizCloud; diff --git a/consumet.ts/src/extractors/voe.ts b/consumet.ts/src/extractors/voe.ts new file mode 100644 index 00000000..8439c980 --- /dev/null +++ b/consumet.ts/src/extractors/voe.ts @@ -0,0 +1,29 @@ +import { VideoExtractor, IVideo } from '../models'; + +class Voe extends VideoExtractor { + protected override serverName = 'voe'; + protected override sources: IVideo[] = []; + + private readonly domains = ['voe.sx']; + + override extract = async (videoUrl: URL): Promise => { + try { + const { data } = await this.client.get(videoUrl.href); + + const links = data.match(/'hls': ?'(http.*?)',/); + const quality = data.match(/'video_height': ?([0-9]+),/)[1]; + + this.sources.push({ + quality: quality, + url: links[1], + isM3U8: links[1].includes('.m3u8'), + }); + + return this.sources; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default Voe; diff --git a/consumet.ts/src/index.ts b/consumet.ts/src/index.ts new file mode 100644 index 00000000..dfe8147b --- /dev/null +++ b/consumet.ts/src/index.ts @@ -0,0 +1,116 @@ +import { ANIME, BOOKS, COMICS, LIGHT_NOVELS, MANGA, MOVIES, META, NEWS } from './providers'; +import { PROVIDERS_LIST } from './utils/providers-list'; +import { + VizCloud, + AsianLoad, + GogoCDN, + Kwik, + MixDrop, + RapidCloud, + BilibiliExtractor, + Filemoon, + StreamSB, + StreamTape, + VidCloud, + StreamLare, + StreamHub, + SmashyStream, + VidMoly, + Mp4Upload, + StreamWish, +} from './extractors'; +import { + IProviderStats, + ISearch, + IAnimeEpisode, + IAnimeInfo, + IAnimeResult, + IEpisodeServer, + IVideo, + LibgenBook, + StreamingServers, + MediaStatus, + SubOrSub, + IMangaResult, + IMangaChapter, + IMangaInfo, + ILightNovelResult, + ILightNovelInfo, + ILightNovelChapter, + ILightNovelChapterContent, + GetComicsComics, + ComicRes, + IMangaChapterPage, + TvType, + IMovieEpisode, + IMovieInfo, + ISource, + ISubtitle, + IMovieResult, + Intro, + Genres, + INewsFeed, + Topics, + INewsInfo, + FuzzyDate, + ITitle, + MediaFormat, + ProxyConfig, +} from './models'; + +export { ANIME, BOOKS, COMICS, MANGA, LIGHT_NOVELS, MOVIES, META, NEWS }; +export { PROVIDERS_LIST }; +export { + Topics, + Genres, + SubOrSub, + StreamingServers, + MediaStatus, + IProviderStats, + IAnimeEpisode, + IAnimeInfo, + IAnimeResult, + IEpisodeServer, + IVideo, + LibgenBook, + IMangaResult, + IMangaChapter, + IMangaInfo, + ILightNovelResult, + ILightNovelInfo, + ILightNovelChapter, + ILightNovelChapterContent, + GetComicsComics, + ComicRes, + ISearch, + IMangaChapterPage, + TvType, + IMovieEpisode, + IMovieInfo, + ISource, + ISubtitle, + IMovieResult, + Intro, + INewsFeed, + INewsInfo, + FuzzyDate, + ITitle, + MediaFormat, + ProxyConfig, + GogoCDN, + StreamSB, + VidCloud, + MixDrop, + Kwik, + RapidCloud, + StreamTape, + StreamHub, + SmashyStream, + VizCloud, + AsianLoad, + BilibiliExtractor, + Filemoon, + Mp4Upload, + StreamWish, + VidMoly, +}; diff --git a/consumet.ts/src/models/anime-parser.ts b/consumet.ts/src/models/anime-parser.ts new file mode 100644 index 00000000..d17ec9b6 --- /dev/null +++ b/consumet.ts/src/models/anime-parser.ts @@ -0,0 +1,31 @@ +import { BaseParser, IAnimeInfo, ISource, IEpisodeServer, ProxyConfig } from '.'; +import { AxiosAdapter } from 'axios'; + +abstract class AnimeParser extends BaseParser { + /** + * if the provider has dub and it's avialable seperatly from sub set this to `true` + */ + protected readonly isDubAvailableSeparately: boolean = false; + /** + * takes anime id + * + * returns anime info (including episodes) + */ + abstract fetchAnimeInfo(animeId: string, ...args: any): Promise; + + /** + * takes episode id + * + * returns episode sources (video links) + */ + abstract fetchEpisodeSources(episodeId: string, ...args: any): Promise; + + /** + * takes episode id + * + * returns episode servers (video links) available + */ + abstract fetchEpisodeServers(episodeId: string): Promise; +} + +export default AnimeParser; diff --git a/consumet.ts/src/models/base-parser.ts b/consumet.ts/src/models/base-parser.ts new file mode 100644 index 00000000..a4161c2f --- /dev/null +++ b/consumet.ts/src/models/base-parser.ts @@ -0,0 +1,14 @@ +import { AxiosAdapter } from 'axios'; + +import { BaseProvider, ProxyConfig } from '.'; + +abstract class BaseParser extends BaseProvider { + /** + * Search for books/anime/manga/etc using the given query + * + * returns a promise resolving to a data object + */ + abstract search(query: string, ...args: any[]): Promise; +} + +export default BaseParser; diff --git a/consumet.ts/src/models/base-provider.ts b/consumet.ts/src/models/base-provider.ts new file mode 100644 index 00000000..e5a27d24 --- /dev/null +++ b/consumet.ts/src/models/base-provider.ts @@ -0,0 +1,60 @@ +import { IProviderStats } from '.'; +import Proxy from './proxy'; + +abstract class BaseProvider extends Proxy { + /** + * Name of the provider + */ + abstract readonly name: string; + + /** + * The main URL of the provider + */ + protected abstract readonly baseUrl: string; + + /** + * Most providers are english based, but if the provider is not english based override this value. + * must be in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format + */ + protected readonly languages: string[] | string = 'en'; + + /** + * override as `true` if the provider **only** supports NSFW content + */ + readonly isNSFW: boolean = false; + + /** + * Logo of the provider (used in the website) or `undefined` if not available. ***128x128px is preferred***\ + * Must be a valid URL (not a data URL) + */ + protected readonly logo: string = + 'https://png.pngtree.com/png-vector/20210221/ourmid/pngtree-error-404-not-found-neon-effect-png-image_2928214.jpg'; + + /** + * The class's path is determined by the provider's directory structure for example:\ + * MangaDex class path is `MANGA.MangaDex`. **(case sensitive)** + */ + protected abstract readonly classPath: string; + + /** + * override as `false` if the provider is **down** or **not working** + */ + readonly isWorking: boolean = true; + + /** + * returns provider stats + */ + get toString(): IProviderStats { + return { + name: this.name, + baseUrl: this.baseUrl, + lang: this.languages, + isNSFW: this.isNSFW, + logo: this.logo, + classPath: this.classPath, + isWorking: this.isWorking, + }; + } +} + +export default BaseProvider; diff --git a/consumet.ts/src/models/base-types.ts b/consumet.ts/src/models/base-types.ts new file mode 100644 index 00000000..763f46df --- /dev/null +++ b/consumet.ts/src/models/base-types.ts @@ -0,0 +1,23 @@ +export interface Book { + title: string; + authors: string[]; + publisher: string; + year: string; + edition: string; + volume: string; + series: string; + isbn: string[]; + image: string; + description: string; + link: string; +} + +export interface Hashes { + AICH: string; + CRC32: string; + eDonkey: string; + MD5: string; + SHA1: string; + SHA256: string[]; + TTH: string; +} diff --git a/consumet.ts/src/models/book-parser.ts b/consumet.ts/src/models/book-parser.ts new file mode 100644 index 00000000..8c55aecb --- /dev/null +++ b/consumet.ts/src/models/book-parser.ts @@ -0,0 +1,5 @@ +import { BaseParser } from '.'; + +abstract class BookParser extends BaseParser {} + +export default BookParser; diff --git a/consumet.ts/src/models/comic-parser.ts b/consumet.ts/src/models/comic-parser.ts new file mode 100644 index 00000000..bb5b154f --- /dev/null +++ b/consumet.ts/src/models/comic-parser.ts @@ -0,0 +1,5 @@ +import { BaseParser } from '.'; + +abstract class ComicParser extends BaseParser {} + +export default ComicParser; diff --git a/consumet.ts/src/models/index.ts b/consumet.ts/src/models/index.ts new file mode 100644 index 00000000..538bac66 --- /dev/null +++ b/consumet.ts/src/models/index.ts @@ -0,0 +1,100 @@ +import BaseProvider from './base-provider'; +import BaseParser from './base-parser'; +import AnimeParser from './anime-parser'; +import BookParser from './book-parser'; +import ComicParser from './comic-parser'; +import VideoExtractor from './video-extractor'; +import MangaParser from './manga-parser'; +import LightNovelParser from './lightnovel-parser'; +import MovieParser from './movie-parser'; +import NewsParser from './news-parser'; +import { + IProviderStats, + ISearch, + IAnimeEpisode, + IAnimeInfo, + IAnimeResult, + IEpisodeServer, + IVideo, + LibgenBook, + StreamingServers, + MediaStatus, + SubOrSub, + IMangaResult, + IMangaChapter, + IMangaInfo, + ILightNovelResult, + ILightNovelInfo, + ILightNovelChapter, + ILightNovelChapterContent, + GetComicsComics, + ComicRes, + IMangaChapterPage, + TvType, + IMovieEpisode, + IMovieInfo, + ISource, + ISubtitle, + IMovieResult, + Intro, + Genres, + INewsFeed, + Topics, + INewsInfo, + FuzzyDate, + ITitle, + MediaFormat, + ProxyConfig, +} from './types'; +import { LibgenBookObject, GetComicsComicsObject } from './type-objects'; + +export { + BaseProvider, + IProviderStats, + BaseParser, + AnimeParser, + BookParser, + IAnimeEpisode, + IAnimeInfo, + IAnimeResult, + IEpisodeServer, + IVideo, + VideoExtractor, + LibgenBook, + LibgenBookObject, + StreamingServers, + MediaStatus, + SubOrSub, + LightNovelParser, + MangaParser, + NewsParser, + IMangaResult, + IMangaChapter, + IMangaInfo, + ILightNovelResult, + ILightNovelInfo, + ILightNovelChapter, + ILightNovelChapterContent, + ComicParser, + GetComicsComics, + GetComicsComicsObject, + ComicRes, + ISearch, + IMangaChapterPage, + TvType, + MovieParser, + IMovieEpisode, + IMovieInfo, + ISource, + ISubtitle, + IMovieResult, + Intro, + Genres, + INewsFeed, + Topics, + INewsInfo, + FuzzyDate, + ITitle, + MediaFormat, + ProxyConfig, +}; diff --git a/consumet.ts/src/models/lightnovel-parser.ts b/consumet.ts/src/models/lightnovel-parser.ts new file mode 100644 index 00000000..19c0678f --- /dev/null +++ b/consumet.ts/src/models/lightnovel-parser.ts @@ -0,0 +1,19 @@ +import { BaseParser } from '.'; + +abstract class LightNovelParser extends BaseParser { + /** + * takes light novel link or id + * + * returns lightNovel info (including chapters) + */ + protected abstract fetchLightNovelInfo(lightNovelUrl: string, ...args: any): Promise; + + /** + * takes chapter id + * + * returns chapter content (text) + */ + protected abstract fetchChapterContent(chapterId: string, ...args: any): Promise; +} + +export default LightNovelParser; diff --git a/consumet.ts/src/models/manga-parser.ts b/consumet.ts/src/models/manga-parser.ts new file mode 100644 index 00000000..b9a8513d --- /dev/null +++ b/consumet.ts/src/models/manga-parser.ts @@ -0,0 +1,19 @@ +import { BaseParser, IMangaInfo, IMangaChapterPage } from '.'; + +abstract class MangaParser extends BaseParser { + /** + * takes manga id + * + * returns manga info with chapters + */ + abstract fetchMangaInfo(mangaId: string, ...args: any): Promise; + + /** + * takes chapter id + * + * returns chapter (image links) + */ + abstract fetchChapterPages(chapterId: string, ...args: any): Promise; +} + +export default MangaParser; diff --git a/consumet.ts/src/models/movie-parser.ts b/consumet.ts/src/models/movie-parser.ts new file mode 100644 index 00000000..ee45e2f7 --- /dev/null +++ b/consumet.ts/src/models/movie-parser.ts @@ -0,0 +1,32 @@ +import { BaseParser, TvType, ISource, IEpisodeServer, IMovieInfo, IAnimeInfo, ProxyConfig } from '.'; +import { AxiosAdapter } from 'axios'; + +abstract class MovieParser extends BaseParser { + /** + * The supported types of the provider (e.g. `TV`, `Movie`) + */ + abstract supportedTypes: Set; + + /** + * takes media id + * + * returns media info (including episodes) + */ + abstract fetchMediaInfo(mediaId: string, type?: string): Promise; + + /** + * takes episode id + * + * returns episode sources (video links) + */ + abstract fetchEpisodeSources(episodeId: string, ...args: any): Promise; + + /** + * takes episode id + * + * returns episode servers (video links) available + */ + abstract fetchEpisodeServers(episodeId: string, ...args: any): Promise; +} + +export default MovieParser; diff --git a/consumet.ts/src/models/news-parser.ts b/consumet.ts/src/models/news-parser.ts new file mode 100644 index 00000000..05cf3019 --- /dev/null +++ b/consumet.ts/src/models/news-parser.ts @@ -0,0 +1,4 @@ +import { BaseProvider } from '.'; + +abstract class NewsParser extends BaseProvider {} +export default NewsParser; diff --git a/consumet.ts/src/models/proxy.ts b/consumet.ts/src/models/proxy.ts new file mode 100644 index 00000000..f9444ced --- /dev/null +++ b/consumet.ts/src/models/proxy.ts @@ -0,0 +1,74 @@ +import axios, { AxiosAdapter, AxiosInstance } from 'axios'; + +import { ProxyConfig } from './types'; + +export class Proxy { + /** + * + * @param proxyConfig The proxy config (optional) + * @param adapter The axios adapter (optional) + */ + constructor(protected proxyConfig?: ProxyConfig, protected adapter?: AxiosAdapter) { + this.client = axios.create(); + + if (proxyConfig) this.setProxy(proxyConfig); + if (adapter) this.setAxiosAdapter(adapter); + } + private validUrl = /^https?:\/\/.+/; + /** + * Set or Change the proxy config + */ + setProxy(proxyConfig: ProxyConfig) { + if (!proxyConfig?.url) return; + + if (typeof proxyConfig?.url === 'string') + if (!this.validUrl.test(proxyConfig.url)) throw new Error('Proxy URL is invalid!'); + + if (Array.isArray(proxyConfig?.url)) { + for (const [i, url] of this.toMap(proxyConfig.url)) + if (!this.validUrl.test(url)) throw new Error(`Proxy URL at index ${i} is invalid!`); + + this.rotateProxy({ ...proxyConfig, urls: proxyConfig.url }); + } + + this.client.interceptors.request.use(config => { + if (proxyConfig?.url) { + config.headers = { + ...config.headers, + 'x-api-key': proxyConfig?.key ?? '', + }; + config.url = `${proxyConfig.url}${config?.url ? config?.url : ''}`; + console.log(config.url); + } + + if (config?.url?.includes('anify')) + config.headers = { + ...config.headers, + 'User-Agent': 'consumet', + }; + + return config; + }); + } + + /** + * Set or Change the axios adapter + */ + setAxiosAdapter(adapter: AxiosAdapter) { + this.client.defaults.adapter = adapter; + } + private rotateProxy = (proxy: Omit & { urls: string[] }) => { + setInterval(() => { + const url = proxy.urls.shift(); + if (url) proxy.urls.push(url); + + this.setProxy({ url: proxy.urls[0], key: proxy.key }); + }, proxy?.rotateInterval ?? 5000); + }; + + private toMap = (arr: T[]): [number, T][] => arr.map((v, i) => [i, v]); + + protected client: AxiosInstance; +} + +export default Proxy; diff --git a/consumet.ts/src/models/type-objects.ts b/consumet.ts/src/models/type-objects.ts new file mode 100644 index 00000000..a3076dba --- /dev/null +++ b/consumet.ts/src/models/type-objects.ts @@ -0,0 +1,50 @@ +import { Hashes } from './base-types'; +import { GetComicsComics, LibgenBook } from './types'; + +export class LibgenBookObject implements LibgenBook { + title = ''; + authors = []; + publisher = ''; + year = ''; + edition = ''; + volume = ''; + series = ''; + isbn = []; + link = ''; + id = ''; + language = ''; + format = ''; + size = ''; + pages = ''; + image = ''; + description = ''; + tableOfContents = ''; + topic = ''; + hashes = new HashesObject(); +} + +class HashesObject implements Hashes { + AICH = ''; + CRC32 = ''; + eDonkey = ''; + MD5 = ''; + SHA1 = ''; + SHA256 = []; + TTH = ''; +} + +export class GetComicsComicsObject implements GetComicsComics { + image = ''; + title = ''; + year = ''; + size = ''; + excerpt = ''; + description = ''; + download = ''; + category = ''; + ufile = ''; + mega = ''; + mediafire = ''; + zippyshare = ''; + readOnline = ''; +} diff --git a/consumet.ts/src/models/types.ts b/consumet.ts/src/models/types.ts new file mode 100644 index 00000000..aba24ce7 --- /dev/null +++ b/consumet.ts/src/models/types.ts @@ -0,0 +1,480 @@ +import { Book, Hashes } from './base-types'; + +export interface IProviderStats { + name: string; + baseUrl: string; + lang: string[] | string; + isNSFW: boolean; + logo: string; + classPath: string; + isWorking: boolean; +} + +export interface ITitle { + romaji?: string; + english?: string; + native?: string; + userPreferred?: string; +} + +export interface IAnimeResult { + id: string; + title: string | ITitle; + url?: string; + image?: string; + imageHash?: string; + cover?: string; + coverHash?: string; + status?: MediaStatus; + rating?: number; + type?: MediaFormat; + releaseDate?: string; + [x: string]: any; // other fields +} + +export interface ISearch { + currentPage?: number; + hasNextPage?: boolean; + totalPages?: number; + /** + * total results must include results from all pages + */ + totalResults?: number; + results: T[]; +} + +export interface Trailer { + id: string; + site?: string; + thumbnail?: string; + thumbnailHash?: string | null; +} + +export interface FuzzyDate { + year?: number; + month?: number; + day?: number; +} + +export enum MediaFormat { + TV = 'TV', + TV_SHORT = 'TV_SHORT', + MOVIE = 'MOVIE', + SPECIAL = 'SPECIAL', + OVA = 'OVA', + ONA = 'ONA', + MUSIC = 'MUSIC', + MANGA = 'MANGA', + NOVEL = 'NOVEL', + ONE_SHOT = 'ONE_SHOT', +} + +export interface IAnimeInfo extends IAnimeResult { + malId?: number | string; + genres?: string[]; + description?: string; + status?: MediaStatus; + totalEpisodes?: number; + /** + * @deprecated use `hasSub` or `hasDub` instead + */ + subOrDub?: SubOrSub; + hasSub?: boolean; + hasDub?: boolean; + synonyms?: string[]; + /** + * two letter representation of coutnry: e.g JP for japan + */ + countryOfOrigin?: string; + isAdult?: boolean; + isLicensed?: boolean; + /** + * `FALL`, `WINTER`, `SPRING`, `SUMMER` + */ + season?: string; + studios?: string[]; + color?: string; + cover?: string; + trailer?: Trailer; + episodes?: IAnimeEpisode[]; + startDate?: FuzzyDate; + endDate?: FuzzyDate; + recommendations?: IAnimeResult[]; + relations?: IAnimeResult[]; +} + +export interface IAnimeEpisodeV2 { + [x: string]: { + id: string; + season_number: number; + title: string; + image: string; + imageHash: string; + description: string; + releaseDate: string; + isHD: boolean; + isAdult: boolean; + isDubbed: boolean; + isSubbed: boolean; + duration: number; + }[]; +} + +export interface IAnimeEpisode { + id: string; + number: number; + title?: string; + description?: string; + isFiller?: boolean; + url?: string; + image?: string; + imageHash?: string; + releaseDate?: string; + [x: string]: unknown; // other fields +} + +export interface IEpisodeServer { + name: string; + url: string; +} + +export interface IVideo { + /** + * The **MAIN URL** of the video provider that should take you to the video + */ + url: string; + /** + * The Quality of the video should include the `p` suffix + */ + quality?: string; + /** + * make sure to set this to `true` if the video is hls + */ + isM3U8?: boolean; + /** + * set this to `true` if the video is dash (mpd) + */ + isDASH?: boolean; + /** + * size of the video in **bytes** + */ + size?: number; + [x: string]: unknown; // other fields +} + +export enum StreamingServers { + AsianLoad = 'asianload', + GogoCDN = 'gogocdn', + StreamSB = 'streamsb', + MixDrop = 'mixdrop', + Mp4Upload = 'mp4upload', + UpCloud = 'upcloud', + VidCloud = 'vidcloud', + StreamTape = 'streamtape', + VizCloud = 'vizcloud', + // same as vizcloud + MyCloud = 'mycloud', + Filemoon = 'filemoon', + VidStreaming = 'vidstreaming', + SmashyStream = 'smashystream', + StreamHub = 'streamhub', + StreamWish = 'streamwish', + VidMoly = 'vidmoly', +} + +export enum MediaStatus { + ONGOING = 'Ongoing', + COMPLETED = 'Completed', + HIATUS = 'Hiatus', + CANCELLED = 'Cancelled', + NOT_YET_AIRED = 'Not yet aired', + UNKNOWN = 'Unknown', +} + +export enum SubOrSub { + SUB = 'sub', + DUB = 'dub', + BOTH = 'both', +} + +export interface IMangaResult { + id: string; + title: string | [lang: string][] | ITitle; + altTitles?: string | string[] | [lang: string][]; + image?: string; + description?: string | [lang: string][] | { [lang: string]: string }; + status?: MediaStatus; + releaseDate?: number | string; + [x: string]: unknown; // other fields +} + +export interface IMangaChapter { + id: string; + title: string; + volume?: number; + pages?: number; + releaseDate?: string; + [x: string]: unknown; // other fields +} + +export interface IMangaInfo extends IMangaResult { + malId?: number | string; + authors?: string[]; + genres?: string[]; + links?: string[]; + characters?: any[]; + recommendations?: IMangaResult[]; + chapters?: IMangaChapter[]; +} + +export interface IMangaChapterPage { + img: string; + page: number; + [x: string]: unknown; // other fields +} + +export interface ILightNovelResult { + id: string; + title: string | ITitle; + url: string; + image?: string; + [x: string]: unknown; // other fields +} + +export interface ILightNovelChapter { + id: string; + title: string; + volume?: number | string; + url?: string; +} + +export interface ILightNovelChapterContent { + novelTitle: string; + chapterTitle: string; + text: string; +} + +export interface ILightNovelInfo extends ILightNovelResult { + authors?: string[]; + genres?: string[]; + description?: string; + chapters?: ILightNovelChapter[]; + status?: MediaStatus; + views?: number; + rating?: number; +} + +export interface LibgenBook extends Book { + id: string; + language: string; + format: string; + size: string; + pages: string; + tableOfContents: string; + topic: string; + hashes: Hashes; +} + +export interface LibgenResult { + result: LibgenBook[]; + hasNextPage: boolean; +} + +export interface GetComicsComics { + image: string; + title: string; + year: string; + size: string; + excerpt: string; + category: string; + description: string; + download: string; + ufile: string; + mega: string; + mediafire: string; + zippyshare: string; + readOnline: string; +} + +export interface ComicRes { + containers: GetComicsComics[]; + hasNextPage: boolean; +} + +export interface ISubtitle { + /** + * The id of the subtitle. **not** required + */ + id?: string; + /** + * The **url** that should take you to the subtitle **directly**. + */ + url: string; + /** + * The language of the subtitle + */ + lang: string; +} + +/** + * The start, and the end of the intro or opening in seconds. + */ +export interface Intro { + start: number; + end: number; +} + +export interface ISource { + headers?: { [k: string]: string }; + intro?: Intro; + outro?: Intro; + subtitles?: ISubtitle[]; + sources: IVideo[]; + download?: string; + embedURL?: string; +} + +/** + * Used **only** for movie/tvshow providers + */ +export enum TvType { + TVSERIES = 'TV Series', + MOVIE = 'Movie', + ANIME = 'Anime', + PEOPLE = 'People', +} + +export interface IMovieEpisode { + id: string; + title: string; + url?: string; + number?: number; + season?: number; + description?: string; + image?: string; + releaseDate?: string; + [x: string]: unknown; // other fields +} + +export interface IMovieResult { + id: string; + title: string | ITitle; + url?: string; + image?: string; + releaseDate?: string; + type?: TvType; + [x: string]: unknown; // other unkown fields +} + +export interface IPeopleResult { + id: string; + name: string; + rating?: string; + image?: string; + movies: IMovieResult[]; + [x: string]: unknown; // other unkown fields +} + +export interface INewsFeed extends INews { + /** topics of the feed */ + topics: Topics[]; + /** preview of the news feed */ + preview: INewsFeedPreview; +} + +export interface INewsInfo extends INews { + /** intro of the news */ + intro: string; + /** description of the news */ + description: string; +} + +interface INews { + /** id of the news */ + id: string; + /** title of the news */ + title: string; + /** time at which the news was uploaded */ + uploadedAt: string; + /** thumbnail image URL of the news */ + thumbnail: string; + /** thumbnail image blurhash code of the news */ + thumbnailHash: string; + /** URL of the news */ + url: string; +} + +interface INewsFeedPreview { + /** intro of the feed */ + intro: string; + /** some contents of the feed */ + full: string; +} + +export interface IMovieInfo extends IMovieResult { + cover?: string; + recommendations?: IMovieResult[]; + genres?: string[]; + description?: string; + rating?: number; + status?: MediaStatus; + duration?: string; + production?: string; + casts?: string[]; + tags?: string[]; + totalEpisodes?: number; + seasons?: { season: number; image?: string; episodes: IMovieEpisode[] }[]; + episodes?: IMovieEpisode[]; +} + +export enum Genres { + ACTION = 'Action', + ADVENTURE = 'Adventure', + CARS = 'Cars', + COMEDY = 'Comedy', + DRAMA = 'Drama', + FANTASY = 'Fantasy', + HORROR = 'Horror', + MAHOU_SHOUJO = 'Mahou Shoujo', + MECHA = 'Mecha', + MUSIC = 'Music', + MYSTERY = 'Mystery', + PSYCHOLOGICAL = 'Psychological', + ROMANCE = 'Romance', + SCI_FI = 'Sci-Fi', + SLICE_OF_LIFE = 'Slice of Life', + SPORTS = 'Sports', + SUPERNATURAL = 'Supernatural', + THRILLER = 'Thriller', +} + +export enum Topics { + ANIME = 'anime', + ANIMATION = 'animation', + MANGA = 'manga', + GAMES = 'games', + NOVELS = 'novels', + LIVE_ACTION = 'live-action', + COVID_19 = 'covid-19', + INDUSTRY = 'industry', + MUSIC = 'music', + PEOPLE = 'people', + MERCH = 'merch', + EVENTS = 'events', +} + +export interface ProxyConfig { + /** + * The proxy URL + * @example https://proxy.com + **/ + url: string | string[]; + /** + * X-API-Key header value (if any) + **/ + key?: string; + /** + * The proxy rotation interval in milliseconds. (default: 5000) + */ + rotateInterval?: number; +} diff --git a/consumet.ts/src/models/video-extractor.ts b/consumet.ts/src/models/video-extractor.ts new file mode 100644 index 00000000..a8f441c9 --- /dev/null +++ b/consumet.ts/src/models/video-extractor.ts @@ -0,0 +1,23 @@ +import { IVideo, ISource } from '.'; +import Proxy from '../models/proxy'; + +abstract class VideoExtractor extends Proxy { + /** + * The server name of the video provider + */ + protected abstract serverName: string; + + /** + * list of videos available + */ + protected abstract sources: IVideo[]; + + /** + * takes video link + * + * returns video sources (video links) available + */ + protected abstract extract(videoUrl: URL, ...args: any): Promise; +} + +export default VideoExtractor; diff --git a/consumet.ts/src/providers/anime/9anime.ts b/consumet.ts/src/providers/anime/9anime.ts new file mode 100644 index 00000000..55220db4 --- /dev/null +++ b/consumet.ts/src/providers/anime/9anime.ts @@ -0,0 +1,417 @@ +import { load } from 'cheerio'; +import { AxiosAdapter } from 'axios'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + IAnimeEpisode, + MediaStatus, + SubOrSub, + IAnimeResult, + IEpisodeServer, + ISource, + StreamingServers, + MediaFormat, + ProxyConfig, +} from '../../models'; +import { StreamTape, VizCloud, Filemoon } from '../../extractors'; +import { USER_AGENT, range } from '../../utils'; + +/** + * **Use at your own risk :)** 9anime devs keep changing the keys every week + */ +class NineAnime extends AnimeParser { + override readonly name = '9Anime'; + private nineAnimeResolver = ''; + private apiKey = ''; + protected override baseUrl = 'https://aniwave.to'; + protected override logo = + 'https://d1nxzqpcg2bym0.cloudfront.net/google_play/com.my.nineanime/87b2fe48-9c36-11eb-8292-21241b1c199b/128x128'; + protected override classPath = 'ANIME.NineAnime'; + override readonly isWorking = false; + + constructor( + nineAnimeResolver?: string, + proxyConfig?: ProxyConfig, + apiKey?: string, + adapter?: AxiosAdapter + ) { + super(proxyConfig, adapter); + this.nineAnimeResolver = nineAnimeResolver ?? this.nineAnimeResolver; + this.apiKey = apiKey ?? this.apiKey; + } + + override async search(query: string, page: number = 1): Promise> { + const searchResult: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + + try { + const vrf = await this.searchVrf(query); + const res = await this.client.get( + `${this.baseUrl}/filter?keyword=${encodeURIComponent(query).replace( + /%20/g, + '+' + )}&vrf=${encodeURIComponent(vrf)}&page=${page}` + ); + + const $ = load(res.data); + + searchResult.hasNextPage = + $(`ul.pagination`).length > 0 + ? $('ul.pagination > li').last().hasClass('disabled') + ? false + : true + : false; + + $('#list-items > div.item').each((i, el) => { + let type = undefined; + switch ($(el).find('div > div.ani > a > div.meta > div > div.right').text()!.trim()) { + case 'MOVIE': + type = MediaFormat.MOVIE; + break; + case 'TV': + type = MediaFormat.TV; + break; + case 'OVA': + type = MediaFormat.OVA; + break; + case 'SPECIAL': + type = MediaFormat.SPECIAL; + break; + case 'ONA': + type = MediaFormat.ONA; + break; + case 'MUSIC': + type = MediaFormat.MUSIC; + break; + } + + searchResult.results.push({ + id: $(el).find('div > div.ani > a').attr('href')?.split('/')[2]!, + title: $(el).find('div > div.info > div.b1 > a').text()!, + url: `${this.baseUrl}${$(el).find('div > div.ani > a').attr('href')}`, + image: $(el).find('div > div.ani > a > img').attr('src'), + type: type, + hasSub: $(el).find('div > div.ani > a .meta .sub').length > 0, + hasDub: $(el).find('div > div.ani > a .meta .dub').length > 0, + }); + }); + + return searchResult; + } catch (err) { + throw new Error((err as Error).message); + } + } + + override async fetchAnimeInfo(animeUrl: string): Promise { + if (!animeUrl.startsWith(this.baseUrl)) animeUrl = `${this.baseUrl}/watch/${animeUrl}`; + + const animeInfo: IAnimeInfo = { + id: '', + title: '', + url: animeUrl, + }; + + try { + const res = await this.client.get(animeUrl); + + const $ = load(res.data); + + animeInfo.id = new URL(`${this.baseUrl}/animeUrl`).pathname.split('/')[2]; + animeInfo.title = $('h1.title').text(); + animeInfo.jpTitle = $('h1.title').attr('data-jp'); + animeInfo.genres = Array.from( + $('div.meta:nth-child(1) > div:nth-child(5) > span > a').map((i, el) => $(el).text()) + ); + animeInfo.image = $('.binfo > div.poster > span > img').attr('src'); + animeInfo.description = $('.content').text()?.trim(); + switch ($('div.meta:nth-child(1) > div:nth-child(1) > span:nth-child(1) > a').text()) { + case 'MOVIE': + animeInfo.type = MediaFormat.MOVIE; + break; + case 'TV': + animeInfo.type = MediaFormat.TV; + break; + case 'OVA': + animeInfo.type = MediaFormat.OVA; + break; + case 'SPECIAL': + animeInfo.type = MediaFormat.SPECIAL; + break; + case 'ONA': + animeInfo.type = MediaFormat.ONA; + break; + case 'MUSIC': + animeInfo.type = MediaFormat.MUSIC; + break; + } + animeInfo.studios = Array.from( + $('div.meta:nth-child(1) > div:nth-child(2) > span:nth-child(1) > a').map( + (i, el) => $(el).text()?.trim()! + ) + ); + animeInfo.releaseDate = $('div.meta:nth-child(1) > div:nth-child(3) > span:nth-child(1)') + .text() + .trim() + .split('to')[0] + ?.trim(); + + switch ($('div.meta:nth-child(1) > div:nth-child(4) > span:nth-child(1)').text()?.trim()) { + case 'Releasing': + animeInfo.status = MediaStatus.ONGOING; + break; + case 'Completed': + animeInfo.status = MediaStatus.COMPLETED; + break; + case 'Cancelled': + animeInfo.status = MediaStatus.CANCELLED; + break; + case 'Unknown': + animeInfo.status = MediaStatus.UNKNOWN; + break; + default: + animeInfo.status = MediaStatus.UNKNOWN; + break; + } + + animeInfo.score = parseFloat( + $('.bmeta > div:nth-child(2) > div:nth-child(2) > span:nth-child(1)')?.text().split('by')[0] + ); + animeInfo.premiered = $( + '.bmeta > div:nth-child(2) > div:nth-child(3) > span:nth-child(1) > a:nth-child(1)' + ).text(); + animeInfo.duration = $('.bmeta > div:nth-child(2) > div:nth-child(4) > span:nth-child(1)').text(); + animeInfo.views = parseInt( + $('.bmeta > div:nth-child(2) > div:nth-child(5) > span:nth-child(1)') + .text() + .split('by') + .join('') + .split(',') + .join('') + .trim() + ); + animeInfo.otherNames = $('.names') + .text() + .split('; ') + .map(name => name?.trim()); + animeInfo.hasSub = $('div#w-info > .binfo > .info > .meta .sub').length == 1; + animeInfo.hasDub = $('div#w-info > .binfo > .info > .meta .dub').length == 1; + + const id = $('#watch-main').attr('data-id')!; + + const vrf = await this.ev(id); + const { + data: { result }, + } = await this.client.get(`${this.baseUrl}/ajax/episode/list/${id}?vrf=${encodeURIComponent(vrf)}`); + const $$ = load(result); + animeInfo.totalEpisodes = $$('div.episodes > ul > li > a').length; + animeInfo.episodes = []; + + const episodes: IAnimeEpisode[] = []; + $$('div.episodes > ul > li > a').map((i, el) => { + $$(el) + .map((i, el) => { + const possibleIds = $$(el).attr('data-ids')?.split(',')!; + const number = parseInt($$(el).attr('data-num')?.toString()!); + const title = $$(el).find('span').text().length > 0 ? $$(el).find('span').text() : undefined; + const isFiller = $$(el).hasClass('filler'); + + episodes.push({ + id: possibleIds[0], + dubId: possibleIds[1], + number: number, + title: title, + isFiller: isFiller, + }); + }) + .get(); + }); + animeInfo.episodes?.push(...episodes); + + return animeInfo; + } catch (err) { + console.log(err); + throw new Error((err as Error).message); + } + } + + override async fetchEpisodeSources( + episodeId: string, + server: StreamingServers = StreamingServers.VizCloud + ): Promise { + if (episodeId.startsWith('http')) { + const serverUrl = new URL(episodeId); + switch (server) { + case StreamingServers.StreamTape: + return { + headers: { Referer: serverUrl.href, 'User-Agent': USER_AGENT }, + sources: await new StreamTape().extract(serverUrl), + }; + case StreamingServers.VizCloud: + case StreamingServers.VidCloud: + return { + headers: { Referer: serverUrl.href, 'User-Agent': USER_AGENT }, + sources: await new VizCloud().extract(serverUrl, this.nineAnimeResolver, this.apiKey), + }; + case StreamingServers.MyCloud: + return { + headers: { Referer: serverUrl.href, 'User-Agent': USER_AGENT }, + sources: await new VizCloud().extract(serverUrl, this.nineAnimeResolver, this.apiKey), + }; + case StreamingServers.Filemoon: + return { + headers: { Referer: serverUrl.href, 'User-Agent': USER_AGENT }, + sources: await new Filemoon().extract(serverUrl), + }; + default: + throw new Error('Server not supported'); + } + } + try { + const servers = await this.fetchEpisodeServers(episodeId); + let s = servers.find(s => s.name === server); + switch (server) { + case StreamingServers.VizCloud: + s = servers.find(s => s.name === 'vidstream')!; + if (!s) throw new Error('Vidstream server found'); + break; + case StreamingServers.StreamTape: + s = servers.find(s => s.name === 'streamtape'); + if (!s) throw new Error('Streamtape server found'); + break; + case StreamingServers.MyCloud: + s = servers.find(s => s.name === 'mycloud'); + if (!s) throw new Error('Mycloud server found'); + break; + case StreamingServers.Filemoon: + s = servers.find(s => s.name === 'filemoon'); + if (!s) throw new Error('Filemoon server found'); + break; + default: + throw new Error('Server not found'); + } + + const serverVrf = ( + await this.client.get( + `${this.nineAnimeResolver}/vrf?query=${encodeURIComponent(s.url)}&apikey=${this.apiKey}` + ) + ).data.url; + const serverSource = ( + await this.client.get(`${this.baseUrl}/ajax/server/${s.url}?vrf=${encodeURIComponent(serverVrf)}`) + ).data; + const embedURL = ( + await this.client.get( + `${this.nineAnimeResolver}/decrypt?query=${encodeURIComponent(serverSource.result.url)}&apikey=${ + this.apiKey + }` + ) + ).data.url; + + if (embedURL.startsWith('http')) { + const response: ISource = await this.fetchEpisodeSources(embedURL, server); + response.embedURL = embedURL; + response.intro = { + start: serverSource?.result?.skip_data?.intro_begin ?? 0, + end: serverSource?.result?.skip_data?.intro_end ?? 0, + }; + + return response; + } else { + throw new Error('Server did not respond correctly'); + } + } catch (err) { + throw new Error((err as Error).message); + } + } + + override async fetchEpisodeServers(episodeId: string): Promise { + if (!episodeId.startsWith(this.baseUrl)) + episodeId = `${this.baseUrl}/ajax/server/list/${episodeId}?vrf=${encodeURIComponent( + await this.ev(episodeId) + )}`; + + const { + data: { result }, + } = await this.client.get(episodeId); + + const $ = load(result); + + const servers: IEpisodeServer[] = []; + $('.type > ul > li').each((i, el) => { + const serverId = $(el).attr('data-link-id')!; + servers.push({ + name: $(el).text().toLocaleLowerCase(), + url: `${serverId}`, + }); + }); + + return servers; + } + + public async ev(query: string, raw = false): Promise { + const { data } = await this.client.get( + `${this.nineAnimeResolver}/vrf?query=${encodeURIComponent(query)}&apikey=${this.apiKey}` + ); + + if (raw) { + return data; + } else { + return data.url; + } + } + + public async searchVrf(query: string, raw = false): Promise { + const { data } = await this.client.get( + `${this.nineAnimeResolver}/9anime-search?query=${encodeURIComponent(query)}&apikey=${this.apiKey}` + ); + + if (raw) { + return data; + } else { + return data.url; + } + } + + public async decrypt(query: string, raw = false): Promise { + const { data } = await this.client.get( + `${this.nineAnimeResolver}/decrypt?query=${encodeURIComponent(query)}&apikey=${this.apiKey}` + ); + + if (raw) { + return data; + } else { + return data.url; + } + } + + public async vizcloud(query: string): Promise { + const { data } = await this.client.get( + `${this.nineAnimeResolver}/vizcloud?query=${encodeURIComponent(query)}&apikey=${this.apiKey}` + ); + return data; + } + + public async customRequest(query: string, action: string): Promise { + const { data } = await this.client.get( + `${this.nineAnimeResolver}/${action}?query=${encodeURIComponent(query)}&apikey=${this.apiKey}` + ); + return data; + } +} + +// (async () => { +// // const nineAnime = new NineAnime(); +// // const searchResults = await nineAnime.search('attack on titan'); +// // const animeInfo = await nineAnime.fetchAnimeInfo('shadowverse-flame.rljqn'); +// // @ts-ignore +// // const episodeSources = await nineAnime.fetchEpisodeSources("ab68", "decrypt"); +// // console.log(await nineAnime.vizcloud("LNPEK8Q0QPXW")); +// // console.log(await nineAnime.decrypt("ab6/", true)); +// // console.log(await nineAnime.customRequest("LNPEK8Q0QPXW", "9anime-search")); +// })(); + +export default NineAnime; diff --git a/consumet.ts/src/providers/anime/anify.ts b/consumet.ts/src/providers/anime/anify.ts new file mode 100644 index 00000000..22e5f88a --- /dev/null +++ b/consumet.ts/src/providers/anime/anify.ts @@ -0,0 +1,278 @@ +import { + AnimeParser, + ISearch, + IAnimeInfo, + MediaStatus, + IAnimeResult, + ISource, + IEpisodeServer, + IAnimeEpisode, + IVideo, + MediaFormat, +} from '../../models'; +import { AxiosAdapter } from 'axios'; +import { ProxyConfig } from '../../models'; + +type ProviderId = '9anime' | 'animepahe' | 'zoro' | 'gogoanime'; + +class Anify extends AnimeParser { + override readonly name = 'Anify'; + protected override baseUrl = 'https://api.anify.tv'; + protected override classPath = 'ANIME.Anify'; + + private readonly actions: { + [k: string]: { format: (episodeId: string) => string; unformat: (episodeId: string) => string }; + } = { + gogoanime: { + format: (episodeId: string) => `/${episodeId}`, + unformat: (episodeId: string) => episodeId.replace('/', ''), + }, + zoro: { + format: (episodeId: string) => `watch/${episodeId.replace('$episode$', '?ep=')}`, + unformat: (episodeId: string) => episodeId.replace('?ep=', '$episode$').split('watch/')[1] + '$sub', + }, + animepahe: { + format: (episodeId: string) => episodeId, + unformat: (episodeId: string) => episodeId, + }, + '9anime': { + format: (episodeId: string) => episodeId, + unformat: (episodeId: string) => episodeId, + }, + }; + + constructor( + protected proxyConfig?: ProxyConfig, + protected adapter?: AxiosAdapter, + protected providerId: ProviderId = 'gogoanime' + ) { + super(proxyConfig, adapter); + } + + /** + * @param query Search query + * @param page Page number (optional) + */ + rawSearch = async (query: string, page: number = 1): Promise => { + const { data } = await this.client.get(`${this.baseUrl}/search/anime/${query}?page=${page}`); + + return data.results; + }; + /** + * @param query Search query + * @param page Page number (optional) + */ + override search = async (query: string, page: number = 1): Promise> => { + const res = { + currentPage: page, + hasNextPage: false, + results: [], + }; + + const { data } = await this.client.get( + `${this.baseUrl}/search-advanced?type=anime&query=${query}&page=${page}` + ); + + if (data.currentPage !== res.currentPage) res.hasNextPage = true; + + res.results = data?.results.map( + (anime: { + id: string; + title: { + english: string; + romaji: string; + native: string; + }; + coverImage: string | null; + bannerImage: string | null; + year: number; + description: string | null; + genres: string[]; + rating: { + anilist: number; + }; + status: string; + mappings: { + [k: string]: string; + }; + type: string; + }) => ({ + id: anime.id, + anilistId: anime.id, + title: anime.title.english ?? anime.title.romaji ?? anime.title.native, + image: anime.coverImage, + cover: anime.bannerImage, + releaseDate: anime.year, + description: anime.description, + genres: anime.genres, + rating: anime.rating.anilist, + status: anime.status as MediaStatus, + mappings: anime.mappings, + type: anime.type as MediaFormat, + }) + ); + return res; + }; + + /** + * @param id Anime id + */ + override fetchAnimeInfo = async (id: string): Promise => { + const animeInfo: IAnimeInfo = { + id: id, + title: '', + }; + + const { data } = await this.client.get(`${this.baseUrl}/info/${id}`).catch(() => { + throw new Error('Anime not found. Please use a valid id!'); + }); + + animeInfo.anilistId = data.id; + animeInfo.title = data.title.english ?? data.title.romaji ?? data.title.native; + animeInfo.image = data.coverImage; + animeInfo.cover = data.bannerImage; + animeInfo.season = data.season; + animeInfo.releaseDate = data.year; + animeInfo.duration = data.duration; + animeInfo.popularity = data.popularity.anilist; + animeInfo.description = data.description; + animeInfo.genres = data.genres; + animeInfo.rating = data.rating.anilist; + animeInfo.status = data.status as MediaStatus; + animeInfo.synonyms = data.synonyms; + animeInfo.mappings = data.mappings; + animeInfo.type = data.type as MediaFormat; + animeInfo.artwork = data.artwork as { + type: 'poster' | 'banner' | 'top_banner' | 'poster' | 'icon' | 'clear_art' | 'clear_logo'; + img: string; + providerId: 'tvdb' | 'kitsu' | 'anilist'; + }[]; + + const providerData = data.episodes.data.filter((e: any) => e.providerId === this.providerId)[0]; + + animeInfo.episodes = providerData.episodes.map( + (episode: { + id: string; + number: number; + isFiller: boolean; + title: string; + description?: string; + img?: string; + rating: number | null; + }): IAnimeEpisode => ({ + id: this.actions[this.providerId].unformat(episode.id), + number: episode.number, + isFiller: episode.isFiller, + title: episode.title, + description: episode.description, + image: episode.img, + rating: episode.rating, + }) + ); + + return animeInfo; + }; + + fetchAnimeInfoByIdRaw = async (id: string): Promise => { + const { data } = await this.client.get(`${this.baseUrl}/info/${id}`).catch(err => { + throw new Error("Backup api seems to be down! Can't fetch anime info"); + }); + + return data; + }; + + /** + * @param id anilist id + */ + fetchAnimeInfoByAnilistId = async ( + id: string, + providerId: '9anime' | 'animepahe' | 'zoro' | 'gogoanime' = 'gogoanime' + ): Promise => { + const animeInfo: IAnimeInfo = { + id: id, + title: '', + }; + + const { data } = await this.client.get(`${this.baseUrl}/media?providerId=${providerId}&id=${id}`); + + animeInfo.anilistId = data.id; + animeInfo.title = data.title.english ?? data.title.romaji ?? data.title.native; + animeInfo.image = data.coverImage; + animeInfo.cover = data.bannerImage; + animeInfo.season = data.season; + animeInfo.releaseDate = data.year; + animeInfo.duration = data.duration; + animeInfo.popularity = data.popularity.anilist; + animeInfo.description = data.description; + animeInfo.genres = data.genres; + animeInfo.rating = data.rating.anilist; + animeInfo.status = data.status as MediaStatus; + animeInfo.synonyms = data.synonyms; + animeInfo.mappings = data.mappings; + animeInfo.type = data.type as MediaFormat; + animeInfo.artwork = data.artwork as { + type: 'poster' | 'banner' | 'top_banner' | 'poster' | 'icon' | 'clear_art' | 'clear_logo'; + img: string; + providerId: 'tvdb' | 'kitsu' | 'anilist'; + }[]; + + const providerData = data.episodes.data.filter((e: any) => e.providerId === this.providerId)[0]; + + animeInfo.episodes = providerData.episodes.map( + (episode: { + id: string; + number: number; + isFiller: boolean; + title: string; + description?: string; + img?: string; + rating: number | null; + }): IAnimeEpisode => ({ + id: this.actions[this.providerId].unformat(episode.id), + number: episode.number, + isFiller: episode.isFiller, + title: episode.title, + description: episode.description, + image: episode.img, + rating: episode.rating, + }) + ); + + return animeInfo; + }; + + override fetchEpisodeSources = async ( + episodeId: string, + episodeNumber: number, + id: number + ): Promise => { + try { + const { data } = await this.client.get( + `${this.baseUrl}/sources?providerId=${this.providerId}&watchId=${this.actions[this.providerId].format( + episodeId + )}&episodeNumber=${episodeNumber}&id=${id}&subType=sub` + ); + + return data; + } catch (err) { + throw new Error('Episode not found!\n' + err); + } + }; + + /** + * @deprecated + */ + override fetchEpisodeServers(episodeId: string): Promise { + throw new Error('Method not implemented.'); + } +} + +export default Anify; + +// (async () => { +// const anify = new Anify(); +// const res = await anify.fetchAnimeInfo('1'); +// console.log(res); +// const souces = await anify.fetchEpisodeSources(res.episodes![0].id, 1, 1); +// console.log(souces); +// })(); diff --git a/consumet.ts/src/providers/anime/animefox.ts b/consumet.ts/src/providers/anime/animefox.ts new file mode 100644 index 00000000..b212897c --- /dev/null +++ b/consumet.ts/src/providers/anime/animefox.ts @@ -0,0 +1,213 @@ +import { load } from 'cheerio'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + MediaStatus, + IAnimeResult, + ISource, + IEpisodeServer, + MediaFormat, +} from '../../models'; + +import { GogoCDN } from '../../extractors'; + +class AnimeFox extends AnimeParser { + override readonly name = 'AnimeFox'; + protected override baseUrl = 'https://animefox.tv'; + protected override logo = 'https://animefox.tv/assets/images/logo.png'; + protected override classPath = 'ANIME.AnimeFox'; + + /** + * @param query Search query + * @param page Page number (optional) + */ + override search = async (query: string, page: number = 1): Promise> => { + try { + const { data } = await this.client.get( + `${this.baseUrl}/search?keyword=${decodeURIComponent(query)}&page=${page}` + ); + + const $ = load(data); + + const hasNextPage = $('.pagination > nav > ul > li').last().hasClass('disabled') ? false : true; + + const searchResults: IAnimeResult[] = []; + + $('div.film_list-wrap > div').each((i, el) => { + let type = undefined; + switch ($(el).find('div.fd-infor > span').text()) { + case 'TV Series': + type = MediaFormat.TV; + break; + case 'Movie': + type = MediaFormat.MOVIE; + break; + case 'Special': + type = MediaFormat.SPECIAL; + break; + case 'OVA': + type = MediaFormat.OVA; + break; + default: + type = MediaFormat.TV; + break; + } + searchResults.push({ + id: $(el).find('div.film-poster > a').attr('href')?.replace('/anime/', '')!, + title: $(el).find('div.film-poster > img').attr('alt')!, + type: type, + image: $(el).find('div.fd-infor > span:nth-child(1)').text()!, + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`!, + episode: parseInt($(el).find('div.tick-eps').text().replace('EP', '').trim())!, + }); + }); + return { + currentPage: page, + hasNextPage: hasNextPage, + results: searchResults, + }; + } catch (err: any) { + throw new Error(err); + } + }; + + /** + * @param id Anime id + */ + override fetchAnimeInfo = async (id: string): Promise => { + const info: IAnimeInfo = { + id: id, + title: '', + }; + try { + const { data } = await this.client.get(`${this.baseUrl}/anime/${id}`); + const $ = load(data); + + info.title = $('h2.film-name').attr('data-jname')!; + info.image = $('img.film-poster-img').attr('data-src')!; + info.description = $('div.anisc-info > div:nth-child(1) > div').text().trim()!; + switch ($('div.anisc-info > div:nth-child(8) > a').text().trim()) { + case 'TV Series': + info.type = MediaFormat.TV; + break; + case 'Movie': + info.type = MediaFormat.MOVIE; + break; + case 'Special': + info.type = MediaFormat.SPECIAL; + break; + case 'OVA': + info.type = MediaFormat.OVA; + break; + default: + info.type = MediaFormat.TV; + break; + } + + info.releaseYear = $('div.anisc-info > div:nth-child(7) > a').text().trim()!; + switch ($('div.anisc-info > div:nth-child(9) > a').text().trim()!) { + case 'Ongoing': + info.status = MediaStatus.ONGOING; + break; + case 'Completed': + info.status = MediaStatus.COMPLETED; + break; + case 'Upcoming': + info.status = MediaStatus.NOT_YET_AIRED; + break; + default: + info.status = MediaStatus.UNKNOWN; + break; + } + info.totalEpisodes = parseInt( + $('div.anisc-info > div:nth-child(4) > span:nth-child(2)').text().trim() + )!; + info.url = `${this.baseUrl}/${id}`; + info.episodes = []; + info.hasSub = $('div.anisc-info > div:nth-child(3) > span:nth-child(2)').text().trim() == 'Subbed'; + info.hasDub = $('div.anisc-info > div:nth-child(3) > span:nth-child(2)').text().trim() == 'Dubbed'; + const episodes = Array.from({ length: info.totalEpisodes }, (_, i) => i + 1); + episodes.forEach((element, i) => + info.episodes?.push({ + id: `${id}-episode-${i + 1}`, + number: i + 1, + title: `${info.title} Episode ${i + 1}`, + url: `${this.baseUrl}/watch/${id}-episode-${i + 1}`, + }) + ); + return info; + } catch (err: any) { + throw new Error(err); + } + }; + + /** + * @param page Page number + */ + fetchRecentEpisodes = async (page: number = 1): Promise> => { + try { + const { data } = await this.client.get(`${this.baseUrl}/latest-added?page=${page}`); + const $ = load(data); + + const hasNextPage = $('.pagination > nav > ul > li').last().hasClass('disabled') ? false : true; + + const recentEpisodes: IAnimeResult[] = []; + + $('div.film_list-wrap > div').each((i, el) => { + recentEpisodes.push({ + id: $(el).find('div.film-poster > a').attr('href')?.replace('/watch/', '')!, + image: $(el).find('div.film-poster > img').attr('data-src')!, + title: $(el).find('div.film-poster > img').attr('alt')!, + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}!`, + episode: parseInt($(el).find('div.tick-eps').text().replace('EP ', '').split('/')[0])!, + }); + }); + + return { + currentPage: page, + hasNextPage: hasNextPage, + results: recentEpisodes, + }; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + }; + + /** + * + * @param episodeId episode id + */ + override fetchEpisodeSources = async (episodeId: string): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/watch/${episodeId}`); + const $ = load(data); + const iframe = $('#iframe-to-load').attr('src') || ''; + const streamUrl = `https://goload.io/streaming.php?id=${iframe.split('=')[1]}`; + return { + sources: await new GogoCDN(this.proxyConfig).extract(new URL(streamUrl)), + }; + } catch (err) { + console.log(err); + throw new Error('Something went wrong. Please try again later.'); + } + }; + + /** + * @deprecated Use fetchEpisodeSources instead + */ + override fetchEpisodeServers = (episodeIs: string): Promise => { + throw new Error('Method not implemented.'); + }; +} + +export default AnimeFox; + +// (async () => { +// const animepahe = new AnimeFox(); +// const sources = await animepahe.fetchEpisodeSources( +// 'youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e-tv-episode-1' +// ); +// console.log(sources); +// })(); diff --git a/consumet.ts/src/providers/anime/animepahe.ts b/consumet.ts/src/providers/anime/animepahe.ts new file mode 100644 index 00000000..ce1efd9c --- /dev/null +++ b/consumet.ts/src/providers/anime/animepahe.ts @@ -0,0 +1,221 @@ +import { load } from 'cheerio'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + MediaStatus, + IAnimeResult, + ISource, + IAnimeEpisode, + IEpisodeServer, + MediaFormat, +} from '../../models'; +import { Kwik } from '../../extractors'; + +class AnimePahe extends AnimeParser { + override readonly name = 'AnimePahe'; + protected override baseUrl = 'https://animepahe.com'; + protected override logo = 'https://animepahe.com/pikacon.ico'; + protected override classPath = 'ANIME.AnimePahe'; + + // private readonly sgProxy = 'https://cors.consumet.stream'; + + /** + * @param query Search query + */ + override search = async (query: string): Promise> => { + try { + const { data } = await this.client.get(`${this.baseUrl}/api?m=search&q=${encodeURIComponent(query)}`); + + const res = { + results: data.data.map((item: any) => ({ + id: `${item.id}/${item.session}`, + title: item.title, + image: item.poster, + rating: item.score, + releaseDate: item.year, + type: item.type, + })), + }; + + return res; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * @param id id format id/session + * @param episodePage Episode page number (optional) default: -1 to get all episodes. number of episode pages can be found in the anime info object + */ + override fetchAnimeInfo = async (id: string, episodePage: number = -1): Promise => { + const animeInfo: IAnimeInfo = { + id: id, + title: '', + }; + + try { + const res = await this.client.get( + `${this.baseUrl}/anime/${id.split('/')[1]}?anime_id=${id.split('/')[0]}` + ); + const $ = load(res.data); + + animeInfo.title = $('div.title-wrapper > h1 > span').first().text(); + animeInfo.image = $('div.anime-poster a').attr('href'); + animeInfo.cover = `https:${$('div.anime-cover').attr('data-src')}`; + animeInfo.description = $('div.anime-summary').text(); + animeInfo.genres = $('div.anime-genre ul li') + .map((i, el) => $(el).find('a').attr('title')) + .get(); + + switch ($('div.col-sm-4.anime-info p:icontains("Status:") a').text().trim()) { + case 'Currently Airing': + animeInfo.status = MediaStatus.ONGOING; + break; + case 'Finished Airing': + animeInfo.status = MediaStatus.COMPLETED; + break; + default: + animeInfo.status = MediaStatus.UNKNOWN; + } + animeInfo.type = $('div.col-sm-4.anime-info > p:nth-child(2) > a') + .text() + .trim() + .toUpperCase() as MediaFormat; + animeInfo.releaseDate = $('div.col-sm-4.anime-info > p:nth-child(5)') + .text() + .split('to')[0] + .replace('Aired:', '') + .trim(); + animeInfo.aired = $('div.col-sm-4.anime-info > p:nth-child(5)') + .text() + .replace('Aired:', '') + .trim() + .replace('\n', ' '); + animeInfo.studios = $('div.col-sm-4.anime-info > p:nth-child(7)') + .text() + .replace('Studio:', '') + .trim() + .split('\n'); + animeInfo.totalEpisodes = parseInt( + $('div.col-sm-4.anime-info > p:nth-child(3)').text().replace('Episodes:', '') + ); + + animeInfo.episodes = []; + if (episodePage < 0) { + const { + data: { last_page, data }, + } = await this.client.get( + `${this.baseUrl}/api?m=release&id=${id.split('/')[1]}&sort=episode_asc&page=1` + ); + + animeInfo.episodePages = last_page; + + animeInfo.episodes.push( + ...data.map( + (item: any) => + ({ + id: `${id.split('/')[1]}/${item.session}`, + number: item.episode, + title: item.title, + image: item.snapshot, + duration: item.duration, + url: `${this.baseUrl}/play/${id.split('/')[1]}/${item.session}`, + } as IAnimeEpisode) + ) + ); + + for (let i = 1; i < last_page; i++) { + animeInfo.episodes.push(...(await this.fetchEpisodes(id.split('/')[1], i + 1))); + } + } else { + animeInfo.episodes.push(...(await this.fetchEpisodes(id.split('/')[1], episodePage))); + } + + return animeInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId episode id + */ + override fetchEpisodeSources = async (episodeId: string): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/play/${episodeId}`, { + headers: { + Referer: `${this.baseUrl}`, + }, + }); + + const $ = load(data); + + const links = $('div#resolutionMenu > button').map((i, el) => ({ + url: $(el).attr('data-src')!, + quality: $(el).text(), + audio: $(el).attr('data-audio'), + })); + + const iSource: ISource = { + headers: { + Referer: 'https://kwik.cx/', + }, + sources: [], + }; + + for (const link of links) { + const res = await new Kwik(this.proxyConfig).extract(new URL(link.url)); + res[0].quality = link.quality; + res[0].isDub = link.audio === 'eng'; + iSource.sources.push(res[0]); + } + + return iSource; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + private fetchEpisodes = async (session: string, page: number): Promise => { + const res = await this.client.get( + `${this.baseUrl}/api?m=release&id=${session}&sort=episode_asc&page=${page}` + ); + + const epData = res.data.data; + + return [ + ...epData.map( + (item: any): IAnimeEpisode => ({ + id: `${session}/${item.session}`, + number: item.episode, + title: item.title, + image: item.snapshot, + duration: item.duration, + url: `${this.baseUrl}/play/${session}/${item.session}`, + }) + ), + ] as IAnimeEpisode[]; + }; + + /** + * @deprecated + * @attention AnimePahe doesn't support this method + */ + override fetchEpisodeServers = (episodeLink: string): Promise => { + throw new Error('Method not implemented.'); + }; +} + +export default AnimePahe; + +// (async () => { +// const animepahe = new AnimePahe(); +// +// const anime = await animepahe.search('Classroom of the elite'); +// const info = await animepahe.fetchAnimeInfo(anime.results[0].id); +// const sources = await animepahe.fetchEpisodeSources(info.episodes![0].id); +// console.log(sources); +// })(); diff --git a/consumet.ts/src/providers/anime/animesaturn.ts b/consumet.ts/src/providers/anime/animesaturn.ts new file mode 100644 index 00000000..931bb675 --- /dev/null +++ b/consumet.ts/src/providers/anime/animesaturn.ts @@ -0,0 +1,212 @@ +import { Cheerio, load } from 'cheerio'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + MediaStatus, + IAnimeResult, + ISource, + IAnimeEpisode, + IEpisodeServer, + StreamingServers, +} from '../../models'; + +import { StreamTape } from '../../utils'; +import { Mp4Player } from '../../extractors'; + +class AnimeSaturn extends AnimeParser { + override readonly name = 'AnimeSaturn'; + protected override baseUrl = 'https://www.animesaturn.tv/'; + protected override logo = 'https://www.animesaturn.tv/immagini/favicon-32x32.png'; + protected override classPath = 'ANIME.AnimeSaturn'; + + /** + * @param query Search query + */ + override search = async (query: string): Promise> => { + // baseUrl/animelist?search={query} + + const data = await this.client.get(`${this.baseUrl}animelist?search=${query}`); + + const $ = await load(data.data); + + if (!$) return { results: [] }; + + const res: { + hasNextPage: boolean; + results: IAnimeResult[]; + } = { + hasNextPage: false, + results: [], + }; + + $('ul.list-group li').each((i, element) => { + const item: IAnimeResult = { + id: $(element)?.find('a.thumb')?.attr('href')?.split('/')?.pop() ?? '', + title: $(element)?.find('h3 a')?.text(), + image: $(element)?.find('img.copertina-archivio')?.attr('src'), + url: $(element)?.find('h3 a')?.attr('href'), + }; + + if (!item.id) throw new Error('Invalid id'); + + res.results.push(item); + }); + + return res; + }; + + /** + * @param id Anime id + */ + override fetchAnimeInfo = async (id: string): Promise => { + const data = await this.client.get(`${this.baseUrl}anime/${id}`); + const $ = await load(data.data); + + const info: IAnimeInfo = { + id, + title: $('div.container.anime-title-as> b').text(), + malID: $('a[href^="https://myanimelist.net/anime/"]').attr('href')?.slice(30, -1), + alID: $('a[href^="https://anilist.co/anime/"]').attr('href')?.slice(25, -1), + genres: + $('div.container a.badge.badge-light') + ?.map((i, element): string => { + return $(element).text(); + }) + .toArray() ?? undefined, + image: $('img.img-fluid')?.attr('src') || undefined, + cover: + $('div.banner') + ?.attr('style') + ?.match(/background:\s*url\(['"]?([^'")]+)['"]?\)/i)?.[1] || undefined, + description: $('#full-trama').text(), + episodes: [], + }; + + const episodes: IAnimeEpisode[] = []; + + $('.tab-pane.fade').each((i, element) => { + $(element) + .find('.bottone-ep') + .each((i, element) => { + const link = $(element).attr('href'); + const episodeNumber = $(element).text().trim().replace('Episodio ', '').trim(); + + episodes.push({ + number: parseInt(episodeNumber), + id: link?.split('/')?.pop() ?? '', + }); + }); + }); + + info.episodes = episodes.sort((a, b) => a.number - b.number); + + return info; + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeSources = async (episodeId: string): Promise => { + const fakeData = await this.client.get(`${this.baseUrl}ep/${episodeId}`); + const $2 = await load(fakeData.data); + + const serverOneUrl = $2("div > a:contains('Guarda lo streaming')").attr('href'); // scrape from server 1 (m3u8 and mp4 urls) + if (serverOneUrl == null) throw new Error('Invalid url'); + + let data = await this.client.get(serverOneUrl); + let $ = await load(data.data); + + const sources: ISource = { + headers: {}, + subtitles: [], + sources: [], + }; + + // M3U8 and MP4 + const scriptTag = $('script').filter(function () { + return $(this).text().includes("jwplayer('player_hls')"); + }); + + let serverOneSource: string | undefined; + + // m3u8 + scriptTag.each((i, element) => { + const scriptText = $(element).text(); + + scriptText.split('\n').forEach(line => { + if (line.includes('file:') && !serverOneSource) { + serverOneSource = line + .split('file:')[1] + .trim() + .replace(/'/g, '') + .replace(/,/g, '') + .replace(/"/g, ''); + } + }); + }); + + // mp4 + if (!serverOneSource) { + serverOneSource = $('#myvideo > source').attr('src'); + } + + if (!serverOneSource) throw new Error('Invalid source'); + + sources.sources.push({ + url: serverOneSource, + isM3U8: serverOneSource.includes('.m3u8'), + }); + + if (serverOneSource.includes('.m3u8')) { + sources.subtitles?.push({ + url: serverOneSource.replace('playlist.m3u8', 'subtitles.vtt'), + lang: 'Spanish', + }); + } + + // STREAMTAPE + const serverTwoUrl = serverOneUrl + '&server=1'; // scrape from server 2 (streamtape) + data = await this.client.get(serverTwoUrl); + $ = await load(data.data); + + const videoUrl = $('.embed-container > iframe').attr('src'); + const serverTwoSource = await new StreamTape(this.proxyConfig, this.adapter).extract(new URL(videoUrl!)); + + if (!serverTwoSource) throw new Error('Invalid source'); + + sources.sources.push({ + url: serverTwoSource[0].url, + isM3U8: serverTwoSource[0].isM3U8, + }); + + return sources; + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeServers = (episodeId: string): Promise => { + throw new Error('Method not implemented.'); + }; +} + +export default AnimeSaturn; + +// Test this dog code +// const animeSaturn = new AnimeSaturn(); + +/*animeSaturn.search('naruto').then((res) => { +console.log(res); + + animeSaturn.fetchAnimeInfo(res.results[0].id).then((res) => { + console.log(res); + + animeSaturn.fetchEpisodeSources(res?.episodes?.at(0)?.id || "0").then((res) => { + console.log(res); + }) + }); +});*/ diff --git a/consumet.ts/src/providers/anime/animeunity.ts b/consumet.ts/src/providers/anime/animeunity.ts new file mode 100644 index 00000000..a2cd94d7 --- /dev/null +++ b/consumet.ts/src/providers/anime/animeunity.ts @@ -0,0 +1,202 @@ +import { Cheerio, load } from 'cheerio'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + IAnimeResult, + ISource, + IEpisodeServer, + SubOrSub, +} from '../../models'; + +class AnimeUnity extends AnimeParser { + override readonly name = 'AnimeUnity'; + protected override baseUrl = 'https://www.animeunity.to'; + protected override logo = 'https://www.animeunity.to/favicon-32x32.png'; + protected override classPath = 'ANIME.AnimeUnity'; + + /** + * @param query Search query + */ + override search = async (query: string): Promise> => { + try { + const res = await this.client.get(`${this.baseUrl}/archivio?title=${query}`); + const $ = load(res.data); + + if (!$) return { results: [] }; + + const items = JSON.parse("" + $('archivio').attr('records') + "") + + const searchResult: { + hasNextPage: boolean; + results: IAnimeResult[]; + } = { + hasNextPage: false, + results: [], + }; + + for (const i in items) { + searchResult.results.push({ + id: `${items[i].id}-${items[i].slug}`, + title: items[i].title ?? items[i].title_eng, + url: `${this.baseUrl}/anime/${items[i].id}-${items[i].slug}`, + image: `${items[i].imageurl}`, + cover: `${items[i].imageurl_cover}`, + subOrDub: `${items[i].dub + ? SubOrSub.DUB + : SubOrSub.SUB}` + }) + } + + return searchResult + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * @param id Anime id + * @param page Page number + */ + override fetchAnimeInfo = async (id: string, page: number = 1): Promise => { + const url = `${this.baseUrl}/anime/${id}` + const episodesPerPage = 120 + const lastPageEpisode = page*episodesPerPage + const firstPageEpisode = lastPageEpisode-119 + const url2 = `${this.baseUrl}/info_api/${id}/1?start_range=${firstPageEpisode}&end_range=${lastPageEpisode}` + + try { + const res = await this.client.get(url); + const $ = load(res.data); + + const totalEpisodes = parseInt($('video-player')?.attr('episodes_count') ?? '0') + const totalPages = Math.round(totalEpisodes/120) + 1 + + if(page < 1 || page > totalPages) + throw new Error(`Argument 'page' for ${id} must be between 1 and ${totalPages}! (You passed ${page})`); + + const animeInfo: IAnimeInfo = { + currentPage: page, + hasNextPage: totalPages > page, + totalPages: totalPages, + id: id, + title: $('h1.title')?.text().trim(), + url: url, + alID: $('.banner')?.attr('style')?.split('/')?.pop()?.split('-')[0], + genres: + $('.info-wrapper.pt-3.pb-3 small')?.map((_, element): string => { + return $(element).text().replace(',', '').trim() + }).toArray() ?? undefined, + totalEpisodes: totalEpisodes, + image: $('img.cover')?.attr('src'), + // image: $('meta[property="og:image"]')?.attr('content'), + cover: $('.banner')?.attr('src') ?? $('.banner')?.attr('style')?.replace('background: url(', ''), + description: $('.description').text().trim(), + episodes: [] + } + + // fetch episodes method 1 (only first page can be fetchedd) + // const items = JSON.parse("" + $('video-player').attr('episodes') + "") + + // fetch episodes method 2 (all pages can be fetched) + const res2 = await this.client.get(url2); + const items = res2.data.episodes + + for(const i in items) { + animeInfo.episodes?.push({ + id: `${id}/${items[i].id}`, + number: parseInt(items[i].number), + url: `${url}/${items[i].id}`, + }) + } + + return animeInfo + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeSources = async (episodeId: string): Promise => { + try { + const res = await this.client.get(`${this.baseUrl}/anime/${episodeId}`); + const $ = load(res.data); + + const episodeSources: ISource = { + sources: [] + } + + const streamUrl = $('video-player').attr('embed_url') + + if(streamUrl) { + const res = await this.client.get(streamUrl); + const $ = load(res.data); + + const domain = $('script:contains("window.video")').text()?.match(/url: '(.*)'/)![1] + const token = $('script:contains("window.video")').text()?.match(/token': '(.*)'/)![1] + const expires = $('script:contains("window.video")').text()?.match(/expires': '(.*)'/)![1] + + const defaultUrl = `${domain}?token=${token}&referer=&expires=${expires}&h=1` + const m3u8Content = await this.client.get(defaultUrl) + + if (m3u8Content.data.includes('EXTM3U')) { + const videoList = m3u8Content.data.split('#EXT-X-STREAM-INF:'); + for (const video of videoList ?? []) { + if (video.includes('BANDWIDTH')) { + const url = video.split('\n')[1]; + const quality = video.split('RESOLUTION=')[1].split('\n')[0].split('x')[1]; + + episodeSources.sources.push({ + url: url, + quality: `${quality}p`, + isM3U8: true, + }); + } + } + } + + episodeSources.sources.push({ + url: defaultUrl, + quality: `default`, + isM3U8: true, + }); + + episodeSources.download = $('script:contains("window.downloadUrl ")').text()?.match(/downloadUrl = '(.*)'/)![1]?.toString() + } + + return episodeSources + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeServers = (episodeId: string): Promise => { + throw new Error('Method not implemented.'); + }; +} + +export default AnimeUnity + +/** + * old episode sources fetching method, keep it here. + */ +// const domain = $('script:contains("window.video")').text()?.match(/url: '(.*)'/)![1] +// const token = $('script:contains("window.video")').text()?.match(/token': '(.*)'/)![1] +// const token360p = $('script:contains("window.video")').text()?.match(/token360p': '(.*)'/)![1] +// const token480p = $('script:contains("window.video")').text()?.match(/token480p': '(.*)'/)![1] +// const token720p = $('script:contains("window.video")').text()?.match(/token720p': '(.*)'/)![1] +// const token1080p = $('script:contains("window.video")').text()?.match(/token1080p': '(.*)'/)![1] +// const expires = $('script:contains("window.video")').text()?.match(/expires': '(.*)'/)![1] + +// episodeSources.sources.push({ +// url: `${domain}?token=${token}&token360p=${token360p}&token480p=${token480p}&token720p=${token720p}&token1080p=${token1080p}&referer=&expires=${expires}`, +// isM3U8: true +// }) diff --git a/consumet.ts/src/providers/anime/bilibili.ts b/consumet.ts/src/providers/anime/bilibili.ts new file mode 100644 index 00000000..aae2625a --- /dev/null +++ b/consumet.ts/src/providers/anime/bilibili.ts @@ -0,0 +1,143 @@ +import { + AnimeParser, + IAnimeEpisode, + IAnimeInfo, + IAnimeResult, + IEpisodeServer, + ISearch, + ISource, + ISubtitle, + ProxyConfig, + SubOrSub, +} from '../../models'; +import { BilibiliExtractor } from '../../extractors'; +import { AxiosAdapter } from 'axios'; + +class Bilibili extends AnimeParser { + override readonly name = 'Bilibili'; + protected override baseUrl = 'https://bilibili.tv'; + protected override logo = + 'https://w7.pngwing.com/pngs/656/356/png-transparent-bilibili-thumbnail-social-media-icons.png'; + protected override classPath = 'ANIME.Bilibili'; + + private apiUrl = 'https://api.bilibili.tv/intl/gateway/web'; + + private cookie = ''; + private locale = 'en_US'; + private sgProxy = 'https://cors.consumet.stream'; + + constructor(cookie?: string, locale?: string, proxyConfig?: ProxyConfig, adapter?: AxiosAdapter) { + super(proxyConfig, adapter); + this.locale = locale ?? this.locale; + if (!cookie) return; + this.cookie = cookie; + } + + override async search(query: string): Promise> { + const { data } = await this.client.get( + `${this.sgProxy}/${this.apiUrl}/v2/search?keyword=${query}&platform=web&pn=1&ps=20&qid=&s_locale=${this.locale}`, + { headers: { cookie: this.cookie } } + ); + if (!data.data.filter((item: any) => item.module.includes('ogv')).length) + return { results: [], totalResults: 0 }; + + const results = data.data.find((item: any) => item.module.includes('ogv')); + + return { + totalResults: results.items.length ?? 0, + results: results.items.map( + (item: any): IAnimeResult => ({ + id: item.season_id, + title: item.title, + image: item.cover, + genres: item.styles.split(' / '), + rating: item.score, + view: item.view, + }) + ), + }; + } + + override async fetchAnimeInfo(id: string): Promise { + try { + const { data } = await this.client.get( + `${this.sgProxy}/https://app.biliintl.com/intl/gateway/v2/ogv/view/app/season2?locale=${this.locale}&platform=android&season_id=${id}`, + { headers: { cookie: this.cookie } } + ); + let counter = 1; + const episodes = data.data.sections.section.flatMap((section: any) => + section.ep_details.map( + (ep: any): IAnimeEpisode => ({ + id: ep.episode_id.toString(), + number: counter++, + title: ep.long_title || ep.title, + image: ep.horizontal_cover, + }) + ) + ); + return { + id, + title: data.data.title, + description: data.data.details.desc.value, + seasons: data.data.season_series.map((season: any) => ({ + id: season.season_id, + title: season.title, + })), + recommendations: data.data.for_you.item_details.map((section: any) => ({ + id: section.season_id, + title: section.title, + image: section.horizontal_cover, + genres: section.styles.split(' / '), + views: section.view, + })), + subOrDub: SubOrSub.SUB, + episodes: episodes, + totalEpisodes: episodes.length, + }; + } catch (err) { + throw err; + } + } + + override async fetchEpisodeSources(episodeId: string, ...args: any): Promise { + try { + const { data } = await this.client.get( + `${this.sgProxy}/${this.apiUrl}/v2/subtitle?s_locale=${this.locale}&platform=web&episode_id=${episodeId}`, + { headers: { cookie: this.cookie } } + ); + const ss = await this.client.get( + `${this.sgProxy}/${this.apiUrl}/playurl?s_locale=${this.locale}&platform=web&ep_id=${episodeId}`, + { headers: { cookie: this.cookie } } + ); + + const sources = await new BilibiliExtractor().extract(episodeId); + return { + sources: sources.sources, + subtitles: data.data.subtitles.map( + (sub: any): ISubtitle => ({ + id: sub.subtitle_id, + lang: sub.lang, + url: `https://api.consumet.org/utils/bilibili/subtitle?url=${sub.url}`, + }) + ), + }; + } catch (err) { + throw err; + } + } + + override async fetchEpisodeServers(episodeId: string): Promise { + throw new Error('Method not implemented.'); + } +} + +// (async () => { +// const source = new Bilibili(); + +// const result = await source.search('classroom of the elite'); +// const info = await source.fetchAnimeInfo(result.results[0].id); +// const episode = await source.fetchEpisodeSources('10143090'); +// console.log(episode); +// })(); + +export default Bilibili; diff --git a/consumet.ts/src/providers/anime/crunchyroll.ts b/consumet.ts/src/providers/anime/crunchyroll.ts new file mode 100644 index 00000000..43b18875 --- /dev/null +++ b/consumet.ts/src/providers/anime/crunchyroll.ts @@ -0,0 +1,144 @@ +import axios, { AxiosAdapter } from 'axios'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + MediaStatus, + IAnimeResult, + ISource, + IAnimeEpisode, + IEpisodeServer, + ISubtitle, + SubOrSub, + ProxyConfig, +} from '../../models'; +import { USER_AGENT } from '../../utils'; + +class Crunchyroll extends AnimeParser { + override readonly name = 'Crunchyroll'; + protected override baseUrl = 'https://cronchy.consumet.stream'; + protected override logo = + 'https://play-lh.googleusercontent.com/CjzbMcLbmTswzCGauGQExkFsSHvwjKEeWLbVVJx0B-J9G6OQ-UCl2eOuGBfaIozFqow'; + protected override classPath = `ANIME.${this.name}`; + + private locale = 'en-US'; + private TOKEN: any | undefined = undefined; + + private get options() { + return { + headers: { + 'User-Agent': USER_AGENT, + Authorization: 'Bearer ' + this.TOKEN?.access_token, + }, + }; + } + + private locales = [ + '[ar-ME] Arabic', + '[ar-SA] Arabic (Saudi Arabia)', + '[de-DE] German', + '[en-US] English', + '[es-419] Spanish (Latin America)', + '[es-ES] Spanish (Spain)', + '[fr-FR] French', + '[he-IL] Hebrew', + '[it-IT] Italian', + '[pt-BR] Portuguese (Brazil)', + '[pt-PT] Portuguese (Portugal)', + '[pl-PL] Polish', + '[ru-RU] Russian', + '[ro-RO] Romanian', + '[sv-SE] Swedish', + '[tr-TR] Turkish', + '[uk-UK] Ukrainian', + '[zh-CN] Chinese (Simplified)', + '[zh-TW] Chinese (Traditional)', + ]; + + private subOrder = [ + 'Subbed', + 'English Dub', + 'German Dub', + 'French Dub', + 'Spanish Dub', + 'Italian Dub', + 'Portuguese Dub', + ]; + + static async create( + locale?: string, + token?: string, + accessToken?: string, + proxyConfig?: ProxyConfig, + adapter?: AxiosAdapter + ) { + const instance = new Crunchyroll(proxyConfig, adapter); + instance.TOKEN = instance.TOKEN ?? (await axios.get(`${instance.baseUrl}/token`)).data; + return instance; + } + + /** + * @param query Search query + */ + override search = async (query: string): Promise> => { + try { + const { data } = await this.client.get(`${this.baseUrl}/search/${query}`, this.options); + + return data; + } catch (error) { + throw new Error(`Couldn't fetch data from ${this.name}`); + } + }; + + /** + * @param id Anime id + * @param mediaType Anime type (series, movie) + * @param fetchAllSeasons Fetch all episode seasons + */ + override fetchAnimeInfo = async ( + id: string, + mediaType: string, + fetchAllSeasons: boolean = false + ): Promise => { + if (mediaType == 'series') { + const { data } = await this.client.get( + `${this.baseUrl}/info/${id}?type=${mediaType}&fetchAllSeasons=${fetchAllSeasons}`, + this.options + ); + + return data; + } else { + throw new Error("Couldn't fetch data from Crunchyroll"); + } + }; + + /** + * + * @param episodeId Episode id + * @param format subtitle format (default: `srt`) (srt, vtt, ass) + * @param type Video type (default: `adaptive_hls` (m3u8)) `adaptive_dash` (dash), `drm_adaptive_dash` (dash with drm) + */ + override fetchEpisodeSources = async (episodeId: string): Promise => { + const { data } = await this.client.get(`${this.baseUrl}/episode/${episodeId}`, this.options); + //TODO: Add hardcoded subtitles for all languages + return data; + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeServers = (episodeId: string): Promise => { + throw new Error('Method not implemented.'); + }; +} + +export default Crunchyroll; + +// (async () => { +// const crunchyroll = await Crunchyroll.create(); +// const search = await crunchyroll.search('spy-x-family'); +// const res = await crunchyroll.fetchAnimeInfo(search.results[0].id, search.results[0].type!); +// const sources = await crunchyroll.fetchEpisodeSources(res.episodes![res.episodes?.length! - 1].id); +// })(); diff --git a/consumet.ts/src/providers/anime/gogoanime.ts b/consumet.ts/src/providers/anime/gogoanime.ts new file mode 100644 index 00000000..09ffc6b7 --- /dev/null +++ b/consumet.ts/src/providers/anime/gogoanime.ts @@ -0,0 +1,586 @@ +import { AxiosAdapter } from 'axios'; +import { load } from 'cheerio'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + IEpisodeServer, + IVideo, + StreamingServers, + MediaStatus, + SubOrSub, + IAnimeResult, + ISource, + MediaFormat, + ProxyConfig, +} from '../../models'; +import { USER_AGENT } from '../../utils'; +import { GogoCDN, StreamSB, StreamWish } from '../../extractors'; + +class Gogoanime extends AnimeParser { + override readonly name = 'Gogoanime'; + protected override baseUrl = 'https://anitaku.so'; + protected override logo = + 'https://play-lh.googleusercontent.com/MaGEiAEhNHAJXcXKzqTNgxqRmhuKB1rCUgb15UrN_mWUNRnLpO5T1qja64oRasO7mn0'; + protected override classPath = 'ANIME.Gogoanime'; + private readonly ajaxUrl = 'https://ajax.gogocdn.net/ajax'; + + + constructor( + customBaseURL?: string, + proxy?: ProxyConfig, + adapter?: AxiosAdapter + ) { + super(...arguments); + this.baseUrl = customBaseURL ? `https://${customBaseURL}` : this.baseUrl; + if (proxy) { + // Initialize proxyConfig if provided + this.setProxy(proxy); + } + if (adapter) { + // Initialize adapter if provided + this.setAxiosAdapter(adapter); + } + } + + /** + * + * @param query search query string + * @param page page number (default 1) (optional) + */ + override search = async (query: string, page: number = 1): Promise> => { + const searchResult: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + try { + const res = await this.client.get( + `${this.baseUrl}/filter.html?keyword=${encodeURIComponent(query)}&page=${page}` + ); + + const $ = load(res.data); + + searchResult.hasNextPage = + $('div.anime_name.new_series > div > div > ul > li.selected').next().length > 0; + + $('div.last_episodes > ul > li').each((i, el) => { + searchResult.results.push({ + id: $(el).find('p.name > a').attr('href')?.split('/')[2]!, + title: $(el).find('p.name > a').text(), + url: `${this.baseUrl}/${$(el).find('p.name > a').attr('href')}`, + image: $(el).find('div > a > img').attr('src'), + releaseDate: $(el).find('p.released').text().trim(), + subOrDub: $(el).find('p.name > a').text().toLowerCase().includes('dub') + ? SubOrSub.DUB + : SubOrSub.SUB, + }); + }); + + return searchResult; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param id anime id + */ + override fetchAnimeInfo = async (id: string): Promise => { + if (!id.includes('gogoanime')) id = `${this.baseUrl}/category/${id}`; + + const animeInfo: IAnimeInfo = { + id: '', + title: '', + url: '', + genres: [], + totalEpisodes: 0, + }; + try { + const res = await this.client.get(id); + + const $ = load(res.data); + + animeInfo.id = new URL(id).pathname.split('/')[2]; + animeInfo.title = $( + 'section.content_left > div.main_body > div:nth-child(2) > div.anime_info_body_bg > h1' + ) + .text() + .trim(); + animeInfo.url = id; + animeInfo.image = $('div.anime_info_body_bg > img').attr('src'); + animeInfo.releaseDate = $('div.anime_info_body_bg > p:nth-child(8)') + .text() + .trim() + .split('Released: ')[1]; + animeInfo.description = $('div.anime_info_body_bg > div:nth-child(6)') + .text() + .trim() + .replace('Plot Summary: ', ''); + + animeInfo.subOrDub = animeInfo.title.toLowerCase().includes('dub') ? SubOrSub.DUB : SubOrSub.SUB; + + animeInfo.type = $('div.anime_info_body_bg > p:nth-child(4) > a') + .text() + .trim() + .toUpperCase() as MediaFormat; + + animeInfo.status = MediaStatus.UNKNOWN; + + switch ($('div.anime_info_body_bg > p:nth-child(9) > a').text().trim()) { + case 'Ongoing': + animeInfo.status = MediaStatus.ONGOING; + break; + case 'Completed': + animeInfo.status = MediaStatus.COMPLETED; + break; + case 'Upcoming': + animeInfo.status = MediaStatus.NOT_YET_AIRED; + break; + default: + animeInfo.status = MediaStatus.UNKNOWN; + break; + } + animeInfo.otherName = $('div.anime_info_body_bg > p:nth-child(10)') + .text() + .replace('Other name: ', '') + .replace(/;/g, ','); + + $('div.anime_info_body_bg > p:nth-child(7) > a').each((i, el) => { + animeInfo.genres?.push($(el).attr('title')!.toString()); + }); + + const ep_start = $('#episode_page > li').first().find('a').attr('ep_start'); + const ep_end = $('#episode_page > li').last().find('a').attr('ep_end'); + const movie_id = $('#movie_id').attr('value'); + const alias = $('#alias_anime').attr('value'); + + const html = await this.client.get( + `${this.ajaxUrl + }/load-list-episode?ep_start=${ep_start}&ep_end=${ep_end}&id=${movie_id}&default_ep=${0}&alias=${alias}` + ); + const $$ = load(html.data); + + animeInfo.episodes = []; + $$('#episode_related > li').each((i, el) => { + animeInfo.episodes?.push({ + id: $(el).find('a').attr('href')?.split('/')[1]!, + number: parseFloat($(el).find(`div.name`).text().replace('EP ', '')), + url: `${this.baseUrl}/${$(el).find(`a`).attr('href')?.trim()}`, + }); + }); + animeInfo.episodes = animeInfo.episodes.reverse(); + + animeInfo.totalEpisodes = parseInt(ep_end ?? '0'); + + return animeInfo; + } catch (err) { + throw new Error(`failed to fetch anime info: ${err}`); + } + }; + + /** + * + * @param episodeId episode id + * @param server server type (default 'GogoCDN') (optional) + */ + override fetchEpisodeSources = async ( + episodeId: string, + server: StreamingServers = StreamingServers.VidStreaming + ): Promise => { + if (episodeId.startsWith('http')) { + const serverUrl = new URL(episodeId); + switch (server) { + case StreamingServers.GogoCDN: + return { + headers: { Referer: serverUrl.href }, + sources: await new GogoCDN(this.proxyConfig, this.adapter).extract(serverUrl), + download: `https://${serverUrl.host}/download${serverUrl.search}`, + }; + case StreamingServers.StreamSB: + return { + headers: { + Referer: serverUrl.href, + watchsb: 'streamsb', + 'User-Agent': USER_AGENT, + }, + sources: await new StreamSB(this.proxyConfig, this.adapter).extract(serverUrl), + download: `https://${serverUrl.host}/download${serverUrl.search}`, + }; + case StreamingServers.StreamWish: + return { + headers: { + Referer: serverUrl.href, + }, + sources: await new StreamWish(this.proxyConfig, this.adapter).extract(serverUrl), + download: `https://${serverUrl.host}/download${serverUrl.search}`, + }; + default: + return { + headers: { Referer: serverUrl.href }, + sources: await new GogoCDN(this.proxyConfig, this.adapter).extract(serverUrl), + download: `https://${serverUrl.host}/download${serverUrl.search}`, + }; + } + } + + try { + const res = await this.client.get(`${this.baseUrl}/${episodeId}`); + + const $ = load(res.data); + + let serverUrl: URL; + + switch (server) { + case StreamingServers.GogoCDN: + serverUrl = new URL(`${$('#load_anime > div > div > iframe').attr('src')}`); + break; + case StreamingServers.VidStreaming: + serverUrl = new URL( + `${$('div.anime_video_body > div.anime_muti_link > ul > li.vidcdn > a').attr('data-video')}` + ); + break; + case StreamingServers.StreamSB: + serverUrl = new URL( + $('div.anime_video_body > div.anime_muti_link > ul > li.streamsb > a').attr('data-video')! + ); + break; + case StreamingServers.StreamWish: + serverUrl = new URL( + $('div.anime_video_body > div.anime_muti_link > ul > li.streamwish > a').attr('data-video')! + ); + break; + default: + serverUrl = new URL(`${$('#load_anime > div > div > iframe').attr('src')}`); + break; + } + + return await this.fetchEpisodeSources(serverUrl.href, server); + } catch (err) { + console.log(err); + throw new Error('Episode not found.'); + } + }; + + /** + * + * @param episodeId episode link or episode id + */ + override fetchEpisodeServers = async (episodeId: string): Promise => { + try { + if (!episodeId.startsWith(this.baseUrl)) episodeId = `${this.baseUrl}/${episodeId}`; + + const res = await this.client.get(episodeId); + + const $ = load(res.data); + + const servers: IEpisodeServer[] = []; + + $('div.anime_video_body > div.anime_muti_link > ul > li').each((i, el) => { + let url = $(el).find('a').attr('data-video'); + if (!url?.startsWith('http')) url = `https:${url}`; + + servers.push({ + name: $(el).find('a').text().replace('Choose this server', '').trim(), + url: url, + }); + }); + + return servers; + } catch (err) { + throw new Error('Episode not found.'); + } + }; + /** + * + * @param episodeId episode link or episode id + */ + fetchAnimeIdFromEpisodeId = async (episodeId: string): Promise => { + try { + if (!episodeId.startsWith(this.baseUrl)) episodeId = `${this.baseUrl}/${episodeId}`; + + const res = await this.client.get(episodeId); + + const $ = load(res.data); + + return ( + $( + '#wrapper_bg > section > section.content_left > div:nth-child(1) > div.anime_video_body > div.anime_video_body_cate > div.anime-info > a' + ).attr('href') as string + ).split('/')[2]; + } catch (err) { + throw new Error('Episode not found.'); + } + }; + /** + * @param page page number (optional) + * @param type type of media. (optional) (default `1`) `1`: Japanese with subtitles, `2`: english/dub with no subtitles, `3`: chinese with english subtitles + */ + fetchRecentEpisodes = async (page: number = 1, type: number = 1): Promise> => { + try { + const res = await this.client.get(`${this.ajaxUrl}/page-recent-release.html?page=${page}&type=${type}`); + + const $ = load(res.data); + + const recentEpisodes: IAnimeResult[] = []; + + $('div.last_episodes.loaddub > ul > li').each((i, el) => { + recentEpisodes.push({ + id: $(el).find('a').attr('href')?.split('/')[1]?.split('-episode')[0]!, + episodeId: $(el).find('a').attr('href')?.split('/')[1]!, + episodeNumber: parseFloat($(el).find('p.episode').text().replace('Episode ', '')), + title: $(el).find('p.name > a').attr('title')!, + image: $(el).find('div > a > img').attr('src'), + url: `${this.baseUrl}${$(el).find('a').attr('href')?.trim()}`, + }); + }); + + const hasNextPage = !$('div.anime_name_pagination.intro > div > ul > li').last().hasClass('selected'); + + return { + currentPage: page, + hasNextPage: hasNextPage, + results: recentEpisodes, + }; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + }; + + fetchGenreInfo = async (genre: string, page: number = 1): Promise> => { + try { + const res = await this.client.get(`${this.baseUrl}/genre/${genre}?page=${page}`); + + const $ = load(res.data); + + const genreInfo: IAnimeResult[] = []; + + $('div.last_episodes > ul > li').each((i, elem) => { + genreInfo.push({ + id: $(elem).find('p.name > a').attr('href')?.split('/')[2] as string, + title: $(elem).find('p.name > a').attr('title') as string, + image: $(elem).find('div > a > img').attr('src'), + released: $(elem).find('p.released').text().replace('Released: ', '').trim(), + url: this.baseUrl + '/' + $(elem).find('p.name > a').attr('href'), + }); + }); + + const paginatorDom = $('div.anime_name_pagination > div > ul > li'); + const hasNextPage = paginatorDom.length > 0 && !paginatorDom.last().hasClass('selected'); + return { + currentPage: page, + hasNextPage: hasNextPage, + results: genreInfo, + }; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + }; + + fetchTopAiring = async (page: number = 1): Promise> => { + try { + const res = await this.client.get(`${this.ajaxUrl}/page-recent-release-ongoing.html?page=${page}`); + + const $ = load(res.data); + + const topAiring: IAnimeResult[] = []; + + $('div.added_series_body.popular > ul > li').each((i, el) => { + topAiring.push({ + id: $(el).find('a:nth-child(1)').attr('href')?.split('/')[2]!, + title: $(el).find('a:nth-child(1)').attr('title')!, + image: $(el).find('a:nth-child(1) > div').attr('style')?.match('(https?://.*.(?:png|jpg))')![0], + url: `${this.baseUrl}${$(el).find('a:nth-child(1)').attr('href')}`, + genres: $(el) + .find('p.genres > a') + .map((i, el) => $(el).attr('title')) + .get(), + episodeId: $(el).find('p:nth-of-type(2) > a').attr('title')!, + episodeNumber: parseFloat($(el).find('p:nth-of-type(2) > a').text().replace('Episode ', '')), + }); + }); + + const hasNextPage = !$('div.anime_name.comedy > div > div > ul > li').last().hasClass('selected'); + + return { + currentPage: page, + hasNextPage: hasNextPage, + results: topAiring, + }; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + }; + + fetchRecentMovies = async (page: number = 1): Promise> => { + try { + const res = await this.client.get(`${this.baseUrl}/anime-movies.html?aph&page=${page}`); + + const $ = load(res.data); + + const recentMovies: IAnimeResult[] = []; + + $('div.last_episodes > ul > li').each((i, el) => { + const a = $(el).find('p.name > a'); + const pRelease = $(el).find('p.released'); + const pName = $(el).find('p.name > a'); + + recentMovies.push({ + id: a.attr('href')?.replace(`/category/`, '')!, + title: pName.attr('title')!, + releaseDate: pRelease.text().replace('Released: ', '').trim(), + image: $(el).find('div > a > img').attr('src'), + url: `${this.baseUrl}${a.attr('href')}`, + }); + }); + + const hasNextPage = !$('div.anime_name.anime_movies > div > div > ul > li').last().hasClass('selected'); + + return { + currentPage: page, + hasNextPage: hasNextPage, + results: recentMovies, + }; + } catch (err) { + console.log(err); + throw new Error('Something went wrong. Please try again later.'); + } + }; + + fetchPopular = async (page: number = 1): Promise> => { + try { + const res = await this.client.get(`${this.baseUrl}/popular.html?page=${page}`); + + const $ = load(res.data); + + const recentMovies: IAnimeResult[] = []; + + $('div.last_episodes > ul > li').each((i, el) => { + const a = $(el).find('p.name > a'); + const pRelease = $(el).find('p.released'); + const pName = $(el).find('p.name > a'); + + recentMovies.push({ + id: a.attr('href')?.replace(`/category/`, '')!, + title: pName.attr('title')!, + releaseDate: pRelease.text().replace('Released: ', '').trim(), + image: $(el).find('div > a > img').attr('src'), + url: `${this.baseUrl}${a.attr('href')}`, + }); + }); + + const hasNextPage = !$('div.anime_name.anime_movies > div > div > ul > li').last().hasClass('selected'); + + return { + currentPage: page, + hasNextPage: hasNextPage, + results: recentMovies, + }; + } catch (err) { + console.log(err); + throw new Error('Something went wrong. Please try again later.'); + } + }; + + fetchGenreList = async (): Promise<{ id: string | undefined; title: string | undefined }[]> => { + const genres: { id: string | undefined; title: string | undefined }[] = []; + let res = null; + try { + res = await this.client.get(`${this.baseUrl}/home.html`); + } catch (err) { + try { + res = await this.client.get(`${this.baseUrl}/`); + } catch (error) { + throw new Error('Something went wrong. Please try again later.'); + } + } + try { + const $ = load(res.data); + $('nav.menu_series.genre.right > ul > li').each((_index, element) => { + const genre = $(element).find('a'); + genres.push({ id: genre.attr('href')?.replace('/genre/', ''), title: genre.attr('title') }!); + }); + return genres; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + }; + fetchDirectDownloadLink = async (downloadUrl: string, captchaToken?: string): Promise<{ source: string | undefined; link: string | undefined }[]> => { + const downloadLinks: { source: string | undefined; link: string | undefined }[] = []; + + const baseUrl = downloadUrl.split('?')[0]; + const idParam = downloadUrl.match(/[?&]id=([^&]+)/); + const animeID = idParam ? idParam[1] : null; + if (!captchaToken) + captchaToken = '03AFcWeA5zy7DBK82U_tctVKelJ6L2duTWac5at2zXjHLX8XqUm8tI6NKWMxGd2gjh1vi2hnEyRhVgbMhdb9WjexRsJkxTt-C-_iIIZ5yC3E5I19G5Q0buSTcIQIZS6tskrz-mDn-d37aWxAJtqbg0Yoo1XsdVc5Yf4sB-9iQxQK-W_9YLep_QaAz8uL17gMMlCz5WZM3dbBEEGmk_qPbJu_pZ8kk-lFPDzd6iBobcpyIDRZgTgD4bYUnby5WZc11i00mrRiRS3m-qSY0lprGaBqoyY1BbRkQZ25AGPp5al4kSwBZqpcVgLrs3bjdo8XVWAe73_XLa8HhqLWbz_m5Ebyl5F9awwL7w4qikGj-AK7v2G8pgjT22kDLIeenQ_ss4jYpmSzgnuTItur9pZVzpPkpqs4mzr6y274AmJjzppRTDH4VFtta_E02-R7Hc1rUD2kCYt9BqsD7kDjmetnvLtBm97q5XgBS8rQfeH4P-xqiTAsJwXlcrPybSjnwPEptqYCPX5St_BSj4NQfSuzZowXu_qKsP4hAaE9L2W36MvqePPlEm6LChBT3tnqUwcEYNe5k7lkAAbunxx8q_X5Q3iEdcFqt9_0GWHebRBd5abEbjbmoqqCoQeZt7AUvkXCRfBDne-bf25ypyTtwgyuvYMYXau3zGUjgPUO9WIotZwyKyrYmjsZJ7TiM'; + + let res = null; + try { + res = await this.client.get(`${baseUrl}?id=${animeID}&captcha_v3=${captchaToken}`); + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + try { + const $ = load(res.data); + $('.dowload').each((_index, element) => { + const link = $(element).find('a'); + if (link.attr('target') != '_blank') { + downloadLinks.push({ source: link.text(), link: link.attr('href') }!); + } + }); + return downloadLinks; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + }; + + fetchAnimeList = async (page: number = 1): Promise> => { + const animeList: IAnimeResult[] = []; + let res = null; + try { + res = await this.client.get(`${this.baseUrl}/anime-list.html?page=${page}`); + const $ = load(res.data); + $('.anime_list_body .listing li').each((_index, element) => { + const genres: string[] = []; + const entryBody = $('p.type', $(element).attr('title')!); + const genresEl = entryBody.first(); + genresEl.find('a').each((_idx, genreAnchor) => { + genres.push($(genreAnchor).attr('title')!); + }); + + const releaseDate = $(entryBody.get(1)).text(); + + const img = $('div', $(element).attr('title')!); + const a = $(element).find('a'); + animeList.push( + { + id: a.attr('href')?.replace(`/category/`, '')!, + title: a.text(), + image: $(img).find('img').attr('src'), + url: `${this.baseUrl}${a.attr('href')}`, + genres, + releaseDate + } + ); + }); + const hasNextPage = !$('div.anime_name.anime_list > div > div > ul > li').last().hasClass('selected'); + return { + currentPage: page, + hasNextPage: hasNextPage, + results: animeList, + }; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + }; +} + +// (async () => { +// const gogo = new Gogoanime(); +// const search = await gogo.fetchEpisodeSources('jigokuraku-dub-episode-1'); +// console.log(search); +// })(); + +export default Gogoanime; diff --git a/consumet.ts/src/providers/anime/index.ts b/consumet.ts/src/providers/anime/index.ts new file mode 100644 index 00000000..c3f3663c --- /dev/null +++ b/consumet.ts/src/providers/anime/index.ts @@ -0,0 +1,25 @@ +import Gogoanime from './gogoanime'; +import NineAnime from './9anime'; +import AnimePahe from './animepahe'; +import Zoro from './zoro'; +import AnimeFox from './animefox'; +import Anify from './anify'; +import Crunchyroll from './crunchyroll'; +import Bilibili from './bilibili'; +import Marin from './marin'; +import AnimeSaturn from './animesaturn'; +import AnimeUnity from './animeunity' + +export default { + Gogoanime, + NineAnime, + AnimePahe, + Zoro, + AnimeFox, + Anify, + Crunchyroll, + Bilibili, + Marin, + AnimeSaturn, + AnimeUnity +}; diff --git a/consumet.ts/src/providers/anime/kickassanime.ts b/consumet.ts/src/providers/anime/kickassanime.ts new file mode 100644 index 00000000..755ec3a4 --- /dev/null +++ b/consumet.ts/src/providers/anime/kickassanime.ts @@ -0,0 +1,55 @@ +import { load } from 'cheerio'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + MediaStatus, + IAnimeResult, + ISource, + IAnimeEpisode, + IEpisodeServer, +} from '../../models'; + +/** + * @attention Cloudflare bypass is **REQUIRED**. + */ +class KickAssAnime extends AnimeParser { + override readonly name = 'KickAssAnime'; + protected override baseUrl = 'https://www2.kickassanime.ro'; + protected override logo = + 'https://user-images.githubusercontent.com/65111632/95666535-4f6dba80-0ba6-11eb-8583-e3a2074590e9.png'; + protected override classPath = 'ANIME.KickAssAnime'; + + /** + * @param query Search query + */ + override search = async (query: string): Promise> => { + throw new Error('Method not implemented.'); + }; + + /** + * @param id Anime id + */ + override fetchAnimeInfo = async (id: string): Promise => { + throw new Error('Method not implemented.'); + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeSources = async (episodeId: string): Promise => { + throw new Error('Method not implemented.'); + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeServers = (episodeId: string): Promise => { + throw new Error('Method not implemented.'); + }; +} + +export default KickAssAnime; diff --git a/consumet.ts/src/providers/anime/marin.ts b/consumet.ts/src/providers/anime/marin.ts new file mode 100644 index 00000000..c3a33c7e --- /dev/null +++ b/consumet.ts/src/providers/anime/marin.ts @@ -0,0 +1,324 @@ +import { load } from 'cheerio'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + MediaStatus, + IAnimeResult, + ISource, + IAnimeEpisode, + IEpisodeServer, +} from '../../models'; + +/** + * @attention Cloudflare bypass is **REQUIRED**. + */ +class Marin extends AnimeParser { + override readonly name = 'Marin'; + protected override baseUrl = 'https://marin.moe'; + protected override logo = 'https://i.pinimg.com/736x/62/8d/3f/628d3f2e60b0aa8c8fa9598e8dae6320.jpg'; + protected override classPath = 'ANIME.Marin'; + + private async getToken(): Promise { + const token: string[] = []; + + const response = await this.client.get('https://marin.moe/anime', { + headers: { + Referer: 'https://marin.moe/anime', + Cookie: '__ddg1_=;__ddg2_=;', + }, + }); + + token.push(response.headers['set-cookie']![1].replace('marin_session=', '')); + token.push(response.headers['set-cookie']![0].replace('XSRF-TOKEN=', '')); + + return token; + } + + public recentEpisodes = async (page: number = 1): Promise> => { + const token = await this.getToken(); + let data; + try { + const response = await this.client.post( + 'https://marin.moe/anime', + { + page: page, + sort: 'rel-d', + filter: { + type: [], + status: [], + content_rating: [], + genre: [], + group: [], + production: [], + source: [], + resolution: [], + audio: [], + subtitle: [], + }, + search: '', + }, + { + headers: { + Origin: 'https://marin.moe/', + Referer: 'https://marin.moe/anime', + Cookie: `__ddg1=;__ddg2_=; XSRF-TOKEN=${token[1]}; marin_session=${token[0]};`, + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', + 'x-xsrf-token': token[1].split(';')[0].replace('%3D', '='), + 'x-inertia': true, + }, + } + ); + data = await response.data; + } catch (error) { + console.log(error); + } + const response_data = { + currentPage: page, + hasNextPage: data.props.anime_list.meta.last_page > page, + results: data.props.anime_list.data.map((el: any) => { + return { + id: el.slug, + title: el.title, + image: el.cover, + releaseDate: el.year, + type: el.type, + }; + }), + }; + return response_data; + }; + + /** + * @param query Search query + */ + override search = async (query: string, page: number = 1): Promise> => { + const token = await this.getToken(); + let data; + try { + const response = await this.client.post( + 'https://marin.moe/anime', + { + page: page, + sort: 'az-a', + filter: { + type: [], + status: [], + content_rating: [], + genre: [], + group: [], + production: [], + source: [], + resolution: [], + audio: [], + subtitle: [], + }, + search: query, + }, + { + headers: { + Origin: 'https://marin.moe/', + Referer: 'https://marin.moe/anime', + Cookie: `__ddg1=;__ddg2_=; XSRF-TOKEN=${token[1]}; marin_session=${token[0]};`, + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', + 'x-xsrf-token': token[1].split(';')[0].replace('%3D', '='), + 'x-inertia': true, + }, + } + ); + data = await response.data; + } catch (error) { + console.log(error); + } + const response_data = { + currentPage: page, + hasNextPage: data.props.anime_list.meta.last_page > page, + results: data.props.anime_list.data.map((el: any) => { + return { + id: el.slug, + title: el.title, + image: el.cover, + releaseDate: el.year, + type: el.type, + }; + }), + }; + return response_data; + }; + + /** + * @param id Anime id + */ + override fetchAnimeInfo = async (id: string): Promise => { + const token = await this.getToken(); + let data; + try { + const response = await this.client.post( + `https://marin.moe/anime/${id}`, + {}, + { + headers: { + Origin: 'https://marin.moe/', + Referer: `https://marin.moe/anime/${id}`, + Cookie: `__ddg1=;__ddg2_=; XSRF-TOKEN=${token[1].split(';')[0]}; marin_session=${ + token[0].split(';')[0] + };`, + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', + 'x-inertia': true, + 'x-inertia-version': '884345c4d568d16e3bb2fb3ae350cca9', + 'x-requested-with': 'XMLHttpRequest', + 'x-xsrf-token': token[1].split(';')[0].replace('%3D', '='), + }, + } + ); + data = await response.data; + + console.log(data); + } catch (error) { + console.log(error); + } + + let episodes: any[] = data.props.episode_list.data; + if (data.props.anime.last_episode > 36) { + for (let index = 2; index < data.props.anime.last_episode / 36; index++) { + const response = await this.client.post( + `https://marin.moe/anime/${id}`, + { filter: { episodes: true, specials: true }, eps_page: index }, + { + headers: { + Origin: 'https://marin.moe/', + Referer: `https://marin.moe/anime/${id}`, + Cookie: `__ddg1=;__ddg2_=; XSRF-TOKEN=${token[1].split(';')[0]}; marin_session=${ + token[0].split(';')[0] + };`, + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', + 'x-inertia': true, + 'x-inertia-version': '884345c4d568d16e3bb2fb3ae350cca9', + 'x-requested-with': 'XMLHttpRequest', + 'x-xsrf-token': token[1].split(';')[0].replace('%3D', '='), + }, + } + ); + const data = await response.data; + episodes = episodes.concat(data.props.episode_list.data); + } + } + //{"filter":{"episodes":true,"specials":true},"eps_page":2} + + const response_data: IAnimeInfo = { + id: id, + title: { + native: data.props.anime.alt_titles['Official Title'][0].text, + romaji: data.props.anime.title, + english: data.props.anime.alt_titles['Official Title'][1].text, + }, + synonyms: + data.props.anime.alt_titles['Synonym']?.map((el: any) => { + return el.text; + }) || [], + image: data.props.anime.cover, + cover: data.props.anime.cover, + description: data.props.anime.description, + status: data.props.anime.status.name, + releaseDate: data.props.anime.release_date, + totalEpisodes: data.props.anime.last_episode, + currentEpisode: data.props.anime.last_episode, + genres: data.props.anime.genre_list.map((el: any) => { + return el.name; + }), + studios: data.props.anime.production_list.map((el: any) => { + return el.name; + }), + type: data.props.anime.type.name, + ageRating: data.props.anime.content_rating.name, + episodes: episodes.map((el: any) => { + return { + id: `${id}/${el.sort}`, + title: el.title, + number: el.sort, + image: el.cover, + airdate: el.release_date, + }; + }), + }; + + return response_data; + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeSources = async (id: string): Promise => { + const token = await this.getToken(); + const cookie = `__ddg1=;__ddg2_=; XSRF-TOKEN=${token[1].split(';')[0]}; marin_session=${ + token[0].split(';')[0] + };`; + let data; + try { + const response = await this.client.post( + `https://marin.moe/anime/${id}`, + {}, + { + headers: { + Origin: 'https://marin.moe/', + Referer: `https://marin.moe/anime/${id}`, + Cookie: cookie, + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', + 'x-inertia': true, + 'x-inertia-version': '884345c4d568d16e3bb2fb3ae350cca9', + 'x-requested-with': 'XMLHttpRequest', + 'x-xsrf-token': token[1].split(';')[0].replace('%3D', '='), + }, + } + ); + data = await response.data; + } catch (error) { + console.log(error); + } + + const response_data = { + headers: { + Cookie: cookie, + }, + sources: data.props.video.data.mirror.map((el: any) => { + return { + url: el.code.file, + quality: el.resolution, + isM3U8: false, + duration: el.code.duration, + thumbnail: el.code.thumbnail, + }; + }), + sprites: data.props.video.data.mirror[0].code.sprite, + spriteVtt: data.props.video.data.mirror[0].code.vtt, + }; + + return response_data; + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeServers = (episodeId: string): Promise => { + throw new Error('Method not implemented.'); + }; +} + +export default Marin; + +// (async () => { +// const marin = new Marin(); +// const search = await marin.search('vermeil in gold'); +// const anime = await marin.fetchAnimeInfo(search.results[0].id); +// const sources = await marin.fetchEpisodeSources(anime.episodes![0].id); + +// console.log(sources); +// })(); diff --git a/consumet.ts/src/providers/anime/zoro.ts b/consumet.ts/src/providers/anime/zoro.ts new file mode 100644 index 00000000..41d7da6c --- /dev/null +++ b/consumet.ts/src/providers/anime/zoro.ts @@ -0,0 +1,524 @@ +import { AxiosAdapter } from 'axios'; +import { CheerioAPI, load } from 'cheerio'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + IAnimeResult, + ISource, + IEpisodeServer, + StreamingServers, + MediaFormat, + SubOrSub, +} from '../../models'; + +import { StreamSB, RapidCloud, MegaCloud, StreamTape } from '../../utils'; +import { USER_AGENT } from '../../utils'; + +class Zoro extends AnimeParser { + override readonly name = 'Zoro'; + protected override baseUrl = 'https://hianime.to'; + protected override logo = + 'https://is3-ssl.mzstatic.com/image/thumb/Purple112/v4/7e/91/00/7e9100ee-2b62-0942-4cdc-e9b93252ce1c/source/512x512bb.jpg'; + protected override classPath = 'ANIME.Zoro'; + + constructor( + customBaseURL?: string + ) { + super(...arguments); + this.baseUrl = customBaseURL ? `https://${customBaseURL}` : this.baseUrl; + } + + /** + * @param query Search query + * @param page Page number (optional) + */ + override search(query: string, page: number = 1): Promise> { + if (0 >= page) { + page = 1; + } + return this.scrapeCardPage(`${this.baseUrl}/search?keyword=${decodeURIComponent(query)}&page=${page}`); + } + + /** + * @param page number + */ + fetchTopAiring(page: number = 1): Promise> { + if (0 >= page) { + page = 1; + } + return this.scrapeCardPage(`${this.baseUrl}/top-airing?page=${page}`); + } + /** + * @param page number + */ + fetchMostPopular(page: number = 1): Promise> { + if (0 >= page) { + page = 1; + } + return this.scrapeCardPage(`${this.baseUrl}/most-popular?page=${page}`); + } + /** + * @param page number + */ + fetchMostFavorite(page: number = 1): Promise> { + if (0 >= page) { + page = 1; + } + return this.scrapeCardPage(`${this.baseUrl}/most-favorite?page=${page}`); + } + /** + * @param page number + */ + fetchLatestCompleted(page: number = 1): Promise> { + if (0 >= page) { + page = 1; + } + return this.scrapeCardPage(`${this.baseUrl}/completed?page=${page}`); + } + /** + * @param page number + */ + fetchRecentlyUpdated(page: number = 1): Promise> { + if (0 >= page) { + page = 1; + } + return this.scrapeCardPage(`${this.baseUrl}/recently-updated?page=${page}`); + } + /** + * @param page number + */ + fetchRecentlyAdded(page: number = 1): Promise> { + if (0 >= page) { + page = 1; + } + return this.scrapeCardPage(`${this.baseUrl}/recently-added?page=${page}`); + } + /** + * @param page number + */ + fetchTopUpcoming(page: number = 1): Promise> { + if (0 >= page) { + page = 1; + } + return this.scrapeCardPage(`${this.baseUrl}/top-upcoming?page=${page}`); + } + /** + * @param studio Studio id, e.g. "toei-animation" + * @param page page number (optional) `default 1` + */ + fetchStudio(studio: string, page: number = 1): Promise> { + if (0 >= page) { + page = 1; + } + return this.scrapeCardPage(`${this.baseUrl}/producer/${studio}?page=${page}`); + } + + /** + * Fetches the schedule for a given date. + * @param date The date in format 'YYYY-MM-DD'. Defaults to the current date. + * @returns A promise that resolves to an object containing the search results. + */ + async fetchSchedule(date: string = new Date().toISOString().slice(0, 10)): Promise> { + try { + const res: ISearch = { + results: [], + }; + const { data: { html } } = await this.client.get(`${this.baseUrl}/ajax/schedule/list?tzOffset=360&date=${date}`); + const $ = load(html); + + $('li').each((i, ele) => { + const card = $(ele); + const title = card.find('.film-name'); + + const id = card.find("a.tsl-link").attr('href')?.split('/')[1].split('?')[0]; + const airingTime = card.find("div.time").text().replace("\n", "").trim(); + const airingEpisode = card.find("div.film-detail div.fd-play button").text().replace("\n", "").trim(); + res.results.push({ + id: id!, + title: title.text(), + japaneseTitle: title.attr('data-jname'), + url: `${this.baseUrl}/${id}`, + airingEpisode: airingEpisode, + airingTime: airingTime, + }); + }) + + return res; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + } + + async fetchSpotlight(): Promise> { + try { + const res: ISearch = { results: [] }; + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + $('#slider div.swiper-wrapper div.swiper-slide').each((i, el) => { + const card = $(el); + const titleElement = card.find('div.desi-head-title'); + const id = card.find('div.desi-buttons .btn-secondary').attr('href')?.match(/\/([^/]+)$/)?.[1] || null; + res.results.push({ + id: id!, + title: titleElement.text(), + japaneseTitle: titleElement.attr('data-jname'), + banner: card.find('deslide-cover-img img').attr('data-src') || null, + rank: parseInt(card.find('.desi-sub-text').text().match(/(\d+)/g)?.[0]!), + url: `${this.baseUrl}/${id}`, + type: card.find('div.sc-detail .scd-item:nth-child(1)').text().trim() as MediaFormat, + duration: card.find('div.sc-detail > div:nth-child(2)').text().trim(), + releaseDate: card.find('div.sc-detail > div:nth-child(3)').text().trim(), + quality: card.find('div.sc-detail > div:nth-child(4)').text().trim(), + sub: parseInt(card.find('div.sc-detail div.tick-sub').text().trim()) || 0, + dub: parseInt(card.find('div.sc-detail div.tick-dub').text().trim()) || 0, + episodes: parseInt(card.find('div.sc-detail div.tick-eps').text()) || 0, + description: card.find('div.desi-description').text().trim() + }); + }); + + return res; + } catch (error) { + throw new Error('Something went wrong. Please try again later.'); + } + } + + async fetchSearchSuggestions(query: string): Promise> { + try { + const encodedQuery = encodeURIComponent(query); + const { data } = await this.client.get(`${this.baseUrl}/ajax/search/suggest?keyword=${encodedQuery}`); + const $ = load(data.html); + const res: ISearch = { + results: [], + }; + + $('.nav-item').each((i, el) => { + const card = $(el); + if (!card.hasClass("nav-bottom")) { + const image = card.find('.film-poster img').attr('data-src'); + const title = card.find('.film-name'); + const id = card.attr('href')?.split('/')[1].split('?')[0]; + + const duration = card.find(".film-infor span").last().text().trim(); + const releaseDate = card.find(".film-infor span:nth-child(1)").text().trim(); + const type = card.find(".film-infor").find("span, i").remove().end().text().trim(); + res.results.push({ + image: image, + id: id!, + title: title.text(), + japaneseTitle: title.attr('data-jname'), + aliasTitle: card.find(".alias-name").text(), + releaseDate: releaseDate, + type: type as MediaFormat, + duration: duration, + url: `${this.baseUrl}/${id}`, + }); + } + }); + + return res; + } catch (error) { + throw new Error('Something went wrong. Please try again later.'); + } + } + + /** + * @param id Anime id + */ + override fetchAnimeInfo = async (id: string): Promise => { + const info: IAnimeInfo = { + id: id, + title: '', + }; + try { + const { data } = await this.client.get(`${this.baseUrl}/watch/${id}`); + const $ = load(data); + + const { mal_id, anilist_id } = JSON.parse($('#syncData').text()); + info.malID = Number(mal_id); + info.alID = Number(anilist_id); + info.title = $('h2.film-name > a.text-white').text(); + info.japaneseTitle = $('div.anisc-info div:nth-child(2) span.name').text(); + info.image = $('img.film-poster-img').attr('src'); + info.description = $('div.film-description').text().trim(); + // Movie, TV, OVA, ONA, Special, Music + info.type = $('span.item').last().prev().prev().text().toUpperCase() as MediaFormat; + info.url = `${this.baseUrl}/${id}`; + info.recommendations = await this.scrapeCard($); + info.relatedAnime = []; + $("#main-sidebar section:nth-child(1) div.anif-block-ul li").each((i, ele) => { + const card = $(ele); + const aTag = card.find('.film-name a'); + const id = aTag.attr('href')?.split('/')[1].split('?')[0]; + info.relatedAnime.push({ + id: id!, + title: aTag.text(), + url: `${this.baseUrl}${aTag.attr('href')}`, + image: card.find('img')?.attr('data-src'), + japaneseTitle: aTag.attr('data-jname'), + type: card.find(".tick").contents().last()?.text()?.trim() as MediaFormat, + sub: parseInt(card.find('.tick-item.tick-sub')?.text()) || 0, + dub: parseInt(card.find('.tick-item.tick-dub')?.text()) || 0, + episodes: parseInt(card.find('.tick-item.tick-eps')?.text()) || 0, + }); + }); + const hasSub: boolean = $('div.film-stats div.tick div.tick-item.tick-sub').length > 0; + const hasDub: boolean = $('div.film-stats div.tick div.tick-item.tick-dub').length > 0; + + if (hasSub) { + info.subOrDub = SubOrSub.SUB; + info.hasSub = hasSub; + } + if (hasDub) { + info.subOrDub = SubOrSub.DUB; + info.hasDub = hasDub; + } + if (hasSub && hasDub) { + info.subOrDub = SubOrSub.BOTH; + } + + const episodesAjax = await this.client.get( + `${this.baseUrl}/ajax/v2/episode/list/${id.split('-').pop()}`, + { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + Referer: `${this.baseUrl}/watch/${id}`, + }, + } + ); + + const $$ = load(episodesAjax.data.html); + + info.totalEpisodes = $$('div.detail-infor-content > div > a').length; + info.episodes = []; + $$('div.detail-infor-content > div > a').each((i, el) => { + const episodeId = $$(el) + .attr('href') + ?.split('/')[2] + ?.replace('?ep=', '$episode$') + ?.concat(`$${info.subOrDub}`)!; + const number = parseInt($$(el).attr('data-number')!); + const title = $$(el).attr('title'); + const url = this.baseUrl + $$(el).attr('href'); + const isFiller = $$(el).hasClass('ssl-item-filler'); + + info.episodes?.push({ + id: episodeId, + number: number, + title: title, + isFiller: isFiller, + url: url, + }); + }); + + return info; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeSources = async ( + episodeId: string, + server: StreamingServers = StreamingServers.VidCloud + ): Promise => { + if (episodeId.startsWith('http')) { + const serverUrl = new URL(episodeId); + switch (server) { + case StreamingServers.VidStreaming: + case StreamingServers.VidCloud: + return { + ...(await new MegaCloud().extract(serverUrl)), + }; + case StreamingServers.StreamSB: + return { + headers: { + Referer: serverUrl.href, + watchsb: 'streamsb', + 'User-Agent': USER_AGENT, + }, + sources: await new StreamSB(this.proxyConfig, this.adapter).extract(serverUrl, true), + }; + case StreamingServers.StreamTape: + return { + headers: { Referer: serverUrl.href, 'User-Agent': USER_AGENT }, + sources: await new StreamTape(this.proxyConfig, this.adapter).extract(serverUrl), + }; + default: + case StreamingServers.VidCloud: + return { + headers: { Referer: serverUrl.href }, + ...(await new MegaCloud().extract(serverUrl)), + }; + } + } + if (!episodeId.includes('$episode$')) throw new Error('Invalid episode id'); + + // Fallback to using sub if no info found in case of compatibility + + // TODO: add both options later + const subOrDub: 'sub' | 'dub' = episodeId.split('$')?.pop() === 'dub' ? 'dub' : 'sub'; + + episodeId = `${this.baseUrl}/watch/${episodeId + .replace('$episode$', '?ep=') + .replace(/\$auto|\$sub|\$dub/gi, '')}`; + + try { + const { data } = await this.client.get( + `${this.baseUrl}/ajax/v2/episode/servers?episodeId=${episodeId.split('?ep=')[1]}` + ); + + const $ = load(data.html); + + /** + * vidtreaming -> 4 + * rapidcloud -> 1 + * streamsb -> 5 + * streamtape -> 3 + */ + let serverId = ''; + try { + switch (server) { + case StreamingServers.VidCloud: + serverId = this.retrieveServerId($, 1, subOrDub); + + // zoro's vidcloud server is rapidcloud + if (!serverId) throw new Error('RapidCloud not found'); + break; + case StreamingServers.VidStreaming: + serverId = this.retrieveServerId($, 4, subOrDub); + + // zoro's vidcloud server is rapidcloud + if (!serverId) throw new Error('vidtreaming not found'); + break; + case StreamingServers.StreamSB: + serverId = this.retrieveServerId($, 5, subOrDub); + + if (!serverId) throw new Error('StreamSB not found'); + break; + case StreamingServers.StreamTape: + serverId = this.retrieveServerId($, 3, subOrDub); + + if (!serverId) throw new Error('StreamTape not found'); + break; + } + } catch (err) { + throw new Error("Couldn't find server. Try another server"); + } + + const { + data: { link }, + } = await this.client.get(`${this.baseUrl}/ajax/v2/episode/sources?id=${serverId}`); + + return await this.fetchEpisodeSources(link, server); + } catch (err) { + throw err; + } + }; + + private retrieveServerId = ($: any, index: number, subOrDub: 'sub' | 'dub') => { + return $(`.ps_-block.ps_-block-sub.servers-${subOrDub} > .ps__-list .server-item`) + .map((i: any, el: any) => ($(el).attr('data-server-id') == `${index}` ? $(el) : null)) + .get()[0] + .attr('data-id')!; + }; + + /** + * @param url string + */ + private scrapeCardPage = async (url: string): Promise> => { + try { + const res: ISearch = { + currentPage: 0, + hasNextPage: false, + totalPages: 0, + results: [], + }; + const { data } = await this.client.get(url); + const $ = load(data); + + const pagination = $('ul.pagination'); + res.currentPage = parseInt(pagination.find('.page-item.active')?.text()); + const nextPage = pagination.find('a[title=Next]')?.attr('href'); + if (nextPage != undefined && nextPage != '') { + res.hasNextPage = true; + } + const totalPages = pagination.find('a[title=Last]').attr('href')?.split('=').pop(); + if (totalPages === undefined || totalPages === '') { + res.totalPages = res.currentPage; + } else { + res.totalPages = parseInt(totalPages); + } + + res.results = await this.scrapeCard($); + if (res.results.length === 0) { + res.currentPage = 0; + res.hasNextPage = false; + res.totalPages = 0; + } + return res; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + }; + + /** + * @param $ cheerio instance + */ + private scrapeCard = async ($: CheerioAPI): Promise => { + try { + const results: IAnimeResult[] = []; + + $('.flw-item').each((i, ele) => { + const card = $(ele); + const atag = card.find('.film-name a'); + const id = atag.attr('href')?.split('/')[1].split('?')[0]; + const type = card + .find('.fdi-item') + ?.first() + ?.text() + .replace(' (? eps)', '') + .replace(/\s\(\d+ eps\)/g, ''); + results.push({ + id: id!, + title: atag.text(), + url: `${this.baseUrl}${atag.attr('href')}`, + image: card.find('img')?.attr('data-src'), + duration: card.find('.fdi-duration')?.text(), + japaneseTitle: atag.attr('data-jname'), + type: type as MediaFormat, + nsfw: card.find('.tick-rate')?.text() === '18+' ? true : false, + sub: parseInt(card.find('.tick-item.tick-sub')?.text()) || 0, + dub: parseInt(card.find('.tick-item.tick-dub')?.text()) || 0, + episodes: parseInt(card.find('.tick-item.tick-eps')?.text()) || 0, + }); + + }); + return results; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + }; + /** + * @deprecated + * @param episodeId Episode id + */ + override fetchEpisodeServers = (episodeId: string): Promise => { + throw new Error('Method not implemented.'); + }; +} + +// (async () => { +// const zoro = new Zoro(); +// const anime = await zoro.search('classroom of the elite'); +// const info = await zoro.fetchAnimeInfo(anime.results[0].id); +// const sources = await zoro.fetchEpisodeSources(info.episodes![0].id); +// console.log(sources); +// })(); + +export default Zoro; diff --git a/consumet.ts/src/providers/books/index.ts b/consumet.ts/src/providers/books/index.ts new file mode 100644 index 00000000..1bb6f945 --- /dev/null +++ b/consumet.ts/src/providers/books/index.ts @@ -0,0 +1,3 @@ +import Libgen from './libgen'; + +export default { Libgen }; diff --git a/consumet.ts/src/providers/books/libgen.ts b/consumet.ts/src/providers/books/libgen.ts new file mode 100644 index 00000000..7597f309 --- /dev/null +++ b/consumet.ts/src/providers/books/libgen.ts @@ -0,0 +1,419 @@ +import { load } from 'cheerio'; +import { BookParser, LibgenBook, LibgenBookObject } from '../../models'; +import { splitAuthor, floorID, formatTitle } from '../../utils'; +import { encode } from 'ascii-url-encoder'; +import { Worker } from 'worker_threads'; +import { LibgenResult } from '../../models/types'; + +class Libgen extends BookParser { + private readonly extensions = ['.rs', '.is', '.st']; + protected override readonly baseUrl = 'http://libgen'; + /** + * @type {string} + */ + override readonly name: string = 'Libgen'; + private readonly downloadIP = 'http://62.182.86.140'; + + protected override logo = + 'https://f-droid.org/repo/com.manuelvargastapia.libgen/en-US/icon_TP2ezvMwW5ovE-wixagF1WCThMUohX3T_kzYhuZQ8aY=.png'; + protected override classPath = 'BOOKS.Libgen'; + + /** + * scrapes a ligen book page by book page url + * + * @param {string} bookUrl - ligen book page url + * @returns {Promise} + */ + scrapeBook = async (bookUrl: string): Promise => { + bookUrl = encodeURIComponent(bookUrl); + const container: LibgenBook = new LibgenBookObject(); + const { data } = await this.client.get(bookUrl); + const $ = load(data); + let rawAuthor = ''; + $('tbody > tr:eq(10)') + .children() + .each((i, el) => { + switch (i) { + case 1: + rawAuthor = $(el).text(); + break; + } + }); + container.authors = splitAuthor(rawAuthor); + + let publisher = ''; + $('tbody > tr:eq(12)') + .children() + .each((i, el) => { + switch (i) { + case 1: + publisher = $(el).text(); + break; + } + }); + container.publisher = publisher; + + let ex = ''; + let size = ''; + $('tbody > tr:eq(18)') + .children() + .each((i, el) => { + switch (i) { + case 1: + size = $(el).text(); + break; + case 3: + ex = $(el).text(); + break; + } + }); + container.format = ex; + container.size = size; + + let lang = ''; + let page = ''; + $('tbody > tr:eq(14)') + .children() + .each((i, el) => { + switch (i) { + case 1: + lang = $(el).text(); + break; + case 3: + page = $(el).text().split('/')[0]; + break; + } + }); + container.pages = page; + container.language = lang; + + let tempTitle = ''; + let tempVolume = ''; + $('tbody > tr:eq(1)') + .children() + .each((i, el) => { + switch (i) { + case 2: + tempTitle = $(el).text(); + break; + case 4: + tempVolume = $(el).text(); + } + }); + container.title = tempTitle; + container.volume = tempVolume; + container.image = `${this.baseUrl}${this.extensions[0]}` + $('img').attr('src'); + let tempIsbn: string[] = []; + let id = ''; + $('tbody > tr:eq(15)') + .children() + .each((i, el) => { + switch (i) { + case 1: + tempIsbn = $(el).text().split(', '); + break; + case 3: + id = $(el).text(); + break; + } + }); + container.id = id; + container.isbn = tempIsbn; + container.description = $('tbody > tr:eq(31)').text() || ''; + container.tableOfContents = $('tbody > tr:eq(32)').text() || ''; + let tempSeries = ''; + $('tbody > tr:eq(11)') + .children() + .each((i, el) => { + switch (i) { + case 1: + tempSeries = $(el).text(); + break; + } + }); + container.series = tempSeries; + let tempTopic = ''; + $('tbody > tr:eq(22)') + .children() + .each((i, el) => { + switch (i) { + case 1: + tempTopic = $(el).text(); + break; + } + }); + container.topic = tempTopic; + let tempEdition = ''; + let year = ''; + $('tbody > tr:eq(13)') + .children() + .each((i, el) => { + switch (i) { + case 1: + year = $(el).text(); + break; + case 3: + tempEdition = $(el).text(); + break; + } + }); + container.year = year; + container.edition = tempEdition; + + for (let p = 2; p <= 8; p++) { + let temp = ''; + $(`tbody tr:eq(${p})`) + .children() + .each((i, el) => { + switch (i) { + case 1: + temp = $(el).text(); + } + }); + switch (p) { + case 2: + container.hashes.AICH = temp; + break; + case 3: + container.hashes.CRC32 = temp; + break; + case 4: + container.hashes.eDonkey = temp; + break; + case 5: + container.hashes.MD5 = temp; + break; + case 6: + container.hashes.SHA1 = temp; + break; + case 7: + container.hashes.SHA256 = temp.split(' '); + break; + case 8: + container.hashes.TTH = temp; + break; + } + } + let realLink: string = ''; + const fakeLink = bookUrl; + for (let i = 0; i < fakeLink.length; i++) { + if ( + fakeLink[i] === 'm' && + fakeLink[i + 1] === 'd' && + fakeLink[i + 2] === '5' && + fakeLink[i + 3] === '=' + ) { + realLink = fakeLink.substring(i + 4, fakeLink.length); + break; + } + } + container.link = `${this.downloadIP}/main/${floorID(container.id)}/${realLink.toLowerCase()}/${encode( + `${container.series == '' ? '' : `(${container.series})`} ${rawAuthor} - ${container.title}-${ + container.publisher + } (${container.year}).${container.format}` + )}`; + return container; + }; + + /** + * scrapes a libgen search page and returns an array of results + * + * @param {string} query - the name of the book + * @param {number} [maxResults=25] - maximum number of results + * @returns {Promise} + */ + override search = async (query: string, page: number = 1): Promise => { + query = encodeURIComponent(query); + const workingExtension = this.extensions[0]; + const containers: LibgenBook[] = []; + const { data } = await this.client.get( + `${this.baseUrl}.rs/search.php?req=${query}&view=simple&res=25&sort=def&sortmode=ASC&page=${page}` + ); + const $ = load(data); + let rawAuthor = ''; + $('table tbody tr').each((i, e) => { + const container: LibgenBook = new LibgenBookObject(); + $(e.children).each((i, e) => { + if ($(e).text() === '\n\t\t\t\t') return; + switch (i) { + case 0: + container.id = $(e).text(); + break; + case 2: + rawAuthor = $(e).text(); + container.authors = splitAuthor(rawAuthor); + break; + case 4: + let potLink: string = ''; + $(e) + .children() + .each((i, el) => { + if (potLink != '') { + return; + } + if ($(el).attr('href')?.at(0) === 'b') { + potLink = $(el).attr('href') || ''; + } + }); + container.link = `${this.baseUrl}${workingExtension}/${potLink}`; + case 6: + container.publisher = $(e).text(); + break; + case 8: + container.year = $(e).text(); + break; + case 10: + container.pages = $(e).text(); + break; + case 12: + container.language = $(e).text(); + break; + case 14: + container.size = $(e).text(); + break; + case 16: + container.format = $(e).text(); + break; + } + }); + containers[i] = container; + }); + containers.shift(); + containers.shift(); + containers.shift(); + containers.pop(); + for (let i = 0; i < containers.length; i++) { + if (containers[i].link == '') { + continue; + } + const data = await this.client.get(containers[i].link); + const $ = load(data.data); + let tempTitle = ''; + let tempVolume = ''; + $('tbody > tr:eq(1)') + .children() + .each((i, el) => { + switch (i) { + case 2: + tempTitle = $(el).text(); + break; + case 4: + tempVolume = $(el).text(); + } + }); + containers[i].title = tempTitle; + containers[i].volume = tempVolume; + containers[i].image = `${this.baseUrl}${workingExtension}` + $('img').attr('src'); + let tempIsbn: string[] = []; + $('tbody > tr:eq(15)') + .children() + .each((i, el) => { + switch (i) { + case 1: + tempIsbn = $(el).text().split(', '); + break; + } + }); + containers[i].isbn = tempIsbn; + containers[i].description = $('tbody > tr:eq(31)').text() || ''; + containers[i].tableOfContents = $('tbody > tr:eq(32)').text() || ''; + let tempSeries = ''; + $('tbody > tr:eq(11)') + .children() + .each((i, el) => { + switch (i) { + case 1: + tempSeries = $(el).text(); + break; + } + }); + containers[i].series = tempSeries; + let tempTopic = ''; + $('tbody > tr:eq(22)') + .children() + .each((i, el) => { + switch (i) { + case 1: + tempTopic = $(el).text(); + break; + } + }); + containers[i].topic = tempTopic; + let tempEdition = ''; + $('tbody > tr:eq(13)') + .children() + .each((i, el) => { + switch (i) { + case 3: + tempEdition = $(el).text(); + break; + } + }); + containers[i].edition = tempEdition; + for (let p = 2; p <= 8; p++) { + let temp = ''; + $(`tbody tr:eq(${p})`) + .children() + .each((i, el) => { + switch (i) { + case 1: + temp = $(el).text(); + } + }); + switch (p) { + case 2: + containers[i].hashes.AICH = temp; + break; + case 3: + containers[i].hashes.CRC32 = temp; + break; + case 4: + containers[i].hashes.eDonkey = temp; + break; + case 5: + containers[i].hashes.MD5 = temp; + break; + case 6: + containers[i].hashes.SHA1 = temp; + break; + case 7: + containers[i].hashes.SHA256 = temp.split(' '); + break; + case 8: + containers[i].hashes.TTH = temp; + break; + } + } + let realLink: string = ''; + const fakeLink = containers[i].link; + for (let i = 0; i < fakeLink.length; i++) { + if ( + fakeLink[i] === 'm' && + fakeLink[i + 1] === 'd' && + fakeLink[i + 2] === '5' && + fakeLink[i + 3] === '=' + ) { + realLink = fakeLink.substring(i + 4, fakeLink.length); + break; + } + } + containers[i].link = `${this.downloadIP}/main/${floorID( + containers[i].id + )}/${realLink.toLowerCase()}/${encode( + `${containers[i].series == '' ? '' : `(${containers[i].series})`} ${rawAuthor} - ${ + containers[i].title + }-${containers[i].publisher} (${containers[i].year}).${containers[i].format}` + )}`; + } + return { + result: containers, + hasNextPage: + $('table:eq(1) tbody tr td:eq(1) font a:eq(0)').text().trim() == '►' || + $('table:eq(1) tbody tr td:eq(1) font a:eq(1)').text().trim() == '►' + ? true + : false, + }; + }; +} + +export default Libgen; diff --git a/consumet.ts/src/providers/comics/getComics.ts b/consumet.ts/src/providers/comics/getComics.ts new file mode 100644 index 00000000..1cb91a11 --- /dev/null +++ b/consumet.ts/src/providers/comics/getComics.ts @@ -0,0 +1,54 @@ +import { load } from 'cheerio'; +import { ComicParser, ComicRes, GetComicsComics, GetComicsComicsObject } from '../../models'; +import { parsePostInfo } from '../../utils'; + +const s = async () => {}; + +class getComics extends ComicParser { + override readonly baseUrl = 'https://getcomics.info/'; + override readonly name = 'GetComics'; + + override readonly logo = + 'https://i0.wp.com/getcomics.info/share/uploads/2020/04/cropped-GetComics-Favicon.png?fit=192%2C192&ssl=1'; + override readonly classPath = 'COMICS.GetComics'; + + override search = async (query: string, page: number | undefined = 1) => { + query = encodeURIComponent(query); + const { data } = await this.client.get(`${this.baseUrl}/page/${page ? page : 1}/?s=${query}`); + const $ = load(data); + const lastPage = $('section section nav:eq(1) ul li:last').text(); + const res: ComicRes = { + containers: [], + hasNextPage: $('a.pagination-older').text() != '', + }; + $('article').each((i, el) => { + const container: GetComicsComics = new GetComicsComicsObject(); + const vals = parsePostInfo($(el).children('div.post-info').text()); + container.image = + $(el).children('div.post-header-image').children('a').children('img').attr('src') || ''; + container.title = $(el).children('div.post-info').children('h1').text(); + container.excerpt = $(el).children('div.post-info').children('p.post-excerpt').text(); + container.year = vals.year; + container.size = vals.size; + container.description = vals.description; + const link = $(el).children('div.post-header-image').children('a').attr('href'); + container.ufile = link || ''; + res.containers.push(container); + }); + for (const container of res.containers) { + if (container.ufile != '') { + const { data } = await this.client.get(container.ufile); + const $ = load(data); + container.download = $('.aio-red[title="Download Now"]').attr('href') || ''; + container.readOnline = $('.aio-red[title="Read Online"]').attr('href') || ''; + container.ufile = $('.aio-blue').attr('href') || ''; + container.mega = $('.aio-purple').attr('href') || ''; + container.mediafire = $('.aio-orange').attr('href') || ''; + container.zippyshare = $('.aio-gray').attr('href') || ''; + } + } + return res; + }; +} + +export default getComics; diff --git a/consumet.ts/src/providers/comics/index.ts b/consumet.ts/src/providers/comics/index.ts new file mode 100644 index 00000000..ca206ea9 --- /dev/null +++ b/consumet.ts/src/providers/comics/index.ts @@ -0,0 +1,3 @@ +import GetComics from './getComics'; + +export default { GetComics }; diff --git a/consumet.ts/src/providers/index.ts b/consumet.ts/src/providers/index.ts new file mode 100644 index 00000000..e390e852 --- /dev/null +++ b/consumet.ts/src/providers/index.ts @@ -0,0 +1,10 @@ +import ANIME from './anime'; +import MANGA from './manga'; +import LIGHT_NOVELS from './light-novels'; +import BOOKS from './books'; +import COMICS from './comics'; +import MOVIES from './movies'; +import META from './meta'; +import NEWS from './news'; + +export { ANIME, MANGA, BOOKS, COMICS, LIGHT_NOVELS, MOVIES, META, NEWS }; diff --git a/consumet.ts/src/providers/light-novels/index.ts b/consumet.ts/src/providers/light-novels/index.ts new file mode 100644 index 00000000..51a40daa --- /dev/null +++ b/consumet.ts/src/providers/light-novels/index.ts @@ -0,0 +1,4 @@ +import ReadLightNovels from './readlightnovels'; +import NovelUpdates from './novelupdates'; + +export default { ReadLightNovels, NovelUpdates }; diff --git a/consumet.ts/src/providers/light-novels/novelupdates.ts b/consumet.ts/src/providers/light-novels/novelupdates.ts new file mode 100644 index 00000000..fde19544 --- /dev/null +++ b/consumet.ts/src/providers/light-novels/novelupdates.ts @@ -0,0 +1,205 @@ +import { load } from 'cheerio'; + +import { + LightNovelParser, + ISearch, + ILightNovelInfo, + ILightNovelChapter, + ILightNovelChapterContent, + ILightNovelResult, + MediaStatus, +} from '../../models'; + +class NovelUpdates extends LightNovelParser { + override readonly name = 'NovelUpdates'; + protected override baseUrl = 'https://www.novelupdates.com'; + + private proxyURL = 'http://translate.google.com/translate?sl=ja&tl=en&u='; + + protected override logo = 'https://www.novelupdates.com/appicon.png'; + protected override classPath = 'LIGHT_NOVELS.NovelUpdates'; + + /** + * + * @param lightNovelUrl light novel link or id + * @param chapterPage chapter page number (optional) if not provided, will fetch all chapter pages. + */ + override fetchLightNovelInfo = async ( + lightNovelUrl: string, + chapterPage: number = -1 + ): Promise => { + if (!lightNovelUrl.startsWith(this.baseUrl)) { + lightNovelUrl = `${this.baseUrl}/series/${lightNovelUrl}`; + } + const lightNovelInfo: ILightNovelInfo = { + id: lightNovelUrl.split('/')?.pop()!, + title: '', + url: lightNovelUrl, + }; + + try { + const page = await fetch(`${this.proxyURL}${encodeURIComponent(lightNovelUrl)}`, { + headers: { + Referer: lightNovelUrl, + }, + }); + + const $ = load(await page.text()); + + if ( + $('title').html() === 'Just a moment...' || + $('title').html() === 'Attention Required! | Cloudflare' + ) { + throw new Error('Client is blocked from accessing the site.'); + } + + lightNovelInfo.title = $('div.seriestitlenu').text()?.trim(); + + lightNovelInfo.image = $('div.seriesimg img').attr('src'); + lightNovelInfo.author = $('div#showauthors a').text(); + lightNovelInfo.genres = $('div#seriesgenre a') + .map((i, el) => $(el).text()) + .get(); + + lightNovelInfo.rating = parseFloat( + $( + 'div.col-xs-12.col-sm-8.col-md-8.desc > div.rate > div.small > em > strong:nth-child(1) > span' + ).text() + ); + lightNovelInfo.views = parseInt( + $('div.col-xs-12.col-sm-4.col-md-4.info-holder > div.info > div:nth-child(4) > span').text() + ); + lightNovelInfo.description = $('div#editdescription').text()?.trim(); + + const status = $('div#editstatus').text()?.trim(); + + if (status.includes('Complete')) { + lightNovelInfo.status = MediaStatus.COMPLETED; + } else if (status.includes('Ongoing')) { + lightNovelInfo.status = MediaStatus.ONGOING; + } else { + lightNovelInfo.status = MediaStatus.UNKNOWN; + } + + const postId = $('input#mypostid').attr('value'); + + lightNovelInfo.chapters = await this.fetchChapters(postId!); + lightNovelInfo.rating = + Number($('h5.seriesother span.uvotes').text()?.split(' /')[0]?.substring(1) ?? 0) * 2; + + return lightNovelInfo; + } catch (err) { + console.error(err); + throw new Error((err as Error).message); + } + }; + private fetchChapters = async (postId: string): Promise => { + const chapters: ILightNovelChapter[] = []; + + const chapterData = ( + await ( + await fetch(`${this.baseUrl}/wp-admin/admin-ajax.php`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Cookie: '_ga=;', + }, + body: `action=nd_getchapters&mypostid=${postId}&mypostid2=0`, + }) + ).text() + ).substring(1); + + const $ = load(chapterData); + $('li.sp_li_chp a[data-id]').each((index, el) => { + const id = $(el).attr('data-id'); + const title = $(el).find('span').text(); + + chapters.push({ + id: id!, + title: title!, + url: `${this.baseUrl}/extnu/${id}`, + }); + }); + + return chapters; + }; + + /** + * + * @param chapterId chapter id or url + */ + override fetchChapterContent = async (chapterId: string): Promise => { + if (!chapterId.startsWith(this.baseUrl)) { + chapterId = `${this.baseUrl}/extnu/${chapterId}`; + } + const contents: ILightNovelChapterContent = { + novelTitle: '', + chapterTitle: '', + text: '', + }; + + try { + const page = await fetch(`${this.proxyURL}${encodeURIComponent(chapterId)}`); + const data = await page.text(); + + const $ = load(data); + + contents.novelTitle = $('title').text(); + contents.chapterTitle = $('title').text(); + + contents.text = data; + + return contents; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param query search query string + */ + override search = async (query: string): Promise> => { + const result: ISearch = { results: [] }; + + try { + const res = await fetch( + `${this.proxyURL}${encodeURIComponent( + `${this.baseUrl}/series-finder/?sf=1&sh=${encodeURIComponent(query)}` + )}`, + { + headers: { + Referer: this.baseUrl, + }, + } + ); + + const $ = load(await res.text()); + + $('div.search_main_box_nu').each((i, el) => { + result.results.push({ + id: $(el) + .find('div.search_body_nu div.search_title a') + .attr('href') + ?.split('/series/')[1] + .split('/')[0]!, + title: $(el).find('div.search_body_nu div.search_title a').text(), + url: $(el).find('div.search_body_nu div.search_title a').attr('href')!, + image: $(el).find('div.search_img_nu img').attr('src'), + }); + }); + + return result; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default NovelUpdates; + +// (async () => { +// const ln = new ReadLightNovels(); +// const chap = await ln.fetchChapterContent('youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e/volume-1-prologue-the-structure-of-japanese-society'); +// console.log(chap); +// })(); diff --git a/consumet.ts/src/providers/light-novels/readlightnovels.ts b/consumet.ts/src/providers/light-novels/readlightnovels.ts new file mode 100644 index 00000000..a0bc49aa --- /dev/null +++ b/consumet.ts/src/providers/light-novels/readlightnovels.ts @@ -0,0 +1,220 @@ +import { load } from 'cheerio'; +import FormData from 'form-data'; + +import { + LightNovelParser, + ISearch, + ILightNovelInfo, + ILightNovelChapter, + ILightNovelChapterContent, + ILightNovelResult, + MediaStatus, +} from '../../models'; +import { USER_AGENT } from '../../utils'; + +class ReadLightNovels extends LightNovelParser { + override readonly name = 'Read Light Novels'; + protected override baseUrl = 'https://readlightnovels.net'; + + protected override logo = 'https://i.imgur.com/RDPjbc6.png'; + protected override classPath = 'LIGHT_NOVELS.ReadLightNovels'; + + /** + * + * @param lightNovelUrl light novel link or id + * @param chapterPage chapter page number (optional) if not provided, will fetch all chapter pages. + */ + override fetchLightNovelInfo = async ( + lightNovelUrl: string, + chapterPage: number = -1 + ): Promise => { + if (!lightNovelUrl.startsWith(this.baseUrl)) { + lightNovelUrl = `${this.baseUrl}/${lightNovelUrl}.html`; + } + const lightNovelInfo: ILightNovelInfo = { + id: lightNovelUrl.split('/')?.pop()?.replace('.html', '')!, + title: '', + url: lightNovelUrl, + }; + + try { + const page = await this.client.get(lightNovelUrl, { + headers: { + Referer: lightNovelUrl, + }, + }); + + const $ = load(page.data); + + const novelId = parseInt($('#id_post').val() as string); + lightNovelInfo.title = $('div.col-xs-12.col-sm-8.col-md-8.desc > h3').text(); + lightNovelInfo.image = $('div.col-xs-12.col-sm-4.col-md-4.info-holder > div.books > div > img').attr( + 'src' + ); + lightNovelInfo.author = $( + 'div.col-xs-12.col-sm-4.col-md-4.info-holder > div.info > div:nth-child(1) > a' + ).text(); + lightNovelInfo.genres = $( + ' div.col-xs-12.col-sm-4.col-md-4.info-holder > div.info > div:nth-child(2) > a' + ) + .map((i, el) => $(el).text()) + .get(); + lightNovelInfo.rating = parseFloat( + $( + 'div.col-xs-12.col-sm-8.col-md-8.desc > div.rate > div.small > em > strong:nth-child(1) > span' + ).text() + ); + lightNovelInfo.views = parseInt( + $('div.col-xs-12.col-sm-4.col-md-4.info-holder > div.info > div:nth-child(4) > span').text() + ); + lightNovelInfo.description = $('div.col-xs-12.col-sm-8.col-md-8.desc > div.desc-text > hr') + .eq(0) + .nextUntil('hr') + .text(); + const pages = Math.max( + ...$('#pagination > ul > li') + .map((i, el) => parseInt($(el).find('a').attr('data-page')!)) + .get() + .filter(x => !isNaN(x)) + ); + + switch ($('div.col-xs-12.col-sm-4.col-md-4.info-holder > div.info > div:nth-child(3) > span').text()) { + case 'Completed': + lightNovelInfo.status = MediaStatus.COMPLETED; + break; + case 'On Going': + lightNovelInfo.status = MediaStatus.ONGOING; + break; + default: + lightNovelInfo.status = MediaStatus.UNKNOWN; + break; + } + + lightNovelInfo.pages = pages; + lightNovelInfo.chapters = []; + if (chapterPage === -1) { + lightNovelInfo.chapters = await this.fetchAllChapters(novelId, pages, lightNovelUrl); + } else { + lightNovelInfo.chapters = await this.fetchChapters(novelId, chapterPage, lightNovelUrl); + } + + return lightNovelInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + private fetchChapters = async ( + novelId: number, + chapterPage: number, + referer: string + ): Promise => { + const chapters: ILightNovelChapter[] = []; + + const bodyFormData = new FormData(); + bodyFormData.append('action', 'tw_ajax'); + bodyFormData.append('type', 'pagination'); + bodyFormData.append('page', chapterPage); + bodyFormData.append('id', novelId); + + const page = await this.client({ + method: 'post', + url: `${this.baseUrl}/wp-admin/admin-ajax.php`, + data: bodyFormData, + headers: { + referer: referer, + 'content-type': 'multipart/form-data', + origin: this.baseUrl, + 'user-agent': USER_AGENT, + }, + }); + + const $ = load(page.data.list_chap); + + for (const chapter of $('ul.list-chapter > li')) { + const subId = $(chapter).find('a').attr('href')!.split('/')?.pop()!.replace('.html', '')!; + const id = $(chapter).find('a').attr('href')!.split('/')[3]; + chapters.push({ + id: `${id}/${subId}`, + title: $(chapter).find('a > span').text().trim(), + url: $(chapter).find('a').attr('href'), + }); + } + return chapters; + }; + private fetchAllChapters = async (novelId: number, pages: number, referer: string): Promise => { + const chapters: ILightNovelChapter[] = []; + + for (const pageNumber of Array.from({ length: pages }, (_, i) => i + 1)) { + const chaptersPage = await this.fetchChapters(novelId, pageNumber, referer); + chapters.push(...chaptersPage); + } + return chapters; + }; + + /** + * + * @param chapterId chapter id or url + */ + override fetchChapterContent = async (chapterId: string): Promise => { + if (!chapterId.startsWith(this.baseUrl)) { + chapterId = `${this.baseUrl}/${chapterId}.html`; + } + const contents: ILightNovelChapterContent = { + novelTitle: '', + chapterTitle: '', + text: '', + }; + + try { + const page = await this.client.get(chapterId); + const $ = load(page.data); + + contents.novelTitle = $('.truyen-title').text(); + contents.chapterTitle = $('.chapter-title').text(); + for (const line of $('div.chapter-content > p')) { + if ($(line).text() != '') { + contents.text += `${$(line).text()}\n`; + } + } + + return contents; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param query search query string + */ + override search = async (query: string): Promise> => { + const result: ISearch = { results: [] }; + try { + const res = await this.client.post(`${this.baseUrl}/?s=${query}`); + const $ = load(res.data); + + $( + 'div.col-xs-12.col-sm-12.col-md-9.col-truyen-main > div:nth-child(1) > div > div:nth-child(2) > div.col-md-3.col-sm-6.col-xs-6.home-truyendecu' + ).each((i, el) => { + result.results.push({ + id: $(el).find('a').attr('href')?.split('/')[3]!.replace('.html', '')!, + title: $(el).find('a > div > h3').text(), + url: $(el).find('a').attr('href')!, + image: $(el).find('a > img').attr('src'), + }); + }); + + return result; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default ReadLightNovels; + +// (async () => { +// const ln = new ReadLightNovels(); +// const chap = await ln.fetchChapterContent('youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e/volume-1-prologue-the-structure-of-japanese-society'); +// console.log(chap); +// })(); diff --git a/consumet.ts/src/providers/manga/asurascans.ts b/consumet.ts/src/providers/manga/asurascans.ts new file mode 100644 index 00000000..5142b5c6 --- /dev/null +++ b/consumet.ts/src/providers/manga/asurascans.ts @@ -0,0 +1,219 @@ +import { CheerioAPI, load } from 'cheerio'; + +import { + MangaParser, + ISearch, + IMangaInfo, + IMangaResult, + MediaStatus, + IMangaChapterPage, + IMangaChapter, +} from '../../models'; + +let cloudscraper: any; + +class AsuraScans extends MangaParser { + override readonly name = 'AsuraScans'; + protected override baseUrl = 'https://www.asurascans.com/'; + protected override logo = 'https://www.asurascans.com/wp-content/uploads/2021/03/Group_1.png'; + protected override classPath = 'MANGA.AsuraScans'; + + constructor() { + try { + cloudscraper = require('cloudscraper'); + } catch (err: any) { + if (err.message.includes("Cannot find module 'request'")) { + throw new Error( + 'Request is not installed. Please install it by running "npm i request" or "yarn add request"' + ); + } else if (err.message.includes("Cannot find module 'cloudscraper'")) { + throw new Error( + 'Cloudscraper is not installed. Please install it by running "npm i cloudscraper" or "yarn add cloudscraper"' + ); + } else { + throw new Error((err as Error).message); + } + } + + super(); + } + + override fetchMangaInfo = async (mangaId: string): Promise => { + const options = { + method: 'GET', + url: `${this.baseUrl}/manga/${mangaId.trim()}`, + headers: { + 'User-Agent': 'Ubuntu Chromium/34.0.1847.116 Chrome/34.0.1847.116 Safari/537.36', + 'Cache-Control': 'private', + Accept: 'application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5', + }, + cloudflareTimeout: 5000, + cloudflareMaxTimeout: 30000, + followAllRedirects: true, + challengesToSolve: 3, + decodeEmails: false, + gzip: true, + }; + + const mangaInfo: IMangaInfo = { + id: mangaId, + title: '', + }; + + try { + const data: string = await cloudscraper(options).then((response: any) => response); + const $: CheerioAPI = load(data); + + const seriesTitleSelector = 'h1.entry-title'; + const seriesArtistSelector = + ".infotable tr:icontains('artist') td:last-child, .tsinfo .imptdt:icontains('artist') i, .fmed b:icontains('artist')+span, span:icontains('artist')"; + const seriesAuthorSelector = + ".infotable tr:icontains('author') td:last-child, .tsinfo .imptdt:icontains('author') i, .fmed b:icontains('author')+span, span:icontains('author')"; + const seriesDescriptionSelector = '.desc, .entry-content[itemprop=description]'; + const seriesAltNameSelector = ".alternative, .wd-full:icontains('alt') span, .alter, .seriestualt"; + const seriesGenreSelector = 'div.gnr a, .mgen a, .seriestugenre a'; + const seriesStatusSelector = + ".infotable tr:icontains('status') td:last-child, .tsinfo .imptdt:icontains('status') i, .fmed b:icontains('status')+span span:icontains('status')"; + const seriesThumbnailSelector = '.infomanga > div[itemprop=image] img, .thumb img'; + const seriesChaptersSelector = + 'div.bxcl li, div.cl li, #chapterlist li, ul li:has(div.chbox):has(div.eph-num)'; + + mangaInfo.title = $(seriesTitleSelector).text().trim(); + mangaInfo.altTitles = $(seriesAltNameSelector).text() + ? $(seriesAltNameSelector) + .text() + .split(',') + .map(item => item.trim()) + : []; + mangaInfo.description = $(seriesDescriptionSelector).text().trim(); + mangaInfo.headerForImage = { Referer: this.baseUrl }; + mangaInfo.image = $(seriesThumbnailSelector).attr('src'); + mangaInfo.genres = $(seriesGenreSelector) + .map((i, el) => $(el).text()) + .get(); + switch ($(seriesStatusSelector).text().trim()) { + case 'Completed': + mangaInfo.status = MediaStatus.COMPLETED; + break; + case 'Ongoing': + mangaInfo.status = MediaStatus.ONGOING; + break; + case 'Dropped': + mangaInfo.status = MediaStatus.CANCELLED; + break; + default: + mangaInfo.status = MediaStatus.UNKNOWN; + break; + } + mangaInfo.authors = $(seriesAuthorSelector).text().replace('-', '').trim() + ? $(seriesAuthorSelector) + .text() + .split(',') + .map(item => item.trim()) + : []; + mangaInfo.artist = $(seriesArtistSelector).text().trim() + ? $(seriesArtistSelector).text().trim() + : 'N/A'; + mangaInfo.chapters = $(seriesChaptersSelector) + .map( + (i, el): IMangaChapter => ({ + id: $(el).find('a').attr('href')?.split('/')[3] ?? '', + title: $(el).find('.lch a, .chapternum').text(), + releasedDate: $(el).find('.chapterdate').text(), + }) + ) + .get(); + return mangaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchChapterPages = async (chapterId: string): Promise => { + const options = { + method: 'GET', + url: `${this.baseUrl}/${chapterId.trim()}`, + headers: { + 'User-Agent': 'Ubuntu Chromium/34.0.1847.116 Chrome/34.0.1847.116 Safari/537.36', + 'Cache-Control': 'private', + Accept: 'application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5', + }, + cloudflareTimeout: 5000, + cloudflareMaxTimeout: 30000, + followAllRedirects: true, + challengesToSolve: 3, + decodeEmails: false, + gzip: true, + }; + + try { + const data: string = await cloudscraper(options).then((response: any) => response); + const $: CheerioAPI = load(data); + + const pageSelector = 'div#readerarea img'; + + const pages = $(pageSelector) + .map( + (i, el): IMangaChapterPage => ({ + img: $(el).attr('src')!, + page: i, + headerForImage: { Referer: this.baseUrl }, + }) + ) + .get(); + + return pages; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param query Search query + */ + override search = async (query: string): Promise> => { + try { + const options = { + method: 'GET', + url: `${this.baseUrl}/?s=${query.replace(/ /g, '%20')}`, + headers: { + 'User-Agent': 'Ubuntu Chromium/34.0.1847.116 Chrome/34.0.1847.116 Safari/537.36', + 'Cache-Control': 'private', + Accept: + 'application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5', + }, + cloudflareTimeout: 5000, + cloudflareMaxTimeout: 30000, + followAllRedirects: true, + challengesToSolve: 3, + decodeEmails: false, + gzip: true, + }; + + const data: string = await cloudscraper(options).then((response: any) => response); + + const $: CheerioAPI = load(data); + + const searchMangaSelector = '.utao .uta .imgu, .listupd .bs .bsx, .listo .bs .bsx'; + + const results = $(searchMangaSelector) + .map( + (i, el): IMangaResult => ({ + id: $(el).find('a').attr('href')?.split('/')[4] ?? '', + title: $(el).find('a').attr('title')!, + image: $(el).find('img').attr('src'), + headerForImage: { Referer: this.baseUrl }, + }) + ) + .get(); + return { + results: results, + }; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default AsuraScans; diff --git a/consumet.ts/src/providers/manga/brmangas.ts b/consumet.ts/src/providers/manga/brmangas.ts new file mode 100644 index 00000000..05f0b1f7 --- /dev/null +++ b/consumet.ts/src/providers/manga/brmangas.ts @@ -0,0 +1,145 @@ +import { load } from 'cheerio'; + +import { + MangaParser, + ISearch, + IMangaInfo, + IMangaResult, + MediaStatus, + IMangaChapterPage, + IMangaChapter, +} from '../../models'; + +class BRMangas extends MangaParser { + override readonly name = 'BRMangas'; + protected override baseUrl = 'https://www.brmangas.net'; + protected override logo = 'https://www.brmangas.net/wp-content/themes/brmangasnew/images/svg/logo.svg'; + protected override classPath = 'MANGA.BRMangas'; + + override fetchMangaInfo = async (mangaId: string): Promise => { + const mangaInfo: IMangaInfo = { + id: mangaId, + title: '', + }; + try { + const { data } = await this.client.get(`${this.baseUrl}/manga/${mangaId}`); + const $ = load(data); + + const title = $('body > div.scroller-inner > div.wrapper > main > section > div > h1.titulo').text(); + const descriptionAndAltTitles = $( + 'body > div.scroller-inner > div.wrapper > main > div > div > div.col > div.serie-texto > div > p:nth-child(3)' + ) + .text() + .split('\n'); + + mangaInfo.title = title.slice(3, title.length - 7).trim(); + mangaInfo.altTitles = descriptionAndAltTitles.filter((_, i) => i > 0); + mangaInfo.description = descriptionAndAltTitles[0]; + mangaInfo.headerForImage = { Referer: this.baseUrl }; + mangaInfo.image = $( + 'body > div.scroller-inner > div.wrapper > main > div > div > div.serie-geral > div.infoall > div.serie-capa > img' + ).attr('src'); + mangaInfo.genres = $( + 'body > div.scroller-inner > div.wrapper > main > div > div > div.serie-geral > div.infoall > div.serie-infos > ul > li:nth-child(3)' + ) + .text() + .slice(11) + .split(',') + .map(genre => genre.trim()); + + mangaInfo.status = MediaStatus.UNKNOWN; + + mangaInfo.views = null; + mangaInfo.authors = [ + $( + 'body > div.scroller-inner > div.wrapper > main > div > div > div.serie-geral > div.infoall > div.serie-infos > ul > li:nth-child(2)' + ) + .text() + .replace('Autor: ', '') + .trim(), + ]; + + mangaInfo.chapters = $( + 'body > div.scroller-inner > div.wrapper > main > div > div > div:nth-child(2) > div.manga > div.container_t > div.lista_manga > ul > li' + ) + .map((i, el): IMangaChapter => { + return { + id: `${$(el).find('a').attr('href')?.split('/')[4]}`, + title: $(el).find('a').text(), + views: null, + releasedDate: null, + }; + }) + .get(); + + return mangaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchChapterPages = async (chapterId: string): Promise => { + try { + const url = `${this.baseUrl}/ler/${chapterId}`; + const { data } = await this.client.get(url); + const $ = load(data); + + const script = $('script'); + + const pageURLs = JSON.parse( + script + .filter((i, el) => $(el).text().includes('imageArray')) + .text() + .trim() + .slice(13, -2) + .replace(/\\/g, '') + ); + + console.log(pageURLs.images); + + const pages = pageURLs.images.map( + (img: any, i: any): IMangaChapterPage => ({ + img: img, + page: i, + title: `Page ${i + 1}`, + headerForImage: { Referer: this.baseUrl }, + }) + ); + + return pages; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param query Search query + */ + override search = async (query: string): Promise> => { + try { + const { data } = await this.client.get(`${this.baseUrl}/?s=${query.replace(/ /g, '+')}`); + const $ = load(data); + + const results = $( + 'body > div.scroller-inner > div.wrapper > main > div.container > div.listagem > div.col' + ) + .map( + (i, row): IMangaResult => ({ + id: $(row).find('div.item > a').attr('href')?.split('/')[4]!, + title: $(row).find('div.item > a > h2').text(), + image: $(row).find('div.item > a > div > img').attr('src'), + headerForImage: { Referer: this.baseUrl }, + }) + ) + .get(); + return { + results: results, + }; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default BRMangas; diff --git a/consumet.ts/src/providers/manga/comick.ts b/consumet.ts/src/providers/manga/comick.ts new file mode 100644 index 00000000..8fa35a8c --- /dev/null +++ b/consumet.ts/src/providers/manga/comick.ts @@ -0,0 +1,279 @@ +import axios, { AxiosError } from 'axios'; +import { IMangaChapterPage, IMangaInfo, IMangaResult, ISearch, MangaParser, MediaStatus } from '../../models'; + +class ComicK extends MangaParser { + override readonly name = 'ComicK'; + protected override baseUrl = 'https://comick.app'; + protected override logo = 'https://th.bing.com/th/id/OIP.fw4WrmAoA2PmKitiyMzUIgAAAA?pid=ImgDet&rs=1'; + protected override classPath = 'MANGA.ComicK'; + + private readonly apiUrl = 'https://api.comick.io'; + + private _axios() { + return axios.create({ + baseURL: this.apiUrl, + headers: { + 'User-Agent': 'Mozilla/5.0', + }, + }); + } + + /** + * @description Fetches info about the manga + * @param mangaId Comic slug + * @returns Promise + */ + override fetchMangaInfo = async (mangaId: string): Promise => { + try { + const req = await this._axios().get(`/comic/${mangaId}`); + const data: Comic = req.data.comic; + + const links = Object.values(data.links ?? []).filter(link => link !== null); + + const mangaInfo: IMangaInfo = { + id: data.hid, + title: data.title, + altTitles: data.md_titles ? data.md_titles.map(title => title.title) : [], + description: data.desc, + genres: data.md_comic_md_genres?.map(genre => genre.md_genres.name), + status: data.status ?? 0 === 0 ? MediaStatus.ONGOING : MediaStatus.COMPLETED, + image: `https://meo.comick.pictures${data.md_covers ? data.md_covers[0].b2key : ''}`, + malId: data.links?.mal, + links: links, + chapters: [], + }; + + const allChapters: ChapterData[] = await this.fetchAllChapters(mangaId, 1); + for (const chapter of allChapters) { + mangaInfo.chapters?.push({ + id: chapter.hid, + title: chapter.title ?? chapter.chap, + chapterNumber: chapter.chap, + volumeNumber: chapter.vol, + releaseDate: chapter.created_at, + }); + } + + return mangaInfo; + } catch (err) { + if ((err as AxiosError).code == 'ERR_BAD_REQUEST') + throw new Error(`[${this.name}] Bad request. Make sure you have entered a valid query.`); + + throw new Error((err as Error).message); + } + }; + + /** + * + * @param chapterId Chapter ID (HID) + * @returns Promise + */ + override fetchChapterPages = async (chapterId: string): Promise => { + try { + const { data } = await this._axios().get(`/chapter/${chapterId}`); + + const pages: { img: string; page: number }[] = []; + + data.chapter.md_images.map((image: { b2key: string; w: string }, index: number) => { + pages.push({ + img: `https://meo.comick.pictures/${image.b2key}?width=${image.w}`, + page: index, + }); + }); + + return pages; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * @param query search query + * @param page page number (default: 1) + * @param limit limit of results to return (default: 20) (max: 100) (min: 1) + */ + override search = async ( + query: string, + page: number = 1, + limit: number = 20 + ): Promise> => { + if (page < 1) throw new Error('Page number must be greater than 1'); + if (limit > 300) throw new Error('Limit must be less than or equal to 300'); + if (limit * (page - 1) >= 10000) throw new Error('not enough results'); + + try { + const req = await this._axios().get( + `/v1.0/search?q=${encodeURIComponent(query)}&limit=${limit}&page=${page}` + ); + + const results: ISearch = { + currentPage: page, + results: [], + }; + + const data: SearchResult[] = await req.data; + + for (const manga of data) { + let cover: Cover | string | null = manga.md_covers ? manga.md_covers[0] : null; + if (cover && cover.b2key != undefined) { + cover = `https://meo.comick.pictures${cover.b2key}`; + } + + results.results.push({ + id: manga.slug, + title: manga.title ?? manga.slug, + altTitles: manga.md_titles ? manga.md_titles.map(title => title.title) : [], + image: cover as string, + }); + } + + return results; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + private fetchAllChapters = async (mangaId: string, page: number): Promise => { + if (page <= 0) { + page = 1; + } + const comicId = await this.getComicId(mangaId); + const req = await this._axios().get(`/comic/${comicId}/chapters?page=${page}`); + return req.data.chapters; + }; + + /** + * @description Fetches the comic HID from the slug + * @param id Comic slug + * @returns Promise empty if not found + */ + private async getComicId(id: string): Promise { + const req = await this._axios().get(`/comic/${id}`); + const data: Comic = req.data['comic']; + return data ? data.hid : ''; + } +} + +// (async () => { +// const md = new MangaDex(); +// const search = await md.search('solo leveling'); +// const manga = await md.fetchMangaInfo(search.results[0].id); +// const chapterPages = await md.fetchChapterPages(manga.chapters![0].id); +// console.log(chapterPages); +// })(); + +export default ComicK; + +interface SearchResult { + title: string; + id: number; + slug: string; + rating: string; + rating_count: number; + follow_count: number; + user_follow_count: number; + content_rating: string; + demographic: number; + md_titles: [MDTitle]; + md_covers: Array; + highlight: string; +} + +interface Cover { + vol: any; + w: number; + h: number; + b2key: string; +} + +interface MDTitle { + title: string; +} + +interface Comic { + hid: string; + title: string; + country: string; + status: number; + links: ComicLinks; + last_chapter: any; + chapter_count: number; + demographic: number; + hentai: boolean; + user_follow_count: number; + follow_rank: number; + comment_count: number; + follow_count: number; + desc: string; + parsed: string; + slug: string; + mismatch: any; + year: number; + bayesian_rating: any; + rating_count: number; + content_rating: string; + translation_completed: boolean; + relate_from: Array; + mies: any; + md_titles: Array; + md_comic_md_genres: Array; + mu_comics: { + licensed_in_english: any; + mu_comic_categories: Array; + }; + md_covers: Array; + iso639_1: string; + lang_name: string; + lang_native: string; +} + +interface ComicLinks { + al: string; + ap: string; + bw: string; + kt: string; + mu: string; + amz: string; + cdj: string; + ebj: string; + mal: string; + raw: string; +} + +interface ComicTitles { + title: string; +} + +interface ComicGenres { + md_genres: { + name: string; + type: string | null; + slug: string; + group: string; + }; +} + +interface ComicCategories { + mu_categories: { + title: string; + slug: string; + }; + positive_vote: number; + negative_vote: number; +} + +interface ChapterData { + id: number; + chap: string; + title: string; + vol: string; + slug: string | null; + lang: string; + created_at: string; + updated_at: string; + up_count: number; + down_count: number; + group_name: string[]; + hid: string; + md_groups: string[]; +} diff --git a/consumet.ts/src/providers/manga/flamescans.ts b/consumet.ts/src/providers/manga/flamescans.ts new file mode 100644 index 00000000..a6e71aeb --- /dev/null +++ b/consumet.ts/src/providers/manga/flamescans.ts @@ -0,0 +1,151 @@ +import { load } from 'cheerio'; + +import { + MangaParser, + ISearch, + IMangaInfo, + IMangaResult, + IMangaChapterPage, + IMangaChapter, + MediaStatus, +} from '../../models'; + +class FlameScans extends MangaParser { + override readonly name = 'FlameScans'; + protected override baseUrl = 'https://flamescans.org/'; + protected override logo = 'https://i.imgur.com/Nt1MW3H.png'; + protected override classPath = 'MANGA.FlameScans'; + + /** + * + * @param query Search query + * + */ + + override search = async (query: string): Promise> => { + try { + const { data } = await this.client.get(`${this.baseUrl}/series/?title=${query.replace(/ /g, '%20')}`); + const $ = load(data); + + const searchMangaSelector = '.utao .uta .imgu, .listupd .bs .bsx, .listo .bs .bsx'; + const results = $(searchMangaSelector) + .map( + (i, el): IMangaResult => ({ + id: $(el).find('a').attr('href')?.split('/series/')[1].replace('/', '') ?? '', + title: $(el).find('a').attr('title') ?? '', + image: $(el).find('img').attr('src'), + }) + ) + .get(); + + return { + results: results, + }; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchMangaInfo = async (mangaId: string): Promise => { + const mangaInfo: IMangaInfo = { + id: mangaId, + title: '', + }; + try { + const { data } = await this.client.get(`${this.baseUrl}/manga/${mangaId}`); + const $ = load(data); + + // base from https://github.com/tachiyomiorg/tachiyomi-extensions/blob/661311c13b3b550e3fa906c1130b77a037ef7a11/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangathemesia/MangaThemesia.kt#L233 + const seriesTitleSelector = 'h1.entry-title'; + const seriesArtistSelector = + ".infotable tr:icontains('artist') td:last-child, .tsinfo .imptdt:icontains('artist') i, .fmed b:icontains('artist')+span, span:icontains('artist')"; + const seriesAuthorSelector = + ".infotable tr:icontains('author') td:last-child, .tsinfo .imptdt:icontains('author') i, .fmed b:icontains('author')+span, span:icontains('author')"; + const seriesDescriptionSelector = '.desc, .entry-content[itemprop=description]'; + const seriesAltNameSelector = + ".alternative > div, .wd-full:icontains('alt') span, .alter, .seriestualt"; + const seriesGenreSelector = 'div.gnr a, .mgen a, .seriestugenre a'; + const seriesStatusSelector = + ".infotable tr:icontains('status') td:last-child, .tsinfo .imptdt:icontains('status') i, .fmed b:icontains('status')+span span:icontains('status')"; + const seriesThumbnailSelector = '.infomanga > div[itemprop=image] img, .thumb img'; + const seriesChaptersSelector = + 'div.bxcl li, div.cl li, #chapterlist li, ul li:has(div.chbox):has(div.eph-num)'; + + mangaInfo.title = $(seriesTitleSelector).text().trim(); + mangaInfo.altTitles = $(seriesAltNameSelector).text() + ? $(seriesAltNameSelector) + .first() + .text() + .split('|') + .map(item => item.replace(/\n/g, ' ').trim()) + : []; + mangaInfo.description = $(seriesDescriptionSelector).text().trim(); + mangaInfo.headerForImage = { Referer: this.baseUrl }; + mangaInfo.image = $(seriesThumbnailSelector).attr('src'); + mangaInfo.genres = $(seriesGenreSelector) + .map((i, el) => $(el).text()) + .get(); + switch ($(seriesStatusSelector).text().trim()) { + case 'Completed': + mangaInfo.status = MediaStatus.COMPLETED; + break; + case 'Ongoing': + mangaInfo.status = MediaStatus.ONGOING; + break; + case 'Dropped': + mangaInfo.status = MediaStatus.CANCELLED; + break; + default: + mangaInfo.status = MediaStatus.UNKNOWN; + break; + } + mangaInfo.authors = $(seriesAuthorSelector).text().replace('-', '').trim() + ? $(seriesAuthorSelector) + .text() + .split(',') + .map(item => item.trim()) + : []; + mangaInfo.artist = $(seriesArtistSelector).text().trim() + ? $(seriesArtistSelector).text().trim() + : 'N/A'; + mangaInfo.chapters = $(seriesChaptersSelector) + .map( + (i, el): IMangaChapter => ({ + id: $(el).find('a').attr('href')?.split('/')[3] ?? '', + title: $(el).find('.lch a, .chapternum').text().trim().replace(/\n/g, ' '), + releasedDate: $(el).find('.chapterdate').text(), + }) + ) + .get(); + + return mangaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchChapterPages = async (chapterId: string): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/${chapterId}`); + const $ = load(data); + + const pageSelector = 'div#readerarea img, #readerarea div.figure_container div.composed_figure'; + + const pages = $(pageSelector) + .map( + (i, el): IMangaChapterPage => ({ + img: $(el).attr('src')!, + page: i, + headerForImage: { Referer: this.baseUrl }, + }) + ) + .get(); + + return pages; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default FlameScans; diff --git a/consumet.ts/src/providers/manga/index.ts b/consumet.ts/src/providers/manga/index.ts new file mode 100644 index 00000000..7ec8dca6 --- /dev/null +++ b/consumet.ts/src/providers/manga/index.ts @@ -0,0 +1,27 @@ +import MangaDex from './mangadex'; +import ComicK from './comick'; +import MangaHere from './mangahere'; +import MangaKakalot from './mangakakalot'; +import Mangasee123 from './mangasee123'; +import Mangapark from './mangapark'; +import MangaPill from './mangapill'; +import MangaReader from './mangareader'; +import AsuraScans from './asurascans'; +import FlameScans from './flamescans'; +import MangaHost from './mangahost'; +import BRMangas from './brmangas'; + +export default { + MangaDex, + ComicK, + MangaHere, + MangaKakalot, + Mangasee123, + Mangapark, + MangaPill, + MangaReader, + AsuraScans, + FlameScans, + MangaHost, + BRMangas, +}; diff --git a/consumet.ts/src/providers/manga/mangadex.ts b/consumet.ts/src/providers/manga/mangadex.ts new file mode 100644 index 00000000..b9bdeb91 --- /dev/null +++ b/consumet.ts/src/providers/manga/mangadex.ts @@ -0,0 +1,328 @@ +import { encode } from 'ascii-url-encoder'; +import { AxiosError, AxiosResponse } from 'axios'; + +import { IMangaChapterPage, IMangaInfo, IMangaResult, ISearch, MangaParser, MediaStatus } from '../../models'; +import { capitalizeFirstLetter, substringBefore } from '../../utils'; + +class MangaDex extends MangaParser { + override readonly name = 'MangaDex'; + protected override baseUrl = 'https://mangadex.org'; + protected override logo = 'https://pbs.twimg.com/profile_images/1391016345714757632/xbt_jW78_400x400.jpg'; + protected override classPath = 'MANGA.MangaDex'; + + private readonly apiUrl = 'https://api.mangadex.org'; + + override fetchMangaInfo = async (mangaId: string): Promise => { + try { + const { data } = await this.client.get(`${this.apiUrl}/manga/${mangaId}`); + const mangaInfo: IMangaInfo = { + id: data.data.id, + title: data.data.attributes.title.en, + altTitles: data.data.attributes.altTitles, + description: data.data.attributes.description, + genres: data.data.attributes.tags + .filter((tag: any) => tag.attributes.group === 'genre') + .map((tag: any) => tag.attributes.name.en), + themes: data.data.attributes.tags + .filter((tag: any) => tag.attributes.group === 'theme') + .map((tag: any) => tag.attributes.name.en), + status: capitalizeFirstLetter(data.data.attributes.status) as MediaStatus, + releaseDate: data.data.attributes.year, + chapters: [], + }; + + const allChapters = await this.fetchAllChapters(mangaId, 0); + for (const chapter of allChapters) { + mangaInfo.chapters?.push({ + id: chapter.id, + title: chapter.attributes.title ? chapter.attributes.title : chapter.attributes.chapter, + chapterNumber: chapter.attributes.chapter, + volumeNumber: chapter.attributes.volume, + pages: chapter.attributes.pages, + }); + } + + const findCoverArt = data.data.relationships.find((rel: any) => rel.type === 'cover_art'); + const coverArt = await this.fetchCoverImage(findCoverArt?.id); + mangaInfo.image = `${this.baseUrl}/covers/${mangaInfo.id}/${coverArt}`; + + return mangaInfo; + } catch (err) { + if ((err as AxiosError).code == 'ERR_BAD_REQUEST') + throw new Error(`[${this.name}] Bad request. Make sure you have entered a valid query.`); + + throw new Error((err as Error).message); + } + }; + + /** + * @currently only supports english + */ + override fetchChapterPages = async (chapterId: string): Promise => { + try { + const res = await this.client.get(`${this.apiUrl}/at-home/server/${chapterId}`); + const pages: { img: string; page: number }[] = []; + + for (const id of res.data.chapter.data) { + pages.push({ + img: `${res.data.baseUrl}/data/${res.data.chapter.hash}/${id}`, + page: parseInt(substringBefore(id, '-').replace(/[^0-9.]/g, '')), + }); + } + return pages; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * @param query search query + * @param page page number (default: 1) + * @param limit limit of results to return (default: 20) (max: 100) (min: 1) + */ + override search = async ( + query: string, + page: number = 1, + limit: number = 20 + ): Promise> => { + if (page <= 0) throw new Error('Page number must be greater than 0'); + if (limit > 100) throw new Error('Limit must be less than or equal to 100'); + if (limit * (page - 1) >= 10000) throw new Error('not enough results'); + + try { + const res = await this.client.get( + `${this.apiUrl}/manga?limit=${limit}&title=${encode(query)}&limit=${limit}&offset=${ + limit * (page - 1) + }&order[relevance]=desc` + ); + + if (res.data.result == 'ok') { + const results: ISearch = { + currentPage: page, + results: [], + }; + + for (const manga of res.data.data) { + results.results.push({ + id: manga.id, + title: Object.values(manga.attributes.title)[0] as string, + altTitles: manga.attributes.altTitles, + description: Object.values(manga.attributes.description)[0] as string, + status: manga.attributes.status, + releaseDate: manga.attributes.year, + contentRating: manga.attributes.contentRating, + lastVolume: manga.attributes.lastVolume, + lastChapter: manga.attributes.lastChapter, + }); + } + + return results; + } else { + throw new Error(res.data.message); + } + } catch (err) { + if ((err as AxiosError).code == 'ERR_BAD_REQUEST') { + throw new Error('Bad request. Make sure you have entered a valid query.'); + } + + throw new Error((err as Error).message); + } + }; + fetchRandom = async ( + ): Promise> => { + try { + const res = await this.client.get( + `${this.apiUrl}/manga/random` + ); + + if (res.data.result == 'ok') { + const results: ISearch = { + currentPage: 1, + results: [], + }; + + results.results.push({ + id: res.data.data.id, + title: Object.values(res.data.data.attributes.title)[0] as string, + altTitles: res.data.data.attributes.altTitles, + description: Object.values(res.data.data.attributes.description)[0] as string, + status: res.data.data.attributes.status, + releaseDate: res.data.data.attributes.year, + contentRating: res.data.data.attributes.contentRating, + lastVolume: res.data.data.attributes.lastVolume, + lastChapter: res.data.data.attributes.lastChapter, + }); + + + return results; + } else { + throw new Error(res.data.message); + } + } catch (err) { + throw new Error((err as Error).message); + } + }; + fetchRecentlyAdded = async ( + page: number = 1, + limit: number = 20 + ): Promise> => { + if (page <= 0) throw new Error('Page number must be greater than 0'); + if (limit > 100) throw new Error('Limit must be less than or equal to 100'); + if (limit * (page - 1) >= 10000) throw new Error('not enough results'); + + try { + const res = await this.client.get( + `${this.apiUrl}/manga?includes[]=cover_art&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&order[createdAt]=desc&hasAvailableChapters=true&limit=${limit}&offset=${ + limit * (page - 1) + }` + ); + + if (res.data.result == 'ok') { + const results: ISearch = { + currentPage: page, + results: [], + }; + + for (const manga of res.data.data) { + results.results.push({ + id: manga.id, + title: Object.values(manga.attributes.title)[0] as string, + altTitles: manga.attributes.altTitles, + description: Object.values(manga.attributes.description)[0] as string, + status: manga.attributes.status, + releaseDate: manga.attributes.year, + contentRating: manga.attributes.contentRating, + lastVolume: manga.attributes.lastVolume, + lastChapter: manga.attributes.lastChapter, + }); + } + + return results; + } else { + throw new Error(res.data.message); + } + } catch (err) { + + throw new Error((err as Error).message); + } + }; + fetchLatestUpdates = async ( + page: number = 1, + limit: number = 20 + ): Promise> => { + if (page <= 0) throw new Error('Page number must be greater than 0'); + if (limit > 100) throw new Error('Limit must be less than or equal to 100'); + if (limit * (page - 1) >= 10000) throw new Error('not enough results'); + + try { + const res = await this.client.get( + `${this.apiUrl}/manga?order[latestUploadedChapter]=desc&limit=${limit}&offset=${ + limit * (page - 1) + }` + ); + + if (res.data.result == 'ok') { + const results: ISearch = { + currentPage: page, + results: [], + }; + + for (const manga of res.data.data) { + results.results.push({ + id: manga.id, + title: Object.values(manga.attributes.title)[0] as string, + altTitles: manga.attributes.altTitles, + description: Object.values(manga.attributes.description)[0] as string, + status: manga.attributes.status, + releaseDate: manga.attributes.year, + contentRating: manga.attributes.contentRating, + lastVolume: manga.attributes.lastVolume, + lastChapter: manga.attributes.lastChapter, + }); + } + + return results; + } else { + throw new Error(res.data.message); + } + } catch (err) { + throw new Error((err as Error).message); + } + }; + fetchPopular = async ( + page: number = 1, + limit: number = 20 + ): Promise> => { + if (page <= 0) throw new Error('Page number must be greater than 0'); + if (limit > 100) throw new Error('Limit must be less than or equal to 100'); + if (limit * (page - 1) >= 10000) throw new Error('not enough results'); + + try { + const res = await this.client.get( + `${this.apiUrl}/manga?includes[]=cover_art&includes[]=artist&includes[]=author&order[followedCount]=desc&contentRating[]=safe&contentRating[]=suggestive&hasAvailableChapters=true&limit=${limit}&offset=${ + limit * (page - 1) + }` + ); + + if (res.data.result == 'ok') { + const results: ISearch = { + currentPage: page, + results: [], + }; + + for (const manga of res.data.data) { + results.results.push({ + id: manga.id, + title: Object.values(manga.attributes.title)[0] as string, + altTitles: manga.attributes.altTitles, + description: Object.values(manga.attributes.description)[0] as string, + status: manga.attributes.status, + releaseDate: manga.attributes.year, + contentRating: manga.attributes.contentRating, + lastVolume: manga.attributes.lastVolume, + lastChapter: manga.attributes.lastChapter, + }); + } + + return results; + } else { + throw new Error(res.data.message); + } + } catch (err) { + throw new Error((err as Error).message); + } + }; + private fetchAllChapters = async ( + mangaId: string, + offset: number, + res?: AxiosResponse + ): Promise => { + if (res?.data?.offset + 96 >= res?.data?.total) { + return []; + } + + const response = await this.client.get( + `${this.apiUrl}/manga/${mangaId}/feed?offset=${offset}&limit=96&order[volume]=desc&order[chapter]=desc&translatedLanguage[]=en` + ); + + return [...response.data.data, ...(await this.fetchAllChapters(mangaId, offset + 96, response))]; + }; + + private fetchCoverImage = async (coverId: string): Promise => { + const { data } = await this.client.get(`${this.apiUrl}/cover/${coverId}`); + + const fileName = data.data.attributes.fileName; + + return fileName; + }; +} + +// (async () => { +// const md = new MangaDex(); +// const search = await md.search('solo leveling'); +// const manga = await md.fetchMangaInfo(search.results[0].id); +// const chapterPages = await md.fetchChapterPages(manga.chapters![0].id); +// console.log(chapterPages); +// })(); + +export default MangaDex; diff --git a/consumet.ts/src/providers/manga/mangahere.ts b/consumet.ts/src/providers/manga/mangahere.ts new file mode 100644 index 00000000..bc44c101 --- /dev/null +++ b/consumet.ts/src/providers/manga/mangahere.ts @@ -0,0 +1,204 @@ +import { load } from 'cheerio'; + +import { MangaParser, ISearch, IMangaInfo, IMangaResult, MediaStatus, IMangaChapterPage } from '../../models'; + +class MangaHere extends MangaParser { + override readonly name = 'MangaHere'; + protected override baseUrl = 'http://www.mangahere.cc'; + protected override logo = 'https://i.pinimg.com/564x/51/08/62/51086247ed16ff8abae2df0bb06448e4.jpg'; + protected override classPath = 'MANGA.MangaHere'; + + override fetchMangaInfo = async (mangaId: string): Promise => { + const mangaInfo: IMangaInfo = { + id: mangaId, + title: '', + }; + try { + const { data } = await this.client.get(`${this.baseUrl}/manga/${mangaId}`, { + headers: { + cookie: 'isAdult=1', + }, + }); + + const $ = load(data); + + mangaInfo.title = $('span.detail-info-right-title-font').text(); + mangaInfo.description = $('div.detail-info-right > p.fullcontent').text(); + mangaInfo.headers = { Referer: this.baseUrl }; + mangaInfo.image = $('div.detail-info-cover > img').attr('src'); + mangaInfo.genres = $('p.detail-info-right-tag-list > a') + .map((i, el) => $(el).attr('title')?.trim()) + .get(); + switch ($('span.detail-info-right-title-tip').text()) { + case 'Ongoing': + mangaInfo.status = MediaStatus.ONGOING; + break; + case 'Completed': + mangaInfo.status = MediaStatus.COMPLETED; + break; + default: + mangaInfo.status = MediaStatus.UNKNOWN; + break; + } + mangaInfo.rating = parseFloat($('span.detail-info-right-title-star > span').last().text()); + mangaInfo.authors = $('p.detail-info-right-say > a') + .map((i, el) => $(el).attr('title')) + .get(); + mangaInfo.chapters = $('ul.detail-main-list > li') + .map((i, el) => ({ + id: $(el).find('a').attr('href')?.split('/manga/')[1].slice(0, -7)!, + title: $(el).find('a > div > p.title3').text(), + releasedDate: $(el).find('a > div > p.title2').text().trim(), + })) + .get(); + + return mangaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchChapterPages = async (chapterId: string): Promise => { + const chapterPages: IMangaChapterPage[] = []; + const url = `${this.baseUrl}/manga/${chapterId}/1.html`; + + try { + const { data } = await this.client.get(url, { + headers: { + cookie: 'isAdult=1', + }, + }); + + const $ = load(data); + + const copyrightHandle = + $('p.detail-block-content').text().match('Dear user') || + $('p.detail-block-content').text().match('blocked'); + if (copyrightHandle) { + throw Error(copyrightHandle.input?.trim()); + } + + const bar = $('script[src*=chapter_bar]').data(); + const html = $.html(); + if (typeof bar !== 'undefined') { + const ss = html.indexOf('eval(function(p,a,c,k,e,d)'); + const se = html.indexOf('', ss); + const s = html.substring(ss, se).replace('eval', ''); + const ds = eval(s) as string; + + const urls = ds.split("['")[1].split("']")[0].split("','"); + + urls.map((url, i) => + chapterPages.push({ + page: i, + img: `https:${url}`, + headerForImage: { Referer: url }, + }) + ); + } else { + let sKey = this.extractKey(html); + const chapterIdsl = html.indexOf('chapterid'); + const chapterId = html.substring(chapterIdsl + 11, html.indexOf(';', chapterIdsl)).trim(); + + const chapterPagesElmnt = $('body > div:nth-child(6) > div > span').children('a'); + + const pages = parseInt(chapterPagesElmnt.last().prev().attr('data-page') ?? '0'); + + const pageBase = url.substring(0, url.lastIndexOf('/')); + + let resText = ''; + for (let i = 1; i <= pages; i++) { + const pageLink = `${pageBase}/chapterfun.ashx?cid=${chapterId}&page=${i}&key=${sKey}`; + + for (let j = 1; j <= 3; j++) { + const { data } = await this.client.get(pageLink, { + headers: { + Referer: url, + 'X-Requested-With': 'XMLHttpRequest', + cookie: 'isAdult=1', + }, + }); + + resText = data as string; + + if (resText) break; + else sKey = ''; + } + + const ds = eval(resText.replace('eval', '')); + + const baseLinksp = ds.indexOf('pix=') + 5; + const baseLinkes = ds.indexOf(';', baseLinksp) - 1; + const baseLink = ds.substring(baseLinksp, baseLinkes); + + const imageLinksp = ds.indexOf('pvalue=') + 9; + const imageLinkes = ds.indexOf('"', imageLinksp); + const imageLink = ds.substring(imageLinksp, imageLinkes); + + chapterPages.push({ + page: i - 1, + img: `https:${baseLink}${imageLink}`, + headerForImage: { Referer: url }, + }); + } + } + return chapterPages; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override search = async (query: string, page: number = 1): Promise> => { + const searchRes: ISearch = { + currentPage: page, + results: [], + }; + try { + const { data } = await this.client.get(`${this.baseUrl}/search?title=${query}&page=${page}`); + const $ = load(data); + + searchRes.hasNextPage = $('div.pager-list-left > a.active').next().text() !== '>'; + + searchRes.results = $('div.container > div > div > ul > li') + .map( + (i, el): IMangaResult => ({ + id: $(el).find('a').attr('href')?.split('/')[2]!, + title: $(el).find('p.manga-list-4-item-title > a').text(), + headerForImage: { Referer: this.baseUrl }, + image: $(el).find('a > img').attr('src'), + description: $(el).find('p').last().text(), + status: + $(el).find('p.manga-list-4-show-tag-list-2 > a').text() === 'Ongoing' + ? MediaStatus.ONGOING + : $(el).find('p.manga-list-4-show-tag-list-2 > a').text() === 'Completed' + ? MediaStatus.COMPLETED + : MediaStatus.UNKNOWN, + }) + ) + .get(); + return searchRes; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * credit: [tachiyomi-extensions](https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master/src/en/mangahere/src/eu/kanade/tachiyomi/extension/en/mangahere/Mangahere.kt) + */ + private extractKey = (html: string) => { + const skss = html.indexOf('eval(function(p,a,c,k,e,d)'); + const skse = html.indexOf('', skss); + const sks = html.substring(skss, skse).replace('eval', ''); + + const skds = eval(sks); + + const sksl = skds.indexOf("'"); + const skel = skds.indexOf(';'); + + const skrs = skds.substring(sksl, skel); + + return eval(skrs) as string; + }; +} + +export default MangaHere; diff --git a/consumet.ts/src/providers/manga/mangahost.ts b/consumet.ts/src/providers/manga/mangahost.ts new file mode 100644 index 00000000..c3101f66 --- /dev/null +++ b/consumet.ts/src/providers/manga/mangahost.ts @@ -0,0 +1,129 @@ +import { load } from 'cheerio'; + +import { + MangaParser, + ISearch, + IMangaInfo, + IMangaResult, + MediaStatus, + IMangaChapterPage, + IMangaChapter, +} from '../../models'; + +class MangaHost extends MangaParser { + override readonly name = 'MangaHost'; + protected override baseUrl = 'https://mangahosted.com'; + protected override logo = 'https://i.imgur.com/OVlhhsR.png'; + protected override classPath = 'MANGA.MangaHost'; + + override fetchMangaInfo = async (mangaId: string): Promise => { + const mangaInfo: IMangaInfo = { + id: mangaId, + title: '', + }; + try { + const { data } = await this.client.get(`${this.baseUrl}/manga/${mangaId}`); + const $ = load(data); + + mangaInfo.title = $('article.ejeCg > h1.title').text(); + mangaInfo.altTitles = $('article.ejeCg > h3.subtitle').text(); + mangaInfo.description = $('div.text > div.paragraph > p').text().replace(/\n/g, '').trim(); + mangaInfo.headerForImage = { Referer: this.baseUrl }; + mangaInfo.image = $('div.widget:nth-child(1) > picture > img').attr('src'); + mangaInfo.genres = $('article.ejeCg:nth-child(1) > div.tags > a.tag ') + .map((i, el) => $(el).text()) + .get(); + + switch ($('h3.subtitle > strong').text()) { + case 'Completo': + mangaInfo.status = MediaStatus.COMPLETED; + break; + case 'Ativo': + mangaInfo.status = MediaStatus.ONGOING; + break; + default: + mangaInfo.status = MediaStatus.UNKNOWN; + } + mangaInfo.views = parseInt( + $('div.classificacao-box-1 > div.text-block-3').text().replace(' views', '').replace(/,/g, '').trim() + ); + mangaInfo.authors = $( + 'div.w-col.w-col-6:nth-child(1) > ul.w-list-unstyled:nth-child(1) > li:nth-child(3) > div:nth-child(1)' + ) + .map((i, el) => $(el).text().replace('Autor: ', '').trim()) + .get(); + + mangaInfo.chapters = $('div.chapters > div.cap') + .map((i, el): IMangaChapter => { + const releasedDate = $(el) + .find('div.card > div.pop-content > small') + .text() + .match(/Adicionado em (.+?)"/); + + return { + id: `${$(el).find('a.btn-caps').text()}`, + title: $(el).find('a.btn-caps').attr('title')!, + views: null, + releasedDate: releasedDate ? releasedDate[1] : '', + }; + }) + .get(); + + return mangaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchChapterPages = async (mangaId: string, chapterId: string): Promise => { + try { + const url = `${this.baseUrl}/manga/${mangaId}/${chapterId}`; + const { data } = await this.client.get(url); + const $ = load(data); + + const pages = $('section#imageWrapper > div > div.read-slideshow > a > img') + .map( + (i, el): IMangaChapterPage => ({ + img: $(el).attr('src')!, + page: i, + title: `Page ${i + 1}`, + headerForImage: { Referer: this.baseUrl }, + }) + ) + .get(); + + return pages; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param query Search query + */ + override search = async (query: string): Promise> => { + try { + const { data } = await this.client.get(`${this.baseUrl}/find/${query.replace(/ /g, '+')}`); + const $ = load(data); + + const results = $('body > div.w-container > main > table > tbody > tr') + .map( + (i, row): IMangaResult => ({ + id: $(row).find('td > a.pull-left').attr('href')?.split('/')[4]!, + title: $(row).find('td > h4.entry-title > a').text(), + image: $(row).find('td > a.pull-left > picture > img.manga').attr('src'), + headerForImage: { Referer: this.baseUrl }, + }) + ) + .get(); + return { + results: results, + }; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default MangaHost; diff --git a/consumet.ts/src/providers/manga/mangakakalot.ts b/consumet.ts/src/providers/manga/mangakakalot.ts new file mode 100644 index 00000000..58f955e6 --- /dev/null +++ b/consumet.ts/src/providers/manga/mangakakalot.ts @@ -0,0 +1,186 @@ +import { load } from 'cheerio'; + +import { + MangaParser, + ISearch, + IMangaInfo, + IMangaResult, + MediaStatus, + IMangaChapterPage, + IMangaChapter, +} from '../../models'; + +class MangaKakalot extends MangaParser { + override readonly name = 'MangaKakalot'; + protected override baseUrl = 'https://mangakakalot.com'; + protected override logo = 'https://techbigs.com/uploads/2022/1/mangakakalot-apkoptimized.jpg'; + protected override classPath = 'MANGA.MangaKakalot'; + + override fetchMangaInfo = async (mangaId: string): Promise => { + const mangaInfo: IMangaInfo = { + id: mangaId, + title: '', + }; + const url = mangaId.includes('read') ? this.baseUrl : 'https://readmanganato.com'; + try { + const { data } = await this.client.get(`${url}/${mangaId}`); + const $ = load(data); + + if (url.includes('mangakakalot')) { + mangaInfo.title = $('div.manga-info-top > ul > li:nth-child(1) > h1').text(); + mangaInfo.altTitles = $('div.manga-info-top > ul > li:nth-child(1) > h2') + .text() + .replace('Alternative :', '') + .split(';'); + mangaInfo.description = $('#noidungm') + .text() + .replace(`${mangaInfo.title} summary:`, '') + .replace(/\n/g, '') + .trim(); + mangaInfo.headerForImage = { Referer: this.baseUrl }; + mangaInfo.image = $('div.manga-info-top > div > img').attr('src'); + mangaInfo.genres = $('div.manga-info-top > ul > li:nth-child(7) > a') + .map((i, el) => $(el).text()) + .get(); + + switch ($('div.manga-info-top > ul > li:nth-child(3)').text().replace('Status :', '').trim()) { + case 'Completed': + mangaInfo.status = MediaStatus.COMPLETED; + break; + case 'Ongoing': + mangaInfo.status = MediaStatus.ONGOING; + break; + default: + mangaInfo.status = MediaStatus.UNKNOWN; + } + mangaInfo.views = parseInt( + $('div.manga-info-top > ul > li:nth-child(6)') + .text() + .replace('View : ', '') + .replace(/,/g, '') + .trim() + ); + mangaInfo.authors = $('div.manga-info-top > ul > li:nth-child(2) > a') + .map((i, el) => $(el).text()) + .get(); + + mangaInfo.chapters = $('div.chapter-list > div.row') + .map( + (i, el): IMangaChapter => ({ + id: $(el).find('span > a').attr('href')?.split('chapter/')[1]!, + title: $(el).find('span > a').text(), + views: parseInt($(el).find('span:nth-child(2)').text()!.replace(/,/g, '').trim()), + releasedDate: $(el).find('span:nth-child(3)').attr('title'), + }) + ) + .get(); + } else { + mangaInfo.title = $(' div.panel-story-info > div.story-info-right > h1').text(); + mangaInfo.altTitles = $( + 'div.story-info-right > table > tbody > tr:nth-child(1) > td.table-value > h2' + ) + .text() + .split(';'); + mangaInfo.description = $('#panel-story-info-description') + .text() + .replace(`Description :`, '') + .replace(/\n/g, '') + .trim(); + mangaInfo.headerForImage = { Referer: 'https://readmanganato.com' }; + mangaInfo.image = $('div.story-info-left > span.info-image > img').attr('src'); + mangaInfo.genres = $('div.story-info-right > table > tbody > tr:nth-child(4) > td.table-value > a') + .map((i, el) => $(el).text()) + .get(); + + switch ($('div.story-info-right > table > tbody > tr:nth-child(3) > td.table-value').text().trim()) { + case 'Completed': + mangaInfo.status = MediaStatus.COMPLETED; + break; + case 'Ongoing': + mangaInfo.status = MediaStatus.ONGOING; + break; + default: + mangaInfo.status = MediaStatus.UNKNOWN; + } + mangaInfo.views = parseInt( + $('div.story-info-right > div > p:nth-child(2) > span.stre-value').text().replace(/,/g, '').trim() + ); + mangaInfo.authors = $('div.story-info-right > table > tbody > tr:nth-child(2) > td.table-value > a') + .map((i, el) => $(el).text()) + .get(); + + mangaInfo.chapters = $('div.container-main-left > div.panel-story-chapter-list > ul > li') + .map( + (i, el): IMangaChapter => ({ + id: $(el).find('a').attr('href')?.split('.com/')[1]! + '$$READMANGANATO', + title: $(el).find('a').text(), + views: parseInt($(el).find('span.chapter-view.text-nowrap').text()!.replace(/,/g, '').trim()), + releasedDate: $(el).find('span.chapter-time.text-nowrap').attr('title'), + }) + ) + .get(); + } + + return mangaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchChapterPages = async (chapterId: string): Promise => { + try { + const url = !chapterId.includes('$$READMANGANATO') + ? `${this.baseUrl}/chapter/${chapterId}` + : `https://readmanganato.com/${chapterId.replace('$$READMANGANATO', '')}`; + const { data } = await this.client.get(url); + const $ = load(data); + + const pages = $('div.container-chapter-reader > img') + .map( + (i, el): IMangaChapterPage => ({ + img: $(el).attr('src')!, + page: i, + title: $(el) + .attr('alt') + ?.replace(/(- Mangakakalot.com)|(- MangaNato.com)/g, ' ') + .trim()!, + headerForImage: { Referer: this.baseUrl }, + }) + ) + .get(); + + return pages; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param query Search query + */ + override search = async (query: string): Promise> => { + try { + const { data } = await this.client.get(`${this.baseUrl}/search/story/${query.replace(/ /g, '_')}`); + const $ = load(data); + + const results = $('div.daily-update > div > div') + .map( + (i, el): IMangaResult => ({ + id: $(el).find('div > h3 > a').attr('href')?.split('/')[3]!, + title: $(el).find('div > h3 > a').text(), + image: $(el).find('a > img').attr('src'), + headerForImage: { Referer: this.baseUrl }, + }) + ) + .get(); + return { + results: results, + }; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default MangaKakalot; diff --git a/consumet.ts/src/providers/manga/mangapark.ts b/consumet.ts/src/providers/manga/mangapark.ts new file mode 100644 index 00000000..7705ac92 --- /dev/null +++ b/consumet.ts/src/providers/manga/mangapark.ts @@ -0,0 +1,114 @@ +import { load } from 'cheerio'; + +import { + MangaParser, + ISearch, + IMangaInfo, + IMangaResult, + IMangaChapterPage, + IMangaChapter, +} from '../../models'; + +class Mangapark extends MangaParser { + override readonly name = 'Mangapark'; + protected override baseUrl = 'https://v2.mangapark.net'; + protected override logo = + 'https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/icon/tachiyomi-en.mangapark-v1.3.23.png'; + protected override classPath = 'MANGA.Mangapark'; + + override fetchMangaInfo = async (mangaId: string, ...args: any): Promise => { + const mangaInfo: IMangaInfo = { id: mangaId, title: '' }; + const url = `${this.baseUrl}/manga/${mangaId}`; + + try { + const { data } = await this.client.get(url); + const $ = load(data); + + mangaInfo.title = $('div.pb-1.mb-2.line-b-f.hd h2 a').text(); + mangaInfo.image = $('img.w-100').attr('src'); + mangaInfo.description = $('.limit-html.summary').text(); + + mangaInfo.chapters = $('.py-1.item') + .get() + .map( + (chapter): IMangaChapter => ({ + /* + See below: if inside of [], removed; if inside of {}, chapterId. + [https://v2.mangapark.net/manga/] {bungou-stray-dogs/i2573185} [/c87/1] + */ + id: + `${mangaId}/` + + $(chapter) + .find('a.ml-1.visited.ch') + .attr('href')! + .split(`/manga/${mangaId}/`) + .toString() + .replace(',', '') + .split('/')[0], + // Get ch.xyz + chapter title, trim l/t whitespace on latter, concatenate. + title: + $(chapter).find('.ml-1.visited.ch').text() + + $(chapter).find('div.d-none.d-md-flex.align-items-center.ml-0.ml-md-1.txt').text().trim(), + // Get 'x time ago' and remove l/t whitespace. + releaseDate: $(chapter).find('span.time').text().trim(), + }) + ); + + return mangaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchChapterPages = async (chapterId: string, ...args: any): Promise => { + const regex = /var _load_pages = \[(.*)\]/gm; + + // Fetches manga with all pages; no /cx/y after. + const url = `${this.baseUrl}/manga/${chapterId}`; + + try { + const { data } = await this.client.get(url); + + const varLoadPages: string = data.match(regex)[0]; + const loadPagesJson = JSON.parse(varLoadPages.replace('var _load_pages = ', '')); + + const pages: IMangaChapterPage[] = loadPagesJson.map((page: { n: string; u: string }) => { + return { page: page.n, img: page.u }; + }); + + return pages; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override search = async ( + query: string, + page: number = 1, + ...args: any[] + ): Promise> => { + const url = `${this.baseUrl}/search?q=${query}&page=${page}`; + + try { + const { data } = await this.client.get(url); + const $ = load(data); + + const results: IMangaResult[] = $('.item') + .get() + .map(item => { + const cover = $(item).find('.cover'); + return { + id: `${cover.attr('href')?.replace('/manga/', '')}`, + title: `${cover.attr('title')}`, + image: `${$(cover).find('img').attr('src')}}`, + }; + }); + + return { results: results }; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default Mangapark; diff --git a/consumet.ts/src/providers/manga/mangapill.ts b/consumet.ts/src/providers/manga/mangapill.ts new file mode 100644 index 00000000..e880fd9a --- /dev/null +++ b/consumet.ts/src/providers/manga/mangapill.ts @@ -0,0 +1,118 @@ +import { load } from 'cheerio'; + +import { + MangaParser, + ISearch, + IMangaInfo, + IMangaResult, + IMangaChapterPage, + IMangaChapter, +} from '../../models'; + +class MangaPill extends MangaParser { + override readonly name = 'MangaPill'; + protected override baseUrl = 'https://mangapill.com'; + protected override logo = + 'https://scontent-man2-1.xx.fbcdn.net/v/t39.30808-6/300819578_399903675586699_2357525969702348451_n.png?_nc_cat=100&ccb=1-7&_nc_sid=09cbfe&_nc_ohc=Md2cQ4wRNWwAX-_U0fz&_nc_ht=scontent-man2-1.xx&oh=00_AfCJjAYDk9bsndz8uyNG-GdFIYcPvdIzbHnetHGzf1pVSw&oe=63BDD131'; + protected override classPath = 'MANGA.MangaPill'; + + /** + * + * @param query Search query + */ + override search = async (query: string): Promise> => { + try { + const { data } = await this.client.get(`${this.baseUrl}/search?q=${encodeURIComponent(query)}`); + const $ = load(data); + + const results = $('div.container div.my-3.justify-end > div') + .map( + (i, el): IMangaResult => ({ + id: $(el).find('a').attr('href')?.split('/manga/')[1]!, + title: $(el).find('div > a > div').text().trim(), + image: $(el).find('a img').attr('data-src'), + }) + ) + .get(); + + return { + results: results, + }; + } catch (err) { + // console.log(err); + throw new Error((err as Error).message); + } + }; + + override fetchMangaInfo = async (mangaId: string): Promise => { + const mangaInfo: IMangaInfo = { + id: mangaId, + title: '', + }; + try { + const { data } = await this.client.get(`${this.baseUrl}/manga/${mangaId}`); + const $ = load(data); + + mangaInfo.title = $('div.container div.my-3 div.flex-col div.mb-3 h1').text().trim(); + mangaInfo.description = $('div.container div.my-3 div.flex-col p.text--secondary') + .text() + .split('\n') + .join(' ')!; + mangaInfo.releaseDate = $('div.container div.my-3 div.flex-col div.gap-3.mb-3 div:contains("Year")') + .text() + .split('Year\n')[1] + .trim(); + mangaInfo.genres = $('div.container div.my-3 div.flex-col div.mb-3:contains("Genres")') + .text() + .split('\n') + .filter((genre: string) => genre !== 'Genres' && genre !== '') + .map(genre => genre.trim()); + + mangaInfo.chapters = $('div.container div.border-border div#chapters div.grid-cols-1 a') + .map( + (i, el): IMangaChapter => ({ + id: $(el).attr('href')?.split('/chapters/')[1]!, + title: $(el).text().trim(), + chapter: $(el).text().split('Chapter ')[1], + }) + ) + .get(); + + return mangaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchChapterPages = async (chapterId: string): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/chapters/${chapterId}`); + const $ = load(data); + + const chapterSelector = $('chapter-page'); + + const pages = chapterSelector + .map( + (i, el): IMangaChapterPage => ({ + img: $(el).find('div picture img').attr('data-src')!, + page: parseFloat($(el).find(`div[data-summary] > div`).text().split('page ')[1]), + }) + ) + .get(); + + return pages; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +// (async () => { +// const manga = new MangaPill(); +// const search = await manga.search('one piece'); +// const info = await manga.fetchMangaInfo(search.results[1].id); +// const pages = await manga.fetchChapterPages(info.chapters![0].id); +// console.log(pages); +// })(); + +export default MangaPill; diff --git a/consumet.ts/src/providers/manga/mangareader.ts b/consumet.ts/src/providers/manga/mangareader.ts new file mode 100644 index 00000000..0223494d --- /dev/null +++ b/consumet.ts/src/providers/manga/mangareader.ts @@ -0,0 +1,127 @@ +import { load } from 'cheerio'; + +import { + MangaParser, + ISearch, + IMangaInfo, + IMangaResult, + IMangaChapterPage, + IMangaChapter, +} from '../../models'; + +class MangaReader extends MangaParser { + override readonly name = 'MangaReader'; + protected override baseUrl = 'https://mangareader.to'; + protected override logo = 'https://pbs.twimg.com/profile_images/1437311892905545728/TO0hFfUr_400x400.jpg'; + protected override classPath = 'MANGA.MangaReader'; + + /** + * + * @param query Search query + */ + override search = async (query: string): Promise> => { + try { + const { data } = await this.client.get(`${this.baseUrl}/search?keyword=${query}`); + const $ = load(data); + + const results = $('div.manga_list-sbs div.mls-wrap div.item') + .map( + (i, el): IMangaResult => ({ + id: $(el).find('a.manga-poster').attr('href')?.split('/')[1]!, + title: $(el).find('div.manga-detail h3.manga-name a').text().trim(), + image: $(el).find('a.manga-poster img').attr('src'), + genres: $(el) + .find(`div.manga-detail div.fd-infor span > a`) + .map((i, genre) => $(genre).text()) + .get(), + }) + ) + .get(); + + return { + results: results, + }; + } catch (err) { + // console.log(err); + throw new Error((err as Error).message); + } + }; + + override fetchMangaInfo = async (mangaId: string): Promise => { + const mangaInfo: IMangaInfo = { + id: mangaId, + title: '', + }; + try { + const { data } = await this.client.get(`${this.baseUrl}/${mangaId}`); + const $ = load(data); + + const container = $('div.container'); + + mangaInfo.title = container.find('div.anisc-detail h2.manga-name').text().trim(); + mangaInfo.image = container.find('img.manga-poster-img').attr('src'); + mangaInfo.description = $('div.modal-body div.description-modal').text().split('\n').join(' ').trim(); + mangaInfo.genres = container + .find('div.sort-desc div.genres a') + .map((i, genre) => $(genre).text().trim()) + .get(); + + mangaInfo.chapters = container + .find(`div.chapters-list-ul ul li`) + .map( + (i, el): IMangaChapter => ({ + id: $(el).find('a').attr('href')?.split('/read/')[1]!, + title: $(el).find('a').attr('title')!.trim(), + chapter: $(el).find('a span.name').text().split('Chapter ')[1]!.split(':')[0], + }) + ) + .get(); + + return mangaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchChapterPages = async (chapterId: string): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/read/${chapterId}`); + const $ = load(data); + + const readingId = $('div#wrapper').attr('data-reading-id'); + + if (!readingId) { + throw new Error('Unable to find pages'); + } + + const ajaxURL = `https://mangareader.to/ajax/image/list/chap/${readingId}?mode=vertical&quality=high`; + const { data: pagesData } = await this.client.get(ajaxURL); + const $PagesHTML = load(pagesData.html); + + const pagesSelector = $PagesHTML('div#main-wrapper div.container-reader-chapter div.iv-card'); + + const pages = pagesSelector + .map( + (i, el): IMangaChapterPage => ({ + img: $(el).attr('data-url')!.replace('&', '&'), + page: i + 1, + }) + ) + .get(); + + return pages; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +// (async () => { +// const manga = new MangaReader(); +// const search = await manga.search('one piece'); +// const info = await manga.fetchMangaInfo(search.results[0].id); +// const pages = await manga.fetchChapterPages(info.chapters![0].id); +// console.log(pages); +// })(); + +export default MangaReader; diff --git a/consumet.ts/src/providers/manga/mangasee123.ts b/consumet.ts/src/providers/manga/mangasee123.ts new file mode 100644 index 00000000..991b5e9a --- /dev/null +++ b/consumet.ts/src/providers/manga/mangasee123.ts @@ -0,0 +1,182 @@ +import { load } from 'cheerio'; +import { isText } from 'domhandler'; + +import { + MangaParser, + ISearch, + IMangaInfo, + IMangaResult, + IMangaChapterPage, + IMangaChapter, +} from '../../models'; + +class Mangasee123 extends MangaParser { + override readonly name = 'MangaSee'; + protected override baseUrl = 'https://mangasee123.com'; + protected override logo = + 'https://scontent.fman4-1.fna.fbcdn.net/v/t1.6435-1/80033336_1830005343810810_419412485691408384_n.png?stp=dst-png_p148x148&_nc_cat=104&ccb=1-7&_nc_sid=1eb0c7&_nc_ohc=XpeoABDI-sEAX-5hLFV&_nc_ht=scontent.fman4-1.fna&oh=00_AT9nIRz5vPiNqqzNpSg2bJymX22rZ1JumYTKBqg_cD0Alg&oe=6317290E'; + protected override classPath = 'MANGA.Mangasee123'; + + // private readonly sgProxy = 'https://cors.consumet.stream'; + + override fetchMangaInfo = async (mangaId: string, ...args: any): Promise => { + const mangaInfo: IMangaInfo = { + id: mangaId, + title: '', + }; + const url = `${this.baseUrl}/manga`; + + try { + const { data } = await this.client.get(`${url}/${mangaId}`); + const $ = load(data); + + const schemaScript = $('body > script:nth-child(15)').get()[0].children[0]; + if (isText(schemaScript)) { + const mainEntity = JSON.parse(schemaScript.data)['mainEntity']; + + mangaInfo.title = mainEntity['name']; + mangaInfo.altTitles = mainEntity['alternateName']; + mangaInfo.genres = mainEntity['genre']; + } + + mangaInfo.image = $('img.bottom-5').attr('src'); + mangaInfo.headerForImage = { Referer: this.baseUrl }; + mangaInfo.description = $('.top-5 .Content').text(); + + const contentScript = $('body > script:nth-child(16)').get()[0].children[0]; + if (isText(contentScript)) { + const chaptersData = this.processScriptTagVariable(contentScript.data, 'vm.Chapters = '); + + mangaInfo.chapters = chaptersData.map( + (i: { [x: string]: any }): IMangaChapter => ({ + id: `${mangaId}-chapter-${this.processChapterNumber(i['Chapter'])}`, + title: `${i['ChapterName'] ?? `Chapter ${this.processChapterNumber(i['Chapter'])}`}`, + releaseDate: i['Date'], + }) + ); + } + + return mangaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchChapterPages = async (chapterId: string, ...args: any): Promise => { + const images: string[] = []; + const url = `${this.baseUrl}/read-online/${chapterId}-page-1.html`; + + try { + const { data } = await this.client.get(`${url}`); + const $ = load(data); + + const chapterScript = $('body > script:nth-child(19)').get()[0].children[0]; + if (isText(chapterScript)) { + const curChapter = this.processScriptTagVariable(chapterScript.data, 'vm.CurChapter = '); + const imageHost = this.processScriptTagVariable(chapterScript.data, 'vm.CurPathName = '); + const curChapterLength = Number(curChapter['Page']); + + for (let i = 0; i < curChapterLength; i++) { + const chapter = this.processChapterForImageUrl(chapterId.replace(/[^0-9.]/g, '')); + const page = `${i + 1}`.padStart(3, '0'); + const mangaId = chapterId.split('-chapter-', 1)[0]; + const imagePath = `https://${imageHost}/manga/${mangaId}/${chapter}-${page}.png`; + + images.push(imagePath); + } + } + + const pages = images.map( + (image, i): IMangaChapterPage => ({ + page: i + 1, + img: image, + headerForImage: { Referer: this.baseUrl }, + }) + ); + + return pages; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override search = async (query: string, ...args: any[]): Promise> => { + const matches = []; + const sanitizedQuery = query.replace(/\s/g, '').toLowerCase(); + + try { + const { data } = await this.client.get(`https://mangasee123.com/_search.php`); + + for (const i in data) { + const sanitizedAlts: string[] = []; + + const item = data[i]; + const altTitles: string[] = data[i]['a']; + + for (const alt of altTitles) { + sanitizedAlts.push(alt.replace(/\s/g, '').toLowerCase()); + } + + if ( + item['s'].replace(/\s/g, '').toLowerCase().includes(sanitizedQuery) || + sanitizedAlts.includes(sanitizedQuery) + ) { + matches.push(item); + } + } + + const results = matches.map( + (val): IMangaResult => ({ + id: val['i'], + title: val['s'], + altTitles: val['a'], + image: `https://temp.compsci88.com/cover/${val['i']}.jpg`, + headerForImage: { Referer: this.baseUrl }, + }) + ); + + return { results: results }; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + private processScriptTagVariable = (script: string, variable: string) => { + const chopFront = script.substring(script.search(variable) + variable.length, script.length); + const chapters = JSON.parse(chopFront.substring(0, chopFront.search(';'))); + + return chapters; + }; + + // e.g. 102055 => [1]--[0205]--[5] + // ? chap dec + private processChapterNumber = (chapter: string): string => { + const decimal = chapter.substring(chapter.length - 1, chapter.length); + chapter = chapter.replace(chapter[0], '').slice(0, -1); + if (decimal == '0') return `${+chapter}`; + + if (chapter.startsWith('0')) chapter = chapter.replace(chapter[0], ''); + + return `${+chapter}.${decimal}`; + }; + + private processChapterForImageUrl = (chapter: string): string => { + if (!chapter.includes('.')) return chapter.padStart(4, '0'); + + const values = chapter.split('.'); + const pad = values[0].padStart(4, '0'); + + return `${pad}.${values[1]}`; + }; +} + +// (async () => { +// const manga = new Mangasee123(); +// const mediaInfo = await manga.search('oyasumi'); +// const mangaInfo = await manga.fetchMangaInfo(mediaInfo.results[0].id); +// const chapterPages = await manga.fetchChapterPages(mangaInfo.chapters![0].id); +// console.log(chapterPages); +// console.log(mediaInfo, mangaInfo); +// })(); + +export default Mangasee123; diff --git a/consumet.ts/src/providers/meta/anilist.ts b/consumet.ts/src/providers/meta/anilist.ts new file mode 100644 index 00000000..66567ce0 --- /dev/null +++ b/consumet.ts/src/providers/meta/anilist.ts @@ -0,0 +1,2338 @@ +import axios, { AxiosAdapter } from 'axios'; +import FormData from 'form-data'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + MediaStatus, + IAnimeResult, + ISource, + IAnimeEpisode, + SubOrSub, + IEpisodeServer, + Genres, + MangaParser, + IMangaChapterPage, + IMangaInfo, + IMangaResult, + IMangaChapter, + ProxyConfig, + MediaFormat, + ITitle, +} from '../../models'; +import { + anilistSearchQuery, + anilistMediaDetailQuery, + kitsuSearchQuery, + anilistTrendingQuery, + anilistPopularQuery, + anilistAiringScheduleQuery, + anilistGenresQuery, + anilistAdvancedQuery, + anilistSiteStatisticsQuery, + anilistCharacterQuery, + range, + getDays, + days, + capitalizeFirstLetter, + isJson, +} from '../../utils'; +import Gogoanime from '../../providers/anime/gogoanime'; +import Anify from '../anime/anify'; +import Zoro from '../anime/zoro'; +import Mangasee123 from '../manga/mangasee123'; +import Crunchyroll from '../anime/crunchyroll'; +import Bilibili from '../anime/bilibili'; +import NineAnime from '../anime/9anime'; +import { USER_AGENT, compareTwoStrings, getHashFromImage, remap } from '../../utils/utils'; + +class Anilist extends AnimeParser { + override readonly name = 'Anilist'; + protected override baseUrl = 'https://anilist.co'; + protected override logo = 'https://upload.wikimedia.org/wikipedia/commons/6/61/AniList_logo.svg'; + protected override classPath = 'META.Anilist'; + + private readonly anilistGraphqlUrl = 'https://graphql.anilist.co'; + private readonly kitsuGraphqlUrl = 'https://kitsu.io/api/graphql'; + private readonly malSyncUrl = 'https://api.malsync.moe'; + private readonly anifyUrl = 'https://api.anify.tv'; + private readonly simklUrl = 'https://api.simkl.com'; + private readonly simklClientKey = '3ffc0bbac820972d56438b7904e02973529b96601e7abf2b241b5bdab1fdf60f'; + provider: AnimeParser; + + /** + * This class maps anilist to kitsu with any other anime provider. + * kitsu is used for episode images, titles and description. + * @param provider anime provider (optional) default: Gogoanime + * @param proxyConfig proxy config (optional) + * @param adapter axios adapter (optional) + */ + constructor( + provider?: AnimeParser, + public proxyConfig?: ProxyConfig, + adapter?: AxiosAdapter, + customBaseURL?: string + ) { + super(proxyConfig, adapter); + this.provider = provider || new Gogoanime(customBaseURL, proxyConfig); + } + + /** + * @param query Search query + * @param page Page number (optional) + * @param perPage Number of results per page (optional) (default: 15) (max: 50) + */ + override search = async ( + query: string, + page: number = 1, + perPage: number = 15 + ): Promise> => { + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistSearchQuery(query, page, perPage), + }; + + try { + let { data, status } = await this.client.post(this.anilistGraphqlUrl, options, { + validateStatus: () => true, + }); + + if (status >= 500 || status == 429) data = await new Anify().rawSearch(query, page); + + const res: ISearch = { + currentPage: data.data!.Page?.pageInfo?.currentPage ?? data.meta?.currentPage, + hasNextPage: data.data!.Page?.pageInfo?.hasNextPage ?? data.meta?.currentPage != data.meta?.lastPage, + results: + data.data?.Page?.media?.map((item: any) => ({ + id: item.id.toString(), + malId: item.idMal, + title: + { + romaji: item.title.romaji, + english: item.title.english, + native: item.title.native, + userPreferred: item.title.userPreferred, + } || item.title.romaji, + status: + item.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + image: item.coverImage?.extraLarge ?? item.coverImage?.large ?? item.coverImage?.medium, + imageHash: getHashFromImage( + item.coverImage?.extraLarge ?? item.coverImage?.large ?? item.coverImage?.medium + ), + cover: item.bannerImage, + coverHash: getHashFromImage(item.bannerImage), + popularity: item.popularity, + description: item.description, + rating: item.averageScore, + genres: item.genres, + color: item.coverImage?.color, + totalEpisodes: item.episodes ?? item.nextAiringEpisode?.episode - 1, + currentEpisodeCount: item?.nextAiringEpisode + ? item?.nextAiringEpisode?.episode - 1 + : item.episodes, + type: item.format, + releaseDate: item.seasonYear, + })) ?? + data.data.map((item: any) => ({ + id: item.anilistId.toString(), + malId: item.mappings!['mal']!, + title: item.title, + status: + item.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + image: item.coverImage ?? item.bannerImage, + imageHash: getHashFromImage(item.coverImage ?? item.bannerImage), + cover: item.bannerImage, + coverHash: getHashFromImage(item.bannerImage), + popularity: item.popularity, + description: item.description, + rating: item.averageScore, + genres: item.genre, + color: item.color, + totalEpisodes: item.currentEpisode, + currentEpisodeCount: item?.nextAiringEpisode + ? item?.nextAiringEpisode?.episode - 1 + : item.currentEpisode, + type: item.format, + releaseDate: item.year, + })), + }; + + return res; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param query Search query (optional) + * @param type Media type (optional) (default: `ANIME`) (options: `ANIME`, `MANGA`) + * @param page Page number (optional) + * @param perPage Number of results per page (optional) (default: `20`) (max: `50`) + * @param format Format (optional) (options: `TV`, `TV_SHORT`, `MOVIE`, `SPECIAL`, `OVA`, `ONA`, `MUSIC`) + * @param sort Sort (optional) (Default: `[POPULARITY_DESC, SCORE_DESC]`) (options: `POPULARITY_DESC`, `POPULARITY`, `TRENDING_DESC`, `TRENDING`, `UPDATED_AT_DESC`, `UPDATED_AT`, `START_DATE_DESC`, `START_DATE`, `END_DATE_DESC`, `END_DATE`, `FAVOURITES_DESC`, `FAVOURITES`, `SCORE_DESC`, `SCORE`, `TITLE_ROMAJI_DESC`, `TITLE_ROMAJI`, `TITLE_ENGLISH_DESC`, `TITLE_ENGLISH`, `TITLE_NATIVE_DESC`, `TITLE_NATIVE`, `EPISODES_DESC`, `EPISODES`, `ID`, `ID_DESC`) + * @param genres Genres (optional) (options: `Action`, `Adventure`, `Cars`, `Comedy`, `Drama`, `Fantasy`, `Horror`, `Mahou Shoujo`, `Mecha`, `Music`, `Mystery`, `Psychological`, `Romance`, `Sci-Fi`, `Slice of Life`, `Sports`, `Supernatural`, `Thriller`) + * @param id anilist Id (optional) + * @param year Year (optional) e.g. `2022` + * @param status Status (optional) (options: `RELEASING`, `FINISHED`, `NOT_YET_RELEASED`, `CANCELLED`, `HIATUS`) + * @param season Season (optional) (options: `WINTER`, `SPRING`, `SUMMER`, `FALL`) + */ + advancedSearch = async ( + query?: string, + type: string = 'ANIME', + page: number = 1, + perPage: number = 20, + format?: string, + sort?: string[], + genres?: Genres[] | string[], + id?: string | number, + year?: number, + status?: string, + season?: string + ): Promise> => { + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistAdvancedQuery(), + variables: { + search: query, + type: type, + page: page, + size: perPage, + format: format, + sort: sort, + genres: genres, + id: id, + year: year ? `${year}%` : undefined, + status: status, + season: season, + }, + }; + + if (genres) { + genres.forEach(genre => { + if (!Object.values(Genres).includes(genre as Genres)) { + throw new Error(`genre ${genre} is not valid`); + } + }); + } + + try { + let { data, status } = await this.client.post(this.anilistGraphqlUrl, options, { + validateStatus: () => true, + }); + + if (status >= 500 && !query) throw new Error('No results found'); + if (status >= 500) data = await new Anify().rawSearch(query!, page); + + const res: ISearch = { + currentPage: data.data?.Page?.pageInfo?.currentPage ?? data.meta?.currentPage, + hasNextPage: data.data?.Page?.pageInfo?.hasNextPage ?? data.meta?.currentPage != data.meta?.lastPage, + totalPages: data.data?.Page?.pageInfo?.lastPage, + totalResults: data.data?.Page?.pageInfo?.total, + results: [], + }; + + res.results.push( + ...(data.data?.Page?.media?.map((item: any) => ({ + id: item.id.toString(), + malId: item.idMal, + title: + { + romaji: item.title.romaji, + english: item.title.english, + native: item.title.native, + userPreferred: item.title.userPreferred, + } || item.title.romaji, + status: + item.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + image: item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium, + imageHash: getHashFromImage( + item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium + ), + cover: item.bannerImage, + coverHash: getHashFromImage(item.bannerImage), + popularity: item.popularity, + totalEpisodes: item.episodes ?? item.nextAiringEpisode?.episode - 1, + currentEpisode: item.nextAiringEpisode?.episode - 1 ?? item.episodes, + countryOfOrigin: item.countryOfOrigin, + description: item.description, + genres: item.genres, + rating: item.averageScore, + color: item.coverImage?.color, + type: item.format, + releaseDate: item.seasonYear, + })) ?? + data.data?.map((item: any) => ({ + id: item.anilistId.toString(), + malId: item.mappings['mal'], + title: item.title, + status: + item.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + image: item.coverImage ?? item.bannerImage, + imageHash: getHashFromImage(item.coverImage ?? item.bannerImage), + cover: item.bannerImage, + coverHash: getHashFromImage(item.bannerImage), + popularity: item.popularity, + description: item.description, + rating: item.averageScore, + genres: item.genre, + color: item.color, + totalEpisodes: item.currentEpisode, + type: item.format, + releaseDate: item.year, + }))) + ); + + return res; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param id Anime id + * @param dub to get dubbed episodes (optional) set to `true` to get dubbed episodes. **ONLY WORKS FOR GOGOANIME** + * @param fetchFiller to get filler boolean on the episode object (optional) set to `true` to get filler boolean on the episode object. + */ + override fetchAnimeInfo = async ( + id: string, + dub: boolean = false, + fetchFiller: boolean = false + ): Promise => { + const animeInfo: IAnimeInfo = { + id: id, + title: '', + }; + + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistMediaDetailQuery(id), + }; + + let fillerEpisodes: { number: string; 'filler-bool': boolean }[]; + try { + let { data, status } = await this.client.post(this.anilistGraphqlUrl, options, { + validateStatus: () => true, + }); + + if (status == 404) + throw new Error('Media not found. Perhaps the id is invalid or the anime is not in anilist'); + if (status == 429) throw new Error('You have been ratelimited by anilist. Please try again later'); + // if (status >= 500) throw new Error('Anilist seems to be down. Please try again later'); + if (status != 200 && status < 429) + throw Error('Media not found. If the problem persists, please contact the developer'); + if (status >= 500) data = await new Anify().fetchAnimeInfoByIdRaw(id); + + animeInfo.malId = data.data?.Media?.idMal ?? data?.mappings?.mal; + animeInfo.title = data.data.Media + ? { + romaji: data.data.Media.title.romaji, + english: data.data.Media.title.english, + native: data.data.Media.title.native, + userPreferred: data.data.Media.title.userPreferred, + } + : (data.data.title as ITitle); + + animeInfo.synonyms = data.data?.Media?.synonyms ?? data?.synonyms; + animeInfo.isLicensed = data.data?.Media?.isLicensed ?? undefined; + animeInfo.isAdult = data.data?.Media?.isAdult ?? undefined; + animeInfo.countryOfOrigin = data.data?.Media?.countryOfOrigin ?? undefined; + + if (data.data?.Media?.trailer?.id) { + animeInfo.trailer = { + id: data.data.Media.trailer.id, + site: data.data.Media.trailer?.site, + thumbnail: data.data.Media.trailer?.thumbnail, + thumbnailHash: getHashFromImage(data.data.Media.trailer?.thumbnail), + }; + } + animeInfo.image = + data.data?.Media?.coverImage?.extraLarge ?? + data.data?.Media?.coverImage?.large ?? + data.data?.Media?.coverImage?.medium ?? + data.coverImage ?? + data.bannerImage; + + animeInfo.imageHash = getHashFromImage( + data.data?.Media?.coverImage?.extraLarge ?? + data.data?.Media?.coverImage?.large ?? + data.data?.Media?.coverImage?.medium ?? + data.coverImage ?? + data.bannerImage + ); + + animeInfo.popularity = data.data?.Media?.popularity ?? data?.popularity; + animeInfo.color = data.data?.Media?.coverImage?.color ?? data?.color; + animeInfo.cover = data.data?.Media?.bannerImage ?? data?.bannerImage ?? animeInfo.image; + animeInfo.coverHash = getHashFromImage( + data.data?.Media?.bannerImage ?? data?.bannerImage ?? animeInfo.image + ); + animeInfo.description = data.data?.Media?.description ?? data?.description; + switch (data.data?.Media?.status ?? data?.status) { + case 'RELEASING': + animeInfo.status = MediaStatus.ONGOING; + break; + case 'FINISHED': + animeInfo.status = MediaStatus.COMPLETED; + break; + case 'NOT_YET_RELEASED': + animeInfo.status = MediaStatus.NOT_YET_AIRED; + break; + case 'CANCELLED': + animeInfo.status = MediaStatus.CANCELLED; + break; + case 'HIATUS': + animeInfo.status = MediaStatus.HIATUS; + default: + animeInfo.status = MediaStatus.UNKNOWN; + } + animeInfo.releaseDate = data.data?.Media?.startDate?.year ?? data.year; + animeInfo.startDate = { + year: data.data.Media.startDate.year, + month: data.data.Media.startDate.month, + day: data.data.Media.startDate.day, + }; + animeInfo.endDate = { + year: data.data.Media.endDate.year, + month: data.data.Media.endDate.month, + day: data.data.Media.endDate.day, + }; + if (data.data.Media.nextAiringEpisode?.airingAt) + animeInfo.nextAiringEpisode = { + airingTime: data.data.Media.nextAiringEpisode?.airingAt, + timeUntilAiring: data.data.Media.nextAiringEpisode?.timeUntilAiring, + episode: data.data.Media.nextAiringEpisode?.episode, + }; + animeInfo.totalEpisodes = data.data.Media?.episodes ?? data.data.Media.nextAiringEpisode?.episode - 1; + animeInfo.currentEpisode = data.data.Media?.nextAiringEpisode?.episode + ? data.data.Media.nextAiringEpisode?.episode - 1 + : data.data.Media?.episodes; + animeInfo.rating = data.data.Media.averageScore; + animeInfo.duration = data.data.Media.duration; + animeInfo.genres = data.data.Media.genres; + animeInfo.season = data.data.Media.season; + animeInfo.studios = data.data.Media.studios.edges.map((item: any) => item.node.name); + animeInfo.subOrDub = dub ? SubOrSub.DUB : SubOrSub.SUB; + animeInfo.type = data.data.Media.format; + animeInfo.recommendations = data.data.Media?.recommendations?.edges?.map((item: any) => ({ + id: item.node.mediaRecommendation?.id, + malId: item.node.mediaRecommendation?.idMal, + title: { + romaji: item.node.mediaRecommendation?.title?.romaji, + english: item.node.mediaRecommendation?.title?.english, + native: item.node.mediaRecommendation?.title?.native, + userPreferred: item.node.mediaRecommendation?.title?.userPreferred, + }, + status: + item.node.mediaRecommendation?.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.node.mediaRecommendation?.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.node.mediaRecommendation?.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.node.mediaRecommendation?.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.node.mediaRecommendation?.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + episodes: item.node.mediaRecommendation?.episodes, + image: + item.node.mediaRecommendation?.coverImage?.extraLarge ?? + item.node.mediaRecommendation?.coverImage?.large ?? + item.node.mediaRecommendation?.coverImage?.medium, + imageHash: getHashFromImage( + item.node.mediaRecommendation?.coverImage?.extraLarge ?? + item.node.mediaRecommendation?.coverImage?.large ?? + item.node.mediaRecommendation?.coverImage?.medium + ), + cover: + item.node.mediaRecommendation?.bannerImage ?? + item.node.mediaRecommendation?.coverImage?.extraLarge ?? + item.node.mediaRecommendation?.coverImage?.large ?? + item.node.mediaRecommendation?.coverImage?.medium, + coverHash: getHashFromImage( + item.node.mediaRecommendation?.bannerImage ?? + item.node.mediaRecommendation?.coverImage?.extraLarge ?? + item.node.mediaRecommendation?.coverImage?.large ?? + item.node.mediaRecommendation?.coverImage?.medium + ), + rating: item.node.mediaRecommendation?.meanScore, + type: item.node.mediaRecommendation?.format, + })); + + animeInfo.characters = data.data?.Media?.characters?.edges?.map((item: any) => ({ + id: item.node?.id, + role: item.role, + name: { + first: item.node.name.first, + last: item.node.name.last, + full: item.node.name.full, + native: item.node.name.native, + userPreferred: item.node.name.userPreferred, + }, + image: item.node.image.large ?? item.node.image.medium, + imageHash: getHashFromImage(item.node.image.large ?? item.node.image.medium), + voiceActors: item.voiceActors.map((voiceActor: any) => ({ + id: voiceActor.id, + language: voiceActor.languageV2, + name: { + first: voiceActor.name.first, + last: voiceActor.name.last, + full: voiceActor.name.full, + native: voiceActor.name.native, + userPreferred: voiceActor.name.userPreferred, + }, + image: voiceActor.image.large ?? voiceActor.image.medium, + imageHash: getHashFromImage(voiceActor.image.large ?? voiceActor.image.medium), + })), + })); + + animeInfo.relations = data.data?.Media?.relations?.edges?.map((item: any) => ({ + id: item.node.id, + relationType: item.relationType, + malId: item.node.idMal, + title: { + romaji: item.node.title.romaji, + english: item.node.title.english, + native: item.node.title.native, + userPreferred: item.node.title.userPreferred, + }, + status: + item.node.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.node.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.node.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.node.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.node.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + episodes: item.node.episodes, + image: item.node.coverImage.extraLarge ?? item.node.coverImage.large ?? item.node.coverImage.medium, + imageHash: getHashFromImage( + item.node.coverImage.extraLarge ?? item.node.coverImage.large ?? item.node.coverImage.medium + ), + color: item.node.coverImage?.color, + type: item.node.format, + cover: + item.node.bannerImage ?? + item.node.coverImage.extraLarge ?? + item.node.coverImage.large ?? + item.node.coverImage.medium, + coverHash: getHashFromImage( + item.node.bannerImage ?? + item.node.coverImage.extraLarge ?? + item.node.coverImage.large ?? + item.node.coverImage.medium + ), + rating: item.node.meanScore, + })); + animeInfo.episodes = await this.fetchDefaultEpisodeList( + { + idMal: animeInfo.malId! as number, + season: data.data.Media.season, + startDate: { year: parseInt(animeInfo.releaseDate!) }, + title: { + english: animeInfo.title?.english!, + romaji: animeInfo.title?.romaji!, + }, + externalLinks: data.data.Media.externalLinks.filter((link: any) => link.type === 'STREAMING'), + }, + dub, + id + ); + + if (fetchFiller) { + const { data: fillerData } = await this.client.get( + `https://raw.githubusercontent.com/saikou-app/mal-id-filler-list/main/fillers/${animeInfo.malId}.json`, + { validateStatus: () => true } + ); + + if (!fillerData.toString().startsWith('404')) { + fillerEpisodes = []; + fillerEpisodes?.push( + ...(fillerData.episodes as { + number: string; + 'filler-bool': boolean; + }[]) + ); + } + } + + animeInfo.episodes = animeInfo.episodes?.map((episode: IAnimeEpisode) => { + if (!episode.image) { + episode.image = animeInfo.image; + episode.imageHash = animeInfo.imageHash; + } + + if ( + fetchFiller && + fillerEpisodes?.length > 0 && + fillerEpisodes?.length >= animeInfo.episodes!.length + ) { + if (fillerEpisodes[episode.number! - 1]) + episode.isFiller = new Boolean(fillerEpisodes[episode.number! - 1]['filler-bool']).valueOf(); + } + + return episode; + }); + + return animeInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeSources = async (episodeId: string, ...args: any): Promise => { + try { + if (this.provider instanceof Anify) return new Anify().fetchEpisodeSources(episodeId, args[0], args[1]); + return this.provider.fetchEpisodeSources(episodeId, ...args); + } catch (err) { + throw new Error(`Failed to fetch episode sources from ${this.provider.name}: ${err}`); + } + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeServers = async (episodeId: string): Promise => { + try { + return this.provider.fetchEpisodeServers(episodeId); + } catch (err) { + throw new Error(`Failed to fetch episode servers from ${this.provider.name}: ${err}`); + } + }; + + private findAnime = async ( + title: { romaji: string; english: string }, + season: string, + startDate: number, + malId: number, + dub: boolean, + anilistId: string, + externalLinks?: any + ): Promise => { + title.english = title.english ?? title.romaji; + title.romaji = title.romaji ?? title.english; + + title.english = title.english.toLowerCase(); + title.romaji = title.romaji.toLowerCase(); + + if (title.english === title.romaji) { + return ( + (await this.findAnimeSlug(title.english, season, startDate, malId, dub, anilistId, externalLinks)) ?? + [] + ); + } + + const romajiPossibleEpisodes = await this.findAnimeSlug( + title.romaji, + season, + startDate, + malId, + dub, + anilistId, + externalLinks + ); + + if (romajiPossibleEpisodes) { + return romajiPossibleEpisodes; + } + + const englishPossibleEpisodes = await this.findAnimeSlug( + title.english, + season, + startDate, + malId, + dub, + anilistId, + externalLinks + ); + return englishPossibleEpisodes ?? []; + }; + + private findSmiklAnime = async (malId: number): Promise => { + const headers = { + 'User-Agent': USER_AGENT, + }; + const { data } = await this.client.get( + `${this.simklUrl}/search/id?mal=${malId}&client_id=${this.simklClientKey}`, + { + headers: headers, + } + ); + + if (data.length === 0) return []; + const simklId = data[0].ids['simkl']; + + // send request to get episodes + const { data: episodes } = await this.client.get( + `${this.simklUrl}/anime/episodes/${simklId}?client_id=${this.simklClientKey}&extended=full`, + { + headers: headers, + } + ); + + episodes.forEach((episode: any) => { + episode.image = `https://wsrv.nl/?url=https://simkl.in/episodes/${episode.img}_w.jpg`; + delete episode.img; + }); + + return episodes; + }; + + private findAnimeSlug = async ( + title: string, + season: string, + startDate: number, + malId: number, + dub: boolean, + anilistId: string, + externalLinks?: any + ): Promise => { + if (this.provider instanceof Anify) return (await this.provider.fetchAnimeInfo(anilistId)).episodes!; + + const slug = title.replace(/[^0-9a-zA-Z]+/g, ' '); + + let possibleAnime: any | undefined; + + if (malId && !(this.provider instanceof Crunchyroll || this.provider instanceof Bilibili)) { + const malAsyncReq = await this.client.get(`${this.malSyncUrl}/mal/anime/${malId}`, { + validateStatus: () => true, + }); + + if (malAsyncReq.status === 200) { + const sitesT = malAsyncReq.data.Sites as { + [k: string]: { + [k: string]: { url: string; page: string; title: string }; + }; + }; + let sites = Object.values(sitesT).map((v, i) => { + const obj: any = [...Object.values(Object.values(sitesT)[i])]; + const pages = obj.map((v: { page: string; url: string; title: string }) => ({ + page: v.page, + url: v.url, + title: v.title, + })); + return pages; + }) as any[]; + + sites = sites.flat(); + + sites.sort((a, b) => { + const targetTitle = malAsyncReq.data.title.toLowerCase(); + + const firstRating = compareTwoStrings(targetTitle, a.title.toLowerCase()); + const secondRating = compareTwoStrings(targetTitle, b.title.toLowerCase()); + + // Sort in descending order + return secondRating - firstRating; + }); + + const possibleSource = sites.find(s => { + if (s.page.toLowerCase() === this.provider.name.toLowerCase()) + if (this.provider instanceof Gogoanime) + return dub ? s.title.toLowerCase().includes('dub') : !s.title.toLowerCase().includes('dub'); + else return true; + return false; + }); + + if (possibleSource) { + try { + possibleAnime = await this.provider.fetchAnimeInfo(possibleSource.url.split('/').pop()!); + } catch (err) { + console.error(err); + possibleAnime = await this.findAnimeRaw(slug); + } + } else possibleAnime = await this.findAnimeRaw(slug); + } else possibleAnime = await this.findAnimeRaw(slug); + } else possibleAnime = await this.findAnimeRaw(slug, externalLinks); + + if (!possibleAnime) return undefined; + + // To avoid a new request, lets match and see if the anime show found is in sub/dub + + const expectedType = dub ? SubOrSub.DUB : SubOrSub.SUB; + + // Have this as a fallback in the meantime for compatibility + if (possibleAnime.subOrDub) { + if (possibleAnime.subOrDub != SubOrSub.BOTH && possibleAnime.subOrDub != expectedType) { + return undefined; + } + } else if ((!possibleAnime.hasDub && dub) || (!possibleAnime.hasSub && !dub)) { + return undefined; + } + + if (this.provider instanceof Zoro) { + // Set the correct episode sub/dub request type + possibleAnime.episodes.forEach((_: any, index: number) => { + if (possibleAnime.subOrDub === SubOrSub.BOTH) { + possibleAnime.episodes[index].id = possibleAnime.episodes[index].id.replace( + `$both`, + dub ? '$dub' : '$sub' + ); + } + }); + } + + if (this.provider instanceof Crunchyroll) { + const nestedEpisodes = Object.keys(possibleAnime.episodes) + .filter((key: any) => key.toLowerCase().includes(dub ? 'dub' : 'sub')) + .sort((first: any, second: any) => { + return ( + (possibleAnime.episodes[first]?.[0].season_number ?? 0) - + (possibleAnime.episodes[second]?.[0].season_number ?? 0) + ); + }) + .map((key: any) => { + const audio = key + .replace(/[0-9]/g, '') + .replace(/(^\w{1})|(\s+\w{1})/g, (letter: string) => letter.toUpperCase()); + possibleAnime.episodes[key].forEach((element: any) => (element.type = audio)); + return possibleAnime.episodes[key]; + }); + return nestedEpisodes.flat(); + } + + if (this.provider instanceof NineAnime) { + possibleAnime.episodes.forEach((_: any, index: number) => { + if (expectedType == SubOrSub.DUB) { + possibleAnime.episodes[index].id = possibleAnime.episodes[index].dubId; + } + + if (possibleAnime.episodes[index].dubId) { + delete possibleAnime.episodes[index].dubId; + } + }); + possibleAnime.episodes = possibleAnime.episodes.filter((el: any) => el.id != undefined); + } + + const possibleProviderEpisodes = possibleAnime.episodes as IAnimeEpisode[]; + + if ( + typeof possibleProviderEpisodes[0]?.image !== 'undefined' && + typeof possibleProviderEpisodes[0]?.title !== 'undefined' && + typeof possibleProviderEpisodes[0]?.description !== 'undefined' + ) + return possibleProviderEpisodes; + + const episodeList = await this.findSmiklAnime(malId); + + // merge id from possibleProviderEpisodes with episodeList + return possibleProviderEpisodes.map((episode: IAnimeEpisode) => { + const found = episodeList.find((ep: any) => ep.episode === episode.number); + if (found) found.id = episode.id; + return found!; + }); + + const options = { + headers: { 'Content-Type': 'application/json' }, + query: kitsuSearchQuery(slug), + }; + + const newEpisodeList = await this.findKitsuAnime(possibleProviderEpisodes, options, season, startDate); + + return newEpisodeList; + }; + + private findKitsuAnime = async ( + possibleProviderEpisodes: IAnimeEpisode[], + options: {}, + season?: string, + startDate?: number + ) => { + const kitsuEpisodes = await this.client.post(this.kitsuGraphqlUrl, options); + const episodesList = new Map(); + if (kitsuEpisodes?.data.data) { + const { nodes } = kitsuEpisodes.data.data.searchAnimeByTitle; + + if (nodes) { + nodes.forEach((node: any) => { + if (node.season === season && node.startDate.trim().split('-')[0] === startDate?.toString()) { + const episodes = node.episodes.nodes; + + for (const episode of episodes) { + const i = episode?.number.toString().replace(/"/g, ''); + + let name = undefined; + let description = undefined; + let thumbnail = undefined; + + let thumbnailHash = undefined; + + if (episode?.description?.en) + description = episode?.description.en.toString().replace(/"/g, '').replace('\\n', '\n'); + if (episode?.thumbnail) { + thumbnail = episode?.thumbnail.original.url.toString().replace(/"/g, ''); + thumbnailHash = getHashFromImage( + episode?.thumbnail.original.url.toString().replace(/"/g, '') + ); + } + + if (episode) { + if (episode.titles?.canonical) name = episode.titles.canonical.toString().replace(/"/g, ''); + episodesList.set(i, { + episodeNum: episode?.number.toString().replace(/"/g, ''), + title: name, + description, + createdAt: episode?.createdAt, + thumbnail, + }); + continue; + } + episodesList.set(i, { + episodeNum: undefined, + title: undefined, + description: undefined, + createdAt: undefined, + thumbnail, + thumbnailHash, + }); + } + } + }); + } + } + + const newEpisodeList: IAnimeEpisode[] = []; + if (possibleProviderEpisodes?.length !== 0) { + possibleProviderEpisodes?.forEach((ep: any, i: any) => { + const j = (i + 1).toString(); + newEpisodeList.push({ + id: ep.id as string, + title: ep.title ?? episodesList.get(j)?.title ?? null, + image: ep.image ?? episodesList.get(j)?.thumbnail ?? null, + imageHash: getHashFromImage(ep.image ?? episodesList.get(j)?.thumbnail ?? null), + number: ep.number as number, + createdAt: ep.createdAt ?? episodesList.get(j)?.createdAt ?? null, + description: ep.description ?? episodesList.get(j)?.description ?? null, + url: (ep.url as string) ?? null, + }); + }); + } + + return newEpisodeList; + }; + + /** + * @param page page number to search for (optional) + * @param perPage number of results per page (optional) + */ + fetchTrendingAnime = async (page: number = 1, perPage: number = 10): Promise> => { + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistTrendingQuery(page, perPage), + }; + + try { + const { data } = await this.client.post(this.anilistGraphqlUrl, options); + + const res: ISearch = { + currentPage: data.data.Page.pageInfo.currentPage, + hasNextPage: data.data.Page.pageInfo.hasNextPage, + results: data.data.Page.media.map((item: any) => ({ + id: item.id.toString(), + malId: item.idMal, + title: + { + romaji: item.title.romaji, + english: item.title.english, + native: item.title.native, + userPreferred: item.title.userPreferred, + } || item.title.romaji, + image: item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium, + imageHash: getHashFromImage( + item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium + ), + trailer: { + id: item.trailer?.id, + site: item.trailer?.site, + thumbnail: item.trailer?.thumbnail, + thumbnailHash: getHashFromImage(item.trailer?.thumbnail), + }, + description: item.description, + status: + item.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + cover: + item.bannerImage ?? item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium, + coverHash: getHashFromImage( + item.bannerImage ?? item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium + ), + rating: item.averageScore, + releaseDate: item.seasonYear, + color: item.coverImage?.color, + genres: item.genres, + totalEpisodes: isNaN(item.episodes) ? 0 : item.episodes ?? item.nextAiringEpisode?.episode - 1 ?? 0, + duration: item.duration, + type: item.format, + })), + }; + return res; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param page page number to search for (optional) + * @param perPage number of results per page (optional) + */ + fetchPopularAnime = async (page: number = 1, perPage: number = 10): Promise> => { + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistPopularQuery(page, perPage), + }; + + try { + const { data } = await this.client.post(this.anilistGraphqlUrl, options); + + const res: ISearch = { + currentPage: data.data.Page.pageInfo.currentPage, + hasNextPage: data.data.Page.pageInfo.hasNextPage, + results: data.data.Page.media.map((item: any) => ({ + id: item.id.toString(), + malId: item.idMal, + title: + { + romaji: item.title.romaji, + english: item.title.english, + native: item.title.native, + userPreferred: item.title.userPreferred, + } || item.title.romaji, + image: item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium, + imageHash: getHashFromImage( + item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium + ), + trailer: { + id: item.trailer?.id, + site: item.trailer?.site, + thumbnail: item.trailer?.thumbnail, + thumbnailHash: getHashFromImage(item.trailer?.thumbnail), + }, + description: item.description, + status: + item.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + cover: + item.bannerImage ?? item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium, + coverHash: getHashFromImage( + item.bannerImage ?? item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium + ), + rating: item.averageScore, + releaseDate: item.seasonYear, + color: item.coverImage?.color, + genres: item.genres, + totalEpisodes: isNaN(item.episodes) ? 0 : item.episodes ?? item.nextAiringEpisode?.episode - 1 ?? 0, + duration: item.duration, + type: item.format, + })), + }; + return res; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param page page number (optional) + * @param perPage number of results per page (optional) + * @param weekStart Filter by the start of the week (optional) (default: todays date) (options: 2 = Monday, 3 = Tuesday, 4 = Wednesday, 5 = Thursday, 6 = Friday, 0 = Saturday, 1 = Sunday) you can use either the number or the string + * @param weekEnd Filter by the end of the week (optional) similar to weekStart + * @param notYetAired if true will return anime that have not yet aired (optional) + * @returns the next airing episodes + */ + fetchAiringSchedule = async ( + page: number = 1, + perPage: number = 20, + weekStart: number | string = (new Date().getDay() + 1) % 7, + weekEnd: number | string = (new Date().getDay() + 7) % 7, + notYetAired: boolean = false + ): Promise> => { + let day1, + day2 = undefined; + + if (typeof weekStart === 'string' && typeof weekEnd === 'string') + [day1, day2] = getDays( + capitalizeFirstLetter(weekStart.toLowerCase()), + capitalizeFirstLetter(weekEnd.toLowerCase()) + ); + else if (typeof weekStart === 'number' && typeof weekEnd === 'number') + [day1, day2] = [weekStart, weekEnd]; + else throw new Error('Invalid weekStart or weekEnd'); + + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistAiringScheduleQuery(page, perPage, day1, day2, notYetAired), + }; + + try { + const { data } = await this.client.post(this.anilistGraphqlUrl, options); + + const res: ISearch = { + currentPage: data.data.Page.pageInfo.currentPage, + hasNextPage: data.data.Page.pageInfo.hasNextPage, + results: data.data.Page.airingSchedules.map((item: any) => ({ + id: item.media.id.toString(), + malId: item.media.idMal, + episode: item.episode, + airingAt: item.airingAt, + title: + { + romaji: item.media.title.romaji, + english: item.media.title.english, + native: item.media.title.native, + userPreferred: item.media.title.userPreferred, + } || item.media.title.romaji, + country: item.media.countryOfOrigin, + image: + item.media.coverImage.extraLarge ?? item.media.coverImage.large ?? item.media.coverImage.medium, + imageHash: getHashFromImage( + item.media.coverImage.extraLarge ?? item.media.coverImage.large ?? item.media.coverImage.medium + ), + description: item.media.description, + cover: + item.media.bannerImage ?? + item.media.coverImage.extraLarge ?? + item.media.coverImage.large ?? + item.media.coverImage.medium, + coverHash: getHashFromImage( + item.media.bannerImage ?? + item.media.coverImage.extraLarge ?? + item.media.coverImage.large ?? + item.media.coverImage.medium + ), + genres: item.media.genres, + color: item.media.coverImage?.color, + rating: item.media.averageScore, + releaseDate: item.media.seasonYear, + type: item.media.format, + })), + }; + return res; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param genres An array of genres to filter by (optional) genres: [`Action`, `Adventure`, `Cars`, `Comedy`, `Drama`, `Fantasy`, `Horror`, `Mahou Shoujo`, `Mecha`, `Music`, `Mystery`, `Psychological`, `Romance`, `Sci-Fi`, `Slice of Life`, `Sports`, `Supernatural`, `Thriller`] + * @param page page number (optional) + * @param perPage number of results per page (optional) + */ + fetchAnimeGenres = async (genres: string[] | Genres[], page: number = 1, perPage: number = 20) => { + if (genres.length === 0) throw new Error('No genres specified'); + + for (const genre of genres) + if (!Object.values(Genres).includes(genre as Genres)) throw new Error('Invalid genre'); + + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistGenresQuery(genres, page, perPage), + }; + try { + const { data } = await this.client.post(this.anilistGraphqlUrl, options); + + const res: ISearch = { + currentPage: data.data.Page.pageInfo.currentPage, + hasNextPage: data.data.Page.pageInfo.hasNextPage, + results: data.data.Page.media.map((item: any) => ({ + id: item.id.toString(), + malId: item.idMal, + title: + { + romaji: item.title.romaji, + english: item.title.english, + native: item.title.native, + userPreferred: item.title.userPreferred, + } || item.title.romaji, + image: item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium, + imageHash: getHashFromImage( + item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium + ), + trailer: { + id: item.trailer?.id, + site: item.trailer?.site, + thumbnail: item.trailer?.thumbnail, + thumbnailHash: getHashFromImage(item.trailer?.thumbnail), + }, + description: item.description, + cover: + item.bannerImage ?? item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium, + coverHash: getHashFromImage( + item.bannerImage ?? item.coverImage.extraLarge ?? item.coverImage.large ?? item.coverImage.medium + ), + rating: item.averageScore, + releaseDate: item.seasonYear, + color: item.coverImage?.color, + genres: item.genres, + totalEpisodes: isNaN(item.episodes) ? 0 : item.episodes ?? item.nextAiringEpisode?.episode - 1 ?? 0, + duration: item.duration, + type: item.format, + })), + }; + return res; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + private findAnimeRaw = async (slug: string, externalLinks?: any) => { + if (this.provider instanceof Crunchyroll && externalLinks) { + const link = externalLinks.find((link: any) => link.site.includes('Crunchyroll')); + if (link) { + const { request } = await this.client.get(link.url, { + validateStatus: () => true, + }); + if (request.res.responseUrl.includes('series') || request.res.responseUrl.includes('watch')) { + const mediaType = request.res.responseUrl.split('/')[3]; + const id = request.res.responseUrl.split('/')[4]; + return await this.provider.fetchAnimeInfo(id, mediaType); + } + } + } + const findAnime = (await this.provider.search(slug)) as ISearch; + + if (findAnime.results.length === 0) return undefined; + + // Sort the retrieved info for more accurate results. + + let topRating = 0; + + findAnime.results.sort((a, b) => { + const targetTitle = slug.toLowerCase(); + + let firstTitle: string; + let secondTitle: string; + + if (typeof a.title == 'string') firstTitle = a.title as string; + else firstTitle = a.title.english ?? a.title.romaji ?? ''; + + if (typeof b.title == 'string') secondTitle = b.title as string; + else secondTitle = b.title.english ?? b.title.romaji ?? ''; + + const firstRating = compareTwoStrings(targetTitle, firstTitle.toLowerCase()); + const secondRating = compareTwoStrings(targetTitle, secondTitle.toLowerCase()); + + if (firstRating > topRating) { + topRating = firstRating; + } + if (secondRating > topRating) { + topRating = secondRating; + } + + // Sort in descending order + return secondRating - firstRating; + }); + + if (topRating >= 0.7) { + if (this.provider instanceof Crunchyroll) { + return await this.provider.fetchAnimeInfo( + findAnime.results[0].id, + findAnime.results[0].type as string + ); + } else { + return await this.provider.fetchAnimeInfo(findAnime.results[0].id); + } + } + + return undefined; + }; + + /** + * @returns a random anime + */ + fetchRandomAnime = async (): Promise => { + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistSiteStatisticsQuery(), + }; + + try { + // const { + // data: { data }, + // } = await this.client.post(this.anilistGraphqlUrl, options); + + // const selectedAnime = Math.floor( + // Math.random() * data.SiteStatistics.anime.nodes[data.SiteStatistics.anime.nodes.length - 1].count + // ); + // const { results } = await this.advancedSearch(undefined, 'ANIME', Math.ceil(selectedAnime / 50), 50); + + const { data: data } = await this.client.get( + 'https://raw.githubusercontent.com/5H4D0WILA/IDFetch/main/ids.txt' + ); + + const ids = data?.trim().split('\n'); + const selectedAnime = Math.floor(Math.random() * ids.length); + return await this.fetchAnimeInfo(ids[selectedAnime]); + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * @param provider The provider to get the episode Ids from (optional) default: `gogoanime` (options: `gogoanime`, `zoro`) + * @param page page number (optional) + * @param perPage number of results per page (optional) + */ + fetchRecentEpisodes = async ( + provider: 'gogoanime' | 'zoro' = 'gogoanime', + page: number = 1, + perPage: number = 25 + ): Promise> => { + try { + const { data } = await this.client.get( + `${this.anifyUrl}/recent?page=${page}&perPage=${perPage}&type=anime` + ); + + const results: IAnimeInfo[] = data?.map((item: any) => { + return { + id: item.id.toString(), + malId: item.mappings.find((item: any) => item.providerType === 'META' && item.providerId === 'mal') + ?.id, + title: { + romaji: item.title?.romaji, + english: item.title?.english, + native: item.title?.native, + // userPreferred: (_f = item.title) === null || _f === void 0 ? void 0 : _f.userPreferred, + }, + image: item.coverImage ?? item.bannerImage, + imageHash: getHashFromImage(item.coverImage ?? item.bannerImage), + rating: item.averageScore, + color: item.anime?.color, + episodeId: `${ + provider === 'gogoanime' + ? item.episodes.data + .find((source: any) => source.providerId.toLowerCase() === 'gogoanime') + ?.episodes.pop()?.id + : item.episodes.data + .find((source: any) => source.providerId.toLowerCase() === 'zoro') + ?.episodes.pop()?.id + }`, + episodeTitle: item.episodes.latest.latestTitle ?? `Episode ${item.currentEpisode}`, + episodeNumber: item.currentEpisode, + genres: item.genre, + type: item.format, + }; + }); + // results = results.filter((item) => item.episodeNumber !== 0 && + // item.episodeId.replace('-enime', '').length > 0 && + // item.episodeId.replace('-enime', '') !== 'undefined'); + return { + currentPage: page, + // hasNextPage: meta.lastPage !== page, + // totalPages: meta.lastPage, + totalResults: results?.length, + results: results, + }; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + private fetchDefaultEpisodeList = async ( + Media: { + idMal: number; + title: { english: string; romaji: string }; + season: string; + startDate: { year: number }; + externalLinks?: any; + }, + dub: boolean, + id: string + ) => { + let episodes: IAnimeEpisode[] = []; + + episodes = await this.findAnime( + { english: Media.title?.english!, romaji: Media.title?.romaji! }, + Media.season!, + Media.startDate.year, + Media.idMal as number, + dub, + id, + Media.externalLinks + ); + + return episodes; + }; + + /** + * @param id anilist id + * @param dub language of the dubbed version (optional) currently only works for gogoanime + * @param fetchFiller to get filler boolean on the episode object (optional) set to `true` to get filler boolean on the episode object. + * @returns episode list **(without anime info)** + */ + fetchEpisodesListById = async (id: string, dub: boolean = false, fetchFiller: boolean = false) => { + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: `query($id: Int = ${id}){ Media(id: $id){ idMal externalLinks { site url } title { romaji english } status season episodes startDate { year month day } endDate { year month day } coverImage {extraLarge large medium} } }`, + }; + + const { + data: { + data: { Media }, + }, + } = await this.client.post(this.anilistGraphqlUrl, options); + + let possibleAnimeEpisodes: IAnimeEpisode[] = []; + let fillerEpisodes: { number: string; 'filler-bool': boolean }[] = []; + if ( + (this.provider instanceof Zoro || this.provider instanceof Gogoanime) && + !dub && + (Media.status === 'RELEASING' || + range({ from: 2000, to: new Date().getFullYear() + 1 }).includes(parseInt(Media.startDate?.year!))) + ) { + try { + possibleAnimeEpisodes = ( + await new Anify().fetchAnimeInfoByAnilistId( + id, + this.provider.name.toLowerCase() as 'gogoanime' | 'zoro' + ) + ).episodes?.map((item: any) => ({ + id: item.slug, + title: item.title, + description: item.description, + number: item.number, + image: item.image, + imageHash: getHashFromImage(item.image), + }))!; + + if (!possibleAnimeEpisodes.length) { + possibleAnimeEpisodes = await this.fetchDefaultEpisodeList(Media, dub, id); + possibleAnimeEpisodes = possibleAnimeEpisodes?.map((episode: IAnimeEpisode) => { + if (!episode.image) { + episode.image = + Media.coverImage.extraLarge ?? Media.coverImage.large ?? Media.coverImage.medium; + episode.imageHash = getHashFromImage( + Media.coverImage.extraLarge ?? Media.coverImage.large ?? Media.coverImage.medium + ); + } + + return episode; + }); + } + } catch (err) { + possibleAnimeEpisodes = await this.fetchDefaultEpisodeList(Media, dub, id); + + possibleAnimeEpisodes = possibleAnimeEpisodes?.map((episode: IAnimeEpisode) => { + if (!episode.image) { + episode.image = Media.coverImage.extraLarge ?? Media.coverImage.large ?? Media.coverImage.medium; + episode.imageHash = getHashFromImage( + Media.coverImage.extraLarge ?? Media.coverImage.large ?? Media.coverImage.medium + ); + } + + return episode; + }); + return possibleAnimeEpisodes; + } + } else possibleAnimeEpisodes = await this.fetchDefaultEpisodeList(Media, dub, id); + + if (fetchFiller) { + const { data: fillerData } = await this.client.get( + `https://raw.githubusercontent.com/saikou-app/mal-id-filler-list/main/fillers/${Media.idMal}.json`, + { + validateStatus: () => true, + } + ); + + if (!fillerData.toString().startsWith('404')) { + fillerEpisodes = []; + fillerEpisodes?.push( + ...(fillerData.episodes as { + number: string; + 'filler-bool': boolean; + }[]) + ); + } + } + + possibleAnimeEpisodes = possibleAnimeEpisodes?.map((episode: IAnimeEpisode) => { + if (!episode.image) { + episode.image = Media.coverImage.extraLarge ?? Media.coverImage.large ?? Media.coverImage.medium; + episode.imageHash = getHashFromImage( + Media.coverImage.extraLarge ?? Media.coverImage.large ?? Media.coverImage.medium + ); + } + + if (fetchFiller && fillerEpisodes?.length > 0 && fillerEpisodes?.length >= Media.episodes) { + if (fillerEpisodes[episode.number! - 1]) + episode.isFiller = new Boolean(fillerEpisodes[episode.number! - 1]['filler-bool']).valueOf(); + } + + return episode; + }); + + return possibleAnimeEpisodes; + }; + + /** + * @param id anilist id + * @returns anilist data for the anime **(without episodes)** (use `fetchEpisodesListById` to get the episodes) (use `fetchAnimeInfo` to get both) + */ + fetchAnilistInfoById = async (id: string) => { + const animeInfo: IAnimeInfo = { + id: id, + title: '', + }; + + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistMediaDetailQuery(id), + }; + + try { + const { data } = await this.client.post(this.anilistGraphqlUrl, options).catch(() => { + throw new Error('Media not found'); + }); + animeInfo.malId = data.data.Media.idMal; + animeInfo.title = { + romaji: data.data.Media.title.romaji, + english: data.data.Media.title.english, + native: data.data.Media.title.native, + userPreferred: data.data.Media.title.userPreferred, + }; + + if (data.data.Media.trailer?.id) { + animeInfo.trailer = { + id: data.data.Media.trailer?.id, + site: data.data.Media.trailer?.site, + thumbnail: data.data.Media.trailer?.thumbnail, + thumbnailHash: getHashFromImage(data.data.Media.trailer?.thumbnail), + }; + } + + animeInfo.synonyms = data.data.Media.synonyms; + animeInfo.isLicensed = data.data.Media.isLicensed; + animeInfo.isAdult = data.data.Media.isAdult; + animeInfo.countryOfOrigin = data.data.Media.countryOfOrigin; + + animeInfo.image = + data.data.Media.coverImage.extraLarge ?? + data.data.Media.coverImage.large ?? + data.data.Media.coverImage.medium; + + animeInfo.imageHash = getHashFromImage( + data.data.Media.coverImage.extraLarge ?? + data.data.Media.coverImage.large ?? + data.data.Media.coverImage.medium + ); + animeInfo.cover = data.data.Media.bannerImage ?? animeInfo.image; + animeInfo.coverHash = getHashFromImage(data.data.Media.bannerImage ?? animeInfo.image); + animeInfo.description = data.data.Media.description; + switch (data.data.Media.status) { + case 'RELEASING': + animeInfo.status = MediaStatus.ONGOING; + break; + case 'FINISHED': + animeInfo.status = MediaStatus.COMPLETED; + break; + case 'NOT_YET_RELEASED': + animeInfo.status = MediaStatus.NOT_YET_AIRED; + break; + case 'CANCELLED': + animeInfo.status = MediaStatus.CANCELLED; + break; + case 'HIATUS': + animeInfo.status = MediaStatus.HIATUS; + default: + animeInfo.status = MediaStatus.UNKNOWN; + } + animeInfo.releaseDate = data.data.Media.startDate.year; + if (data.data.Media.nextAiringEpisode?.airingAt) + animeInfo.nextAiringEpisode = { + airingTime: data.data.Media.nextAiringEpisode?.airingAt, + timeUntilAiring: data.data.Media.nextAiringEpisode?.timeUntilAiring, + episode: data.data.Media.nextAiringEpisode?.episode, + }; + + animeInfo.totalEpisodes = data.data.Media?.episodes ?? data.data.Media.nextAiringEpisode?.episode - 1; + animeInfo.currentEpisode = data.data.Media?.nextAiringEpisode?.episode + ? data.data.Media.nextAiringEpisode?.episode - 1 + : data.data.Media?.episodes || undefined; + animeInfo.rating = data.data.Media.averageScore; + animeInfo.duration = data.data.Media.duration; + animeInfo.genres = data.data.Media.genres; + animeInfo.studios = data.data.Media.studios.edges.map((item: any) => item.node.name); + animeInfo.season = data.data.Media.season; + animeInfo.popularity = data.data.Media.popularity; + animeInfo.type = data.data.Media.format; + animeInfo.startDate = { + year: data.data.Media.startDate?.year, + month: data.data.Media.startDate?.month, + day: data.data.Media.startDate?.day, + }; + animeInfo.endDate = { + year: data.data.Media.endDate?.year, + month: data.data.Media.endDate?.month, + day: data.data.Media.endDate?.day, + }; + animeInfo.recommendations = data.data.Media.recommendations.edges.map((item: any) => ({ + id: item.node.mediaRecommendation.id, + malId: item.node.mediaRecommendation.idMal, + title: { + romaji: item.node.mediaRecommendation.title.romaji, + english: item.node.mediaRecommendation.title.english, + native: item.node.mediaRecommendation.title.native, + userPreferred: item.node.mediaRecommendation.title.userPreferred, + }, + status: + item.node.mediaRecommendation.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.node.mediaRecommendation.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.node.mediaRecommendation.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.node.mediaRecommendation.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.node.mediaRecommendation.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + episodes: item.node.mediaRecommendation.episodes, + image: + item.node.mediaRecommendation.coverImage.extraLarge ?? + item.node.mediaRecommendation.coverImage.large ?? + item.node.mediaRecommendation.coverImage.medium, + imageHash: getHashFromImage( + item.node.mediaRecommendation.coverImage.extraLarge ?? + item.node.mediaRecommendation.coverImage.large ?? + item.node.mediaRecommendation.coverImage.medium + ), + cover: + item.node.mediaRecommendation.bannerImage ?? + item.node.mediaRecommendation.coverImage.extraLarge ?? + item.node.mediaRecommendation.coverImage.large ?? + item.node.mediaRecommendation.coverImage.medium, + coverHash: + item.node.mediaRecommendation.bannerImage ?? + item.node.mediaRecommendation.coverImage.extraLarge ?? + item.node.mediaRecommendation.coverImage.large ?? + item.node.mediaRecommendation.coverImage.medium, + rating: item.node.mediaRecommendation.meanScore, + type: item.node.mediaRecommendation.format, + })); + + animeInfo.characters = data.data.Media.characters.edges.map((item: any) => ({ + id: item.node.id, + role: item.role, + name: { + first: item.node.name.first, + last: item.node.name.last, + full: item.node.name.full, + native: item.node.name.native, + userPreferred: item.node.name.userPreferred, + }, + image: item.node.image.large ?? item.node.image.medium, + imageHash: getHashFromImage(item.node.image.large ?? item.node.image.medium), + voiceActors: item.voiceActors.map((voiceActor: any) => ({ + id: voiceActor.id, + language: voiceActor.languageV2, + name: { + first: voiceActor.name.first, + last: voiceActor.name.last, + full: voiceActor.name.full, + native: voiceActor.name.native, + userPreferred: voiceActor.name.userPreferred, + }, + image: voiceActor.image.large ?? voiceActor.image.medium, + imageHash: getHashFromImage(voiceActor.image.large ?? voiceActor.image.medium), + })), + })); + animeInfo.color = data.data.Media.coverImage?.color; + animeInfo.relations = data.data.Media.relations.edges.map((item: any) => ({ + id: item.node.id, + malId: item.node.idMal, + relationType: item.relationType, + title: { + romaji: item.node.title.romaji, + english: item.node.title.english, + native: item.node.title.native, + userPreferred: item.node.title.userPreferred, + }, + status: + item.node.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.node.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.node.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.node.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.node.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + episodes: item.node.episodes, + + image: item.node.coverImage.extraLarge ?? item.node.coverImage.large ?? item.node.coverImage.medium, + imageHash: getHashFromImage( + item.node.coverImage.extraLarge ?? item.node.coverImage.large ?? item.node.coverImage.medium + ), + cover: + item.node.bannerImage ?? + item.node.coverImage.extraLarge ?? + item.node.coverImage.large ?? + item.node.coverImage.medium, + coverHash: getHashFromImage( + item.node.bannerImage ?? + item.node.coverImage.extraLarge ?? + item.node.coverImage.large ?? + item.node.coverImage.medium + ), + rating: item.node.meanScore, + type: item.node.format, + })); + + return animeInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * TODO: finish this (got lazy) + * @param id staff id from anilist + * + */ + fetchStaffById = async (id: number) => { + throw new Error('Not implemented yet'); + }; + + /** + * + * @param id character id from anilist + */ + fetchCharacterInfoById = async (id: string) => { + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistCharacterQuery(), + variables: { + id: id, + }, + }; + + try { + const { + data: { + data: { Character }, + }, + } = await this.client.post(this.anilistGraphqlUrl, options); + + const height = Character.description.match(/__Height:__(.*)/)?.[1].trim(); + const weight = Character.description.match(/__Weight:__(.*)/)?.[1].trim(); + const hairColor = Character.description.match(/__Hair Color:__(.*)/)?.[1].trim(); + const eyeColor = Character.description.match(/__Eye Color:__(.*)/)?.[1].trim(); + const relatives = Character.description + .match(/__Relatives:__(.*)/)?.[1] + .trim() + .split(/(, \[)/g) + .filter((g: string) => !g.includes(', [')) + .map((r: string) => ({ + id: r.match(/\/(\d+)/)?.[1], + name: r.match(/([^)]+)\]/)?.[1].replace(/\[/g, ''), + relationship: r.match(/\(([^)]+)\).*?(\(([^)]+)\))/)?.[3], + })); + const race = Character.description + .match(/__Race:__(.*)/)?.[1] + .split(', ') + .map((r: string) => r.trim()); + const rank = Character.description.match(/__Rank:__(.*)/)?.[1]; + const occupation = Character.description.match(/__Occupation:__(.*)/)?.[1]; + const previousPosition = Character.description.match(/__Previous Position:__(.*)/)?.[1]?.trim(); + const partner = Character.description + .match(/__Partner:__(.*)/)?.[1] + .split(/(, \[)/g) + .filter((g: string) => !g.includes(', [')) + .map((r: string) => ({ + id: r.match(/\/(\d+)/)?.[1], + name: r.match(/([^)]+)\]/)?.[1].replace(/\[/g, ''), + })); + const dislikes = Character.description.match(/__Dislikes:__(.*)/)?.[1]; + const sign = Character.description.match(/__Sign:__(.*)/)?.[1]; + const zodicSign = Character.description.match(/__Zodiac sign:__(.*)/)?.[1]?.trim(); + const zodicAnimal = Character.description.match(/__Zodiac Animal:__(.*)/)?.[1]?.trim(); + const themeSong = Character.description.match(/__Theme Song:__(.*)/)?.[1]?.trim(); + Character.description = Character.description.replace( + /__Theme Song:__(.*)\n|__Race:__(.*)\n|__Height:__(.*)\n|__Relatives:__(.*)\n|__Rank:__(.*)\n|__Zodiac sign:__(.*)\n|__Zodiac Animal:__(.*)\n|__Weight:__(.*)\n|__Eye Color:__(.*)\n|__Hair Color:__(.*)\n|__Dislikes:__(.*)\n|__Sign:__(.*)\n|__Partner:__(.*)\n|__Previous Position:__(.*)\n|__Occupation:__(.*)\n/gm, + '' + ); + + const characterInfo = { + id: Character.id, + name: { + first: Character.name?.first, + last: Character.name?.last, + full: Character.name?.full, + native: Character.name?.native, + userPreferred: Character.name?.userPreferred, + alternative: Character.name?.alternative, + alternativeSpoiler: Character.name?.alternativeSpoiler, + }, + image: Character.image?.large ?? Character.image?.medium, + imageHash: getHashFromImage(Character.image?.large ?? Character.image?.medium), + description: Character.description, + gender: Character.gender, + dateOfBirth: { + year: Character.dateOfBirth?.year, + month: Character.dateOfBirth?.month, + day: Character.dateOfBirth?.day, + }, + bloodType: Character.bloodType, + age: Character.age, + hairColor: hairColor, + eyeColor: eyeColor, + height: height, + weight: weight, + occupation: occupation, + partner: partner, + relatives: relatives, + race: race, + rank: rank, + previousPosition: previousPosition, + dislikes: dislikes, + sign: sign, + zodicSign: zodicSign, + zodicAnimal: zodicAnimal, + themeSong: themeSong, + relations: Character.media.edges?.map((v: any) => ({ + id: v.node.id, + malId: v.node.idMal, + role: v.characterRole, + title: { + romaji: v.node.title?.romaji, + english: v.node.title?.english, + native: v.node.title?.native, + userPreferred: v.node.title?.userPreferred, + }, + status: + v.node.status == 'RELEASING' + ? MediaStatus.ONGOING + : v.node.status == 'FINISHED' + ? MediaStatus.COMPLETED + : v.node.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : v.node.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : v.node.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + episodes: v.node.episodes, + image: v.node.coverImage?.extraLarge ?? v.node.coverImage?.large ?? v.node.coverImage?.medium, + imageHash: getHashFromImage( + v.node.coverImage?.extraLarge ?? v.node.coverImage?.large ?? v.node.coverImage?.medium + ), + rating: v.node.averageScore, + releaseDate: v.node.startDate?.year, + type: v.node.format, + color: v.node.coverImage?.color, + })), + }; + + return characterInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * Anilist Anime class + */ + static Anime = this; + + /** + * Anilist Manga Class + */ + static Manga = class Manga { + provider: MangaParser; + + /** + * Maps anilist manga to any manga provider (mangadex, mangasee, etc) + * @param provider MangaParser + */ + constructor(provider?: MangaParser) { + this.provider = provider || new Mangasee123(); + } + + /** + * + * @param query query to search for + * @param page (optional) page number (default: `1`) + * @param perPage (optional) number of results per page (default: `20`) + */ + search = async ( + query: string, + page: number = 1, + perPage: number = 20 + ): Promise> => { + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistSearchQuery(query, page, perPage, 'MANGA'), + }; + + try { + const { data } = await axios.post(new Anilist().anilistGraphqlUrl, options); + + const res: ISearch = { + currentPage: data.data.Page.pageInfo.currentPage, + hasNextPage: data.data.Page.pageInfo.hasNextPage, + results: data.data.Page.media.map( + (item: any): IMangaResult => ({ + id: item.id.toString(), + malId: item.idMal, + title: + { + romaji: item.title.romaji, + english: item.title.english, + native: item.title.native, + userPreferred: item.title.userPreferred, + } || item.title.romaji, + status: + item.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + image: item.coverImage?.extraLarge ?? item.coverImage?.large ?? item.coverImage?.medium, + imageHash: getHashFromImage( + item.coverImage?.extraLarge ?? item.coverImage?.large ?? item.coverImage?.medium + ), + cover: item.bannerImage, + coverHash: getHashFromImage(item.bannerImage), + popularity: item.popularity, + description: item.description, + rating: item.averageScore, + genres: item.genres, + color: item.coverImage?.color, + totalChapters: item.chapters, + volumes: item.volumes, + type: item.format, + releaseDate: item.seasonYear, + }) + ), + }; + + return res; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param chapterId chapter id + * @param args args to pass to the provider (if any) + * @returns + */ + fetchChapterPages = (chapterId: string, ...args: any): Promise => { + return this.provider.fetchChapterPages(chapterId, ...args); + }; + + fetchMangaInfo = async (id: string, ...args: any): Promise => { + const mangaInfo: IMangaInfo = { + id: id, + title: '', + }; + + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistMediaDetailQuery(id), + }; + + try { + const { data } = await axios.post(new Anilist().anilistGraphqlUrl, options).catch(err => { + throw new Error('Media not found'); + }); + mangaInfo.malId = data.data.Media.idMal; + mangaInfo.title = { + romaji: data.data.Media.title.romaji, + english: data.data.Media.title.english, + native: data.data.Media.title.native, + userPreferred: data.data.Media.title.userPreferred, + }; + + if (data.data.Media.trailer?.id) { + mangaInfo.trailer = { + id: data.data.Media.trailer.id, + site: data.data.Media.trailer?.site, + thumbnail: data.data.Media.trailer?.thumbnail, + thumbnailHash: getHashFromImage(data.data.Media.trailer?.thumbnail), + }; + } + mangaInfo.image = + data.data.Media.coverImage.extraLarge ?? + data.data.Media.coverImage.large ?? + data.data.Media.coverImage.medium; + + mangaInfo.imageHash = getHashFromImage( + data.data.Media.coverImage.extraLarge ?? + data.data.Media.coverImage.large ?? + data.data.Media.coverImage.medium + ); + mangaInfo.popularity = data.data.Media.popularity; + mangaInfo.color = data.data.Media.coverImage?.color; + mangaInfo.cover = data.data.Media.bannerImage ?? mangaInfo.image; + mangaInfo.coverHash = getHashFromImage(data.data.Media.bannerImage ?? mangaInfo.image); + mangaInfo.description = data.data.Media.description; + switch (data.data.Media.status) { + case 'RELEASING': + mangaInfo.status = MediaStatus.ONGOING; + break; + case 'FINISHED': + mangaInfo.status = MediaStatus.COMPLETED; + break; + case 'NOT_YET_RELEASED': + mangaInfo.status = MediaStatus.NOT_YET_AIRED; + break; + case 'CANCELLED': + mangaInfo.status = MediaStatus.CANCELLED; + break; + case 'HIATUS': + mangaInfo.status = MediaStatus.HIATUS; + default: + mangaInfo.status = MediaStatus.UNKNOWN; + } + mangaInfo.releaseDate = data.data.Media.startDate.year; + mangaInfo.startDate = { + year: data.data.Media.startDate.year, + month: data.data.Media.startDate.month, + day: data.data.Media.startDate.day, + }; + mangaInfo.endDate = { + year: data.data.Media.endDate.year, + month: data.data.Media.endDate.month, + day: data.data.Media.endDate.day, + }; + mangaInfo.rating = data.data.Media.averageScore; + mangaInfo.genres = data.data.Media.genres; + mangaInfo.season = data.data.Media.season; + mangaInfo.studios = data.data.Media.studios.edges.map((item: any) => item.node.name); + mangaInfo.type = data.data.Media.format; + mangaInfo.recommendations = data.data.Media.recommendations.edges.map((item: any) => ({ + id: item.node.mediaRecommendation?.id, + malId: item.node.mediaRecommendation?.idMal, + title: { + romaji: item.node.mediaRecommendation?.title?.romaji, + english: item.node.mediaRecommendation?.title?.english, + native: item.node.mediaRecommendation?.title?.native, + userPreferred: item.node.mediaRecommendation?.title?.userPreferred, + }, + status: + item.node.mediaRecommendation?.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.node.mediaRecommendation?.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.node.mediaRecommendation?.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.node.mediaRecommendation?.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.node.mediaRecommendation?.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + chapters: item.node.mediaRecommendation?.chapters, + image: + item.node.mediaRecommendation?.coverImage?.extraLarge ?? + item.node.mediaRecommendation?.coverImage?.large ?? + item.node.mediaRecommendation?.coverImage?.medium, + imageHash: getHashFromImage( + item.node.mediaRecommendation?.coverImage?.extraLarge ?? + item.node.mediaRecommendation?.coverImage?.large ?? + item.node.mediaRecommendation?.coverImage?.medium + ), + cover: + item.node.mediaRecommendation?.bannerImage ?? + item.node.mediaRecommendation?.coverImage?.extraLarge ?? + item.node.mediaRecommendation?.coverImage?.large ?? + item.node.mediaRecommendation?.coverImage?.medium, + coverHash: getHashFromImage( + item.node.mediaRecommendation?.bannerImage ?? + item.node.mediaRecommendation?.coverImage?.extraLarge ?? + item.node.mediaRecommendation?.coverImage?.large ?? + item.node.mediaRecommendation?.coverImage?.medium + ), + rating: item.node.mediaRecommendation?.meanScore, + type: item.node.mediaRecommendation?.format, + })); + + mangaInfo.characters = data.data.Media.characters.edges.map((item: any) => ({ + id: item.node?.id, + role: item.role, + name: { + first: item.node.name.first, + last: item.node.name.last, + full: item.node.name.full, + native: item.node.name.native, + userPreferred: item.node.name.userPreferred, + }, + image: item.node.image.large ?? item.node.image.medium, + imageHash: getHashFromImage(item.node.image.large ?? item.node.image.medium), + })); + + mangaInfo.relations = data.data.Media.relations.edges.map((item: any) => ({ + id: item.node.id, + relationType: item.relationType, + malId: item.node.idMal, + title: { + romaji: item.node.title.romaji, + english: item.node.title.english, + native: item.node.title.native, + userPreferred: item.node.title.userPreferred, + }, + status: + item.node.status == 'RELEASING' + ? MediaStatus.ONGOING + : item.node.status == 'FINISHED' + ? MediaStatus.COMPLETED + : item.node.status == 'NOT_YET_RELEASED' + ? MediaStatus.NOT_YET_AIRED + : item.node.status == 'CANCELLED' + ? MediaStatus.CANCELLED + : item.node.status == 'HIATUS' + ? MediaStatus.HIATUS + : MediaStatus.UNKNOWN, + chapters: item.node.chapters, + image: item.node.coverImage.extraLarge ?? item.node.coverImage.large ?? item.node.coverImage.medium, + imageHash: getHashFromImage( + item.node.coverImage.extraLarge ?? item.node.coverImage.large ?? item.node.coverImage.medium + ), + color: item.node.coverImage?.color, + type: item.node.format, + cover: + item.node.bannerImage ?? + item.node.coverImage.extraLarge ?? + item.node.coverImage.large ?? + item.node.coverImage.medium, + coverHash: getHashFromImage( + item.node.bannerImage ?? + item.node.coverImage.extraLarge ?? + item.node.coverImage.large ?? + item.node.coverImage.medium + ), + rating: item.node.meanScore, + })); + + mangaInfo.chapters = await new Anilist().findManga( + this.provider, + { + english: mangaInfo.title.english!, + romaji: mangaInfo.title.romaji!, + }, + mangaInfo.malId as number + ); + mangaInfo.chapters = mangaInfo.chapters.reverse(); + + return mangaInfo; + } catch (error) { + throw Error((error as Error).message); + } + }; + }; + + private findMangaSlug = async ( + provider: MangaParser, + title: string, + malId: number + ): Promise => { + const slug = title.replace(/[^0-9a-zA-Z]+/g, ' '); + + let possibleManga: any; + + if (malId) { + const malAsyncReq = await this.client.get(`${this.malSyncUrl}/mal/manga/${malId}`, { + validateStatus: () => true, + }); + + if (malAsyncReq.status === 200) { + const sitesT = malAsyncReq.data.Sites as { + [k: string]: { + [k: string]: { url: string; page: string; title: string }; + }; + }; + let sites = Object.values(sitesT).map((v, i) => { + const obj: any = [...Object.values(Object.values(sitesT)[i])]; + const pages: any = obj.map((v: any) => ({ + page: v.page, + url: v.url, + title: v.title, + })); + return pages; + }) as any[]; + + sites = sites.flat(); + + const possibleSource = sites.find(s => s.page.toLowerCase() === provider.name.toLowerCase()); + + if (possibleSource) + possibleManga = await provider.fetchMangaInfo(possibleSource.url.split('/').pop()!); + else possibleManga = await this.findMangaRaw(provider, slug, title); + } else possibleManga = await this.findMangaRaw(provider, slug, title); + } else possibleManga = await this.findMangaRaw(provider, slug, title); + + const possibleProviderChapters = possibleManga.chapters; + + return possibleProviderChapters; + }; + + private findMangaRaw = async (provider: MangaParser, slug: string, title: string) => { + const findAnime = (await provider.search(slug)) as ISearch; + + if (findAnime.results.length === 0) return []; + // TODO: use much better way than this + + const possibleManga = findAnime.results.find( + (manga: IMangaResult) => + title.toLowerCase() == (typeof manga.title === 'string' ? manga.title.toLowerCase() : '') + ); + + if (!possibleManga) return (await provider.fetchMangaInfo(findAnime.results[0].id)) as IMangaInfo; + return (await provider.fetchMangaInfo(possibleManga.id)) as IMangaInfo; + }; + + private findManga = async ( + provider: MangaParser, + title: { romaji: string; english: string }, + malId: number + ): Promise => { + title.english = title.english ?? title.romaji; + title.romaji = title.romaji ?? title.english; + + title.english = title.english.toLowerCase(); + title.romaji = title.romaji.toLowerCase(); + + if (title.english === title.romaji) return await this.findMangaSlug(provider, title.english, malId); + + const romajiPossibleEpisodes = this.findMangaSlug(provider, title.romaji, malId); + + if (romajiPossibleEpisodes) { + return romajiPossibleEpisodes; + } + + const englishPossibleEpisodes = this.findMangaSlug(provider, title.english, malId); + return englishPossibleEpisodes; + }; +} + +(async () => { + const ani = new Anilist(new Gogoanime()); + const res = await ani.fetchAnimeInfo('21'); + console.log(res); + // const anime = await ani.fetchAnimeInfo('21'); + // console.log(anime.episodes); + // const sources = await ani.fetchEpisodeSources(anime.episodes![0].id, anime.episodes![0].number, anime.id); + // console.log(sources); +})(); + +export default Anilist; diff --git a/consumet.ts/src/providers/meta/index.ts b/consumet.ts/src/providers/meta/index.ts new file mode 100644 index 00000000..a4feb92b --- /dev/null +++ b/consumet.ts/src/providers/meta/index.ts @@ -0,0 +1,5 @@ +import Anilist from './anilist'; +import Myanimelist from './mal'; +import TMDB from './tmdb'; + +export default { Anilist, Myanimelist, TMDB }; diff --git a/consumet.ts/src/providers/meta/mal.ts b/consumet.ts/src/providers/meta/mal.ts new file mode 100644 index 00000000..be3b9caf --- /dev/null +++ b/consumet.ts/src/providers/meta/mal.ts @@ -0,0 +1,743 @@ +import { load } from 'cheerio'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + MediaStatus, + IAnimeResult, + ISource, + IAnimeEpisode, + SubOrSub, + IEpisodeServer, + MediaFormat, +} from '../../models'; +import { substringAfter, substringBefore, compareTwoStrings, kitsuSearchQuery, range } from '../../utils'; +import Gogoanime from '../anime/gogoanime'; +import Zoro from '../anime/zoro'; +import Crunchyroll from '../anime/crunchyroll'; +import Anify from '../anime/anify'; +import Bilibili from '../anime/bilibili'; + +class Myanimelist extends AnimeParser { + override readonly name = 'Myanimelist'; + protected override baseUrl = 'https://myanimelist.net/'; + protected override logo = 'https://en.wikipedia.org/wiki/MyAnimeList#/media/File:MyAnimeList.png'; + protected override classPath = 'META.Myanimelist'; + + private readonly anilistGraphqlUrl = 'https://graphql.anilist.co'; + private readonly kitsuGraphqlUrl = 'https://kitsu.io/api/graphql'; + private readonly malSyncUrl = 'https://api.malsync.moe'; + private readonly anifyUrl = 'https://api.anify.tv'; + provider: AnimeParser; + + /** + * This class maps myanimelist to kitsu with any other anime provider. + * kitsu is used for episode images, titles and description. + * @param provider anime provider (optional) default: Gogoanime + */ + constructor(provider?: AnimeParser) { + super(); + this.provider = provider || new Gogoanime(); + } + + private malStatusToMediaStatus(status: string): MediaStatus { + if (status == 'currently airing') return MediaStatus.ONGOING; + else if (status == 'finished airing') return MediaStatus.COMPLETED; + else if (status == 'not yet aired') return MediaStatus.NOT_YET_AIRED; + return MediaStatus.UNKNOWN; + } + + private async populateEpisodeList( + episodes: IAnimeEpisode[], + url: string, + count: number = 1 + ): Promise { + try { + const { data } = await this.client.request({ + method: 'get', + url: `${url}?p=${count}`, + headers: { + 'user-agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.35', + }, + }); + + let hasEpisodes = false; + const $ = load(data); + for (const elem of $('.video-list').toArray()) { + const href = $(elem).attr('href'); + const image = $(elem).find('img').attr('data-src'); + const titleDOM = $(elem).find('.episode-title'); + const title = titleDOM?.text(); + titleDOM.remove(); + + const numberDOM = $(elem).find('.title').text().split(' '); + let number = 0; + if (numberDOM.length > 1) { + number = Number(numberDOM[1]); + } + if (href && href.indexOf('myanimelist.net/anime') > -1) { + hasEpisodes = true; + episodes.push({ + id: '', + number, + title, + image, + }); + } + } + + if (hasEpisodes) await this.populateEpisodeList(episodes, url, ++count); + } catch (err) { + console.error(err); + } + } + + override search = async (query: string, page: number = 1): Promise> => { + const searchResults: ISearch = { + currentPage: page, + results: [], + }; + + const { data } = await this.client.request({ + method: 'get', + url: `https://myanimelist.net/anime.php?q=${query}&cat=anime&show=${50 * (page - 1)}`, + headers: { + 'user-agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.35', + }, + }); + + const $ = load(data); + + const pages = $('.normal_header').find('span').children(); + const maxPage = parseInt(pages.last().text()); + const hasNextPage = page < maxPage; + searchResults.hasNextPage = hasNextPage; + + $('tr').each((i, item) => { + const id = $(item).find('.hoverinfo_trigger').attr('href')?.split('anime/')[1].split('/')[0]; + const title = $(item).find('strong').text(); + const description = $(item).find('.pt4').text().replace('...read more.', '...'); + const type = $(item).children().eq(2).text().trim(); + const episodeCount = $(item).children().eq(3).text().trim(); + const score = (parseFloat($(item).children().eq(4).text()) * 10).toFixed(0); + const imageTmp = $(item).children().first().find('img').attr('data-src'); + const imageUrl = `https://cdn.myanimelist.net/images/anime/${imageTmp?.split('anime/')[1]}`; + + if (title != '') { + searchResults.results.push({ + id: id ?? '', + title: title, + image: imageUrl, + rating: parseInt(score), + description: description, + totalEpisodes: parseInt(episodeCount), + type: + type == 'TV' + ? MediaFormat.TV + : type == 'TV_SHORT' + ? MediaFormat.TV_SHORT + : type == 'MOVIE' + ? MediaFormat.MOVIE + : type == 'SPECIAL' + ? MediaFormat.SPECIAL + : type == 'OVA' + ? MediaFormat.OVA + : type == 'ONA' + ? MediaFormat.ONA + : type == 'MUSIC' + ? MediaFormat.MUSIC + : type == 'MANGA' + ? MediaFormat.MANGA + : type == 'NOVEL' + ? MediaFormat.NOVEL + : type == 'ONE_SHOT' + ? MediaFormat.ONE_SHOT + : undefined, + }); + } + }); + + return searchResults; + }; + + /** + * + * @param animeId anime id + * @param fetchFiller fetch filler episodes + */ + fetchAnimeInfo = async ( + animeId: string, + dub: boolean = false, + fetchFiller: boolean = false + ): Promise => { + try { + const animeInfo = await this.fetchMalInfoById(animeId); + + const titleWithLanguages = animeInfo?.title as { + english: string; + romaji: string; + native: string; + userPreferred: string; + }; + let fillerEpisodes: { number: string; 'filler-bool': boolean }[]; + if ( + (this.provider instanceof Zoro || this.provider instanceof Gogoanime) && + !dub && + (animeInfo.status === MediaStatus.ONGOING || + range({ from: 2000, to: new Date().getFullYear() + 1 }).includes(animeInfo.startDate?.year!)) + ) { + try { + animeInfo.episodes = ( + await new Anify( + this.proxyConfig, + this.adapter, + this.provider.name.toLowerCase() as 'gogoanime' | 'zoro' | '9anime' | 'animepahe' + ).fetchAnimeInfo(animeId) + ).episodes?.map((item: any) => ({ + id: item.slug, + title: item.title, + description: item.description, + number: item.number, + image: item.image, + })); + animeInfo.episodes?.reverse(); + } catch (err) { + animeInfo.episodes = await this.findAnimeSlug( + titleWithLanguages?.english || + titleWithLanguages?.romaji || + titleWithLanguages?.native || + titleWithLanguages?.userPreferred, + animeInfo.season!, + animeInfo.startDate?.year!, + animeId, + dub + ); + + animeInfo.episodes = animeInfo.episodes?.map((episode: IAnimeEpisode) => { + if (!episode.image) episode.image = animeInfo.image; + + return episode; + }); + + return animeInfo; + } + } else + animeInfo.episodes = await this.findAnimeSlug( + titleWithLanguages?.english || + titleWithLanguages?.romaji || + titleWithLanguages?.native || + titleWithLanguages?.userPreferred, + animeInfo.season!, + animeInfo.startDate?.year!, + animeId, + dub + ); + + if (fetchFiller) { + const { data: fillerData } = await this.client({ + baseURL: `https://raw.githubusercontent.com/saikou-app/mal-id-filler-list/main/fillers/${animeId}.json`, + method: 'GET', + validateStatus: () => true, + }); + + if (!fillerData.toString().startsWith('404')) { + fillerEpisodes = []; + fillerEpisodes?.push( + ...(fillerData.episodes as { + number: string; + 'filler-bool': boolean; + }[]) + ); + } + } + + animeInfo.episodes = animeInfo.episodes?.map((episode: IAnimeEpisode) => { + if (!episode.image) episode.image = animeInfo.image; + + if ( + fetchFiller && + fillerEpisodes?.length > 0 && + fillerEpisodes?.length >= animeInfo.episodes!.length + ) { + if (fillerEpisodes[episode.number! - 1]) + episode.isFiller = new Boolean(fillerEpisodes[episode.number! - 1]['filler-bool']).valueOf(); + } + + return episode; + }); + + return animeInfo; + } catch (err) { + console.error(err); + throw err; + } + }; + + override fetchEpisodeSources = async (episodeId: string, ...args: any): Promise => { + try { + if (episodeId.includes('/') && this.provider instanceof Anify) + return new Anify().fetchEpisodeSources(episodeId, args[0], args[1]); + return this.provider.fetchEpisodeSources(episodeId, ...args); + } catch (err) { + throw new Error(`Failed to fetch episode sources from ${this.provider.name}: ${err}`); + } + }; + + fetchEpisodeServers(episodeId: string): Promise { + return this.provider.fetchEpisodeServers(episodeId); + } + + private findAnimeRaw = async (slug: string, externalLinks?: any) => { + if (externalLinks && this.provider instanceof Crunchyroll) { + if (externalLinks.map((link: any) => link.site.includes('Crunchyroll'))) { + const link = externalLinks.find((link: any) => link.site.includes('Crunchyroll')); + const { request } = await this.client.get(link.url, { + validateStatus: () => true, + }); + const mediaType = request.res.responseUrl.split('/')[3]; + const id = request.res.responseUrl.split('/')[4]; + + return await this.provider.fetchAnimeInfo(id, mediaType); + } + } + const findAnime = (await this.provider.search(slug)) as ISearch; + if (findAnime.results.length === 0) return []; + + // Sort the retrieved info for more accurate results. + + findAnime.results.sort((a, b) => { + const targetTitle = slug.toLowerCase(); + + let firstTitle: string; + let secondTitle: string; + + if (typeof a.title == 'string') firstTitle = a.title as string; + else firstTitle = a.title.english ?? a.title.romaji ?? ''; + + if (typeof b.title == 'string') secondTitle = b.title as string; + else secondTitle = b.title.english ?? b.title.romaji ?? ''; + + const firstRating = compareTwoStrings(targetTitle, firstTitle.toLowerCase()); + const secondRating = compareTwoStrings(targetTitle, secondTitle.toLowerCase()); + + // Sort in descending order + return secondRating - firstRating; + }); + + if (this.provider instanceof Crunchyroll) { + return await this.provider.fetchAnimeInfo(findAnime.results[0].id, findAnime.results[0].type as string); + } + // TODO: use much better way than this + return (await this.provider.fetchAnimeInfo(findAnime.results[0].id)) as IAnimeInfo; + }; + + private findAnimeSlug = async ( + title: string, + season: string, + startDate: number, + malId: string, + dub: boolean, + externalLinks?: any + ): Promise => { + if (this.provider instanceof Anify) return (await this.provider.fetchAnimeInfo(malId)).episodes!; + + // console.log({ title }); + const slug = title?.replace(/[^0-9a-zA-Z]+/g, ' '); + + let possibleAnime: any | undefined; + + if (malId && !(this.provider instanceof Crunchyroll || this.provider instanceof Bilibili)) { + const malAsyncReq = await this.client({ + method: 'GET', + url: `${this.malSyncUrl}/mal/anime/${malId}`, + validateStatus: () => true, + }); + + if (malAsyncReq.status === 200) { + const sitesT = malAsyncReq.data.Sites as { + [k: string]: { + [k: string]: { url: string; page: string; title: string }; + }; + }; + let sites = Object.values(sitesT).map((v, i) => { + const obj = [...Object.values(Object.values(sitesT)[i])]; + const pages = obj.map(v => ({ + page: v.page, + url: v.url, + title: v.title, + })); + return pages; + }) as any[]; + + sites = sites.flat(); + + sites.sort((a, b) => { + const targetTitle = malAsyncReq.data.title.toLowerCase(); + + const firstRating = compareTwoStrings(targetTitle, a.title.toLowerCase()); + const secondRating = compareTwoStrings(targetTitle, b.title.toLowerCase()); + + // Sort in descending order + return secondRating - firstRating; + }); + + const possibleSource = sites.find(s => { + if (s.page.toLowerCase() === this.provider.name.toLowerCase()) + if (this.provider instanceof Gogoanime) + return dub ? s.title.toLowerCase().includes('dub') : !s.title.toLowerCase().includes('dub'); + else return true; + return false; + }); + + if (possibleSource) { + try { + possibleAnime = await this.provider.fetchAnimeInfo(possibleSource.url.split('/').pop()!); + } catch (err) { + console.error(err); + possibleAnime = await this.findAnimeRaw(slug); + } + } else possibleAnime = await this.findAnimeRaw(slug); + } else possibleAnime = await this.findAnimeRaw(slug); + } else possibleAnime = await this.findAnimeRaw(slug, externalLinks); + + // To avoid a new request, lets match and see if the anime show found is in sub/dub + + const expectedType = dub ? SubOrSub.DUB : SubOrSub.SUB; + + if (possibleAnime.subOrDub != SubOrSub.BOTH && possibleAnime.subOrDub != expectedType) { + return []; + } + + if (this.provider instanceof Zoro) { + // Set the correct episode sub/dub request type + possibleAnime.episodes.forEach((_: any, index: number) => { + if (possibleAnime.subOrDub === SubOrSub.BOTH) { + possibleAnime.episodes[index].id = possibleAnime.episodes[index].id.replace( + `$both`, + dub ? '$dub' : '$sub' + ); + } + }); + } + + if (this.provider instanceof Crunchyroll) { + const nestedEpisodes = Object.keys(possibleAnime.episodes) + .filter((key: any) => key.toLowerCase().includes(dub ? 'dub' : 'sub')) + .sort((first: any, second: any) => { + return ( + (possibleAnime.episodes[first]?.[0].season_number ?? 0) - + (possibleAnime.episodes[second]?.[0].season_number ?? 0) + ); + }) + .map((key: any) => { + const audio = key + .replace(/[0-9]/g, '') + .replace(/(^\w{1})|(\s+\w{1})/g, (letter: string) => letter.toUpperCase()); + possibleAnime.episodes[key].forEach((element: any) => (element.type = audio)); + return possibleAnime.episodes[key]; + }); + return nestedEpisodes.flat(); + } + + const possibleProviderEpisodes = possibleAnime.episodes as IAnimeEpisode[]; + + if ( + typeof possibleProviderEpisodes[0]?.image !== 'undefined' && + typeof possibleProviderEpisodes[0]?.title !== 'undefined' && + typeof possibleProviderEpisodes[0]?.description !== 'undefined' + ) + return possibleProviderEpisodes; + + const options = { + headers: { 'Content-Type': 'application/json' }, + query: kitsuSearchQuery(slug), + }; + + const newEpisodeList = await this.findKitsuAnime(possibleProviderEpisodes, options, season, startDate); + + return newEpisodeList; + }; + + private findKitsuAnime = async ( + possibleProviderEpisodes: IAnimeEpisode[], + options: {}, + season?: string, + startDate?: number + ) => { + const kitsuEpisodes = await this.client.post(this.kitsuGraphqlUrl, options); + const episodesList = new Map(); + if (kitsuEpisodes?.data.data) { + const { nodes } = kitsuEpisodes.data.data.searchAnimeByTitle; + + if (nodes) { + nodes.forEach((node: any) => { + if (node.season === season && node.startDate.trim().split('-')[0] === startDate?.toString()) { + const episodes = node.episodes.nodes; + + for (const episode of episodes) { + const i = episode?.number.toString().replace(/"/g, ''); + let name = undefined; + let description = undefined; + let thumbnail = undefined; + + if (episode?.description?.en) + description = episode?.description.en.toString().replace(/"/g, '').replace('\\n', '\n'); + if (episode?.thumbnail) + thumbnail = episode?.thumbnail.original.url.toString().replace(/"/g, ''); + + if (episode) { + if (episode.titles?.canonical) name = episode.titles.canonical.toString().replace(/"/g, ''); + episodesList.set(i, { + episodeNum: episode?.number.toString().replace(/"/g, ''), + title: name, + description, + thumbnail, + }); + continue; + } + episodesList.set(i, { + episodeNum: undefined, + title: undefined, + description: undefined, + thumbnail, + }); + } + } + }); + } + } + + const newEpisodeList: IAnimeEpisode[] = []; + if (possibleProviderEpisodes?.length !== 0) { + possibleProviderEpisodes?.forEach((ep: any, i: any) => { + const j = (i + 1).toString(); + newEpisodeList.push({ + id: ep.id as string, + title: ep.title ?? episodesList.get(j)?.title ?? null, + image: ep.image ?? episodesList.get(j)?.thumbnail ?? null, + number: ep.number as number, + description: ep.description ?? episodesList.get(j)?.description ?? null, + url: (ep.url as string) ?? null, + }); + }); + } + + return newEpisodeList; + }; + + /** + * + * @param id anime id + * @returns anime info without streamable episodes + */ + fetchMalInfoById = async (id: string): Promise => { + const animeInfo: IAnimeInfo = { + id: id, + title: '', + }; + + const { data } = await this.client.request({ + method: 'GET', + url: `https://myanimelist.net/anime/${id}`, + headers: { + 'user-agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.35', + }, + }); + + const $ = load(data); + const episodes: IAnimeEpisode[] = []; + const desc = $('[itemprop="description"]').first().text(); + const imageElem = $('[itemprop="image"]').first(); + const image = imageElem.attr('src') || imageElem.attr('data-image') || imageElem.attr('data-src'); + const genres: string[] = []; + const genreDOM = $('[itemprop="genre"]').get(); + + genreDOM.forEach(elem => genres.push($(elem).text())); + + animeInfo.genres = genres; + animeInfo.image = image; + animeInfo.description = desc; + animeInfo.title = { + english: $('.js-alternative-titles.hide').children().eq(0).text().replace('English: ', '').trim(), + romaji: $('.title-name').text(), + native: $('.js-alternative-titles.hide').parent().children().eq(9).text().trim(), + userPreferred: $('.js-alternative-titles.hide').children().eq(0).text().replace('English: ', '').trim(), + }; + + animeInfo.synonyms = $('.js-alternative-titles.hide') + .parent() + .children() + .eq(8) + .text() + .replace('Synonyms:', '') + .trim() + .split(','); + animeInfo.studios = []; + animeInfo.popularity = parseInt( + $('.numbers.popularity').text().trim().replace('Popularity #', '').trim() + ); + + const producers: string[] = []; + $('a').each(function (i: number, link: any) { + if ( + $(link).attr('href')?.includes('producer') && + $(link).parent().children().eq(0).text() == 'Producers:' + ) { + producers.push($(link).text()); + } + }); + animeInfo.producers = producers; + // animeInfo.episodes = episodes; + + const teaserDOM = $('.video-promotion > a'); + if (teaserDOM.length > 0) { + const teaserURL = $(teaserDOM).attr('href'); + const style = $(teaserDOM).attr('style'); + if (teaserURL) { + animeInfo.trailer = { + id: substringAfter(teaserURL, 'embed/').split('?')[0], + site: 'https://youtube.com/watch?v=', + thumbnail: style ? substringBefore(substringAfter(style, "url('"), "'") : '', + }; + } + } + const ops = $('.theme-songs.js-theme-songs.opnening').find('tr').get(); + + const ignoreList = ['Apple Music', 'Youtube Music', 'Amazon Music', 'Spotify']; + animeInfo.openings = ops.map((element: any) => { + //console.log($(element).text().trim()); + const name = $(element).children().eq(1).children().first().text().trim(); + if (!ignoreList.includes(name)) { + if ($(element).find('.theme-song-index').length != 0) { + const index = $(element).find('.theme-song-index').text().trim(); + const band = $(element).find('.theme-song-artist').text().trim(); + const episodes = $(element).find('.theme-song-episode').text().trim(); + //console.log($(element).children().eq(1).text().trim().split(index)[1]); + + return { + name: $(element).children().eq(1).text().trim().split(index)[1].split(band)[0].trim(), + band: band.replace('by ', ''), + episodes: episodes, + }; + } else { + const band = $(element).find('.theme-song-artist').text().trim(); + const episodes = $(element).find('.theme-song-episode').text().trim(); + return { + name: $(element).children().eq(1).text().trim().split(band)[0].trim(), + band: band.replace('by ', ''), + episodes: episodes, + }; + } + } + }); + animeInfo.openings = (animeInfo.openings as any[]).filter(function (element: any) { + return element !== undefined; + }); + + const eds = $('.theme-songs.js-theme-songs.ending').find('tr').get(); + animeInfo.endings = eds.map((element: any) => { + //console.log($(element).text().trim()); + const name = $(element).children().eq(1).children().first().text().trim(); + if (!ignoreList.includes(name)) { + if ($(element).find('.theme-song-index').length != 0) { + const index = $(element).find('.theme-song-index').text().trim(); + const band = $(element).find('.theme-song-artist').text().trim(); + const episodes = $(element).find('.theme-song-episode').text().trim(); + //console.log($(element).children().eq(1).text().trim().split(index)[1]); + + return { + name: $(element).children().eq(1).text().trim().split(index)[1].split(band)[0].trim(), + band: band.replace('by ', ''), + episodes: episodes, + }; + } else { + const band = $(element).find('.theme-song-artist').text().trim(); + const episodes = $(element).find('.theme-song-episode').text().trim(); + return { + name: $(element).children().eq(1).text().trim().split(band)[0].trim(), + band: band.replace('by ', ''), + episodes: episodes, + }; + } + } + }); + animeInfo.endings = (animeInfo.endings as any[]).filter(function (element: any) { + return element !== undefined; + }); + + const description = $('.spaceit_pad').get(); + + description.forEach((elem: any) => { + const text = $(elem).text().toLowerCase().trim(); + const key = text.split(':')[0]; + const value = substringAfter(text, `${key}:`).trim(); + switch (key) { + case 'status': + animeInfo.status = this.malStatusToMediaStatus(value); + break; + case 'episodes': + animeInfo.totalEpisodes = parseInt(value); + if (isNaN(animeInfo.totalEpisodes)) animeInfo.totalEpisodes = 0; + break; + case 'premiered': + animeInfo.season = value.split(' ')[0].toUpperCase(); + break; + case 'aired': + const dates = value.split('to'); + if (dates.length >= 2) { + const start = dates[0].trim(); + const end = dates[1].trim(); + const startDate = new Date(start); + const endDate = new Date(end); + + if (startDate.toString() !== 'Invalid Date') { + animeInfo.startDate = { + day: startDate.getDate(), + month: startDate.getMonth(), + year: startDate.getFullYear(), + }; + } + + if (endDate.toString() != 'Invalid Date') { + animeInfo.endDate = { + day: endDate.getDate(), + month: endDate.getMonth(), + year: endDate.getFullYear(), + }; + } + } + + break; + + case 'score': + animeInfo.rating = parseFloat(value); + break; + case 'studios': + for (const studio of $(elem).find('a')) animeInfo.studios?.push($(studio).text()); + break; + case 'rating': + animeInfo.ageRating = value; + } + }); + + // Only works on certain animes, so it is unreliable + // let videoLink = $('.mt4.ar a').attr('href'); + // if (videoLink) { + // await this.populateEpisodeList(episodes, videoLink); + // } + return animeInfo; + }; +} + +export default Myanimelist; + +// (async () => { +// const mal = new Myanimelist(); +// // const search = await mal.search('one piece'); +// const info = await mal.fetchAnimeInfo('21', true); +// //console.log(info); +// })(); diff --git a/consumet.ts/src/providers/meta/tmdb.ts b/consumet.ts/src/providers/meta/tmdb.ts new file mode 100644 index 00000000..6a422b4c --- /dev/null +++ b/consumet.ts/src/providers/meta/tmdb.ts @@ -0,0 +1,449 @@ +import { + ISearch, + IAnimeInfo, + IAnimeResult, + ISource, + IEpisodeServer, + MovieParser, + TvType, + IMovieResult, + IMovieInfo, + ProxyConfig, + IMovieEpisode, +} from '../../models'; +import { IPeopleResult } from '../../models/types'; +import { compareTwoStrings } from '../../utils'; +import FlixHQ from '../movies/flixhq'; +import { AxiosAdapter } from 'axios'; + +class TMDB extends MovieParser { + override readonly name = 'TMDB'; + protected override baseUrl = 'https://www.themoviedb.org'; + protected apiUrl = 'https://api.themoviedb.org/3'; + protected override logo = 'https://pbs.twimg.com/profile_images/1243623122089041920/gVZIvphd_400x400.jpg'; + protected override classPath = 'META.TMDB'; + override supportedTypes = new Set([TvType.MOVIE, TvType.TVSERIES, TvType.ANIME]); + + private provider: MovieParser; + + constructor( + private apiKey: string = '5201b54eb0968700e693a30576d7d4dc', + provider?: MovieParser, + proxyConfig?: ProxyConfig, + adapter?: AxiosAdapter + ) { + super(proxyConfig, adapter); + this.provider = provider || new FlixHQ(); + } + + /** + * @param type trending type: tv series, movie, people or all + * @param timePeriod trending time period day or week + * @param page page number + */ + fetchTrending = async ( + type: string | 'all', + timePeriod: 'day' | 'week' = 'day', + page: number = 1 + ): Promise> => { + const trendingUrl = `${this.apiUrl}/trending/${ + type.toLowerCase() === TvType.MOVIE.toLowerCase() + ? 'movie' + : type.toLowerCase() === TvType.TVSERIES.toLowerCase() + ? 'tv' + : type.toLowerCase() === TvType.PEOPLE.toLowerCase() + ? 'person' + : 'all' + }/${timePeriod}?page=${page}&api_key=${this.apiKey}&language=en-US`; + + const result: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + + try { + const { data } = await this.client.get(trendingUrl); + + if (data.results.length < 1) return result; + + result.hasNextPage = page + 1 <= data.total_pages; + result.currentPage = page; + result.totalResults = data.total_results; + result.totalPages = data.total_pages; + + result.results = data.results.map((result: any) => { + if (result.media_type !== 'person') { + const date = new Date(result?.release_date || result?.first_air_date); + + const movie: IMovieResult = { + id: result.id, + title: result?.title || result?.name, + image: `https://image.tmdb.org/t/p/original${result?.poster_path}`, + type: result.media_type === 'movie' ? TvType.MOVIE : TvType.TVSERIES, + rating: result?.vote_average || 0, + releaseDate: `${date.getFullYear()}` || '0', + }; + + return movie; + } else { + const user: IPeopleResult = { + id: result.id, + name: result.name, + rating: result.popularity, + image: `https://image.tmdb.org/t/p/original${result?.profile_path}`, + movies: [], + }; + + user.movies = result['known_for'].map((movie: any) => { + const date = new Date(movie?.release_date || movie?.first_air_date); + + const xmovie: IMovieResult = { + id: movie.id, + title: movie?.title || movie?.name, + image: `https://image.tmdb.org/t/p/original${movie?.poster_path}`, + type: movie.media_type === 'movie' ? TvType.MOVIE : TvType.TVSERIES, + rating: movie?.vote_average || 0, + releaseDate: `${date.getFullYear()}` || '0', + }; + + return xmovie; + }); + + return user; + } + }); + + return result; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * @param query search query + * @param page page number + */ + override search = async ( + query: string, + page: number = 1 + ): Promise> => { + const searchUrl = `${this.apiUrl}/search/multi?api_key=${this.apiKey}&language=en-US&page=${page}&include_adult=false&query=${query}`; + + const search: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + + try { + const { data } = await this.client.get(searchUrl); + + if (data.results.length < 1) return search; + + search.hasNextPage = page + 1 <= data.total_pages; + search.currentPage = page; + search.totalResults = data.total_results; + search.totalPages = data.total_pages; + + data.results.forEach((result: any) => { + const date = new Date(result?.release_date || result?.first_air_date); + + const movie: IMovieResult = { + id: result.id, + title: result?.title || result?.name, + image: `https://image.tmdb.org/t/p/original${result?.poster_path}`, + type: result.media_type === 'movie' ? TvType.MOVIE : TvType.TVSERIES, + rating: result?.vote_average || 0, + releaseDate: `${date.getFullYear()}` || '0', + }; + + return search.results.push(movie); + }); + + return search; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * @param id media id (anime or movie/tv) + * @param type movie or tv + */ + override fetchMediaInfo = async (mediaId: string, type: string): Promise => { + type = type.toLowerCase() === 'movie' ? 'movie' : 'tv'; + const infoUrl = `${this.apiUrl}/${type}/${mediaId}?api_key=${this.apiKey}&language=en-US&append_to_response=release_dates,watch/providers,alternative_titles,credits,external_ids,images,keywords,recommendations,reviews,similar,translations,videos&include_image_language=en`; + + const info: IMovieInfo = { + id: mediaId, + title: '', + }; + + try { + //request api to get media info from tmdb + const { data } = await this.client.get(infoUrl); + + //get provider id from title and year (if available) to get the correct provider id for the movie/tv series (e.g. flixhq) + const providerId = await this.findIdFromTitle(data?.title || data?.name, { + type: type === 'movie' ? TvType.MOVIE : TvType.TVSERIES, + totalSeasons: data?.number_of_seasons, + totalEpisodes: data?.number_of_episodes, + year: new Date(data?.release_year || data?.first_air_date).getFullYear(), + }); + + //fetch media info from provider + const InfoFromProvider = await this.provider.fetchMediaInfo(providerId as string); + + info.id = providerId as string; + + //check if the movie so episode id does not show on tv shows + if (type === 'movie') info.episodeId = InfoFromProvider?.episodes![0]?.id; + + info.title = data?.title || data?.name; + info.translations = data?.translations?.translations.map((translation: any) => ({ + title: translation.data?.title || data?.name || undefined, + description: translation.data?.overview || undefined, + language: translation?.english_name || undefined, + })); + + //images + info.image = `https://image.tmdb.org/t/p/original${data?.poster_path}`; + info.cover = `https://image.tmdb.org/t/p/original${data?.backdrop_path}`; + info.logos = data?.images?.logos.map( + (logo: { file_path: string; aspect_ratio: number; width: number }) => ({ + url: `https://image.tmdb.org/t/p/original${logo.file_path}`, + aspectRatio: logo?.aspect_ratio, + width: logo?.width, + }) + ); + + info.type = type === 'movie' ? TvType.MOVIE : TvType.TVSERIES; + info.rating = data?.vote_average || 0; + info.releaseDate = data?.release_date || data?.first_air_date; + info.description = data?.overview; + info.genres = data?.genres.map((genre: { name: string }) => genre.name); + info.duration = data?.runtime || data?.episode_run_time[0]; + info.totalEpisodes = data?.number_of_episodes; + info.totalSeasons = data?.number_of_seasons as number; + info.directors = data?.credits?.crew + .filter((crew: { job: string }) => crew.job === 'Director') + .map((crew: { name: string }) => crew.name); + info.writers = data?.credits?.crew + .filter((crew: { job: string }) => crew.job === 'Screenplay') + .map((crew: { name: string }) => crew.name); + info.actors = data?.credits?.cast.map((cast: { name: string }) => cast.name); + info.trailer = { + id: data?.videos?.results[0]?.key, + site: data?.videos?.results[0]?.site, + url: `https://www.youtube.com/watch?v=${data?.videos?.results[0]?.key}`, + }; + + info.mappings = { + imdb: data?.external_ids?.imdb_id || undefined, + tmdb: data?.id || undefined, + }; + + info.similar = + data?.similar?.results?.length <= 0 + ? undefined + : data?.similar?.results.map((result: any) => { + return { + id: result.id, + title: result.title || result.name, + image: `https://image.tmdb.org/t/p/original${result.poster_path}`, + type: type === 'movie' ? TvType.MOVIE : TvType.TVSERIES, + rating: result.vote_average || 0, + releaseDate: result.release_date || result.first_air_date, + }; + }); + + info.recommendations = + data?.recommendations?.results?.length <= 0 + ? undefined + : data?.recommendations?.results.map((result: any) => { + return { + id: result.id, + title: result.title || result.name, + image: `https://image.tmdb.org/t/p/original${result.poster_path}`, + type: type === 'movie' ? TvType.MOVIE : TvType.TVSERIES, + rating: result.vote_average || 0, + releaseDate: result.release_date || result.first_air_date, + }; + }); + + const totalSeasons = (info?.totalSeasons as number) || 0; + if (type === 'tv' && totalSeasons > 0) { + const seasonUrl = (season: string) => + `${this.apiUrl}/tv/${mediaId}/season/${season}?api_key=${this.apiKey}`; + + info.seasons = []; + const seasons = info.seasons as any[]; + + const providerEpisodes = InfoFromProvider?.episodes as any[]; + + if (providerEpisodes?.length < 1) return info; + + info.nextAiringEpisode = data?.next_episode_to_air + ? { + season: data.next_episode_to_air?.season_number || undefined, + episode: data.next_episode_to_air?.episode_number || undefined, + releaseDate: data.next_episode_to_air?.air_date || undefined, + title: data.next_episode_to_air?.name || undefined, + description: data.next_episode_to_air?.overview || undefined, + runtime: data.next_episode_to_air?.runtime || undefined, + } + : undefined; + + for (let i = 1; i <= totalSeasons; i++) { + const { data: seasonData } = await this.client.get(seasonUrl(i.toString())); + + //find season in each episode (providerEpisodes) + const seasonEpisodes = providerEpisodes?.filter(episode => episode.season === i); + const episodes = + seasonData?.episodes?.length <= 0 + ? undefined + : seasonData?.episodes.map((episode: any): IMovieEpisode => { + //find episode in each season (seasonEpisodes) + const episodeFromProvider = seasonEpisodes?.find( + ep => ep.number === episode.episode_number + ); + + return { + id: episodeFromProvider?.id, + title: episode.name, + episode: episode.episode_number, + season: episode.season_number, + releaseDate: episode.air_date, + description: episode.overview, + url: episodeFromProvider?.url || undefined, + img: !episode?.still_path + ? undefined + : { + mobile: `https://image.tmdb.org/t/p/w300${episode.still_path}`, + hd: `https://image.tmdb.org/t/p/w780${episode.still_path}`, + }, + }; + }); + + seasons.push({ + season: i, + image: !seasonData?.poster_path + ? undefined + : { + mobile: `https://image.tmdb.org/t/p/w300${seasonData.poster_path}`, + hd: `https://image.tmdb.org/t/p/w780${seasonData.poster_path}`, + }, + episodes, + isReleased: seasonData?.episodes[0]?.air_date > new Date().toISOString() ? false : true, + }); + } + } + } catch (err) { + throw new Error((err as Error).message); + } + + return info; + }; + + /** + * Find the id of a media from its title. and extra data. (year, totalSeasons, totalEpisodes) + * @param title + * @param extraData + * @returns id of the media + */ + private findIdFromTitle = async ( + title: string, + extraData: { + type: TvType; + year?: number; + totalSeasons?: number; + totalEpisodes?: number; + [key: string]: any; + } + ): Promise => { + //clean title + title = title.replace(/[^a-zA-Z0-9 ]/g, '').toLowerCase(); + + const findMedia = (await this.provider.search(title)) as ISearch; + if (findMedia.results.length === 0) return ''; + + // console.log(findMedia.results); + // console.log(extraData); + + // Sort the retrieved info for more accurate results. + findMedia.results.sort((a, b) => { + const targetTitle = title; + + let firstTitle: string; + let secondTitle: string; + + if (typeof a.title == 'string') firstTitle = a?.title as string; + else firstTitle = (a?.title as string) ?? ''; + + if (typeof b.title == 'string') secondTitle = b.title as string; + else secondTitle = (b?.title as string) ?? ''; + + const firstRating = compareTwoStrings(targetTitle, firstTitle.toLowerCase()); + const secondRating = compareTwoStrings(targetTitle, secondTitle.toLowerCase()); + + // Sort in descending order + return secondRating - firstRating; + }); + + //remove results that dont match the type + findMedia.results = findMedia.results.filter(result => { + if (extraData.type === TvType.MOVIE) return (result.type as string) === TvType.MOVIE; + else if (extraData.type === TvType.TVSERIES) return (result.type as string) === TvType.TVSERIES; + else return result; + }); + + // if extraData contains a year, filter out the results that don't match the year + if (extraData && extraData.year && extraData.type === TvType.MOVIE) { + findMedia.results = findMedia.results.filter(result => { + return result.releaseDate?.split('-')[0] === extraData.year; + }); + } + + // console.log({ test1: findMedia.results }); + + // Check if the result contains the total number of seasons and compare it to the extraData. + // Allow for a range of ±2 seasons and ensure that the seasons value is a number. + if (extraData && extraData.totalSeasons && extraData.type === TvType.TVSERIES) { + findMedia.results = findMedia.results.filter(result => { + const totalSeasons = (result.seasons as number) || 0; + const extraDataSeasons = (extraData.totalSeasons as number) || 0; + return totalSeasons >= extraDataSeasons - 2 && totalSeasons <= extraDataSeasons + 2; + }); + } + + // console.log(findMedia.results); + + return findMedia?.results[0]?.id || undefined; + }; + + /** + * @param id media id (anime or movie/tv) + * @param args optional arguments + */ + override fetchEpisodeSources = async (id: string, ...args: any): Promise => { + return this.provider.fetchEpisodeSources(id, ...args); + }; + + /** + * @param episodeId episode id + * @param args optional arguments + **/ + override fetchEpisodeServers = async (episodeId: string, ...args: any): Promise => { + return this.provider.fetchEpisodeServers(episodeId, ...args); + }; +} + +// (async () => { +// const tmdb = new TMDB(); +// const search = await tmdb.search('the flash'); +// const info = await tmdb.fetchMediaInfo(search.results[0].id, search.results![0].type as string); +// // console.log(info); +// })(); + +export default TMDB; diff --git a/consumet.ts/src/providers/movies/dramacool.ts b/consumet.ts/src/providers/movies/dramacool.ts new file mode 100644 index 00000000..8e0f05ff --- /dev/null +++ b/consumet.ts/src/providers/movies/dramacool.ts @@ -0,0 +1,219 @@ +import { load } from 'cheerio'; +import { AxiosAdapter } from 'axios'; + +import { + MovieParser, + TvType, + IMovieInfo, + IEpisodeServer, + StreamingServers, + ISource, + IMovieResult, + ISearch, + MediaStatus, +} from '../../models'; +import { MixDrop, AsianLoad, StreamTape, StreamSB } from '../../extractors'; + +class DramaCool extends MovieParser { + override readonly name = 'DramaCool'; + protected override baseUrl = 'https://dramacool.com.pa'; + protected override logo = + 'https://play-lh.googleusercontent.com/IaCb2JXII0OV611MQ-wSA8v_SAs9XF6E3TMDiuxGGXo4wp9bI60GtDASIqdERSTO5XU'; + protected override classPath = 'MOVIES.DramaCool'; + override supportedTypes = new Set([TvType.MOVIE, TvType.TVSERIES]); + + override search = async (query: string, page: number = 1): Promise> => { + try { + const searchResult: ISearch = { + currentPage: page, + totalPages: page, + hasNextPage: false, + results: [], + }; + + const { data } = await this.client.get( + `${this.baseUrl}/search?keyword=${query.replace(/[\W_]+/g, '-')}&page=${page}` + ); + + const $ = load(data); + + const navSelector = 'ul.pagination'; + + searchResult.hasNextPage = + $(navSelector).length > 0 ? !$(navSelector).children().last().hasClass('selected') : false; + + const lastPage = $(navSelector).children().last().find('a').attr('href'); + if ( lastPage != undefined && lastPage != "" && lastPage.includes("page=") ) + { + const maxPage = new URLSearchParams(lastPage).get("page"); + if (maxPage != null && !isNaN(parseInt(maxPage))) + searchResult.totalPages = parseInt(maxPage); + else if (searchResult.hasNextPage) + searchResult.totalPages = page + 1; + }else if (searchResult.hasNextPage) + searchResult.totalPages = page + 1; + + $('div.block > div.tab-content > ul.list-episode-item > li').each((i, el) => { + searchResult.results.push({ + id: $(el).find('a').attr('href')?.slice(1)!, + title: $(el).find('a > h3').text(), + url: `${this.baseUrl}${$(el).find('a').attr('href')}`, + image: $(el).find('a > img').attr('data-original'), + }); + }); + return searchResult; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchMediaInfo = async (mediaId: string): Promise => { + try { + const realMediaId = mediaId; + if (!mediaId.startsWith(this.baseUrl)) mediaId = `${this.baseUrl}/${mediaId}`; + + const mediaInfo: IMovieInfo = { + id: '', + title: '', + }; + + const { data } = await this.client.get(mediaId); + const $ = load(data); + + mediaInfo.id = realMediaId; + + const duration = $('div.details div.info p:contains("Duration:")').first().text().trim(); + if ( duration != "" ) + mediaInfo.duration = duration.replace("Duration:", "").trim(); + const status = $('div.details div.info p:contains("Status:")').find('a').first().text().trim(); + switch (status) { + case 'Ongoing': + mediaInfo.status = MediaStatus.ONGOING; + break; + case 'Completed': + mediaInfo.status = MediaStatus.COMPLETED; + break; + default: + mediaInfo.status = MediaStatus.UNKNOWN; + break; + } + mediaInfo.genres = []; + const genres = $('div.details div.info p:contains("Genre:")'); + genres.each((_index, element) => { + $(element).find('a').each((_, anchorElement) => { + mediaInfo.genres?.push($(anchorElement).text()); + }); + }); + + mediaInfo.title = $('.info > h1:nth-child(1)').text(); + mediaInfo.otherNames = $('.other_name > a') + .map((i, el) => $(el).text().trim()) + .get(); + mediaInfo.image = $('div.details > div.img > img').attr('src'); + mediaInfo.description = $('div.details div.info p:nth-child(6)').text(); + mediaInfo.releaseDate = this.removeContainsFromString( + $('div.details div.info p:contains("Released:")').text(), + 'Released' + ); + + mediaInfo.episodes = []; + $('div.content-left > div.block-tab > div > div > ul > li').each((i, el) => { + mediaInfo.episodes?.push({ + id: $(el).find('a').attr('href')?.split('.html')[0].slice(1)!, + title: $(el).find('h3').text().replace(mediaInfo.title.toString(), '').trim(), + episode: parseFloat( + $(el).find('a').attr('href')?.split('-episode-')[1].split('.html')[0].split('-').join('.')! + ), + subType: $(el).find('span.type').text(), + releaseDate: $(el).find('span.time').text(), + url: `${this.baseUrl}${$(el).find('a').attr('href')}`, + }); + }); + mediaInfo.episodes.reverse(); + + return mediaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override async fetchEpisodeServers(episodeId: string, ...args: any): Promise { + try { + const episodeServers: IEpisodeServer[] = []; + + if (!episodeId.includes('.html')) episodeId = `${this.baseUrl}/${episodeId}.html`; + + const { data } = await this.client.get(episodeId); + const $ = load(data); + + $('div.anime_muti_link > ul > li').map(async (i, ele) => { + const url = $(ele).attr('data-video')!; + let name = $(ele).attr('class')!.replace('selected', '').trim(); + if (name.includes('Standard')) { + name = StreamingServers.AsianLoad; + } + episodeServers.push({ + name: name, + url: url.startsWith('//') ? url?.replace('//', 'https://') : url, + }); + }); + + return episodeServers; + } catch (err) { + throw new Error((err as Error).message); + } + } + + override fetchEpisodeSources = async ( + episodeId: string, + server: StreamingServers = StreamingServers.AsianLoad + ): Promise => { + if (episodeId.startsWith('http')) { + const serverUrl = new URL(episodeId); + switch (server) { + case StreamingServers.AsianLoad: + return { + ...(await new AsianLoad(this.proxyConfig, this.adapter).extract(serverUrl)), + }; + case StreamingServers.MixDrop: + return { + sources: await new MixDrop(this.proxyConfig, this.adapter).extract(serverUrl), + }; + case StreamingServers.StreamTape: + return { + sources: await new StreamTape(this.proxyConfig, this.adapter).extract(serverUrl), + }; + case StreamingServers.StreamSB: + return { + sources: await new StreamSB(this.proxyConfig, this.adapter).extract(serverUrl), + }; + default: + throw new Error('Server not supported'); + } + } + + try { + if (!episodeId.includes('.html')) episodeId = `${this.baseUrl}/${episodeId}.html`; + + const servers = await this.fetchEpisodeServers(episodeId); + const i = servers.findIndex(s => s.name.toLowerCase() === server.toLowerCase()); + if (i === -1) { + throw new Error(`Server ${server} not found`); + } + const serverUrl: URL = new URL( + servers.filter(s => s.name.toLowerCase() === server.toLowerCase())[0].url + ); + + return await this.fetchEpisodeSources(serverUrl.href, server); + } catch (err) { + throw new Error((err as Error).message); + } + }; + + private removeContainsFromString = (str: string, contains: string) => { + contains = contains.toLowerCase(); + return str.toLowerCase().replace(/\n/g, '').replace(`${contains}:`, '').trim(); + }; +} + +export default DramaCool; diff --git a/consumet.ts/src/providers/movies/flixhq.ts b/consumet.ts/src/providers/movies/flixhq.ts new file mode 100644 index 00000000..a7d2db82 --- /dev/null +++ b/consumet.ts/src/providers/movies/flixhq.ts @@ -0,0 +1,474 @@ +import { load } from 'cheerio'; + +import { + MovieParser, + TvType, + IMovieInfo, + IEpisodeServer, + StreamingServers, + ISource, + IMovieResult, + ISearch, +} from '../../models'; +import { MixDrop, VidCloud } from '../../extractors'; + +class FlixHQ extends MovieParser { + override readonly name = 'FlixHQ'; + protected override baseUrl = 'https://flixhq.to'; + protected override logo = 'https://upload.wikimedia.org/wikipedia/commons/7/7a/MyAnimeList_Logo.png'; + protected override classPath = 'MOVIES.FlixHQ'; + override supportedTypes = new Set([TvType.MOVIE, TvType.TVSERIES]); + + /** + * + * @param query search query string + * @param page page number (default 1) (optional) + */ + override search = async (query: string, page: number = 1): Promise> => { + const searchResult: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + try { + const { data } = await this.client.get( + `${this.baseUrl}/search/${query.replace(/[\W_]+/g, '-')}?page=${page}` + ); + + const $ = load(data); + + const navSelector = 'div.pre-pagination:nth-child(3) > nav:nth-child(1) > ul:nth-child(1)'; + + searchResult.hasNextPage = + $(navSelector).length > 0 ? !$(navSelector).children().last().hasClass('active') : false; + + $('.film_list-wrap > div.flw-item').each((i, el) => { + const releaseDate = $(el).find('div.film-detail > div.fd-infor > span:nth-child(1)').text(); + searchResult.results.push({ + id: $(el).find('div.film-poster > a').attr('href')?.slice(1)!, + title: $(el).find('div.film-detail > h2 > a').attr('title')!, + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + seasons: releaseDate.includes('SS') ? parseInt(releaseDate.split('SS')[1]) : undefined, + type: + $(el).find('div.film-detail > div.fd-infor > span.float-right').text() === 'Movie' + ? TvType.MOVIE + : TvType.TVSERIES, + }); + }); + + return searchResult; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param mediaId media link or id + */ + override fetchMediaInfo = async (mediaId: string): Promise => { + if (!mediaId.startsWith(this.baseUrl)) { + mediaId = `${this.baseUrl}/${mediaId}`; + } + + const movieInfo: IMovieInfo = { + id: mediaId.split('to/').pop()!, + title: '', + url: mediaId, + }; + try { + const { data } = await this.client.get(mediaId); + const $ = load(data); + const recommendationsArray: IMovieResult[] = []; + + $( + 'div.movie_information > div.container > div.m_i-related > div.film-related > section.block_area > div.block_area-content > div.film_list-wrap > div.flw-item' + ).each((i, el) => { + recommendationsArray.push({ + id: $(el).find('div.film-poster > a').attr('href')?.slice(1)!, + title: $(el).find('div.film-detail > h3.film-name > a').text(), + image: $(el).find('div.film-poster > img').attr('data-src'), + duration: + $(el).find('div.film-detail > div.fd-infor > span.fdi-duration').text().replace('m', '') ?? null, + type: + $(el).find('div.film-detail > div.fd-infor > span.fdi-type').text().toLowerCase() === 'tv' + ? TvType.TVSERIES + : TvType.MOVIE ?? null, + }); + }); + + const uid = $('.watch_block').attr('data-id')!; + movieInfo.cover = $('div.w_b-cover').attr('style')?.slice(22).replace(')', '').replace(';', ''); + movieInfo.title = $('.heading-name > a:nth-child(1)').text(); + movieInfo.image = $('.m_i-d-poster > div:nth-child(1) > img:nth-child(1)').attr('src'); + movieInfo.description = $('.description').text(); + movieInfo.type = movieInfo.id.split('/')[0] === 'tv' ? TvType.TVSERIES : TvType.MOVIE; + movieInfo.releaseDate = $('div.row-line:nth-child(3)').text().replace('Released: ', '').trim(); + movieInfo.genres = $('div.row-line:nth-child(2) > a') + .map((i, el) => $(el).text().split('&')) + .get() + .map(v => v.trim()); + movieInfo.casts = $('div.row-line:nth-child(5) > a') + .map((i, el) => $(el).text()) + .get(); + movieInfo.tags = $('div.row-line:nth-child(6) > h2') + .map((i, el) => $(el).text()) + .get(); + movieInfo.production = $('div.row-line:nth-child(4) > a:nth-child(2)').text(); + movieInfo.country = $('div.row-line:nth-child(1) > a:nth-child(2)').text(); + movieInfo.duration = $('span.item:nth-child(3)').text(); + movieInfo.rating = parseFloat($('span.item:nth-child(2)').text()); + movieInfo.recommendations = recommendationsArray as any; + const ajaxReqUrl = (id: string, type: string, isSeasons: boolean = false) => + `${this.baseUrl}/ajax/${type === 'movie' ? type : `v2/${type}`}/${ + isSeasons ? 'seasons' : 'episodes' + }/${id}`; + + if (movieInfo.type === TvType.TVSERIES) { + const { data } = await this.client.get(ajaxReqUrl(uid, 'tv', true)); + const $$ = load(data); + const seasonsIds = $$('.dropdown-menu > a') + .map((i, el) => $(el).attr('data-id')) + .get(); + + movieInfo.episodes = []; + let season = 1; + for (const id of seasonsIds) { + const { data } = await this.client.get(ajaxReqUrl(id, 'season')); + const $$$ = load(data); + + $$$('.nav > li') + .map((i, el) => { + const episode = { + id: $$$(el).find('a').attr('id')!.split('-')[1], + title: $$$(el).find('a').attr('title')!, + number: parseInt($$$(el).find('a').attr('title')!.split(':')[0].slice(3).trim()), + season: season, + url: `${this.baseUrl}/ajax/v2/episode/servers/${$$$(el).find('a').attr('id')!.split('-')[1]}`, + }; + movieInfo.episodes?.push(episode); + }) + .get(); + season++; + } + } else { + movieInfo.episodes = [ + { + id: uid, + title: movieInfo.title + ' Movie', + url: `${this.baseUrl}/ajax/movie/episodes/${uid}`, + }, + ]; + } + + return movieInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId episode id + * @param mediaId media id + * @param server server type (default `VidCloud`) (optional) + */ + override fetchEpisodeSources = async ( + episodeId: string, + mediaId: string, + server: StreamingServers = StreamingServers.UpCloud + ): Promise => { + if (episodeId.startsWith('http')) { + const serverUrl = new URL(episodeId); + switch (server) { + case StreamingServers.MixDrop: + return { + headers: { Referer: serverUrl.href }, + sources: await new MixDrop(this.proxyConfig, this.adapter).extract(serverUrl), + }; + case StreamingServers.VidCloud: + return { + headers: { Referer: serverUrl.href }, + ...(await new VidCloud(this.proxyConfig, this.adapter).extract(serverUrl, true)), + }; + case StreamingServers.UpCloud: + return { + headers: { Referer: serverUrl.href }, + ...(await new VidCloud(this.proxyConfig, this.adapter).extract(serverUrl)), + }; + default: + return { + headers: { Referer: serverUrl.href }, + sources: await new MixDrop(this.proxyConfig, this.adapter).extract(serverUrl), + }; + } + } + + try { + const servers = await this.fetchEpisodeServers(episodeId, mediaId); + + const i = servers.findIndex(s => s.name === server); + + if (i === -1) { + throw new Error(`Server ${server} not found`); + } + + const { data } = await this.client.get( + `${this.baseUrl}/ajax/get_link/${servers[i].url.split('.').slice(-1).shift()}` + ); + + const serverUrl: URL = new URL(data.link); + + return await this.fetchEpisodeSources(serverUrl.href, mediaId, server); + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId takes episode link or movie id + * @param mediaId takes movie link or id (found on movie info object) + */ + override fetchEpisodeServers = async (episodeId: string, mediaId: string): Promise => { + if (!episodeId.startsWith(this.baseUrl + '/ajax') && !mediaId.includes('movie')) + episodeId = `${this.baseUrl}/ajax/v2/episode/servers/${episodeId}`; + else episodeId = `${this.baseUrl}/ajax/movie/episodes/${episodeId}`; + + try { + const { data } = await this.client.get(episodeId); + const $ = load(data); + + const servers = $('.nav > li') + .map((i, el) => { + const server = { + name: mediaId.includes('movie') + ? $(el).find('a').attr('title')!.toLowerCase() + : $(el).find('a').attr('title')!.slice(6).trim().toLowerCase(), + url: `${this.baseUrl}/${mediaId}.${ + !mediaId.includes('movie') + ? $(el).find('a').attr('data-id') + : $(el).find('a').attr('data-linkid') + }`.replace( + !mediaId.includes('movie') ? /\/tv\// : /\/movie\//, + !mediaId.includes('movie') ? '/watch-tv/' : '/watch-movie/' + ), + }; + return server; + }) + .get(); + return servers; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchRecentMovies = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const movies = $( + 'section.block_area:contains("Latest Movies") > div:nth-child(2) > div:nth-child(1) > div.flw-item' + ) + .map((i, el) => { + const releaseDate = $(el).find('div.film-detail > div.fd-infor > span:nth-child(1)').text(); + const movie: any = { + id: $(el).find('div.film-poster > a').attr('href')?.slice(1)!, + title: $(el).find('div.film-detail > h3.film-name > a').attr('title')!, + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + duration: $(el).find('div.film-detail > div.fd-infor > span.fdi-duration').text() || null, + type: + $(el).find('div.film-detail > div.fd-infor > span.float-right').text() === 'Movie' + ? TvType.MOVIE + : TvType.TVSERIES, + }; + return movie; + }) + .get(); + return movies; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchRecentTvShows = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const tvshows = $( + 'section.block_area:contains("Latest TV Shows") > div:nth-child(2) > div:nth-child(1) > div.flw-item' + ) + .map((i, el) => { + const tvshow = { + id: $(el).find('div.film-poster > a').attr('href')?.slice(1)!, + title: $(el).find('div.film-detail > h3.film-name > a').attr('title')!, + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + season: $(el).find('div.film-detail > div.fd-infor > span:nth-child(1)').text(), + latestEpisode: $(el).find('div.film-detail > div.fd-infor > span:nth-child(3)').text() || null, + type: + $(el).find('div.film-detail > div.fd-infor > span.float-right').text() === 'Movie' + ? TvType.MOVIE + : TvType.TVSERIES, + }; + return tvshow; + }) + .get(); + return tvshows; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchTrendingMovies = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const movies = $('div#trending-movies div.film_list-wrap div.flw-item') + .map((i, el) => { + const releaseDate = $(el).find('div.film-detail > div.fd-infor > span:nth-child(1)').text(); + const movie: any = { + id: $(el).find('div.film-poster > a').attr('href')?.slice(1)!, + title: $(el).find('div.film-detail > h3.film-name > a').attr('title')!, + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + duration: $(el).find('div.film-detail > div.fd-infor > span.fdi-duration').text() || null, + type: + $(el).find('div.film-detail > div.fd-infor > span.float-right').text() === 'Movie' + ? TvType.MOVIE + : TvType.TVSERIES, + }; + return movie; + }) + .get(); + return movies; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchTrendingTvShows = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const tvshows = $('div#trending-tv div.film_list-wrap div.flw-item') + .map((i, el) => { + const tvshow = { + id: $(el).find('div.film-poster > a').attr('href')?.slice(1)!, + title: $(el).find('div.film-detail > h3.film-name > a').attr('title')!, + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + season: $(el).find('div.film-detail > div.fd-infor > span:nth-child(1)').text(), + latestEpisode: $(el).find('div.film-detail > div.fd-infor > span:nth-child(3)').text() || null, + type: + $(el).find('div.film-detail > div.fd-infor > span.float-right').text() === 'Movie' + ? TvType.MOVIE + : TvType.TVSERIES, + }; + return tvshow; + }) + .get(); + return tvshows; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchByCountry = async (country: string, page: number = 1): Promise> => { + const result: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + const navSelector = 'div.pre-pagination:nth-child(3) > nav:nth-child(1) > ul:nth-child(1)'; + + try { + const { data } = await this.client.get(`${this.baseUrl}/country/${country}/?page=${page}`); + const $ = load(data); + + result.hasNextPage = + $(navSelector).length > 0 ? !$(navSelector).children().last().hasClass('active') : false; + + $('div.container > section.block_area > div.block_area-content > div.film_list-wrap > div.flw-item') + .each((i, el) => { + result.results.push({ + id: $(el).find('div.film-poster > a').attr('href')?.slice(1) ?? '', + title: $(el).find('div.film-detail > h2.film-name > a').attr('title') ?? '', + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + season: $(el).find('div.film-detail > div.fd-infor > span:nth-child(1)').text(), + latestEpisode: $(el).find('div.film-detail > div.fd-infor > span:nth-child(3)').text() ?? null, + type: + $(el).find('div.film-detail > div.fd-infor > span.float-right').text() === 'Movie' + ? TvType.MOVIE + : TvType.TVSERIES, + + }) + }) + .get(); + return result; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchByGenre = async (genre: string, page: number = 1): Promise> => { + const result: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + try { + const { data } = await this.client.get( + `${this.baseUrl}/genre/${genre}?page=${page}` + ); + + const $ = load(data); + + const navSelector = 'div.pre-pagination:nth-child(3) > nav:nth-child(1) > ul:nth-child(1)'; + + result.hasNextPage = + $(navSelector).length > 0 ? !$(navSelector).children().last().hasClass('active') : false; + + $('.film_list-wrap > div.flw-item').each((i, el) => { + const releaseDate = $(el).find('div.film-detail > div.fd-infor > span:nth-child(1)').text(); + result.results.push({ + id: $(el).find('div.film-poster > a').attr('href')?.slice(1) ?? '', + title: $(el).find('div.film-detail > h2 > a').attr('title') ?? '', + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + seasons: releaseDate.includes('SS') ? parseInt(releaseDate.split('SS')[1]) : undefined, + type: + $(el).find('div.film-detail > div.fd-infor > span.float-right').text() === 'Movie' + ? TvType.MOVIE + : TvType.TVSERIES, + }); + }); + + return result; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +// (async () => { +// const movie = new FlixHQ(); +// const search = await movie.search('the flash'); +// // const movieInfo = await movie.fetchEpisodeSources('1168337', 'tv/watch-vincenzo-67955'); +// // const recentTv = await movie.fetchTrendingTvShows(); +// // const genre = await movie.fetchByCountry('KR') +// // console.log(genre) +// })(); + +export default FlixHQ; diff --git a/consumet.ts/src/providers/movies/fmovies.ts b/consumet.ts/src/providers/movies/fmovies.ts new file mode 100644 index 00000000..285bafd1 --- /dev/null +++ b/consumet.ts/src/providers/movies/fmovies.ts @@ -0,0 +1,298 @@ +import { load } from 'cheerio'; +import { AxiosAdapter } from 'axios'; +import { substringAfter, substringBeforeLast } from '../../utils/utils'; + +import { + MovieParser, + TvType, + IMovieInfo, + IEpisodeServer, + StreamingServers, + ISource, + IMovieResult, + ISearch, + IMovieEpisode, + ProxyConfig, +} from '../../models'; +import { StreamTape, VizCloud } from '../../extractors'; + +class Fmovies extends MovieParser { + override readonly name = 'Fmovies'; + protected override baseUrl = 'https://fmovies.to'; + protected override logo = 'https://s1.bunnycdn.ru/assets/sites/fmovies/logo2.png'; + protected override classPath = 'MOVIES.Fmovies'; + override supportedTypes = new Set([TvType.MOVIE, TvType.TVSERIES]); + + private fmoviesResolver = ''; + private apiKey = ''; + + constructor(fmoviesResolver?: string, proxyConfig?: ProxyConfig, apiKey?: string, adapter?: AxiosAdapter) { + super(proxyConfig && proxyConfig.url ? proxyConfig : undefined, adapter); + this.fmoviesResolver = fmoviesResolver ?? this.fmoviesResolver; + this.apiKey = apiKey ?? this.apiKey; + } + + /** + * + * @param query search query string + * @param page page number (default 1) (optional) + */ + override search = async (query: string, page: number = 1): Promise> => { + const searchResult: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + try { + query = query.replace(/[\W_]+/g, '+'); + const vrf = await this.ev(query); + + const { data } = await this.client.get( + `${this.baseUrl}/search?keyword=${query}&vrf=${vrf}&page=${page}` + ); + + const $ = load(data); + + searchResult.hasNextPage = $('.pagination')?.find('.active').next().hasClass('disabled'); + + $('.filmlist > div.item').each((i, el) => { + const releaseDate = $(el).find('.meta').text(); + searchResult.results.push({ + id: $(el).find('a.title').attr('href')!.slice(1), + title: $(el).find('a.title').text()!, + url: `${this.baseUrl}/${$(el).find('a.title').attr('href')!.slice(1)}`, + image: $(el).find('img').attr('src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : parseInt(releaseDate).toString(), + seasons: releaseDate.includes('SS') ? parseInt(releaseDate.split('SS')[1]) : undefined, + type: $(el).find('i.type').text() === 'Movie' ? TvType.MOVIE : TvType.TVSERIES, + }); + }); + + return searchResult; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param mediaId media link or id + */ + override fetchMediaInfo = async (mediaId: string): Promise => { + if (!mediaId.startsWith(this.baseUrl)) { + mediaId = `${this.baseUrl}/${mediaId}`; + } + + const movieInfo: IMovieInfo = { + id: mediaId.split('to/').pop()!, + title: '', + url: mediaId, + }; + try { + const { data } = await this.client.get(mediaId); + + const $ = load(data); + const uid = $('#watch').attr('data-id')!; + + // TODO + // const recommendationsArray: IMovieResult[] = []; + // $( + // 'div.movie_information > div.container > div.m_i-related > div.film-related > section.block_area > div.block_area-content > div.film_list-wrap > div.flw-item' + // ).each((i, el) => { + // recommendationsArray.push({ + // id: $(el).find('div.film-poster > a').attr('href')?.slice(1)!, + // title: $(el).find('div.film-detail > h3.film-name > a').text(), + // image: $(el).find('div.film-poster > img').attr('data-src'), + // duration: + // $(el).find('div.film-detail > div.fd-infor > span.fdi-duration').text().replace('m', '') ?? null, + // type: + // $(el).find('div.film-detail > div.fd-infor > span.fdi-type').text().toLowerCase() === 'tv' + // ? TvType.TVSERIES + // : TvType.MOVIE ?? null, + // }); + // }); + const container = $('.watch-extra'); + movieInfo.cover = substringBeforeLast( + substringAfter($('#watch').find('.play')?.attr('style') ?? '', 'url('), + ')' + ); + movieInfo.title = container.find(`h1[itemprop="name"]`).text(); + movieInfo.image = container.find(`img[itemprop="image"]`).attr('src'); + movieInfo.description = container.find('div[itemprop="description"]')?.text()?.trim(); + movieInfo.type = movieInfo.id.split('/')[0] === 'series' ? TvType.TVSERIES : TvType.MOVIE; + movieInfo.releaseDate = container.find('span[itemprop="dateCreated"]')?.text()?.trim(); + + // TODO + // movieInfo.genres = $('div.row-line:nth-child(2) > a') + // .map((i, el) => $(el).text().split('&')) + // .get() + // .map(v => v.trim()); + // movieInfo.casts = $('div.row-line:nth-child(5) > a') + // .map((i, el) => $(el).text()) + // .get(); + // movieInfo.tags = $('div.row-line:nth-child(6) > h2') + // .map((i, el) => $(el).text()) + // .get(); + // movieInfo.production = $('div.row-line:nth-child(4) > a:nth-child(2)').text(); + // movieInfo.country = $('div.row-line:nth-child(1) > a:nth-child(2)').text(); + // movieInfo.duration = $('span.item:nth-child(3)').text(); + // movieInfo.rating = parseFloat($('span.item:nth-child(2)').text()); + // movieInfo.recommendations = recommendationsArray as any; + + const ajaxData = (await this.client.get(await this.ajaxReqUrl(uid))).data; + const $$ = load(ajaxData.html); + + movieInfo.episodes = []; + $$('.episode').each((i, el) => { + const episode: IMovieEpisode = { + id: $(el).find('a').attr('data-kname')!, + title: $(el).find('a')?.attr('title') ?? '', + }; + + if (movieInfo.type === TvType.TVSERIES) { + episode.number = parseInt($(el).find('a')?.attr('data-kname')!.split('-')[1]); + episode.season = parseInt($(el).find('a')?.attr('data-kname')!.split('-')[0]); + } + + movieInfo.episodes?.push(episode); + }); + + return movieInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId episode id + * @param mediaId media id + * @param server server type (default `Vizcloud`) (optional) + */ + override fetchEpisodeSources = async ( + episodeId: string, + mediaId: string, + server: StreamingServers = StreamingServers.VizCloud + ): Promise => { + if (episodeId.startsWith('http')) { + const serverUrl = new URL(episodeId); + switch (server) { + case StreamingServers.StreamTape: + return { + headers: { Referer: serverUrl.href }, + sources: await new StreamTape().extract(serverUrl), + }; + default: + return { + headers: { Referer: serverUrl.href }, + sources: await new VizCloud().extract(serverUrl, this.fmoviesResolver, this.apiKey), + }; + } + } + + try { + const servers = await this.fetchEpisodeServers(episodeId, mediaId); + const selectedServer = servers.find(s => s.name === server); + + if (!selectedServer) { + throw new Error(`Server ${server} not found`); + } + + const { data } = await this.client.get(`${this.baseUrl}/ajax/episode/info?id=${selectedServer.url}`); + + const serverUrl: URL = new URL(await this.decrypt(data.url)); + + return await this.fetchEpisodeSources(serverUrl.href, mediaId, server); + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId takes episode link or movie id + * @param mediaId takes movie link or id (found on movie info object) + */ + override fetchEpisodeServers = async (episodeId: string, mediaId: string): Promise => { + if (!mediaId.startsWith(this.baseUrl)) { + mediaId = `${this.baseUrl}/${mediaId}`; + } + + try { + const { data } = await this.client.get(mediaId); + const $ = load(data); + const uid = $('#watch').attr('data-id')!; + const epsiodeServers: IEpisodeServer[] = []; + + const ajaxData = (await this.client.get(await this.ajaxReqUrl(uid))).data; + const $$ = load(ajaxData.html); + const servers: { [key: string]: string } = {}; + + $$('.server').each((i, el) => { + const serverId = $(el).attr('data-id')!; + let serverName = $(el).text().toLowerCase().split('server')[1].trim(); + if (serverName == 'vidstream') { + serverName = 'vizcloud'; + } + servers[serverId] = serverName; + }); + + const el = $$(`a[data-kname="${episodeId}"]`); + try { + const serverString: { [key: string]: string } = JSON.parse(el.attr('data-ep')!); + for (const serverId in serverString) { + epsiodeServers.push({ + name: servers[serverId], + url: serverString[serverId], + }); + } + + return epsiodeServers; + } catch (err) { + console.log(err); + throw new Error('Episode not found'); + } + } catch (err) { + throw new Error('Episode not found'); + } + }; + + private async ev(query: string): Promise { + const { data } = await this.client.get( + `${this.fmoviesResolver}/fmovies-vrf?query=${encodeURIComponent(query)}&apikey=${this.apiKey}` + ); + return encodeURIComponent(data.url); + } + + private async decrypt(query: string): Promise { + const { data } = await this.client.get( + `${this.fmoviesResolver}/fmovies-decrypt?query=${encodeURIComponent(query)}&apikey=${this.apiKey}` + ); + return data.url; + } + + private async ajaxReqUrl(id: string) { + const vrf = await this.ev(id); + return `${this.baseUrl}/ajax/film/servers?id=${id}&vrf=${vrf}&token=`; + } +} + +// (async () => { +// const movie = new Fmovies("https://9anime.enimax.xyz", {url: "https://proxy.vnxservers.com/"}, "848624aaffec43808c86f5e47e3fa5b0"); +// const search = await movie.search('friends'); + +// // const search = await movie.fetchMediaInfo('series/friends-3rvj9'); +// // const search = await movie.fetchMediaInfo('movie/chimes-at-midnight-1qvnw'); +// // const search = await movie.fetchEpisodeSources('1-full','movie/chimes-at-midnight-1qvnw'); +// // const search = await movie.fetchMediaInfo('series/friends-3rvj9'); +// // console.log(JSON.stringify(movieInfo)); + +// console.log( +// search +// ); + +// // const recentTv = await movie.fetchTrendingTvShows(); +// // console.log(search); +// })(); + +export default Fmovies; diff --git a/consumet.ts/src/providers/movies/goku.ts b/consumet.ts/src/providers/movies/goku.ts new file mode 100644 index 00000000..62d64f9c --- /dev/null +++ b/consumet.ts/src/providers/movies/goku.ts @@ -0,0 +1,416 @@ +import { load } from 'cheerio'; + +import { + MovieParser, + TvType, + IMovieInfo, + IEpisodeServer, + StreamingServers, + ISource, + IMovieResult, + ISearch, + IMovieEpisode, +} from '../../models'; +import { MixDrop, VidCloud } from '../../extractors'; + +class Goku extends MovieParser { + override readonly name = 'Goku'; + protected override baseUrl = 'https://goku.sx'; + protected override logo = + 'https://img.goku.sx/xxrz/400x400/100/9c/e7/9ce7510639c4204bfe43904fad8f361f/9ce7510639c4204bfe43904fad8f361f.png'; + protected override classPath = 'MOVIES.Goku'; + override supportedTypes = new Set([TvType.MOVIE, TvType.TVSERIES]); + + /** + * + * @param query search query string + * @param page page number (default 1) (optional) + */ + override search = async (query: string, page: number = 1): Promise> => { + const searchResult: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + try { + const { data } = await this.client.get( + `${this.baseUrl}/search?keyword=${query.replace(/[\W_]+/g, '-')}&page=${page}` + ); + const $ = load(data); + + searchResult.hasNextPage = + $('.page-link').length > 0 ? $('.page-link').last().attr('title') === 'Last' : false; + + $('div.section-items > div.item').each((i, el) => { + const releaseDate = $(el).find('div.movie-info div.info-split > div:nth-child(1)').text(); + const rating = $(el).find('div.movie-info div.info-split div.is-rated').text(); + searchResult.results.push({ + id: $(el).find('.is-watch > a').attr('href')?.replace('/', '') ?? '', + title: $(el).find('div.movie-info h3.movie-name').text(), + url: `${this.baseUrl}${$(el).find('.is-watch > a').attr('href')}`, + image: $(el).find('div.movie-thumbnail > a > img').attr('src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + rating: isNaN(parseInt(rating)) ? undefined : parseFloat(rating), + type: + $(el).find('.is-watch > a').attr('href')?.indexOf('watch-series') ?? -1 > -1 + ? TvType.TVSERIES + : TvType.MOVIE, + }); + }); + + return searchResult; + + // const { data } = await this.client.get( + // `${this.baseUrl}/ajax/movie/search?keyword=${query.replace(/[\W_]+/g, '-')}&page=${page}` + // ); + // const $ = load(data); + + // $('div.item').each((i, ele) => { + // const url = $(ele).find('a')?.attr('href'); + // const releaseDate = $(ele).find('div.info-split > div:first-child').text(); + // const rating = $(ele).find('.is-rated').text(); + // searchResult.results.push({ + // id: url?.replace(this.baseUrl, '') ?? '', + // title: $(ele).find('h3.movie-name').text(), + // releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + // image: $(ele).find('div.movie-thumbnail > a > img').attr('src'), + // rating: isNaN(parseInt(rating)) ? undefined : parseFloat(rating), + // type: releaseDate.toLocaleLowerCase() !== 'tv' ? TvType.MOVIE : TvType.TVSERIES, + // }); + // }); + + // return searchResult; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param mediaId media link or id + */ + override fetchMediaInfo = async (mediaId: string): Promise => { + if (mediaId.startsWith(this.baseUrl)) { + mediaId = mediaId.replace(this.baseUrl + '/', ''); + } + + try { + const { data } = await this.client.get(`${this.baseUrl}/${mediaId}`); + const $ = load(data); + + const mediaInfo: IMovieInfo = { + id: mediaId, + title: '', + url: `${this.baseUrl}/${mediaId}`, + }; + + mediaInfo.title = $('div.movie-detail > div.is-name > h3').text(); + mediaInfo.image = $('.movie-thumbnail > img').attr('src'); + mediaInfo.description = $('.is-description > .text-cut').text(); + mediaInfo.type = mediaId.indexOf('watch-series') > -1 ? TvType.TVSERIES : TvType.MOVIE; + mediaInfo.genres = $("div.name:contains('Genres:')") + .siblings() + .find('a') + .map((i, el) => $(el).text()) + .get(); + mediaInfo.casts = $("div.name:contains('Cast:')") + .siblings() + .find('a') + .map((i, el) => $(el).text()) + .get(); + mediaInfo.production = $("div.name:contains('Production:')") + .siblings() + .find('a') + .map((i, el) => $(el).text()) + .get() + .join(); + mediaInfo.duration = $("div.name:contains('Duration:')").siblings().text().split('\n').join('').trim(); + + if (mediaInfo.type === TvType.TVSERIES) { + const { data } = await this.client.get( + `${this.baseUrl}/ajax/movie/seasons/${mediaInfo.id.split('-').pop()}` + ); + const $$ = load(data); + + const seasonsIds = $$('.dropdown-menu > a') + .map((i, el) => { + const seasonsId = $(el).text().replace('Season', '').trim(); + return { + id: $(el).attr('data-id'), + season: isNaN(parseInt(seasonsId)) ? undefined : parseInt(seasonsId), + }; + }) + .get(); + + mediaInfo.episodes = []; + + for (const season of seasonsIds) { + const { data } = await this.client.get(`${this.baseUrl}/ajax/movie/season/episodes/${season.id}`); + const $$$ = load(data); + + $$$('.item') + .map((i, el) => { + const episode = { + id: $$$(el).find('a').attr('data-id') ?? '', + title: $$$(el).find('a').attr('title') ?? '', + number: parseInt($$$(el).find('a').text()?.split(':')[0].trim().substring(3) ?? ''), + season: season.season, + url: $$$(el).find('a').attr('href'), + }; + mediaInfo.episodes?.push(episode); + }) + .get(); + } + } else { + mediaInfo.episodes = []; + $('meta').map((i, ele) => { + if ($(ele).attr('property') === 'og:url') { + const episode: IMovieEpisode = { + id: $(ele).attr('content')?.split('/').pop() ?? '', + title: mediaInfo.title.toString(), + url: $(ele).attr('content'), + }; + mediaInfo.episodes?.push(episode); + } + }); + } + + return mediaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId episode id + * @param mediaId media id + * @param server server type (default `VidCloud`) (optional) + */ + override fetchEpisodeSources = async ( + episodeId: string, + mediaId: string, + server: StreamingServers = StreamingServers.UpCloud + ): Promise => { + if (episodeId.startsWith('http')) { + const serverUrl = new URL(episodeId); + switch (server) { + case StreamingServers.MixDrop: + return { + headers: { Referer: serverUrl.href }, + sources: await new MixDrop(this.proxyConfig, this.adapter).extract(serverUrl), + }; + case StreamingServers.VidCloud: + return { + headers: { Referer: serverUrl.href }, + ...(await new VidCloud(this.proxyConfig, this.adapter).extract(serverUrl, true)), + }; + case StreamingServers.UpCloud: + return { + headers: { Referer: serverUrl.href }, + ...(await new VidCloud(this.proxyConfig, this.adapter).extract(serverUrl)), + }; + default: + return { + headers: { Referer: serverUrl.href }, + sources: await new MixDrop(this.proxyConfig, this.adapter).extract(serverUrl), + }; + } + } + + try { + const servers = await this.fetchEpisodeServers(episodeId, mediaId); + + const i = servers.findIndex(s => s.name.toLowerCase() === server.toLowerCase()); + + if (i === -1) { + throw new Error(`Server ${server} not found`); + } + + const serverUrl: URL = new URL( + servers.filter(s => s.name.toLowerCase() === server.toLowerCase())[0].url + ); + + return await this.fetchEpisodeSources(serverUrl.href, mediaId, server); + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId takes episode link or movie id + * @param mediaId takes movie link or id (found on movie info object) + */ + override fetchEpisodeServers = async (episodeId: string, mediaId: string): Promise => { + try { + const epsiodeServers: IEpisodeServer[] = []; + const { data } = await this.client.get(`${this.baseUrl}/ajax/movie/episode/servers/${episodeId}`); + const $ = load(data); + + const servers = $('.dropdown-menu > a') + .map((i, ele) => ({ + name: $(ele).text(), + id: $(ele).attr('data-id') ?? '', + })) + .get(); + + for (const server of servers) { + const { data } = await this.client.get( + `${this.baseUrl}/ajax/movie/episode/server/sources/${server.id}` + ); + + epsiodeServers.push({ + name: server.name, + url: data.data.link, + }); + } + + return epsiodeServers; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchRecentMovies = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const movies = $('.section-last') + .first() + .find('.item') + .map((i, ele) => { + const releaseDate = $(ele).find('.info-split').children().first().text(); + const movie: any = { + id: $(ele).find('.is-watch > a').attr('href')?.replace('/', '')!, + title: $(ele).find('.movie-name').text(), + url: `${this.baseUrl}${$(ele).find('.is-watch > a').attr('href')}`, + image: $(ele).find('.movie-thumbnail > a > img').attr('src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + duration: $(ele).find('.info-split > div:nth-child(3)').text(), + type: + $(ele).find('.is-watch > a').attr('href')?.indexOf('watch-movie') ?? -1 > -1 + ? TvType.MOVIE + : TvType.TVSERIES, + }; + return movie; + }) + .get(); + return movies; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchRecentTvShows = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const tvShowes = $('.section-last') + .last() + .find('.item') + .map((i, ele) => { + const tvshow: any = { + id: $(ele).find('.is-watch > a').attr('href')?.replace('/', '')!, + title: $(ele).find('.movie-name').text(), + url: `${this.baseUrl}${$(ele).find('.is-watch > a').attr('href')}`, + image: $(ele).find('.movie-thumbnail > a > img').attr('src'), + season: $(ele) + .find('.info-split > div:nth-child(2)') + .text() + .split('/')[0] + .replace('SS ', '') + .trim(), + latestEpisode: $(ele) + .find('.info-split > div:nth-child(2)') + .text() + .split('/')[1] + .replace('EPS ', '') + .trim(), + type: + $(ele).find('.is-watch > a').attr('href')?.indexOf('watch-series') ?? -1 > -1 + ? TvType.TVSERIES + : TvType.MOVIE, + }; + return tvshow; + }) + .get(); + return tvShowes; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchTrendingMovies = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const movies = $('#trending-movies') + .find('.item') + .map((i, ele) => { + const releaseDate = $(ele).find('.info-split').children().first().text(); + const movie: any = { + id: $(ele).find('.is-watch > a').attr('href')?.replace('/', '')!, + title: $(ele).find('.movie-name').text(), + url: `${this.baseUrl}${$(ele).find('.is-watch > a').attr('href')}`, + image: $(ele).find('.movie-thumbnail > a > img').attr('src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + duration: $(ele).find('.info-split > div:nth-child(3)').text(), + type: + $(ele).find('.is-watch > a').attr('href')?.indexOf('watch-movie') ?? -1 > -1 + ? TvType.MOVIE + : TvType.TVSERIES, + }; + return movie; + }) + .get(); + return movies; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchTrendingTvShows = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const tvShowes = $('#trending-series') + .find('.item') + .map((i, ele) => { + const tvshow: any = { + id: $(ele).find('.is-watch > a').attr('href')?.replace('/', '')!, + title: $(ele).find('.movie-name').text(), + url: `${this.baseUrl}${$(ele).find('.is-watch > a').attr('href')}`, + image: $(ele).find('.movie-thumbnail > a > img').attr('src'), + season: $(ele) + .find('.info-split > div:nth-child(2)') + .text() + .split('/')[0] + .replace('SS ', '') + .trim(), + latestEpisode: $(ele) + .find('.info-split > div:nth-child(2)') + .text() + .split('/')[1] + .replace('EPS ', '') + .trim(), + type: + $(ele).find('.is-watch > a').attr('href')?.indexOf('watch-series') ?? -1 > -1 + ? TvType.TVSERIES + : TvType.MOVIE, + }; + return tvshow; + }) + .get(); + return tvShowes; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default Goku; diff --git a/consumet.ts/src/providers/movies/index.ts b/consumet.ts/src/providers/movies/index.ts new file mode 100644 index 00000000..52f480f0 --- /dev/null +++ b/consumet.ts/src/providers/movies/index.ts @@ -0,0 +1,21 @@ +import DramaCool from './dramacool'; +import FlixHQ from './flixhq'; +import Fmovies from './fmovies'; +import Goku from './goku'; +import KissAsian from './kissasian'; +import MovieHdWatch from './movidhdwatch'; +import SmashyStream from './smashystream'; +import Turkish from './turkish123'; +import ViewAsian from './viewAsian'; + +export default { + DramaCool, + FlixHQ, + Fmovies, + Goku, + KissAsian, + MovieHdWatch, + SmashyStream, + ViewAsian, + Turkish, +}; diff --git a/consumet.ts/src/providers/movies/kissasian.ts b/consumet.ts/src/providers/movies/kissasian.ts new file mode 100644 index 00000000..dc0412e0 --- /dev/null +++ b/consumet.ts/src/providers/movies/kissasian.ts @@ -0,0 +1,240 @@ +import { load } from 'cheerio'; + +import { + MovieParser, + TvType, + IMovieInfo, + IEpisodeServer, + StreamingServers, + ISource, + IMovieResult, + ISearch, + MediaStatus, +} from '../../models'; +import { Mp4Upload, StreamWish, VidMoly } from '../../extractors'; +class KissAsian extends MovieParser { + override readonly name = 'KissAsian'; + protected override baseUrl = 'https://kissasian.mx'; + protected override logo = 'https://kissasian.mx/Content/images/logo.png'; + protected override classPath = 'MOVIES.KissAsian'; + override supportedTypes = new Set([TvType.MOVIE, TvType.TVSERIES]); + + override search = async (query: string, page: number = 1): Promise> => { + try { + const searchResult: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + + const response = await this.client.post( + `${this.baseUrl}/Search/Drama`, + `keyword=${query.replace(/[\W_]+/g, '-')}`, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + const $ = load(response.data); + + $('div.item-list > div.list').each((i, el) => { + searchResult.results.push({ + id: $(el).find('div.info > p > a').attr('href')?.slice(1)!, + title: $(el).find('div.info > p > a').text().trim(), + url: `${this.baseUrl}${$(el).find('div.info > p > a').attr('href')}`, + image: `${this.baseUrl}${$(el).find('div.cover > a > img').attr('src')}`, + }); + }); + + if (searchResult.results.length === 0) { + searchResult.results.push({ + id: response.request.res.responseUrl.replace(/https?:\/\/[^\/]*\/?/i, ''), + title: $('div.content').first().find('div.heading > h3').text().trim(), + url: response.request.res.responseUrl, + image: `${this.baseUrl}${$('div.content').first().find('div.cover > img').attr('src')}`, + }); + } + + return searchResult; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchMediaInfo = async (mediaId: string): Promise => { + try { + const realMediaId = mediaId; + if (!mediaId.startsWith(this.baseUrl)) mediaId = `${this.baseUrl}/${mediaId}`; + + const mediaInfo: IMovieInfo = { + id: '', + title: '', + }; + + const { data } = await this.client.post(mediaId, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + const $ = load(data); + + mediaInfo.id = realMediaId; + mediaInfo.title = $('div.content').first().find('div.heading > h3').text().trim(); + mediaInfo.image = `${this.baseUrl}${$('div.content').first().find('div.cover > img').attr('src')}`; + mediaInfo.otherNames = $('span:contains(Other name:)') + .siblings() + .map((i, el) => $(el).text()!.trim()) + .get(); + mediaInfo.description = $('div.summary1 > p').text().trim(); + mediaInfo.releaseDate = $('span:contains(Date aired:)') + .parent() + .text() + .split('Date aired:') + .pop() + ?.replace(/\t/g, '') + .replace(/\n/g, '') + .trim(); + mediaInfo.genre = $('span:contains(Genres:)') + .siblings('a') + .map((i, el) => $(el).text()!.trim()) + .get(); + mediaInfo.country = $('span:contains(Country:)').siblings('a').text().trim(); + + switch ($('span:contains(Status:)').parent().text().split('Status:').pop()?.trim()) { + case 'Ongoing': + mediaInfo.status = MediaStatus.ONGOING; + break; + case 'Completed': + mediaInfo.status = MediaStatus.COMPLETED; + break; + case 'Cancelled': + mediaInfo.status = MediaStatus.CANCELLED; + break; + case 'Unknown': + mediaInfo.status = MediaStatus.UNKNOWN; + break; + default: + mediaInfo.status = MediaStatus.UNKNOWN; + break; + } + + mediaInfo.episodes = []; + $('ul.list li.episodeSub').each((i, el) => { + mediaInfo.episodes?.push({ + id: $(el).find('a').attr('href')?.slice(1)!, + title: $(el).find('a').text().trim(), + episode: $(el).find('a').text()?.split('Episode').pop()?.trim(), + url: `${this.baseUrl}${$(el).find('a').attr('href')}`, + }); + }); + mediaInfo.episodes.reverse(); + + return mediaInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override async fetchEpisodeServers(episodeId: string): Promise { + try { + const episodeServers: IEpisodeServer[] = []; + + const { data } = await this.client.post(`${this.baseUrl}/${episodeId}`, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + const $ = load(data); + episodeServers.push({ + name: $('ul.mirrorTab > li > a.actived').text().trim(), + url: $('iframe#mVideo').attr('src')!, + }); + + await Promise.all( + $('ul.mirrorTab > li > a.ign').map(async (i, ele) => { + const { data } = await this.client.post(`${this.baseUrl}${$(ele).attr('href')}`, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + const $$ = load(data); + if ($$('ul.mirrorTab > li > a.actived').text().trim()) { + const url = $$('iframe#mVideo').attr('src')!; + episodeServers.push({ + name: $$('ul.mirrorTab > li > a.actived').text().trim(), + url: url.startsWith('https') ? url : url.replace('//', 'https://'), + }); + } + }) + ); + + episodeServers.map(element => { + switch (element.name) { + case 'VM': + element.name = StreamingServers.VidMoly; + break; + case 'SW': + element.name = StreamingServers.StreamWish; + break; + case 'MP': + element.name = StreamingServers.Mp4Upload; + break; + default: + break; + } + }); + + return episodeServers; + } catch (err) { + throw new Error((err as Error).message); + } + } + + override fetchEpisodeSources = async ( + episodeId: string, + server: StreamingServers = StreamingServers.Mp4Upload + ): Promise => { + if (episodeId.startsWith('http')) { + const serverUrl = new URL(episodeId); + switch (server) { + case StreamingServers.VidMoly: + return { + sources: await new VidMoly(this.proxyConfig, this.adapter).extract(serverUrl), + }; + case StreamingServers.StreamWish: + return { + sources: await new StreamWish(this.proxyConfig, this.adapter).extract(serverUrl), + }; + case StreamingServers.Mp4Upload: + return { + sources: await new Mp4Upload(this.proxyConfig, this.adapter).extract(serverUrl), + }; + default: + throw new Error('Server not supported'); + } + } + + try { + const servers = await this.fetchEpisodeServers(episodeId); + const i = servers.findIndex(s => s.name.toLowerCase() === server.toLowerCase()); + + if (i === -1) { + throw new Error(`Server ${server} not found`); + } + + const serverUrl: URL = new URL( + servers.filter(s => s.name.toLowerCase() === server.toLowerCase())[0].url + ); + + return await this.fetchEpisodeSources(serverUrl.href, server); + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default KissAsian; diff --git a/consumet.ts/src/providers/movies/movidhdwatch.ts b/consumet.ts/src/providers/movies/movidhdwatch.ts new file mode 100644 index 00000000..58d7e32e --- /dev/null +++ b/consumet.ts/src/providers/movies/movidhdwatch.ts @@ -0,0 +1,422 @@ +import { load } from 'cheerio'; + +import { + MovieParser, + TvType, + IMovieInfo, + IEpisodeServer, + StreamingServers, + ISource, + IMovieResult, + ISearch, + IMovieEpisode, +} from '../../models'; +import { MixDrop, VidCloud } from '../../extractors'; + +class MovieHdWatch extends MovieParser { + override readonly name = 'MovieHdWatch'; + protected override baseUrl = 'https://movieshd.watch'; + protected override logo = + 'https://img.movieshd.watch/xxrz/400x400/100/ee/63/ee6317c38904ee048676164b0852207d/ee6317c38904ee048676164b0852207d.png'; + protected override classPath = 'MOVIES.MovieHdWatch'; + override supportedTypes = new Set([TvType.MOVIE, TvType.TVSERIES]); + + /** + * + * @param query search query string + * @param page page number (default 1) (optional) + */ + override search = async (query: string, page: number = 1): Promise> => { + const searchResult: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + try { + const { data } = await this.client.get( + `${this.baseUrl}/search/${query.replace(/[\W_]+/g, '-')}?page=${page}` + ); + const $ = load(data); + + const navSelector = 'div.pre-pagination:nth-child(3) > nav:nth-child(1) > ul:nth-child(1)'; + + searchResult.hasNextPage = + $(navSelector).length > 0 ? !$(navSelector).children().last().hasClass('active') : false; + + $('.film_list-wrap > div.flw-item').each((i, el) => { + const releaseDate = $(el).find('div.film-detail > div.film-infor > span:nth-child(2)').text(); + const duration = $(el).find('div.film-detail > div.film-infor > span:nth-child(4)').text(); + searchResult.results.push({ + id: $(el).find('div.film-poster > a').attr('href')?.slice(1) ?? '', + title: $(el).find('div.film-detail > h2 > a').attr('title') ?? '', + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + seasons: releaseDate.includes('SS') ? parseInt(releaseDate.split('SS')[1]) : undefined, + duration: !duration.includes('EPS') ? duration : undefined, + type: $(el).find('div.film-poster > a').attr('href')?.includes('tv/') + ? TvType.TVSERIES + : TvType.MOVIE, + }); + }); + + return searchResult; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param mediaId media link or id + */ + override fetchMediaInfo = async (mediaId: string): Promise => { + if (mediaId.startsWith(this.baseUrl)) { + mediaId = mediaId.replace(this.baseUrl + '/', ''); + } + + try { + const { data } = await this.client.get(`${this.baseUrl}/${mediaId}`); + const $ = load(data); + const recommendationsArray: IMovieResult[] = []; + + const movieInfo: IMovieInfo = { + id: mediaId, + title: '', + url: `${this.baseUrl}/${mediaId}`, + }; + + const uid = $('.detail_page-watch').attr('data-id')!; + movieInfo.cover = $('div.dp-w-cover').attr('style')?.slice(22).replace(')', '').replace(';', ''); + movieInfo.title = $('.heading-name > a:nth-child(1)').text(); + movieInfo.image = $('.film-poster > img').attr('src'); + movieInfo.description = $('.description') + .text() + .replace(/(\r\n|\n|\r)/gm, '') + .trim(); + movieInfo.type = movieInfo.id.split('/')[0] === 'tv' ? TvType.TVSERIES : TvType.MOVIE; + movieInfo.releaseDate = $('div.elements') + .find('div.row-line') + .first() + .text() + .replace('Released: ', '') + .trim(); + movieInfo.genres = $('div.row-line:nth-child(2)') + .first() + .find('a') + .map((i, el) => $(el).text().split('&')) + .get() + .map(v => v.trim()); + movieInfo.casts = $('div.row-line:nth-child(3)') + .first() + .find('a') + .map((i, el) => $(el).attr('title')) + .get(); + movieInfo.production = $('div.row-line:nth-child(3)') + .last() + .find('a') + .map((i, el) => $(el).attr('title')) + .get() + .join(); + movieInfo.country = $('div.row-line:nth-child(2)') + .last() + .find('a') + .map((i, el) => $(el).attr('title')) + .get(); + movieInfo.duration = $('div.row-line:nth-child(1)') + .last() + .text() + .replace(/(\r\n|\n| |\r)/gm, '') + .replace('Duration:', '') + .trim(); + movieInfo.rating = parseFloat($('div.dp-i-stats > span.item.mr-2').text().replace('IMDB: ', '')); + + $('div.film_list-wrap > div.flw-item').each((i, el) => { + const releaseDate = $(el).find('div.film-detail > div.film-infor > span:nth-child(2)').text(); + const duration = $(el).find('div.film-detail > div.film-infor > span:nth-child(4)').text(); + recommendationsArray.push({ + id: $(el).find('div.film-poster > a').attr('href')?.slice(1)!, + title: $(el).find('div.film-detail > h3.film-name > a').text(), + image: $(el).find('div.film-poster > img').attr('data-src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + seasons: releaseDate.includes('SS') ? parseInt(releaseDate.split('SS')[1]) : undefined, + duration: !duration.includes('EPS') ? duration : undefined, + type: $(el).find('div.film-poster > a').attr('href')?.includes('tv/') + ? TvType.TVSERIES + : TvType.MOVIE, + }); + }); + + movieInfo.recommendations = recommendationsArray; + const ajaxReqUrl = (id: string, type: string, isSeasons: boolean = false) => + `${this.baseUrl}/ajax/${type === 'movie' ? type : `v2/${type}`}/${ + isSeasons ? 'seasons' : 'episodes' + }/${id}`; + + if (movieInfo.type === TvType.TVSERIES) { + const { data } = await this.client.get(ajaxReqUrl(uid, 'tv', true)); + const $$ = load(data); + const seasonsIds = $$('.dropdown-menu > a') + .map((i, el) => $(el).attr('data-id')) + .get(); + + movieInfo.episodes = []; + let season = 1; + for (const id of seasonsIds) { + const { data } = await this.client.get(ajaxReqUrl(id, 'season')); + const $$$ = load(data); + + $$$('.nav > li') + .map((i, el) => { + const episode: IMovieEpisode = { + id: $$$(el).find('a').attr('id')!.split('-')[1], + title: $$$(el).find('a').attr('title')!, + number: parseInt($$$(el).find('a').attr('title')!.split(':')[0].slice(3).trim()), + season: season, + url: `${this.baseUrl}/ajax/v2/episode/servers/${$$$(el).find('a').attr('id')!.split('-')[1]}`, + }; + movieInfo.episodes?.push(episode); + }) + .get(); + season++; + } + } else { + movieInfo.episodes = [ + { + id: uid, + title: movieInfo.title, + url: `${this.baseUrl}/ajax/movie/episodes/${uid}`, + }, + ]; + } + + return movieInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId episode id + * @param mediaId media id + * @param server server type (default `VidCloud`) (optional) + */ + override fetchEpisodeSources = async ( + episodeId: string, + mediaId: string, + server: StreamingServers = StreamingServers.UpCloud + ): Promise => { + if (episodeId.startsWith('http')) { + const serverUrl = new URL(episodeId); + switch (server) { + case StreamingServers.MixDrop: + return { + headers: { Referer: serverUrl.href }, + sources: await new MixDrop(this.proxyConfig, this.adapter).extract(serverUrl), + }; + case StreamingServers.VidCloud: + return { + headers: { Referer: serverUrl.href }, + ...(await new VidCloud(this.proxyConfig, this.adapter).extract(serverUrl, true)), + }; + case StreamingServers.UpCloud: + return { + headers: { Referer: serverUrl.href }, + ...(await new VidCloud(this.proxyConfig, this.adapter).extract(serverUrl)), + }; + default: + return { + headers: { Referer: serverUrl.href }, + sources: await new MixDrop(this.proxyConfig, this.adapter).extract(serverUrl), + }; + } + } + + try { + const servers = await this.fetchEpisodeServers(episodeId, mediaId); + + const i = servers.findIndex(s => s.name.toLowerCase() === server.toLowerCase()); + + if (i === -1) { + throw new Error(`Server ${server} not found`); + } + + const serverUrl: URL = new URL( + servers.filter(s => s.name.toLowerCase() === server.toLowerCase())[0].url + ); + + return await this.fetchEpisodeSources(serverUrl.href, mediaId, server); + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId takes episode link or movie id + * @param mediaId takes movie link or id (found on movie info object) + */ + override fetchEpisodeServers = async (episodeId: string, mediaId: string): Promise => { + if (!episodeId.startsWith(this.baseUrl + '/ajax') && !mediaId.includes('movie')) + episodeId = `${this.baseUrl}/ajax/v2/episode/servers/${episodeId}`; + else episodeId = `${this.baseUrl}/ajax/movie/episodes/${episodeId}`; + + try { + const { data } = await this.client.get(episodeId); + const $ = load(data); + const servers: IEpisodeServer[] = []; + + await Promise.all( + $('.nav > li').map(async (i, el) => { + const server: IEpisodeServer = { + name: $(el).find('a').attr('title')!.slice(6).trim(), + url: `${this.baseUrl}/${mediaId}.${ + !mediaId.includes('movie') + ? $(el).find('a').attr('data-id') + : $(el).find('a').attr('data-linkid') + }`.replace( + !mediaId.includes('movie') ? /\/tv\// : /\/movie\//, + !mediaId.includes('movie') ? '/watch-tv/' : '/watch-movie/' + ), + }; + + const { data } = await this.client.get( + `${this.baseUrl}/ajax/get_link/${server.url.split('.').slice(-1).shift()}` + ); + + const serverUrl: URL = new URL(data.link); + server.url = serverUrl.href; + + servers.push(server); + }) + ); + + return servers; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchRecentMovies = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const movies = $('.section-id-02') + .find('div.flw-item') + .map((i, el) => { + const releaseDate = $(el).find('div.film-detail > div.film-infor > span:nth-child(2)').text(); + const duration = $(el).find('div.film-detail > div.film-infor > span:nth-child(4)').text(); + const movie: IMovieResult = { + id: $(el).find('div.film-poster > a').attr('href')?.slice(1) ?? '', + title: $(el).find('div.film-detail > h3.film-name > a').attr('title') ?? '', + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + duration: !duration.includes('EPS') ? duration : undefined, + type: $(el).find('div.film-poster > a').attr('href')?.includes('movie/') + ? TvType.MOVIE + : TvType.TVSERIES, + }; + return movie; + }) + .get(); + return movies; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchRecentTvShows = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const tvshows = $('.section-id-03') + .find('div.flw-item') + .map((i, el) => { + const season = $(el).find('div.film-detail > div.film-infor > span:nth-child(2)').text(); + const episode = $(el).find('div.film-detail > div.film-infor > span:nth-child(4)').text(); + const tvshow = { + id: $(el).find('div.film-poster > a').attr('href')?.slice(1) ?? '', + title: $(el).find('div.film-detail > h3.film-name > a').attr('title') ?? '', + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + season: season.includes('SS') ? parseInt(season.split('SS')[1]) : undefined, + latestEpisode: episode.includes('EPS') ? parseInt(episode.split('EPS')[1]) : undefined, + type: $(el).find('div.film-poster > a').attr('href')?.includes('tv/') + ? TvType.TVSERIES + : TvType.MOVIE, + }; + return tvshow; + }) + .get(); + return tvshows; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchTrendingMovies = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const movies = $('#trending-movies') + .find('div.flw-item') + .map((i, el) => { + const releaseDate = $(el).find('div.film-detail > div.film-infor > span:nth-child(2)').text(); + const duration = $(el).find('div.film-detail > div.film-infor > span:nth-child(4)').text(); + const movie: IMovieResult = { + id: $(el).find('div.film-poster > a').attr('href')?.slice(1) ?? '', + title: $(el).find('div.film-detail > h3.film-name > a').attr('title') ?? '', + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + duration: !duration.includes('EPS') ? duration : undefined, + type: $(el).find('div.film-poster > a').attr('href')?.includes('movie/') + ? TvType.MOVIE + : TvType.TVSERIES, + }; + return movie; + }) + .get(); + return movies; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + fetchTrendingTvShows = async (): Promise => { + try { + const { data } = await this.client.get(`${this.baseUrl}/home`); + const $ = load(data); + + const tvshows = $('#trending-tv') + .find('div.flw-item') + .map((i, el) => { + const season = $(el).find('div.film-detail > div.film-infor > span:nth-child(2)').text(); + const episode = $(el).find('div.film-detail > div.film-infor > span:nth-child(4)').text(); + const tvshow = { + id: $(el).find('div.film-poster > a').attr('href')?.slice(1) ?? '', + title: $(el).find('div.film-detail > h3.film-name > a').attr('title') ?? '', + url: `${this.baseUrl}${$(el).find('div.film-poster > a').attr('href')}`, + image: $(el).find('div.film-poster > img').attr('data-src'), + season: season.includes('SS') ? parseInt(season.split('SS')[1]) : undefined, + latestEpisode: episode.includes('EPS') ? parseInt(episode.split('EPS')[1]) : undefined, + type: $(el).find('div.film-poster > a').attr('href')?.includes('tv/') + ? TvType.TVSERIES + : TvType.MOVIE, + }; + return tvshow; + }) + .get(); + return tvshows; + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default MovieHdWatch; diff --git a/consumet.ts/src/providers/movies/smashystream.ts b/consumet.ts/src/providers/movies/smashystream.ts new file mode 100644 index 00000000..9aa39e3c --- /dev/null +++ b/consumet.ts/src/providers/movies/smashystream.ts @@ -0,0 +1,133 @@ +import { + MovieParser, + TvType, + IMovieInfo, + IEpisodeServer, + ISource, + IMovieResult, + ISearch, +} from '../../models'; +import { load } from 'cheerio'; +import { SmashyStream as SS } from '../../extractors'; + +class SmashyStream extends MovieParser { + override readonly name = 'Smashystream'; + protected override baseUrl = 'https://embed.smashystream.com'; + protected override logo = 'https://smashystream.xyz/logo.png'; + protected override classPath = 'MOVIES.SmashyStream'; + override supportedTypes = new Set([TvType.MOVIE, TvType.TVSERIES]); + + override search = async (): Promise> => { + throw new Error('Method not implemented.'); + }; + + override fetchMediaInfo = async (): Promise => { + throw new Error('Method not implemented.'); + }; + + override fetchEpisodeServers = async ( + tmdbId: string, + season?: number, + episode?: number + ): Promise => { + try { + const epsiodeServers: IEpisodeServer[] = []; + + let url = `${this.baseUrl}/playere.php?tmdb=${tmdbId}`; + if (season) { + url = `${this.baseUrl}/playere.php?tmdb=${tmdbId}&season=${season}&episode=${episode}`; + } + const { data } = await this.client.get(url); + const $ = load(data); + + await Promise.all( + $('div#_default-servers a.server') + .map(async (i, el) => { + const streamLink = $(el).attr('data-id') ?? ''; + + epsiodeServers.push({ + name: $(el).text().replace(/ +/g, ' ').trim(), + url: streamLink, + }); + }) + .get() + ); + + return epsiodeServers; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchEpisodeSources = async ( + tmdbId: string, + season?: number, + episode?: number, + server?: string + ): Promise => { + try { + const servers = await this.fetchEpisodeServers(tmdbId, season, episode); + const selectedServer = servers.find(s => s.name.toLowerCase() === server?.toLowerCase()); + + if (!selectedServer) { + let url = `${this.baseUrl}/playere.php?tmdb=${tmdbId}`; + if (season) { + url = `${this.baseUrl}/playere.php?tmdb=${tmdbId}&season=${season}&episode=${episode}`; + } + + return { + headers: { Referer: this.baseUrl }, + ...(await new SS(this.proxyConfig, this.adapter).extract(new URL(url))), + }; + } + + if (selectedServer.url.includes('/ffix')) { + return { + headers: { Referer: this.baseUrl }, + ...(await new SS(this.proxyConfig, this.adapter).extractSmashyFfix(selectedServer.url)), + }; + } + + if (selectedServer.url.includes('/watchx')) { + return { + headers: { Referer: this.baseUrl }, + ...(await new SS(this.proxyConfig, this.adapter).extractSmashyWatchX(selectedServer.url)), + }; + } + + if (selectedServer.url.includes('/nflim')) { + return { + headers: { Referer: this.baseUrl }, + ...(await new SS(this.proxyConfig, this.adapter).extractSmashyNFlim(selectedServer.url)), + }; + } + + if (selectedServer.url.includes('/fx')) { + return { + headers: { Referer: this.baseUrl }, + ...(await new SS(this.proxyConfig, this.adapter).extractSmashyFX(selectedServer.url)), + }; + } + + if (selectedServer.url.includes('/cf')) { + return { + headers: { Referer: this.baseUrl }, + ...(await new SS(this.proxyConfig, this.adapter).extractSmashyCF(selectedServer.url)), + }; + } + + if (selectedServer.url.includes('/eemovie')) { + return { + headers: { Referer: this.baseUrl }, + ...(await new SS(this.proxyConfig, this.adapter).extractSmashyEEMovie(selectedServer.url)), + }; + } + + return await this.fetchEpisodeSources(selectedServer.url, season, episode, server); + } catch (err) { + throw new Error((err as Error).message); + } + }; +} + +export default SmashyStream; diff --git a/consumet.ts/src/providers/movies/turkish123.ts b/consumet.ts/src/providers/movies/turkish123.ts new file mode 100644 index 00000000..7f27538c --- /dev/null +++ b/consumet.ts/src/providers/movies/turkish123.ts @@ -0,0 +1,105 @@ +import { load } from 'cheerio'; +import { IAnimeInfo, IEpisodeServer, IMovieInfo, ISource, MovieParser, TvType } from '../../models'; +import axios from 'axios'; + +export default class Turkish extends MovieParser { + name: string = 'Turkish123'; + protected baseUrl: string = 'https://turkish123.ac/'; + protected classPath: string = 'MOVIES.Turkish'; + supportedTypes: Set = new Set([TvType.TVSERIES]); + + async fetchMediaInfo(mediaId: string): Promise { + const info: IMovieInfo = { id: mediaId, title: '' }; + + try { + const { data } = await this.client(this.baseUrl + mediaId, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Referer: this.baseUrl, + }, + }); + const $ = load(data); + info.image = $('#content-cover') + .attr('style')! + .match(/url\((.*?)\)/)![1]; + info.title = $('.mvic-desc > h1').text(); + info.description = $('.f-desc') + .text() + .replace(/[\n\t\b]/g, ''); + info.romaji = $('.yellowi').text(); + info.tags = $('.mvici-left > p:nth-child(3)') + .find('a') + .map((_, e) => $(e).text()) + .get(); + info.rating = parseFloat($('.imdb-r').text()); + info.releaseDate = $('.mvici-right > p:nth-child(3)').find('a').first().text(); + info.totalEpisodes = $('.les-content > a').length; + info.episodes = $('.les-content > a') + .map((i, e) => ({ + id: $(e).attr('href')!.split('/').slice(-2)[0], + title: `Episode ${i + 1}`, + })) + .get(); + } catch (error) {} + return info; + } + async fetchEpisodeSources(episodeId: string): Promise { + const source: ISource = { sources: [{ url: '' }], headers: { Referer: 'https://tukipasti.com' } }; + try { + const { data } = await this.client(this.baseUrl + episodeId, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Referer: this.baseUrl, + }, + }); + const resp = (await this.client(data.match(/"(https:\/\/tukipasti.com\/t\/.*?)"/)![1])).data; + source.sources[0].url = resp.match(/var urlPlay = '(.*?)'/)![1]; + } catch (error) {} + return source; + } + fetchEpisodeServers(): Promise { + throw new Error('Method not implemented.'); + } + async search(q: string): Promise { + const params = `wp-admin/admin-ajax.php?s=${q}&action=searchwp_live_search&swpengine=default&swpquery=${q}`; + try { + const { data } = await this.client(this.baseUrl + params, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Referer: this.baseUrl, + }, + }); + const $ = load(data); + const result: IMovieInfo[] = []; + $('li') + .not('.ss-bottom') + .each((_, ele) => { + result.push({ + id: $(ele).find('a').attr('href')!.replace(this.baseUrl, '').replace('/', ''), + image: + $(ele) + .find('a') + .attr('style')! + .match(/url\((.*?)\)/)![1] ?? '', + title: $(ele).find('.ss-title').text(), + tags: $(ele) + .find('.ss-info >a') + .not('.ss-title') + .map((_, e) => $(e).text()) + .get() + .filter(v => v != 'NULL'), + }); + }); + return result; + } catch (error) { + console.log(error); + } + return []; + } +} diff --git a/consumet.ts/src/providers/movies/ummagurau.ts b/consumet.ts/src/providers/movies/ummagurau.ts new file mode 100644 index 00000000..ccd23f2a --- /dev/null +++ b/consumet.ts/src/providers/movies/ummagurau.ts @@ -0,0 +1,95 @@ +import { load } from 'cheerio'; +import { + IEpisodeServer, + IMovieInfo, + IMovieResult, + ISearch, + ISource, + MovieParser, + TvType, +} from '../../models'; + +class Ummangurau extends MovieParser { + override readonly name = 'Ummangurau'; + protected override baseUrl = 'https://www1.ummagurau.com'; + protected override logo = 'https://www1.ummagurau.com/images/group_1/theme_8/logo.png?v=0.1'; + protected override classPath = `MOVIES.${this.name}`; + override supportedTypes = new Set([TvType.MOVIE, TvType.TVSERIES]); + + override search = async (query: string, page: number = 1) => { + const searchResult: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + try { + const { data } = await this.client.get( + `${this.baseUrl}/search/${query.replace(/[\W_]+/g, '-')}?page=${page}` + ); + + const $ = load(data); + + searchResult.hasNextPage = + $("nav[area-label='Page navigation']").html() === null + ? false + : page < + Number( + $("nav ul li a[title='Last']")!.attr('href')![ + $("nav ul li a[title='Last']")!.attr('href')!.length - 1 + ] + ); + + $('div.flw-item').each((i, e) => { + searchResult.results.push({ + id: `${$(e).find('a.film-poster-ahref')?.attr('href')?.slice(1)}`, + title: `${$(e).find('h2.film-name a').attr('href')}`, + url: `${this.baseUrl}${$(e).find('.film-poster a').attr('href')}`, + image: `${$(e).find('.film-poster img').attr('data-src')}`, + type: $(e).find('span.fdi-type').text() === 'Movie' ? TvType.MOVIE : TvType.TVSERIES, + }); + }); + return searchResult; + } catch (e) { + throw new Error((e as Error).message); + } + }; + + override fetchMediaInfo = async (mediaId: string): Promise => { + if (!mediaId.startsWith(this.baseUrl)) { + mediaId = `${this.baseUrl}/${mediaId}`; + } + + const movieInfo: IMovieInfo = { + id: mediaId.split('com/')[-1], + title: '', + url: mediaId, + }; + try { + const { data } = await this.client.get(mediaId); + const $ = load(data); + + movieInfo.title = `${$('.heading-name a').text()}`; + movieInfo.image = `${$('img.film-poster-img').attr('src')}`; + movieInfo.description = `${$('.description').text()}`; + movieInfo.type = $("a[title='TV Shows']").text() === '' ? TvType.TVSERIES : TvType.MOVIE; + movieInfo.releaseDate = $('div.row-line').text().replace('Released: ', '').trim(); + movieInfo.genres = $('div.row-line:eq(1) a') + .text() + .trim() + .split(', ') + .map(v => v.trim()); + } catch (err) { + throw new Error((err as Error).message); + } + + return movieInfo; + }; + + override fetchEpisodeServers(mediaLink: string, ...args: any): Promise { + throw new Error('Method not implemented.'); + } + + override fetchEpisodeSources(mediaId: string, ...args: any): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/consumet.ts/src/providers/movies/viewAsian.ts b/consumet.ts/src/providers/movies/viewAsian.ts new file mode 100644 index 00000000..f4203b50 --- /dev/null +++ b/consumet.ts/src/providers/movies/viewAsian.ts @@ -0,0 +1,183 @@ +import { AxiosAdapter } from 'axios'; +import { load } from 'cheerio'; + +import { + MovieParser, + TvType, + IMovieInfo, + IEpisodeServer, + StreamingServers, + ISource, + IMovieResult, + ISearch, + ProxyConfig, +} from '../../models'; +import { MixDrop, AsianLoad, StreamTape, StreamSB } from '../../extractors'; + +class ViewAsian extends MovieParser { + override readonly name = 'ViewAsian'; + protected override baseUrl = 'https://viewasian.co'; + protected override logo = 'https://viewasian.co/images/logo.png'; + protected override classPath = 'MOVIES.ViewAsian'; + override supportedTypes = new Set([TvType.MOVIE, TvType.TVSERIES]); + + override search = async (query: string, page: number = 1): Promise> => { + const searchResult: ISearch = { + currentPage: page, + hasNextPage: false, + results: [], + }; + + try { + const { data } = await this.client.get( + `${this.baseUrl}/movie/search/${query.replace(/[\W_]+/g, '-')}?page=${page}` + ); + + const $ = load(data); + + const navSelector = 'div#pagination > nav:nth-child(1) > ul:nth-child(1)'; + + searchResult.hasNextPage = + $(navSelector).length > 0 ? !$(navSelector).children().last().hasClass('active') : false; + + $('.movies-list-full > div.ml-item').each((i, el) => { + const releaseDate = $(el).find('div.ml-item > div.mli-info > span:nth-child(1)').text(); + searchResult.results.push({ + id: $(el).find('a').attr('href')?.slice(1)!, + title: $(el).find('a').attr('title')!, + url: `${this.baseUrl}${$(el).find('a').attr('href')}`, + image: $(el).find('a > img').attr('data-original'), + releaseDate: isNaN(parseInt(releaseDate)) ? undefined : releaseDate, + // type: + // $(el) + // .find("div.film-detail > div.fd-infor > span.float-right") + // .text() === "Movie" + // ? TvType.MOVIE + // : TvType.TVSERIES, + }); + }); + + return searchResult; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + override fetchMediaInfo = async (mediaId: string): Promise => { + const realMediaId = mediaId; + if (!mediaId.startsWith(this.baseUrl)) + mediaId = `${this.baseUrl}/watch/${mediaId.split('/').slice(1)}/watching.html`; + + const mediaInfo: IMovieInfo = { + id: '', + title: '', + }; + + try { + const { data } = await this.client.get(mediaId); + + const $ = load(data); + + mediaInfo.id = realMediaId; + mediaInfo.title = $('.detail-mod h3').text(); + mediaInfo.banner = $('.detail-mod > dm-thumb > img').attr('src'); + mediaInfo.otherNames = $('.other-name a') + .map((i, el) => $(el).attr('title')!.trim()) + .get(); + mediaInfo.description = $('.desc').text().trim(); + mediaInfo.genre = $('.mvic-info p:contains(Genre) > a') + .map((i, el) => $(el).text().split(',').join('').trim()) + .get(); + mediaInfo.description = $('.desc').text().trim(); + // mediaInfo.status = $('.mvic-info p:contains(Status)').text().replace('Status: ', '').trim(); + mediaInfo.director = $('.mvic-info p:contains(Director)').text().replace('Director: ', '').trim(); + mediaInfo.country = $('.mvic-info p:contains(Country) a').text().trim(); + mediaInfo.releaseDate = $('.mvic-info p:contains(Release)').text().replace('Release: ', '').trim(); + + mediaInfo.episodes = []; + $('ul#episodes-sv-1 li').each((i, el) => { + mediaInfo.episodes?.push({ + id: $(el).find('a').attr('href')!.replace('?ep=', '$episode$'), + title: $(el).find('a').attr('title')!.trim(), + episode: $(el).find('a').attr('episode-data'), + url: `${this.baseUrl}${$(el).find('a').attr('href')}`, + }); + }); + + return mediaInfo; + } catch (err) { + throw err; + } + }; + + override fetchEpisodeSources = async ( + episodeId: string, + server: StreamingServers = StreamingServers.AsianLoad + ): Promise => { + if (episodeId.startsWith('http')) { + const serverUrl = new URL(episodeId); + switch (server) { + case StreamingServers.AsianLoad: + return { + ...(await new AsianLoad(this.proxyConfig, this.adapter).extract(serverUrl)), + }; + case StreamingServers.MixDrop: + return { + sources: await new MixDrop(this.proxyConfig, this.adapter).extract(serverUrl), + }; + case StreamingServers.StreamTape: + return { + sources: await new StreamTape(this.proxyConfig, this.adapter).extract(serverUrl), + }; + case StreamingServers.StreamSB: + return { + sources: await new StreamSB(this.proxyConfig, this.adapter).extract(serverUrl), + }; + default: + throw new Error('Server not supported'); + } + } + if (!episodeId.includes('$episode$')) throw new Error('Invalid episode id'); + episodeId = `${episodeId.replace('$episode$', '?ep=')}`; + + // return episodeId; + try { + if (!episodeId.startsWith(this.baseUrl)) episodeId = `${this.baseUrl}/${episodeId}`; + + const { data } = await this.client.get(episodeId); + + const $ = load(data); + + let serverUrl = ''; + switch (server) { + // asianload is the same as the standard server + case StreamingServers.AsianLoad: + serverUrl = `https:${$('.anime:contains(Asianload)').attr('data-video')}`; + if (!serverUrl.includes('pladrac')) throw new Error('Try another server'); + break; + case StreamingServers.MixDrop: + serverUrl = $('.mixdrop').attr('data-video') as string; + if (!serverUrl.includes('mixdrop')) throw new Error('Try another server'); + break; + case StreamingServers.StreamTape: + serverUrl = $('.streamtape').attr('data-video') as string; + if (!serverUrl.includes('streamtape')) throw new Error('Try another server'); + break; + case StreamingServers.StreamSB: + serverUrl = $('.streamsb').attr('data-video') as string; + if (!serverUrl.includes('stream')) throw new Error('Try another server'); + break; + } + + return await this.fetchEpisodeSources(serverUrl, server); + } catch (err) { + throw err; + } + }; + + override fetchEpisodeServers(episodeId: string, ...args: any): Promise { + throw new Error('Method not implemented.'); + } +} + +export default ViewAsian; diff --git a/consumet.ts/src/providers/news/animenewsnetwork.ts b/consumet.ts/src/providers/news/animenewsnetwork.ts new file mode 100644 index 00000000..d5a865d0 --- /dev/null +++ b/consumet.ts/src/providers/news/animenewsnetwork.ts @@ -0,0 +1,126 @@ +import { load } from 'cheerio'; +import axios from 'axios'; +import { getHashFromImage } from '../../utils/utils'; +import { NewsParser, INewsFeed, Topics, INewsInfo } from '../../models'; + +class NewsFeed implements INewsFeed { + constructor( + public title: string, + public id: string, + public uploadedAt: string, + public topics: Topics[], + public preview: INewsFeed['preview'], + public thumbnail: string, + public thumbnailHash: string, + public url: string + ) {} + + public async getInfo(): Promise { + return await scrapNewsInfo(this.url).catch((err: Error) => { + throw new Error(err.message); + }); + } +} + +async function scrapNewsInfo(url: string): Promise { + const { data } = await axios.get(url); + const $ = load(data); + const title = $('#page_header').text().replace('News', '').trim(); + const intro = $('.intro').first().text().trim(); + const description = $('.meat > p').text().trim().split('\n\n').join('\n'); + const time = $('#page-title > small > time').text().trim(); + const thumbnailSlug = $('.meat > figure.fright').first().find('img').attr('data-src'); + + const thumbnail = thumbnailSlug + ? `https://animenewsnetwork.com${thumbnailSlug}` + : 'https://i.imgur.com/KkkVr1g.png'; + + const thumbnailHash = getHashFromImage( + thumbnailSlug ? `https://animenewsnetwork.com${thumbnailSlug}` : 'https://i.imgur.com/KkkVr1g.png' + ); + + return { + id: url.split('news/')[1], + title, + uploadedAt: time, + intro, + description, + thumbnail, + thumbnailHash, + url, + }; +} + +class AnimeNewsNetwork extends NewsParser { + override readonly name = 'Anime News Network'; + protected override baseUrl = 'https://www.animenewsnetwork.com'; + protected override classPath = 'NEWS.ANN'; + protected override logo = 'https://i.imgur.com/KkkVr1g.png'; + + /** + * @param topic Topic for fetching the feeds + */ + public fetchNewsFeeds = async (topic?: Topics): Promise => + await axios + .get( + `${this.baseUrl}/news${topic && Object.values(Topics).includes(topic) ? `/?topic=${topic}` : ''}` + ) + .then(({ data }) => { + const $ = load(data); + const feeds: NewsFeed[] = []; + $('.herald.box.news').each((i, el) => { + const thumbnailSlug = $(el).find('.thumbnail').attr('data-src'); + const thumbnail = thumbnailSlug ? `${this.baseUrl}${thumbnailSlug}` : this.logo; + const thumbnailHash = getHashFromImage( + thumbnailSlug ? `${this.baseUrl}${thumbnailSlug}` : this.logo + ); + const title = $(el).find('h3').text().trim(); + const slug = $(el).find('h3 > a').attr('href') || ''; + const url = `${this.baseUrl}${slug}`; + const byline = $(el).find('.byline'); + const time = byline.find('time').text().trim(); + const topics: Topics[] = []; + byline.find('.topics > a').each((i, el) => { + topics.push($(el).text().trim() as Topics); + }); + const El = $(el).find('.preview'); + const preview = { + intro: El.find('.intro').text().trim(), + full: El.find('.full').text().replace('―', '').trim(), + }; + feeds.push( + new NewsFeed( + title, + slug.replace('/news/', ''), + time, + topics, + preview, + thumbnail, + thumbnailHash, + url + ) + ); + }); + return feeds; + }) + .catch((err: Error) => { + throw new Error(err.message); + }); + + /** + * @param id ID of the news from Anime News Network + * @example + * fetchNewsInfo('2022-08-26/higurashi-no-naku-koro-ni-rei-oni-okoshi-hen-manga-ends/.188996') // --> https://www.animenewsnetwork.com/news/2022-08-26/higurashi-no-naku-koro-ni-rei-oni-okoshi-hen-manga-ends/.188996 + */ + public fetchNewsInfo = async (id: string): Promise => { + if (!id || typeof id !== 'string') + throw new TypeError( + `The type of parameter "id" should be of type "string", received type "${typeof id}" instead` + ); + return await scrapNewsInfo(`${this.baseUrl}/news/${id}`).catch((err: Error) => { + throw new Error(err.message); + }); + }; +} + +export default AnimeNewsNetwork; diff --git a/consumet.ts/src/providers/news/index.ts b/consumet.ts/src/providers/news/index.ts new file mode 100644 index 00000000..d6d11e26 --- /dev/null +++ b/consumet.ts/src/providers/news/index.ts @@ -0,0 +1,3 @@ +import { default as ANN } from './animenewsnetwork'; + +export default { ANN }; diff --git a/consumet.ts/src/providers/others/index.ts b/consumet.ts/src/providers/others/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/consumet.ts/src/utils/getComics.ts b/consumet.ts/src/utils/getComics.ts new file mode 100644 index 00000000..35c2fb7e --- /dev/null +++ b/consumet.ts/src/utils/getComics.ts @@ -0,0 +1,47 @@ +export const parsePostInfo = (post: string) => { + let year = ''; + let size = ''; + let description = ''; + let sizeDone = false; + for (let i = 0; i < post.length; i++) { + if ( + i + 5 < post.length && + post[i] == 'Y' && + post[i + 1] == 'e' && + post[i + 2] == 'a' && + post[i + 3] == 'r' && + post[i + 4] == ' ' && + post[i + 5] == ':' + ) { + year = post[i + 7] + post[i + 8] + post[i + 9] + post[i + 10]; + i += 9; + } else if ( + i + 5 < post.length && + post[i] == 'S' && + post[i + 1] == 'i' && + post[i + 2] == 'z' && + post[i + 3] == 'e' && + post[i + 4] == ' ' && + post[i + 5] == ':' + ) { + let j = i + 7; + const temp = j; + for (; j < temp + 4; j++) { + if (!isNaN(Number(post[j]))) { + size += post[j]; + } else { + break; + } + } + size += post[j] + post[j + 1]; + i += j - i; + i += 2; + sizeDone = true; + } + if (sizeDone) { + description += post[i]; + } + } + description = description.substring(0, description.length - 12); + return { year, size, description }; +}; diff --git a/consumet.ts/src/utils/index.ts b/consumet.ts/src/utils/index.ts new file mode 100644 index 00000000..635f30e7 --- /dev/null +++ b/consumet.ts/src/utils/index.ts @@ -0,0 +1,89 @@ +import { + GogoCDN, + StreamSB, + VidCloud, + MixDrop, + Kwik, + RapidCloud, + MegaCloud, + StreamTape, + VizCloud, + Filemoon, + BilibiliExtractor, + AsianLoad, + SmashyStream, + StreamHub, + VidMoly, +} from '../extractors'; +import { + USER_AGENT, + splitAuthor, + floorID, + formatTitle, + genElement, + capitalizeFirstLetter, + range, + getDays, + days, + isJson, + convertDuration, + substringAfter, + substringBefore, + compareTwoStrings, +} from './utils'; +import { + anilistSearchQuery, + anilistMediaDetailQuery, + kitsuSearchQuery, + anilistTrendingQuery, + anilistPopularQuery, + anilistAiringScheduleQuery, + anilistGenresQuery, + anilistAdvancedQuery, + anilistSiteStatisticsQuery, + anilistCharacterQuery, +} from './queries'; +import { parsePostInfo } from './getComics'; + +export { + USER_AGENT, + GogoCDN, + StreamSB, + SmashyStream, + StreamHub, + splitAuthor, + floorID, + formatTitle, + parsePostInfo, + genElement, + capitalizeFirstLetter, + VidCloud, + MixDrop, + Kwik, + anilistSearchQuery, + anilistMediaDetailQuery, + kitsuSearchQuery, + range, + RapidCloud, + MegaCloud, + StreamTape, + VizCloud, + anilistTrendingQuery, + anilistPopularQuery, + anilistAiringScheduleQuery, + anilistGenresQuery, + anilistAdvancedQuery, + anilistSiteStatisticsQuery, + Filemoon, + anilistCharacterQuery, + getDays, + days, + isJson, + convertDuration, + BilibiliExtractor, + AsianLoad, + substringAfter, + substringBefore, + compareTwoStrings, + VidMoly, +}; diff --git a/consumet.ts/src/utils/providers-list.ts b/consumet.ts/src/utils/providers-list.ts new file mode 100644 index 00000000..72c33aee --- /dev/null +++ b/consumet.ts/src/utils/providers-list.ts @@ -0,0 +1,48 @@ +import { ANIME, MANGA, BOOKS, COMICS, LIGHT_NOVELS, MOVIES, META, NEWS } from '../providers'; + +/** + * List of providers + * + * add new providers here (order does not matter) + */ +export const PROVIDERS_LIST = { + ANIME: [ + new ANIME.NineAnime(), + new ANIME.AnimeFox(), + new ANIME.AnimePahe(), + new ANIME.Bilibili(), + new ANIME.Crunchyroll(), + new ANIME.Anify(), + new ANIME.Gogoanime(), + new ANIME.Zoro(), + new ANIME.Marin(), + ], + MANGA: [ + new MANGA.MangaDex(), + new MANGA.MangaHere(), + new MANGA.MangaKakalot(), + new MANGA.Mangapark(), + new MANGA.MangaPill(), + new MANGA.MangaReader(), + new MANGA.Mangasee123(), + new MANGA.ComicK(), + new MANGA.FlameScans(), + new MANGA.MangaHost(), + new MANGA.BRMangas(), + ], + BOOKS: [new BOOKS.Libgen()], + COMICS: [new COMICS.GetComics()], + LIGHT_NOVELS: [new LIGHT_NOVELS.ReadLightNovels()], + MOVIES: [ + new MOVIES.DramaCool(), + new MOVIES.FlixHQ(), + new MOVIES.Fmovies(), + new MOVIES.Goku(), + new MOVIES.KissAsian(), + new MOVIES.MovieHdWatch(), + new MOVIES.ViewAsian(), + ], + NEWS: [new NEWS.ANN()], + META: [new META.Anilist(), new META.TMDB(), new META.Myanimelist()], + OTHERS: [], +}; diff --git a/consumet.ts/src/utils/queries.ts b/consumet.ts/src/utils/queries.ts new file mode 100644 index 00000000..2d499e19 --- /dev/null +++ b/consumet.ts/src/utils/queries.ts @@ -0,0 +1,42 @@ +export const anilistAdvancedQuery = () => + `query ($page: Int, $id: Int, $type: MediaType, $isAdult: Boolean = false, $search: String, $format: [MediaFormat], $status: MediaStatus, $size: Int, $countryOfOrigin: CountryCode, $source: MediaSource, $season: MediaSeason, $seasonYear: Int, $year: String, $onList: Boolean, $yearLesser: FuzzyDateInt, $yearGreater: FuzzyDateInt, $episodeLesser: Int, $episodeGreater: Int, $durationLesser: Int, $durationGreater: Int, $chapterLesser: Int, $chapterGreater: Int, $volumeLesser: Int, $volumeGreater: Int, $licensedBy: [String], $isLicensed: Boolean, $genres: [String], $excludedGenres: [String], $tags: [String], $excludedTags: [String], $minimumTagRank: Int, $sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) { Page(page: $page, perPage: $size) { pageInfo { total perPage currentPage lastPage hasNextPage } media(id: $id, type: $type, season: $season, format_in: $format, status: $status, countryOfOrigin: $countryOfOrigin, source: $source, search: $search, onList: $onList, seasonYear: $seasonYear, startDate_like: $year, startDate_lesser: $yearLesser, startDate_greater: $yearGreater, episodes_lesser: $episodeLesser, episodes_greater: $episodeGreater, duration_lesser: $durationLesser, duration_greater: $durationGreater, chapters_lesser: $chapterLesser, chapters_greater: $chapterGreater, volumes_lesser: $volumeLesser, volumes_greater: $volumeGreater, licensedBy_in: $licensedBy, isLicensed: $isLicensed, genre_in: $genres, genre_not_in: $excludedGenres, tag_in: $tags, tag_not_in: $excludedTags, minimumTagRank: $minimumTagRank, sort: $sort, isAdult: $isAdult) { id idMal status(version: 2) title { userPreferred romaji english native } bannerImage coverImage{ extraLarge large medium color } episodes season popularity description format seasonYear genres averageScore countryOfOrigin nextAiringEpisode { airingAt timeUntilAiring episode } } } }`; +export const anilistSearchQuery = ( + query: string, + page: number, + perPage: number, + type: 'ANIME' | 'MANGA' = 'ANIME' +) => + `query ($page: Int = ${page}, $id: Int, $type: MediaType = ${type}, $search: String = "${query}", $isAdult: Boolean = false, $size: Int = ${perPage}) { Page(page: $page, perPage: $size) { pageInfo { total perPage currentPage lastPage hasNextPage } media(id: $id, type: $type, search: $search, isAdult: $isAdult) { id idMal status(version: 2) title { userPreferred romaji english native } bannerImage popularity coverImage{ extraLarge large medium color } episodes format season description seasonYear chapters volumes averageScore genres nextAiringEpisode { airingAt timeUntilAiring episode } } } }`; +export const anilistMediaDetailQuery = (id: string) => + `query ($id: Int = ${id}) { Media(id: $id) { id idMal title { english native romaji } synonyms countryOfOrigin isLicensed isAdult externalLinks { url site type language } coverImage { extraLarge large color } startDate { year month day } endDate { year month day } bannerImage season seasonYear description type format status(version: 2) episodes duration chapters volumes trailer { id site thumbnail } genres source averageScore popularity meanScore nextAiringEpisode { airingAt timeUntilAiring episode } characters(sort: ROLE) { edges { role node { id name { first middle last full native userPreferred } image { large medium } } voiceActors(sort: LANGUAGE) { id languageV2 name { first middle last full native userPreferred } image { large medium } } } } recommendations { edges { node { id mediaRecommendation { id idMal title { romaji english native userPreferred } status episodes coverImage { extraLarge large medium color } bannerImage format chapters meanScore nextAiringEpisode { episode timeUntilAiring airingAt } } } } } relations { edges { id relationType node { id idMal status coverImage { extraLarge large medium color } bannerImage title { romaji english native userPreferred } episodes chapters format nextAiringEpisode { airingAt timeUntilAiring episode } meanScore } } } studios(isMain: true) { edges { isMain node { id name } } } } }`; +export const anilistTrendingQuery = ( + page: number = 1, + perPage: number = 20, + type: 'ANIME' | 'MANGA' = 'ANIME' +) => + `query ($page: Int = ${page}, $id: Int, $type: MediaType = ${type}, $isAdult: Boolean = false, $size: Int = ${perPage}, $sort: [MediaSort] = [TRENDING_DESC, POPULARITY_DESC]) { Page(page: $page, perPage: $size) { pageInfo { total perPage currentPage lastPage hasNextPage } media(id: $id, type: $type, isAdult: $isAdult, sort: $sort) { id idMal status(version: 2) title { userPreferred romaji english native } genres trailer { id site thumbnail } description format bannerImage coverImage{ extraLarge large medium color } episodes meanScore duration season seasonYear averageScore nextAiringEpisode { airingAt timeUntilAiring episode } } } }`; +export const anilistPopularQuery = ( + page: number = 1, + perPage: number = 20, + type: 'ANIME' | 'MANGA' = 'ANIME' +) => + `query ($page: Int = ${page}, $id: Int, $type: MediaType = ${type}, $isAdult: Boolean = false, $size: Int = ${perPage}, $sort: [MediaSort] = [POPULARITY_DESC]) { Page(page: $page, perPage: $size) { pageInfo { total perPage currentPage lastPage hasNextPage } media(id: $id, type: $type, isAdult: $isAdult, sort: $sort) { id idMal status(version: 2) title { userPreferred romaji english native } trailer { id site thumbnail } format genres bannerImage description coverImage { extraLarge large medium color } episodes meanScore duration season seasonYear averageScore nextAiringEpisode { airingAt timeUntilAiring episode } } } }`; +export const anilistGenresQuery = (genres: string[], page: number = 1, perPage: number = 20) => + `query ($genres: [String] = ${JSON.stringify( + genres + )}, $page: Int = ${page}, $type: MediaType = ANIME, $isAdult: Boolean = false, $size: Int = ${perPage}) {Page(page: $page, perPage: $size) { pageInfo { total perPage currentPage lastPage hasNextPage } media(type: $type, isAdult: $isAdult, genre_in: $genres) { id idMal status(version: 2) title { userPreferred romaji english native } trailer { id site thumbnail } format bannerImage description coverImage { extraLarge large medium color } episodes meanScore duration season seasonYear averageScore nextAiringEpisode { airingAt timeUntilAiring episode } } } }`; +export const anilistAiringScheduleQuery = ( + page: number = 1, + perPage: number = 20, + weekStart: number, + weekEnd: number, + notYetAired: boolean +) => + `query { Page(page: ${page}, perPage: ${perPage}) { pageInfo { total perPage currentPage lastPage hasNextPage } airingSchedules( notYetAired: ${notYetAired}, airingAt_greater: ${weekStart}, airingAt_lesser: ${weekEnd}) { airingAt episode media { id description idMal title { romaji english userPreferred native } countryOfOrigin description popularity bannerImage coverImage { extraLarge large medium color } genres averageScore seasonYear format } } } }`; +export const anilistSiteStatisticsQuery = () => `query { SiteStatistics { anime { nodes { count } } } }`; +export const anilistCharacterQuery = () => + `query character($id: Int) { Character(id: $id) { id name { first middle last full native userPreferred alternative alternativeSpoiler } image { large medium } description gender dateOfBirth { year month day } bloodType age favourites media { edges { characterRole node { id idMal title { romaji english native userPreferred } coverImage { extraLarge large medium color } averageScore startDate { year month day } episodes format status } } } } }`; +export const anilistStaffQuery = () => + `query staff($id: Int, $sort: [MediaSort], $characterPage: Int, $staffPage: Int, $onList: Boolean, $type: MediaType, $withCharacterRoles: Boolean = false, $withStaffRoles: Boolean = false) { Staff(id: $id) { id name { first middle last full native userPreferred alternative } image { large } description favourites isFavourite isFavouriteBlocked age gender yearsActive homeTown bloodType primaryOccupations dateOfBirth { year month day } dateOfDeath { year month day } language: languageV2 characterMedia(page: $characterPage, sort: $sort, onList: $onList) @include(if: $withCharacterRoles) { pageInfo { total perPage currentPage lastPage hasNextPage } edges { characterRole characterName node { id type bannerImage isAdult title { userPreferred } coverImage { large } startDate { year } mediaListEntry { id status } } characters { id name { userPreferred } image { large } } } } staffMedia(page: $staffPage, type: $type, sort: $sort, onList: $onList) @include(if: $withStaffRoles) { pageInfo { total perPage currentPage lastPage hasNextPage } edges { staffRole node { id type isAdult title { userPreferred } coverImage { large } mediaListEntry { id status } } } } } }`; +export const kitsuSearchQuery = (query: string) => + `query{searchAnimeByTitle(first:5, title:"${query}"){ nodes {id season startDate titles { localized } episodes(first: 2000){ nodes { number createdAt titles { canonical } description thumbnail { original { url } } } } } } }`; diff --git a/consumet.ts/src/utils/utils.ts b/consumet.ts/src/utils/utils.ts new file mode 100644 index 00000000..384ca5b4 --- /dev/null +++ b/consumet.ts/src/utils/utils.ts @@ -0,0 +1,213 @@ +// import sharp from 'sharp'; +import { load } from 'cheerio'; +// import * as blurhash from 'blurhash'; +import { ProxyConfig } from '../models'; +import axios, { AxiosRequestConfig } from 'axios'; + +// Converts object to tuples of [prop name,prop type] +// So { a: 'Hello', b: 'World', c: '!!!' } +// will be [a, 'Hello'] | [b, 'World'] | [c, '!!!'] +type TuplesFromObject = { + [P in keyof T]: [P, T[P]]; +}[keyof T]; +// Gets all property keys of a specified value type +// So GetKeyByValue<{ a: 'Hello', b: 'World', c: '!!!' }, 'Hello'> = 'a' +type GetKeyByValue = TuplesFromObject extends infer TT + ? TT extends [infer P, V] + ? P + : never + : never; + +export const remap = < + T extends { [key: string]: any }, + V extends string, // needed to force string literal types for mapping values + U extends { [P in keyof T]: V } +>( + original: T, + mapping: U +) => { + const remapped: any = {}; + + Object.keys(original).forEach(k => { + remapped[mapping[k]] = original[k]; + }); + return remapped as { + // Take all the values in the map, + // so given { a: 'Hello', b: 'World', c: '!!!' } U[keyof U] will produce 'Hello' | 'World' | '!!!' + [P in U[keyof U]]: T[GetKeyByValue]; // Get the original type of the key in T by using GetKeyByValue to get to the original key + }; +}; + +export const USER_AGENT = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'; +export const days = ['Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']; + +export const splitAuthor = (authors: string) => { + const res: string[] = []; + let eater = ''; + for (let i = 0; i < authors.length; i++) { + if (authors[i] == ' ' && (authors[i - 1] == ',' || authors[i - 1] == ';')) { + continue; + } + if (authors[i] == ',' || authors[i] == ';') { + res.push(eater.trim()); + eater = ''; + continue; + } + eater += authors[i]; + } + res.push(eater); + return res; +}; + +export const floorID = (id: string) => { + let imp = ''; + for (let i = 0; i < id?.length - 3; i++) { + imp += id[i]; + } + const idV = parseInt(imp); + return idV * 1000; +}; + +export const formatTitle = (title: string) => { + const result = title.replace(/[0-9]/g, ''); + return result.trim(); +}; + +export const genElement = (s: string, e: string) => { + if (s == '') return; + const $ = load(e); + let i = 0; + let str = ''; + let el = $(); + for (; i < s.length; i++) { + if (s[i] == ' ') { + el = $(str); + str = ''; + i++; + break; + } + str += s[i]; + } + for (; i < s.length; i++) { + if (s[i] == ' ') { + el = $(el).children(str); + str = ''; + continue; + } + str += s[i]; + } + el = $(el).children(str); + return el; +}; + +export const range = ({ from = 0, to = 0, step = 1, length = Math.ceil((to - from) / step) }) => + Array.from({ length }, (_, i) => from + i * step); + +export const capitalizeFirstLetter = (s: string) => s?.charAt(0).toUpperCase() + s.slice(1); + +export const getDays = (day1: string, day2: string) => { + const day1Index = days.indexOf(capitalizeFirstLetter(day1)) - 1; + const day2Index = days.indexOf(capitalizeFirstLetter(day2)) - 1; + const now = new Date(); + const day1Date = new Date(); + const day2Date = new Date(); + day1Date.setDate(now.getDate() + ((day1Index + 7 - now.getDay()) % 7)); + day2Date.setDate(now.getDate() + ((day2Index + 7 - now.getDay()) % 7)); + day1Date.setHours(0, 0, 0, 0); + day2Date.setHours(0, 0, 0, 0); + return [day1Date.getTime() / 1000, day2Date.getTime() / 1000]; +}; + +export const isJson = (str: string) => { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; +}; + +export function convertDuration(milliseconds: number) { + let seconds = Math.floor(milliseconds / 1000); + let minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + seconds = seconds % 60; + minutes = minutes % 60; + + return `PT${hours}H${minutes}M${seconds}S`; +} + +export const compareTwoStrings = (first: string, second: string): number => { + first = first.replace(/\s+/g, ''); + second = second.replace(/\s+/g, ''); + + if (first === second) return 1; // identical or empty + if (first.length < 2 || second.length < 2) return 0; // if either is a 0-letter or 1-letter string + + const firstBigrams = new Map(); + for (let i = 0; i < first.length - 1; i++) { + const bigram = first.substring(i, i + 2); + const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) + 1 : 1; + + firstBigrams.set(bigram, count); + } + + let intersectionSize = 0; + for (let i = 0; i < second.length - 1; i++) { + const bigram = second.substring(i, i + 2); + const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) : 0; + + if (count > 0) { + firstBigrams.set(bigram, count - 1); + intersectionSize++; + } + } + + return (2.0 * intersectionSize) / (first.length + second.length - 2); +}; + +export const substringAfter = (str: string, toFind: string) => { + const index = str.indexOf(toFind); + return index == -1 ? '' : str.substring(index + toFind.length); +}; + +export const substringBefore = (str: string, toFind: string) => { + const index = str.indexOf(toFind); + return index == -1 ? '' : str.substring(0, index); +}; + +export const substringAfterLast = (str: string, toFind: string) => { + const index = str.lastIndexOf(toFind); + return index == -1 ? '' : str.substring(index + toFind.length); +}; + +export const substringBeforeLast = (str: string, toFind: string) => { + const index = str.lastIndexOf(toFind); + return index == -1 ? '' : str.substring(0, index); +}; + +// const generateHash = async (url: string) => { +// let returnedBuffer; + +// const response = await fetch(url); +// const arrayBuffer = await response.arrayBuffer(); +// returnedBuffer = Buffer.from(arrayBuffer); + +// // const { info, data } = await sharp(returnedBuffer).ensureAlpha().raw().toBuffer({ +// // resolveWithObject: true, +// // }); + +// return blurhash.encode(new Uint8ClampedArray(data), info.width, info.height, 4, 3); +// }; + +export const getHashFromImage = (url: string) => { + if (url?.length === 0) { + return ''; + } else { + let hash!: string; + // generateHash(url).then(hashKey => (hash = hashKey)); + return 'hash'; + } +}; diff --git a/consumet.ts/test/anime/anify.test.ts b/consumet.ts/test/anime/anify.test.ts new file mode 100644 index 00000000..71f5ed5b --- /dev/null +++ b/consumet.ts/test/anime/anify.test.ts @@ -0,0 +1,16 @@ +import { ANIME } from '../../src/providers'; + +jest.setTimeout(120000); + +const enime = new ANIME.Anify(); + +test('returns a filled array of anime list', async () => { + const data = await enime.search('spy x family'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of anime data', async () => { + const s = await enime.search('spy x family'); + const data = await enime.fetchAnimeInfo(s.results[0].id); + expect(data).not.toBeNull(); +}); diff --git a/consumet.ts/test/anime/animefox.test.ts b/consumet.ts/test/anime/animefox.test.ts new file mode 100644 index 00000000..1120b8f1 --- /dev/null +++ b/consumet.ts/test/anime/animefox.test.ts @@ -0,0 +1,29 @@ +import { ANIME } from '../../src/providers'; + +jest.setTimeout(120000); + +const animefox = new ANIME.AnimeFox(); + +test('returns a filled array of anime list', async () => { + const animefox = new ANIME.AnimeFox(); + const data = await animefox.search('Overlord IV'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of anime data', async () => { + const res = await animefox.search('Overlord IV'); + const data = await animefox.fetchAnimeInfo(res.results[0].id); // Overlord IV id + expect(data).not.toBeNull(); + expect(data.description).not.toBeNull(); + expect(data.episodes).not.toEqual([]); +}); + +test('returns a filled array of recent animes', async () => { + const data = await animefox.fetchRecentEpisodes(); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of episode sources', async () => { + const data = await animefox.fetchEpisodeSources('overlord-iv-episode-1'); + expect(data.sources).not.toEqual([]); +}); diff --git a/consumet.ts/test/anime/animepahe.test.ts b/consumet.ts/test/anime/animepahe.test.ts new file mode 100644 index 00000000..907e96b2 --- /dev/null +++ b/consumet.ts/test/anime/animepahe.test.ts @@ -0,0 +1,23 @@ +import { ANIME } from '../../src/providers'; + +jest.setTimeout(120000); + +const animepahe = new ANIME.AnimePahe(); + +test('returns a filled array of anime list', async () => { + const data = await animepahe.search('Overlord IV'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of anime data', async () => { + const res = await animepahe.search('Overlord IV'); + const data = await animepahe.fetchAnimeInfo(res.results[0].id); // Overlord IV id + expect(data).not.toBeNull(); +}); + +test('returns a filled object of episode sources', async () => { + const res = await animepahe.search('Overlord IV'); + const data1 = await animepahe.fetchAnimeInfo(res.results[0].id); + const data = await animepahe.fetchEpisodeSources(data1.episodes![0].id); // Episode 1 of Overlord IV + expect(data.sources).not.toEqual([]); +}); diff --git a/consumet.ts/test/anime/animesaturn.test.ts b/consumet.ts/test/anime/animesaturn.test.ts new file mode 100644 index 00000000..336c5bc1 --- /dev/null +++ b/consumet.ts/test/anime/animesaturn.test.ts @@ -0,0 +1,26 @@ +import { ANIME } from '../../src/providers'; + +jest.setTimeout(120000); + +const animesaturn = new ANIME.AnimeSaturn(); +const animeName = 'Tokyo Revengers'; + +test('returns a filled array of anime list', async () => { + const data = await animesaturn.search(animeName); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of anime data', async () => { + const res = await animesaturn.search(animeName); + const data = await animesaturn.fetchAnimeInfo(res.results[0].id); + expect(data).not.toBeNull(); + expect(data.description).not.toBeNull(); + expect(data.episodes).not.toEqual([]); +}); + +test('returns a filled object of episode sources', async () => { + const res = await animesaturn.search(animeName); + const info = await animesaturn.fetchAnimeInfo(res.results[0].id); + const data = await animesaturn.fetchEpisodeSources(info.episodes![0].id); + expect(data.sources).not.toEqual([]); +}); diff --git a/consumet.ts/test/anime/animeunity.test.ts b/consumet.ts/test/anime/animeunity.test.ts new file mode 100644 index 00000000..42dece22 --- /dev/null +++ b/consumet.ts/test/anime/animeunity.test.ts @@ -0,0 +1,25 @@ +import { ANIME } from '../../src/providers'; + +jest.setTimeout(120000); + +const animeunity = new ANIME.AnimeUnity() +const animeName = 'Demon Slayer: Kimetsu no Yaiba Hashira Training Arc'; + +test('returns a filled array of anime list', async () => { + const data = await animeunity.search(animeName); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of anime data', async () => { + const res = await animeunity.search(animeName); + const data = await animeunity.fetchAnimeInfo(res.results[0].id, 1); + expect(data).not.toBeNull(); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of episode sources', async () => { + const res = await animeunity.search(animeName); + const info = await animeunity.fetchAnimeInfo(res.results[0].id, 1); + const data = await animeunity.fetchEpisodeSources(info.episodes![0].id) + expect(data.sources).not.toEqual([]); +}); diff --git a/consumet.ts/test/anime/gogoanime.test.ts b/consumet.ts/test/anime/gogoanime.test.ts new file mode 100644 index 00000000..ba500a98 --- /dev/null +++ b/consumet.ts/test/anime/gogoanime.test.ts @@ -0,0 +1,75 @@ +import { info } from 'console'; +import { ANIME } from '../../src/providers'; + +jest.setTimeout(120000); + +const gogoanime = new ANIME.Gogoanime(); + +test('returns a filled array of anime list', async () => { + const data = await gogoanime.search('spy x family'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of anime data', async () => { + const data = await gogoanime.fetchAnimeInfo('spy-x-family'); + expect(data).not.toBeNull(); +}); + +test('Returns genre info for gogoanime', async () => { + const data = await gogoanime.fetchGenreInfo('cars', 2); + expect(data).not.toEqual([]); +}); + +test('returns a filled array of servers', async () => { + const data = await gogoanime.fetchEpisodeServers('spy-x-family-episode-9'); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of episode sources', async () => { + const data = await gogoanime.fetchEpisodeSources('spy-x-family-episode-9'); + expect(data.sources).not.toEqual([]); +}); + +test('returns a filled array of available genres', async () => { + const data = await gogoanime.fetchGenreList(); + expect(data).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await gogoanime.fetchAnimeList(); + expect(data).not.toEqual([]); + + const resultSample = data.results[0]; + expect(resultSample).toHaveProperty('genres'); + expect(resultSample).toHaveProperty('releaseDate'); +}); + +test('returns a filled array of recent episodes', async () => { + const data = await gogoanime.fetchRecentEpisodes(); + expect(data).not.toEqual([]); +}); + +test('returns a filled array of recent movies', async () => { + const data = await gogoanime.fetchRecentMovies(); + expect(data).not.toEqual([]); +}); + +test('returns a filled array of popular anime', async () => { + const data = await gogoanime.fetchPopular(); + expect(data).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await gogoanime.fetchTopAiring(); + expect(data).not.toEqual([]); + + const resultSample = data.results[0]; + expect(resultSample).toHaveProperty('genres'); + expect(resultSample).toHaveProperty('episodeNumber'); + expect(resultSample).toHaveProperty('episodeId'); +}); + +test('returns a filled array of direct download link', async () => { + const data = await gogoanime.fetchDirectDownloadLink('https://embtaku.pro/download?id=MjE4NTQ2&token=-uq9s5PsPto2lD8SC6NBqQ&expires=1711622781'); + expect(data).not.toEqual([]); +}); \ No newline at end of file diff --git a/consumet.ts/test/anime/zoro.test.ts b/consumet.ts/test/anime/zoro.test.ts new file mode 100644 index 00000000..af1ff190 --- /dev/null +++ b/consumet.ts/test/anime/zoro.test.ts @@ -0,0 +1,81 @@ +import { ANIME } from '../../src/providers'; + +jest.setTimeout(120000); + +const zoro = new ANIME.Zoro("hianime.to"); + +test('returns a filled array of anime list', async () => { + const data = await zoro.search('Overlord IV'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await zoro.fetchTopAiring(); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await zoro.fetchMostPopular(); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await zoro.fetchMostFavorite(); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await zoro.fetchLatestCompleted(); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await zoro.fetchRecentlyUpdated(); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await zoro.fetchRecentlyAdded(); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await zoro.fetchTopUpcoming(); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await zoro.fetchStudio('studio-pierrot') + expect(data.results).not.toEqual([]); +}) + +test('returns a filled array of anime list', async () => { + const data = await zoro.fetchSchedule(); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await zoro.fetchSpotlight(); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of anime list', async () => { + const data = await zoro.fetchSearchSuggestions("one piece"); + console.log(data); + expect(data.results).not.toEqual([]); +}) + +test('returns a filled object of anime data', async () => { + const res = await zoro.search('Overlord IV'); + const data = await zoro.fetchAnimeInfo("one-piece-100"); // Overlord IV id + expect(data).not.toBeNull(); + expect(data.description).not.toBeNull(); + expect(data.episodes).not.toEqual([]); +}); + +test('returns a filled object of episode sources', async () => { + const res = await zoro.search('Overlord IV'); + const info = await zoro.fetchAnimeInfo(res.results[3].id); + const data = await zoro.fetchEpisodeSources(info.episodes![0].id); // Overlord IV episode 1 id + expect(data.sources).not.toEqual([]); +}); diff --git a/consumet.ts/test/books/libgen.test.ts b/consumet.ts/test/books/libgen.test.ts new file mode 100644 index 00000000..5d5b5998 --- /dev/null +++ b/consumet.ts/test/books/libgen.test.ts @@ -0,0 +1,17 @@ +import { BOOKS } from '../../src/providers'; + +jest.setTimeout(120000); + +const libgen = new BOOKS.Libgen(); + +test('returns true', async () => { + const data = await libgen.search('batman', 1); + expect(data.hasNextPage).toEqual(true); +}); + +test('does nothing', async () => { + const data = await libgen.scrapeBook( + 'http://libgen.rs/book/index.php?md5=511972AA87FD4DA91350A6079F887588' + ); + expect(data).not.toBeNull(); +}); diff --git a/consumet.ts/test/comics/getComics.test.ts b/consumet.ts/test/comics/getComics.test.ts new file mode 100644 index 00000000..10b7a5a7 --- /dev/null +++ b/consumet.ts/test/comics/getComics.test.ts @@ -0,0 +1,10 @@ +import { COMICS } from '../../src/providers'; + +jest.setTimeout(120000); + +const comics = new COMICS.GetComics(); + +test('GetComics returns the correct page and is not empty', async () => { + const data = await comics.search('adam', 2); + expect(data.hasNextPage).toEqual(true); +}); diff --git a/consumet.ts/test/light-novels/novelupdates.test.ts b/consumet.ts/test/light-novels/novelupdates.test.ts new file mode 100644 index 00000000..17b966e7 --- /dev/null +++ b/consumet.ts/test/light-novels/novelupdates.test.ts @@ -0,0 +1,16 @@ +import { LIGHT_NOVELS } from '../../src/providers'; + +jest.setTimeout(120000); + +const novelupdates = new LIGHT_NOVELS.NovelUpdates(); + +test('returns a filled array of light novels', async () => { + const data = await novelupdates.search('slime'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of light novel info', async () => { + const data = await novelupdates.fetchLightNovelInfo('you-are-the-daughter-of-my-first-love'); + expect(data.chapters).not.toEqual([]); + expect(data.description).not.toEqual(''); +}); diff --git a/consumet.ts/test/light-novels/readlightnovels.test.ts b/consumet.ts/test/light-novels/readlightnovels.test.ts new file mode 100644 index 00000000..4068d7e9 --- /dev/null +++ b/consumet.ts/test/light-novels/readlightnovels.test.ts @@ -0,0 +1,16 @@ +import { LIGHT_NOVELS } from '../../src/providers'; + +jest.setTimeout(120000); + +const readlightnovels = new LIGHT_NOVELS.ReadLightNovels(); + +test('returns a filled array of light novels', async () => { + const data = await readlightnovels.search('slime'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of light novel info', async () => { + const data = await readlightnovels.fetchLightNovelInfo('tensei-shitara-slime-datta-ken'); + expect(data.chapters).not.toEqual([]); + expect(data.description).not.toEqual(''); +}); diff --git a/consumet.ts/test/manga/asurascans.test.ts b/consumet.ts/test/manga/asurascans.test.ts new file mode 100644 index 00000000..9fa97fca --- /dev/null +++ b/consumet.ts/test/manga/asurascans.test.ts @@ -0,0 +1,23 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +const asura = new MANGA.AsuraScans(); + +test('returns a filled array of manga', async () => { + const data = await asura.search('solo leveling'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of chapters', async () => { + const data = await asura.search('solo leveling'); + const chapters = await asura.fetchMangaInfo(data.results[0].id); + expect(chapters.chapters).not.toEqual([]); +}); + +test('returns a filled array of pages', async () => { + const data = await asura.search('solo leveling'); + const chapters = await asura.fetchMangaInfo(data.results[0].id); + const pages = await asura.fetchChapterPages(chapters.chapters![0].id); + expect(pages).not.toEqual([]); +}); diff --git a/consumet.ts/test/manga/brmangas.test.ts b/consumet.ts/test/manga/brmangas.test.ts new file mode 100644 index 00000000..781993c6 --- /dev/null +++ b/consumet.ts/test/manga/brmangas.test.ts @@ -0,0 +1,11 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +const brmangas = new MANGA.BRMangas(); + +test('returns a filled array of manga', async () => { + const data = await brmangas.search('punpun'); + + expect(data.results).not.toEqual([]); +}); diff --git a/consumet.ts/test/manga/comick.test.ts b/consumet.ts/test/manga/comick.test.ts new file mode 100644 index 00000000..6fa30b3d --- /dev/null +++ b/consumet.ts/test/manga/comick.test.ts @@ -0,0 +1,23 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +const comick = new MANGA.ComicK(); + +test('returns a filled array of manga', async () => { + const data = await comick.search('one piece'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of chapters', async () => { + const data = await comick.search('one piece'); + const chapters = await comick.fetchMangaInfo(data.results[0].id); + expect(chapters.chapters).not.toEqual([]); +}); + +test('returns a filled array of pages', async () => { + const data = await comick.search('one piece'); + const chapters = await comick.fetchMangaInfo(data.results[0].id); + const pages = await comick.fetchChapterPages(chapters.chapters![0].id); + expect(pages).not.toEqual([]); +}); diff --git a/consumet.ts/test/manga/flamescans.test.ts b/consumet.ts/test/manga/flamescans.test.ts new file mode 100644 index 00000000..f35b8798 --- /dev/null +++ b/consumet.ts/test/manga/flamescans.test.ts @@ -0,0 +1,23 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +const flame = new MANGA.FlameScans(); + +test('returns a filled array of manga', async () => { + const data = await flame.search('returners magic'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of chapters', async () => { + const data = await flame.search('returners magic'); + const chapters = await flame.fetchMangaInfo(data.results[0].id); + expect(chapters.chapters).not.toEqual([]); +}); + +test('returns a filled array of pages', async () => { + const data = await flame.search('returners magic'); + const chapters = await flame.fetchMangaInfo(data.results[0].id); + const pages = await flame.fetchChapterPages(chapters.chapters![0].id); + expect(pages).not.toEqual([]); +}); diff --git a/consumet.ts/test/manga/mangaPill.test.ts b/consumet.ts/test/manga/mangaPill.test.ts new file mode 100644 index 00000000..c4649695 --- /dev/null +++ b/consumet.ts/test/manga/mangaPill.test.ts @@ -0,0 +1,22 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +// run: yarn test --watch --verbose false mangapill.test.ts + +const mangaPill = new MANGA.MangaPill(); + +test('Search: returns a filled array of manga.', async () => { + const data = await mangaPill.search('one piece'); + expect(data.results).not.toEqual([]); +}); + +test('fetchMangaInfo: returns filled manga info when given a mangaId.', async () => { + const data = await mangaPill.fetchMangaInfo('2/one-piece'); + expect(data).not.toEqual({}); +}); + +test('fetchChapterPages: returns filled page data when given a chapterId.', async () => { + const data = await mangaPill.fetchChapterPages('2-11069000/one-piece-chapter-1069'); + expect(data).not.toEqual([]); +}); diff --git a/consumet.ts/test/manga/mangaReader.test.ts b/consumet.ts/test/manga/mangaReader.test.ts new file mode 100644 index 00000000..52e81fb3 --- /dev/null +++ b/consumet.ts/test/manga/mangaReader.test.ts @@ -0,0 +1,22 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +// run: yarn test --watch --verbose false mangareader.test.ts + +const mangaReader = new MANGA.MangaReader(); + +test('Search: returns a filled array of manga.', async () => { + const data = await mangaReader.search('one piece'); + expect(data.results).not.toEqual([]); +}); + +test('fetchMangaInfo: returns filled manga info when given a mangaId.', async () => { + const data = await mangaReader.fetchMangaInfo('one-piece-colored-edition-55493'); + expect(data).not.toEqual({}); +}); + +test('fetchChapterPages: returns filled page data when given a chapterId.', async () => { + const data = await mangaReader.fetchChapterPages('one-piece-colored-edition-55493/en/chapter-1004'); + expect(data).not.toEqual([]); +}); diff --git a/consumet.ts/test/manga/mangadex.test.ts b/consumet.ts/test/manga/mangadex.test.ts new file mode 100644 index 00000000..04ca5a18 --- /dev/null +++ b/consumet.ts/test/manga/mangadex.test.ts @@ -0,0 +1,10 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +const mangadex = new MANGA.MangaDex(); + +test('returns a filled array of manga', async () => { + const data = await mangadex.search('one piece'); + expect(data.results).not.toEqual([]); +}); diff --git a/consumet.ts/test/manga/mangahere.test.ts b/consumet.ts/test/manga/mangahere.test.ts new file mode 100644 index 00000000..1a466e61 --- /dev/null +++ b/consumet.ts/test/manga/mangahere.test.ts @@ -0,0 +1,10 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +const mangahere = new MANGA.MangaHere(); + +test('returns a filled array of manga', async () => { + const data = await mangahere.search('slime'); + expect(data.results).not.toEqual([]); +}); diff --git a/consumet.ts/test/manga/mangahost.test.ts b/consumet.ts/test/manga/mangahost.test.ts new file mode 100644 index 00000000..01efe1d9 --- /dev/null +++ b/consumet.ts/test/manga/mangahost.test.ts @@ -0,0 +1,11 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +const mangahost = new MANGA.MangaHost(); + +test('returns a filled array of manga', async () => { + const data = await mangahost.search('punpun'); + + expect(data.results).not.toEqual([]); +}); diff --git a/consumet.ts/test/manga/mangakakalot.test.ts b/consumet.ts/test/manga/mangakakalot.test.ts new file mode 100644 index 00000000..10fbfa03 --- /dev/null +++ b/consumet.ts/test/manga/mangakakalot.test.ts @@ -0,0 +1,10 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +const mangakakalot = new MANGA.MangaKakalot(); + +test('returns a filled array of manga', async () => { + const data = await mangakakalot.search('Tomodachi Game'); + expect(data.results).not.toEqual([]); +}); diff --git a/consumet.ts/test/manga/mangapark.test.ts b/consumet.ts/test/manga/mangapark.test.ts new file mode 100644 index 00000000..2b561c5a --- /dev/null +++ b/consumet.ts/test/manga/mangapark.test.ts @@ -0,0 +1,22 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +// run: yarn test --watch --verbose false mangapark.test.ts + +const mangapark = new MANGA.Mangapark(); + +test('Search: returns a filled array of manga.', async () => { + const data = await mangapark.search('Demon Slayer'); + expect(data.results).not.toEqual([]); +}); + +test('fetchMangaInfo: returns filled manga info when given a mangaId.', async () => { + const data = await mangapark.fetchMangaInfo('kimetsu-no-yaiba-gotouge-koyoharu'); + expect(data).not.toEqual({}); +}); + +test('fetchChapterPages: returns filled page data when given a chapterId.', async () => { + const data = await mangapark.fetchChapterPages('kimetsu-no-yaiba-gotouge-koyoharu/i2325814'); + expect(data).not.toEqual([]); +}); diff --git a/consumet.ts/test/manga/mangasee123.test.ts b/consumet.ts/test/manga/mangasee123.test.ts new file mode 100644 index 00000000..b8720fd6 --- /dev/null +++ b/consumet.ts/test/manga/mangasee123.test.ts @@ -0,0 +1,22 @@ +import { MANGA } from '../../src/providers'; + +jest.setTimeout(120000); + +// run: yarn test --watch --verbose false mangasee123.test.ts + +const mangasee123 = new MANGA.Mangasee123(); + +test('Search: returns a filled array of manga.', async () => { + const data = await mangasee123.search('Call of the Night'); + expect(data.results).not.toEqual([]); +}); + +test('fetchMangaInfo: returns filled manga info when given a mangaId.', async () => { + const data = await mangasee123.fetchMangaInfo('Yofukashi-no-Uta'); + expect(data).not.toEqual({}); +}); + +test('fetchChapterPages: returns filled page data when given a chapterId.', async () => { + const data = await mangasee123.fetchChapterPages('Yofukashi-no-Uta-chapter-1'); + expect(data).not.toEqual([]); +}); diff --git a/consumet.ts/test/meta/anilist.test.ts b/consumet.ts/test/meta/anilist.test.ts new file mode 100644 index 00000000..b4a9d802 --- /dev/null +++ b/consumet.ts/test/meta/anilist.test.ts @@ -0,0 +1,54 @@ +import { META } from '../../src/providers'; + +jest.setTimeout(120000); + +const anilist = new META.Anilist(); + +test('returns a filled array of anime list', async () => { + const data = await anilist.search('spy x family'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of anime data', async () => { + const data = await anilist.fetchAnimeInfo('140960'); + expect(data).not.toBeNull(); + expect(data.episodes).not.toEqual([]); + expect(data.description).not.toBeNull(); +}); + +test('returns episodes for sub and dub not available', async () => { + const subData = await anilist.fetchEpisodesListById('949', false); + expect(subData).not.toBeNull(); + expect(subData).not.toEqual([]); + + const dubData = await anilist.fetchEpisodesListById('949', true); + expect(dubData).not.toBeNull(); + expect(dubData).not.toEqual([]); +}); + +test('returns a filled array of servers', async () => { + const data = await anilist.fetchEpisodeServers('spy-x-family-episode-9'); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of episode sources', async () => { + const data = await anilist.fetchEpisodeSources('spy-x-family-episode-9'); + expect(data.sources).not.toEqual([]); +}); + +test('returns a filled array of trending anime', async () => { + const data = await anilist.fetchTrendingAnime(1, 10); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of popular anime', async () => { + const data = await anilist.fetchPopularAnime(1, 10); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of airing schedule', async () => { + const weekStart = Math.ceil(Date.now() / 1000); + const data = await anilist.fetchAiringSchedule(1, 20, weekStart, weekStart + 604800, true); + expect(data.results).not.toEqual([]); +}); +(''); diff --git a/consumet.ts/test/meta/tmdb.test.ts b/consumet.ts/test/meta/tmdb.test.ts new file mode 100644 index 00000000..b5976b3e --- /dev/null +++ b/consumet.ts/test/meta/tmdb.test.ts @@ -0,0 +1,50 @@ +import { TvType } from '../../src/models'; +import { META } from '../../src/providers'; + +jest.setTimeout(120000); + +// run: yarn test --watch --verbose false tmdb.test.ts + +const tmdb = new META.TMDB(); + +test('returns a filled array of movie list', async () => { + const data = await tmdb.search('the flash'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of trending movie list', async () => { + const data = await tmdb.fetchTrending('movie'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of trending tv-series list', async () => { + const data = await tmdb.fetchTrending('TV Series'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of trending people list', async () => { + const data = await tmdb.fetchTrending('People'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled array of all trending list', async () => { + const data = await tmdb.fetchTrending('all'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of anime data', async () => { + const data = await tmdb.fetchMediaInfo('85937', 'tv'); + expect(data).not.toBeNull(); + expect(data.episodes).not.toEqual([]); + expect(data.description).not.toBeNull(); +}); + +test('returns a filled array of servers', async () => { + const data = await tmdb.fetchEpisodeServers('2899', 'tv/watch-the-flash-39535'); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of episode sources', async () => { + const data = await tmdb.fetchEpisodeSources('2899', 'tv/watch-the-flash-39535'); + expect(data.sources).not.toEqual([]); +}); diff --git a/consumet.ts/test/movies/dramacool.test.ts b/consumet.ts/test/movies/dramacool.test.ts new file mode 100644 index 00000000..66f7dc8b --- /dev/null +++ b/consumet.ts/test/movies/dramacool.test.ts @@ -0,0 +1,47 @@ +import { MOVIES } from '../../src/providers'; + +jest.setTimeout(120000); + +// run: yarn test --watch --verbose false dramacool.test.ts + +const dramaCool = new MOVIES.DramaCool(); + +test('Search: returns a filled array of movies/TV.', async () => { + const data = await dramaCool.search('Vincenzo'); + expect(data.results).not.toEqual([]); +}); + +test('fetchMediaInfo: returns filled movie/TV info when given a mediaId.', async () => { + const data = await dramaCool.fetchMediaInfo('drama-detail/vincenzo'); + expect(data).not.toEqual({}); +}); + +test('fetchEpisodeServers: returns filled object of streaming sources when given an episodeId.', async () => { + const data = await dramaCool.fetchEpisodeServers('vincenzo-2021-episode-1'); + expect(data).not.toEqual({}); +}); + +test('fetchEpisodeSources: returns filled object of streaming sources when given an episodeId.', async () => { + const data = await dramaCool.fetchEpisodeSources('vincenzo-2021-episode-1'); + expect(data).not.toEqual({}); +}); + +test('fetchMediaInfo: returns genres list when given a mediaId.', async () => { + const data = await dramaCool.fetchMediaInfo('drama-detail/vincenzo'); + expect(data.genres?.length).not.toEqual([]); +}); + +test('fetchMediaInfo: returns status when given a mediaId.', async () => { + const data = await dramaCool.fetchMediaInfo('drama-detail/vincenzo'); + expect(data.status).not.toEqual(undefined); +}); + +test('fetchMediaInfo: returns duration (if available) when given a mediaId.', async () => { + const data = await dramaCool.fetchMediaInfo('drama-detail/kimi-ga-kokoro-wo-kuretakara'); + expect(data.duration).not.toEqual(undefined); +}); + +test('Search: returns totalPages when search: Love.', async () => { + const data = await dramaCool.search('Love'); + expect(data.totalPages).not.toEqual(1); +}); \ No newline at end of file diff --git a/consumet.ts/test/movies/flixhq.test.ts b/consumet.ts/test/movies/flixhq.test.ts new file mode 100644 index 00000000..ecdb80e7 --- /dev/null +++ b/consumet.ts/test/movies/flixhq.test.ts @@ -0,0 +1,31 @@ +import { MOVIES } from '../../src/providers'; + +jest.setTimeout(120000); + +const flixhq = new MOVIES.FlixHQ(); + +test('returns a filled array of movies/tv', async () => { + const data = await flixhq.search('vincenzo'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of movies/tv data', async () => { + const data = await flixhq.fetchMediaInfo('tv/watch-vincenzo-67955'); + expect(data.description).not.toEqual(''); + expect(data.episodes).not.toEqual([]); +}); + +test('returns a filled object of streaming sources', async () => { + const episodeSources = await flixhq.fetchEpisodeSources('1167571', 'tv/watch-vincenzo-67955'); + expect(episodeSources.sources).not.toEqual([]); +}); + +test('returns a filled object of movies/tv data by country', async () => { + const data = await flixhq.fetchByCountry('KR'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of movies/tv data by genre', async () => { + const data = await flixhq.fetchByGenre('drama'); + expect(data.results).not.toEqual([]); +}); diff --git a/consumet.ts/test/movies/goku.test.ts b/consumet.ts/test/movies/goku.test.ts new file mode 100644 index 00000000..dd915d83 --- /dev/null +++ b/consumet.ts/test/movies/goku.test.ts @@ -0,0 +1,62 @@ +import { MOVIES } from '../../src/providers'; + +jest.setTimeout(120000); + +const goku = new MOVIES.Goku(); + +test('returns a filled array of movies/tv', async () => { + const data = await goku.search('batman'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of movies data', async () => { + const data = await goku.fetchMediaInfo('watch-movie/watch-batman-begins-19636'); + expect(data.description).not.toEqual(''); + expect(data.episodes).not.toEqual([]); +}); + +test('returns a filled object of streaming movie servers', async () => { + const data = await goku.fetchEpisodeServers('1064170', 'watch-movie/watch-batman-begins-19636'); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of streaming movie sources', async () => { + const data = await goku.fetchEpisodeSources('1064170', 'watch-movie/watch-batman-begins-19636'); + expect(data.sources).not.toEqual([]); +}); + +test('returns a filled object of tv data', async () => { + const data = await goku.fetchMediaInfo('watch-series/watch-batman-39276'); + expect(data.description).not.toEqual(''); + expect(data.episodes).not.toEqual([]); +}); + +test('returns a filled object of streaming tv servers', async () => { + const data = await goku.fetchEpisodeServers('46259', 'watch-series/watch-batman-39276'); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of streaming tv sources', async () => { + const data = await goku.fetchEpisodeSources('46259', 'watch-series/watch-batman-39276'); + expect(data.sources).not.toEqual([]); +}); + +test('returns a filled object of recent movies', async () => { + const data = await goku.fetchRecentMovies(); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of recent tv shows', async () => { + const data = await goku.fetchRecentTvShows(); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of trending movies', async () => { + const data = await goku.fetchTrendingMovies(); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of trending tv shows', async () => { + const data = await goku.fetchTrendingTvShows(); + expect(data).not.toEqual([]); +}); diff --git a/consumet.ts/test/movies/kissasian.test.ts b/consumet.ts/test/movies/kissasian.test.ts new file mode 100644 index 00000000..2f74d382 --- /dev/null +++ b/consumet.ts/test/movies/kissasian.test.ts @@ -0,0 +1,27 @@ +import { MOVIES } from '../../src/providers'; + +jest.setTimeout(120000); + +// run: yarn test --watch --verbose false viewasian.test.ts + +const kissAsian = new MOVIES.KissAsian(); + +test('Search: returns a filled array of movies/TV.', async () => { + const data = await kissAsian.search('vincenzo'); + expect(data.results).not.toEqual([]); +}); + +test('fetchMediaInfo: returns filled movie/TV info when given a mediaId.', async () => { + const data = await kissAsian.fetchMediaInfo('Drama/Vincenzo'); + expect(data).not.toEqual({}); +}); + +test('fetchEpisodeServers: returns filled object of streaming sources when given an episodeId.', async () => { + const data = await kissAsian.fetchEpisodeServers('Drama/Vincenzo/Episode-1?id=62565'); + expect(data).not.toEqual({}); +}); + +test('fetchEpisodeSources: returns filled object of streaming sources when given an episodeId.', async () => { + const data = await kissAsian.fetchEpisodeSources('Drama/Vincenzo/Episode-1?id=62565'); + expect(data).not.toEqual({}); +}); diff --git a/consumet.ts/test/movies/moviehdwatch.test.ts b/consumet.ts/test/movies/moviehdwatch.test.ts new file mode 100644 index 00000000..bc00f20e --- /dev/null +++ b/consumet.ts/test/movies/moviehdwatch.test.ts @@ -0,0 +1,62 @@ +import { MOVIES } from '../../src/providers'; + +jest.setTimeout(120000); + +const moviesHd = new MOVIES.MovieHdWatch(); + +test('returns a filled array of movies/tv', async () => { + const data = await moviesHd.search('batman'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of movies data', async () => { + const data = await moviesHd.fetchMediaInfo('movie/watch-the-batman-online-16076'); + expect(data.description).not.toEqual(''); + expect(data.episodes).not.toEqual([]); +}); + +test('returns a filled object of streaming movie servers', async () => { + const data = await moviesHd.fetchEpisodeServers('16076', 'movie/watch-the-batman-online-16076'); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of streaming movie sources', async () => { + const data = await moviesHd.fetchEpisodeSources('16076', 'movie/watch-the-batman-online-16076'); + expect(data.sources).not.toEqual([]); +}); + +test('returns a filled object of tv data', async () => { + const data = await moviesHd.fetchMediaInfo('tv/watch-batman-online-39276'); + expect(data.description).not.toEqual(''); + expect(data.episodes).not.toEqual([]); +}); + +test('returns a filled object of streaming tv servers', async () => { + const data = await moviesHd.fetchEpisodeServers('46259', 'tv/watch-batman-online-39276'); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of streaming tv sources', async () => { + const data = await moviesHd.fetchEpisodeSources('46259', 'tv/watch-batman-online-39276'); + expect(data.sources).not.toEqual([]); +}); + +test('returns a filled object of recent movies', async () => { + const data = await moviesHd.fetchRecentMovies(); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of recent tv shows', async () => { + const data = await moviesHd.fetchRecentTvShows(); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of trending movies', async () => { + const data = await moviesHd.fetchTrendingMovies(); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of trending tv shows', async () => { + const data = await moviesHd.fetchTrendingTvShows(); + expect(data).not.toEqual([]); +}); diff --git a/consumet.ts/test/movies/smashystream.test.ts b/consumet.ts/test/movies/smashystream.test.ts new file mode 100644 index 00000000..22705c3d --- /dev/null +++ b/consumet.ts/test/movies/smashystream.test.ts @@ -0,0 +1,35 @@ +import { MOVIES } from '../../src/providers'; + +jest.setTimeout(120000); + +const smashyStream = new MOVIES.SmashyStream(); + +test('returns a filled object of streaming movie servers', async () => { + const data = await smashyStream.fetchEpisodeServers('697843'); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of streaming movie sources', async () => { + const data = await smashyStream.fetchEpisodeSources('697843', undefined, undefined, 'Player F'); + expect(data.sources).not.toEqual([]); +}); + +test('returns a filled object of streaming movie sources for all players', async () => { + const data = await smashyStream.fetchEpisodeSources('697843'); + expect(data.sources).not.toEqual([]); +}); + +test('returns a filled object of streaming tv servers', async () => { + const data = await smashyStream.fetchEpisodeServers('1399', 1, 10); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of streaming tv sources', async () => { + const data = await smashyStream.fetchEpisodeSources('1399', 1, 1, 'Player F'); + expect(data.sources).not.toEqual([]); +}); + +test('returns a filled object of streaming tv sources for all players', async () => { + const data = await smashyStream.fetchEpisodeSources('1399', 1, 1); + expect(data.sources).not.toEqual([]); +}); diff --git a/consumet.ts/test/movies/turkish123.test.ts b/consumet.ts/test/movies/turkish123.test.ts new file mode 100644 index 00000000..78788e31 --- /dev/null +++ b/consumet.ts/test/movies/turkish123.test.ts @@ -0,0 +1,21 @@ +import { MOVIES } from '../../src/providers'; + +jest.setTimeout(120000); + +const turk = new MOVIES.Turkish(); + +test('returns a filled array of tv', async () => { + const data = await turk.search('sen'); + expect(data).not.toEqual([]); +}); + +test('returns info of movie', async () => { + const data = await turk.fetchMediaInfo('sen-cal-kapimi'); + expect(data.episodes).not.toEqual([]); + expect(data.title).not.toEqual(''); +}); + +test('returns a m3u8 links', async () => { + const data = await turk.fetchEpisodeSources('sen-cal-kapimi-episode-2'); + expect(data.sources[0].url).not.toEqual(''); +}); diff --git a/consumet.ts/test/movies/viewAsian.test.ts b/consumet.ts/test/movies/viewAsian.test.ts new file mode 100644 index 00000000..406a7968 --- /dev/null +++ b/consumet.ts/test/movies/viewAsian.test.ts @@ -0,0 +1,22 @@ +import { MOVIES } from '../../src/providers'; + +jest.setTimeout(120000); + +// run: yarn test --watch --verbose false viewasian.test.ts + +const viewAsian = new MOVIES.ViewAsian(); + +test('Search: returns a filled array of movies/TV.', async () => { + const data = await viewAsian.search('Vincenzo'); + expect(data.results).not.toEqual([]); +}); + +test('fetchMediaInfo: returns filled movie/TV info when given a mediaId.', async () => { + const data = await viewAsian.fetchMediaInfo('drama/vincenzo'); + expect(data).not.toEqual({}); +}); + +test('fetchEpisodeSources: returns filled object of streaming sources when given an episodeId.', async () => { + const data = await viewAsian.fetchEpisodeSources('/watch/vincenzo/watching.html$episode$20'); + expect(data).not.toEqual({}); +}); diff --git a/consumet.ts/test/news/animenewsnetwork.test.ts b/consumet.ts/test/news/animenewsnetwork.test.ts new file mode 100644 index 00000000..f7788c49 --- /dev/null +++ b/consumet.ts/test/news/animenewsnetwork.test.ts @@ -0,0 +1,17 @@ +import { NEWS } from '../../src/providers'; + +jest.setTimeout(120000); + +test('returns a filled array of news feeds', async () => { + const ann = new NEWS.ANN(); + const data = await ann.fetchNewsFeeds(); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of news data', async () => { + const ann = new NEWS.ANN(); + const data = await ann.fetchNewsInfo( + '2022-08-26/higurashi-no-naku-koro-ni-rei-oni-okoshi-hen-manga-ends/.188996' + ); + expect(data.description).not.toEqual(''); +}); diff --git a/consumet.ts/test/streamlare.test.ts b/consumet.ts/test/streamlare.test.ts new file mode 100644 index 00000000..9e4020b6 --- /dev/null +++ b/consumet.ts/test/streamlare.test.ts @@ -0,0 +1,10 @@ +import { StreamLare } from '../src/extractors'; + +const streamlare = new StreamLare(); + +const testicles = async () => { + const data = await streamlare.extract(new URL('https://slmaxed.com/v/RWwM7lM8ZPPzZKbo')); + console.log(data); +}; + +testicles(); diff --git a/consumet.ts/tsconfig.json b/consumet.ts/tsconfig.json new file mode 100644 index 00000000..37761c75 --- /dev/null +++ b/consumet.ts/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "commonjs", + "rootDir": "src", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "outDir": "dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "lib": ["esnext"], + "include": ["src"] +}