├── requirements.txt
├── scripts
├── __init__.py
├── common.py
├── git.py
├── generator_source.py
├── generator_readme.py
├── generate_extension.py
├── model.py
├── versions.json
└── README-temp.md
├── javascript
├── icon
│ ├── .nomedia
│ ├── ja.weloma.jpg
│ ├── all.kisskh.jpg
│ ├── all.xprime.jpg
│ ├── ta.moviesda.jpg
│ └── en.subsplease.jpg
├── novel
│ └── src
│ │ ├── .nomedia
│ │ └── en
│ │ └── novelbuddy.js
├── anime
│ └── src
│ │ ├── en
│ │ ├── subsplease.js
│ │ ├── animegg.js
│ │ ├── animeparadise.js
│ │ ├── animeonsen.js
│ │ ├── sudatchi.js
│ │ ├── animez.js
│ │ ├── gojo.js
│ │ ├── kaido.js
│ │ └── anixl.js
│ │ └── all
│ │ └── dramacool.js
└── manga
│ └── src
│ ├── en
│ ├── weebcentral.js
│ ├── mangapill.js
│ └── readcomiconline.js
│ └── ja
│ └── weloma.js
├── images
├── add-all-repositories.png
├── add-anime-repository.png
├── add-manga-repository.png
├── add-novel-repository.png
├── add-all-repositories-livecontainer.png
├── add-anime-repository-livecontainer.png
├── add-manga-repository-livecontainer.png
└── add-novel-repository-livecontainer.png
├── repo.json
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── request_source.yml
│ ├── request_feature.yml
│ └── report_issue.yml
└── workflows
│ └── gen_index.yml
├── novel_index.json
├── index.json
├── .gitignore
├── README.md
├── CONTRIBUTING-JS.md
└── LICENSE
/requirements.txt:
--------------------------------------------------------------------------------
1 | pytz
--------------------------------------------------------------------------------
/scripts/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/javascript/icon/.nomedia:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/javascript/novel/src/.nomedia:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/javascript/icon/ja.weloma.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/javascript/icon/ja.weloma.jpg
--------------------------------------------------------------------------------
/images/add-all-repositories.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/images/add-all-repositories.png
--------------------------------------------------------------------------------
/images/add-anime-repository.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/images/add-anime-repository.png
--------------------------------------------------------------------------------
/images/add-manga-repository.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/images/add-manga-repository.png
--------------------------------------------------------------------------------
/images/add-novel-repository.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/images/add-novel-repository.png
--------------------------------------------------------------------------------
/javascript/icon/all.kisskh.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/javascript/icon/all.kisskh.jpg
--------------------------------------------------------------------------------
/javascript/icon/all.xprime.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/javascript/icon/all.xprime.jpg
--------------------------------------------------------------------------------
/javascript/icon/ta.moviesda.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/javascript/icon/ta.moviesda.jpg
--------------------------------------------------------------------------------
/javascript/icon/en.subsplease.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/javascript/icon/en.subsplease.jpg
--------------------------------------------------------------------------------
/repo.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Mangayomi Swak extensions",
3 | "website": "https://github.com/Swakshan/mangayomi-swak-extensions"
4 | }
--------------------------------------------------------------------------------
/images/add-all-repositories-livecontainer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/images/add-all-repositories-livecontainer.png
--------------------------------------------------------------------------------
/images/add-anime-repository-livecontainer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/images/add-anime-repository-livecontainer.png
--------------------------------------------------------------------------------
/images/add-manga-repository-livecontainer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/images/add-manga-repository-livecontainer.png
--------------------------------------------------------------------------------
/images/add-novel-repository-livecontainer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/HEAD/images/add-novel-repository-livecontainer.png
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: ⚠️ Application issue
4 | url: https://github.com/NAME/mangayomi/issues/new/choose
5 | about: Issues and requests about the app itself should be opened in the mangayomi repository instead
6 | - name: Mangayomi app GitHub repository
7 | url: https://github.com/NAME/mangayomi
8 | about: Issues about the app itself should be opened here instead.
9 |
--------------------------------------------------------------------------------
/novel_index.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Novelbuddy",
4 | "id": 2507947282,
5 | "baseUrl": "https://novelbuddy.com",
6 | "lang": "en",
7 | "typeSource": "single",
8 | "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://novelbuddy.com/",
9 | "dateFormat": "",
10 | "dateFormatLocale": "",
11 | "isNsfw": false,
12 | "hasCloudflare": false,
13 | "sourceCodeUrl": "https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/javascript/novel/src/en/novelbuddy.js",
14 | "apiUrl": "",
15 | "version": "0.0.9",
16 | "isManga": false,
17 | "itemType": 2,
18 | "isFullData": false,
19 | "appMinVerReq": "0.5.0",
20 | "additionalParams": "",
21 | "sourceCodeLanguage": 1,
22 | "notes": ""
23 | }
24 | ]
--------------------------------------------------------------------------------
/.github/workflows/gen_index.yml:
--------------------------------------------------------------------------------
1 | name: Generate json index & ReadMe
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - "javascript/**"
9 | workflow_dispatch: # Allows manual triggering
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v3
18 |
19 | - name: Set timezone
20 | uses: szenius/set-timezone@v2.0
21 | with:
22 | timezoneLinux: "Asia/Kolkata"
23 |
24 | - name: Set up Python
25 | uses: actions/setup-python@v5
26 | with:
27 | python-vercode: "3.x"
28 |
29 | - name: Install modules
30 | run: |
31 | pip install -r requirements.txt
32 |
33 | - name: Update extensions
34 | run: |
35 | python scripts/generator_source.py
36 |
37 | - name: Update ReadMe
38 | run: |
39 | python scripts/generator_readme.py
40 |
41 | - name: Commit and Push Changes
42 | run: |
43 | python scripts/git.py
44 |
--------------------------------------------------------------------------------
/scripts/common.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import json, re
3 |
4 | def extensionInfo(filepath):
5 | def fix_json(json_str):
6 | # Remove trailing commas before closing } or ]
7 | json_str = re.sub(r",\s*([}\]])", r"\1", json_str)
8 | return json.loads(json_str)
9 |
10 | data = readFile(filepath)
11 | s = "const mangayomiSources = "
12 | e = ";"
13 |
14 | start = data.find(s) + len(s)
15 | end = data.find(e)
16 | cont = data[start:end]
17 | return fix_json(data[start:end])[0]
18 |
19 | def readFile(fileName):
20 | f = open(fileName, 'r')
21 | data = f.read()
22 | f.close()
23 | return data
24 |
25 | def writeFile(fileName,data):
26 | f = open(fileName, 'w',encoding='utf-8')
27 | f.write(data)
28 | f.close()
29 | return True
30 |
31 | def readJsonFile(fileName):
32 | data = readFile(fileName)
33 | return json.loads(data)
34 |
35 | def writeJsonFile(fileName,data):
36 | f = open(fileName, "w",encoding="utf-8")
37 | json.dump(data,f,indent=4,ensure_ascii=False)
38 | f.close()
39 | print(f"DONE: {fileName}")
40 |
41 |
42 | def generateHash(lang,name):
43 | idStr = f"mangayomi-js-{lang}.{name}"
44 | h = 0
45 | for c in idStr:
46 | h = (((h << 5) - h) + ord(c)) & 0xFFFFFFFF
47 | return h
48 |
49 | def getParentPath():
50 | script_dir = Path(__file__).resolve().parent
51 | return script_dir.parent
52 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/request_source.yml:
--------------------------------------------------------------------------------
1 | name: 🌐 Source request
2 | description: Suggest a new source
3 | labels: [Source request]
4 | body:
5 |
6 | - type: input
7 | id: name
8 | attributes:
9 | label: Source name
10 | placeholder: |
11 | Example: "Not Real Source"
12 | validations:
13 | required: true
14 |
15 | - type: input
16 | id: link
17 | attributes:
18 | label: Source link
19 | placeholder: |
20 | Example: "https://notrealsource.org"
21 | validations:
22 | required: true
23 |
24 | - type: input
25 | id: language
26 | attributes:
27 | label: Source language
28 | placeholder: |
29 | Example: "English"
30 | validations:
31 | required: true
32 |
33 | - type: textarea
34 | id: other-details
35 | attributes:
36 | label: Other details
37 | placeholder: |
38 | Additional details and attachments.
39 |
40 | - type: checkboxes
41 | id: acknowledgements
42 | attributes:
43 | label: Acknowledgements
44 | description: Your issue will be closed if you haven't done these steps.
45 | options:
46 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
47 | required: true
48 | - label: I have written a title with source name.
49 | required: true
50 | - label: I have checked that the extension does not already exist by searching the [GitHub repository](https://github.com/NAME/REPO-NAME/) and verified it does not appear in the code base.
51 | required: true
52 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/request_feature.yml:
--------------------------------------------------------------------------------
1 | name: ⭐ Feature request
2 | description: Suggest a feature to improve an existing source
3 | labels: [Feature request]
4 | body:
5 |
6 | - type: input
7 | id: source
8 | attributes:
9 | label: Source name
10 | description: |
11 | You can find the extension name in **Browse → Extensions**.
12 | placeholder: |
13 | Example: "AniWatch"
14 | validations:
15 | required: true
16 |
17 | - type: input
18 | id: language
19 | attributes:
20 | label: Source language
21 | placeholder: |
22 | Example: "English"
23 | validations:
24 | required: true
25 |
26 | - type: textarea
27 | id: feature-description
28 | attributes:
29 | label: Describe your suggested feature
30 | description: How can an existing extension be improved?
31 | placeholder: |
32 | Example:
33 | "It should work like this..."
34 | validations:
35 | required: true
36 |
37 | - type: textarea
38 | id: other-details
39 | attributes:
40 | label: Other details
41 | placeholder: |
42 | Additional details and attachments.
43 |
44 | - type: checkboxes
45 | id: acknowledgements
46 | attributes:
47 | label: Acknowledgements
48 | description: Your issue will be closed if you haven't done these steps.
49 | options:
50 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
51 | required: true
52 | - label: I have written a short but informative title.
53 | required: true
54 | - label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/NAME/mangayomi/issues/new/choose).
55 | required: true
56 |
--------------------------------------------------------------------------------
/scripts/git.py:
--------------------------------------------------------------------------------
1 | import os
2 | from common import readJsonFile, readFile, writeFile, getParentPath, writeJsonFile
3 |
4 | main_dir = getParentPath()
5 | scripts_dir = main_dir / "scripts"
6 |
7 | def getCommitMsg():
8 | prev = readJsonFile(scripts_dir/"prev_versions.json")
9 | new = readJsonFile(scripts_dir/"versions.json")
10 |
11 | added = []
12 | updated = []
13 | deleted = []
14 |
15 | for extType in new:
16 | new_data = new[extType]
17 | prev_data = prev[extType]
18 | for ext in new_data:
19 | if ext not in prev_data:
20 | added.append(ext)
21 | continue
22 | new_version = new_data[ext]['version']
23 | prev_version = prev_data[ext]['version']
24 | if new_version!=prev_version:
25 | updated.append(ext)
26 |
27 | for extType in prev:
28 | new_data = new[extType]
29 | prev_data = prev[extType]
30 | for ext in prev_data:
31 | if ext not in new_data:
32 | deleted.append(ext)
33 |
34 |
35 | commitMsg = ""
36 | if len(added):
37 | commitMsg+="➕: "+", ".join(added)+" "
38 | if len(updated):
39 | commitMsg+="♻️: "+", ".join(updated)+" "
40 | if len(deleted):
41 | commitMsg+="💀: "+", ".join(deleted)+" "
42 |
43 | if not len(commitMsg):
44 | commitMsg+="Updated"
45 | return f"🤖:: {commitMsg}"
46 |
47 | def run(cmd):
48 | os.system(cmd)
49 |
50 |
51 | commit_msg = getCommitMsg()
52 |
53 | MAIL_ID = "github-actions[bot]@users.noreply.github.com"
54 | NAME = "github-actions[bot]"
55 | run(f'git config --global user.email "{MAIL_ID}"')
56 | run(f'git config --global user.name "{NAME}"')
57 | run("git checkout main")
58 | run(f'git add .')
59 | run(f'git commit -m "{commit_msg}"')
60 | run(f'git push origin main --force')
--------------------------------------------------------------------------------
/scripts/generator_source.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import os, shutil, json, re
3 | from glob import glob
4 | from pprint import pp
5 | from common import readFile, readJsonFile, writeJsonFile, getParentPath,extensionInfo
6 | from model import Source, ItemType
7 |
8 | def formatExtenstionInfo(info):
9 | exids = info["ids"] if "ids" in info else info["id"] if "id" in info else None
10 | exlangs = (
11 | info["langs"]
12 | if "langs" in info
13 | else [info["lang"]] if "lang" in info else ["all"]
14 | )
15 |
16 | bkInfo = info
17 | bkInfo.pop("ids", None)
18 | bkInfo.pop("langs", None)
19 | rd = []
20 |
21 | for lang in exlangs:
22 | id = exids
23 | if type(exids) is dict:
24 | id = exids[lang] if exids is not None and lang in exids else None
25 | bkInfo["id"] = id
26 | bkInfo["lang"] = lang
27 | pkgPath = bkInfo["pkgPath"]
28 | bkInfo["ItemType"] = (
29 | ItemType.manga
30 | if "manga/" in pkgPath
31 | else ItemType.anime if "anime/" in pkgPath else ItemType.novel
32 | )
33 | bkInfo["sourceCodeUrl"] = (
34 | "https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/javascript/"
35 | + pkgPath
36 | )
37 | rd.append(Source.fromJSON(bkInfo).toJSON())
38 | print("DONE: Ext-" + info["name"])
39 | return rd
40 |
41 |
42 | main_dir = getParentPath()
43 |
44 | root_folder = main_dir / "javascript/"
45 | js_files = glob(os.path.join(root_folder, "**", "*.js"), recursive=True)
46 |
47 | animeList = []
48 | mangaList = []
49 | novelList = []
50 |
51 |
52 | try:
53 | for filePath in js_files:
54 | paths = Path(filePath).resolve().parts
55 |
56 | info = extensionInfo(filePath)
57 | formattedInfo: list = formatExtenstionInfo(info)
58 |
59 | if "anime" in paths:
60 | animeList.extend(formattedInfo)
61 | elif "manga" in paths:
62 | mangaList.extend(formattedInfo)
63 | else:
64 | novelList.extend(formattedInfo)
65 |
66 | writeJsonFile(main_dir / "anime_index.json", animeList)
67 | writeJsonFile(main_dir / "index.json", mangaList)
68 | writeJsonFile(main_dir / "novel_index.json", novelList)
69 | except Exception as e:
70 | print("ERR: " + paths[len(paths) - 1])
71 | print(e)
72 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/report_issue.yml:
--------------------------------------------------------------------------------
1 | name: 🐞 Issue report
2 | description: Report a source issue
3 | labels: [Bug]
4 | body:
5 |
6 | - type: input
7 | id: source
8 | attributes:
9 | label: Source information
10 | description: |
11 | You can find the extension name and version in **Browse → Extensions**.
12 | placeholder: |
13 | Example: "Gogoanime 0.0.35 (English)"
14 | validations:
15 | required: true
16 |
17 | - type: input
18 | id: language
19 | attributes:
20 | label: Source language
21 | placeholder: |
22 | Example: "English"
23 | validations:
24 | required: true
25 |
26 | - type: textarea
27 | id: reproduce-steps
28 | attributes:
29 | label: Steps to reproduce
30 | description: Provide an example of the issue.
31 | placeholder: |
32 | Example:
33 | 1. First step
34 | 2. Second step
35 | 3. Issue here
36 | validations:
37 | required: true
38 |
39 | - type: textarea
40 | id: expected-behavior
41 | attributes:
42 | label: Expected behavior
43 | placeholder: |
44 | Example:
45 | "This should happen..."
46 | validations:
47 | required: true
48 |
49 | - type: textarea
50 | id: actual-behavior
51 | attributes:
52 | label: Actual behavior
53 | placeholder: |
54 | Example:
55 | "This happened instead..."
56 | validations:
57 | required: true
58 |
59 | - type: input
60 | id: mangayomi-version
61 | attributes:
62 | label: Mangayomi version
63 | description: |
64 | You can find your Mangayomi version in **More → About**.
65 | placeholder: |
66 | Example: "0.5.1"
67 | validations:
68 | required: true
69 |
70 | - type: input
71 | id: device
72 | attributes:
73 | label: Device
74 | description: List your device, model and the OS version.
75 | placeholder: |
76 | Example: "Google Pixel 5 Android 11"
77 | validations:
78 | required: true
79 |
80 | - type: textarea
81 | id: other-details
82 | attributes:
83 | label: Other details
84 | placeholder: |
85 | Additional details and attachments.
86 |
87 | - type: checkboxes
88 | id: acknowledgements
89 | attributes:
90 | label: Acknowledgements
91 | description: Your issue will be closed if you haven't done these steps.
92 | options:
93 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
94 | required: true
95 | - label: I have written a short but informative title.
96 | required: true
97 | - label: I have updated all installed extensions.
98 | required: true
99 | - label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/NAME/mangayomi/issues/new/choose).
100 | required: true
101 |
--------------------------------------------------------------------------------
/scripts/generator_readme.py:
--------------------------------------------------------------------------------
1 | from common import readJsonFile, readFile, writeFile, getParentPath, writeJsonFile
2 | from model import UpdateInfo, Source, ItemType
3 | from datetime import datetime
4 | from pytz import timezone
5 | from pprint import pp
6 | import shutil
7 |
8 | main_dir = getParentPath()
9 | scripts_dir = main_dir / "scripts"
10 |
11 |
12 | def generateVersionData():
13 | versionPath = scripts_dir / "versions.json"
14 | oldData = readJsonFile(versionPath)
15 |
16 | FILES = ["", "anime_", "novel_"]
17 | title = ["manga", "anime", "novel"]
18 |
19 | currentDT = datetime.now(timezone("Asia/Kolkata")).timestamp()
20 |
21 | newData = {}
22 | for file in FILES:
23 | ind = main_dir / f"{file}index.json"
24 | data = readJsonFile(ind)
25 | itemType = title[FILES.index(file)]
26 | oldDataCat = oldData[str(itemType)]
27 | collection = {}
28 | for item in data:
29 | item: Source = Source().fromJSON(item)
30 | name = item.name
31 | lang = item.lang
32 | version = item.version
33 |
34 | if name in oldDataCat:
35 | oldInfo = oldDataCat[name]
36 | oldVersion = oldInfo["version"]
37 | if version == oldVersion:
38 | collection[name] = oldInfo
39 | continue
40 |
41 | info = None
42 | if name in collection:
43 | info: UpdateInfo = UpdateInfo().fromJSON(collection[name])
44 | info.setLang(lang)
45 | else:
46 | version = item.version
47 | updTime = currentDT
48 |
49 | info: UpdateInfo = UpdateInfo(
50 | name=name, version=version, lastUpd=updTime
51 | )
52 | info.setLang(lang)
53 |
54 | if info is not None:
55 | collection[name] = info.toJSON()
56 |
57 | newData[str(itemType)] = dict(
58 | sorted(
59 | collection.items(), key=lambda item: item[1]["lastUpd"], reverse=True
60 | )
61 | )
62 |
63 | writeJsonFile(versionPath, newData)
64 |
65 |
66 | def generateExtensionList():
67 | tz = timezone("Asia/Kolkata")
68 | lines = []
69 | lines.append("## Available Extensions List")
70 | lines.append("")
71 | lines.append(
72 | 'Expand list
\n'
73 | )
74 |
75 | data = readJsonFile(scripts_dir / "versions.json")
76 | for category, items in data.items():
77 | catData = data[category]
78 | if len(catData) < 1:
79 | continue
80 | lines.append(f"## {category.title()}\n")
81 | lines.append("| Name | Version | Language | Last Updated |")
82 | lines.append("|------|---------|----------|---------------|")
83 | for item in items:
84 | item = catData[item]
85 | lastUpd = datetime.fromtimestamp(item["lastUpd"], tz).strftime(
86 | "%Y/%m/%d %H:%M IST"
87 | )
88 | lines.append(
89 | f"| {item['name']} | {item['version']} | {item['langs']} | {lastUpd} |"
90 | )
91 | lines.append("") # Add blank line between sections
92 |
93 | lines.append(" ")
94 | print("DONE: Table")
95 | return "\n".join(lines)
96 |
97 | shutil.copy(scripts_dir / "versions.json",scripts_dir / "prev_versions.json")
98 | generateVersionData()
99 | extTable = generateExtensionList()
100 | temp = readFile(scripts_dir / "README-temp.md")
101 | tempData = temp.replace("{{Extension Table}}", extTable)
102 |
103 | readMePath = main_dir / "README.md"
104 | writeFile(readMePath, tempData)
105 | print("DONE: README.md")
106 |
--------------------------------------------------------------------------------
/scripts/generate_extension.py:
--------------------------------------------------------------------------------
1 | from common import readFile, writeFile, generateHash, getParentPath
2 | from model import ItemType, Source
3 | import json, os
4 |
5 |
6 | def createFunction(isAsync: bool, funcName: str, args: list, code=""):
7 | arg = ""
8 | if len(args) > 0:
9 | arg = ", ".join(args)
10 |
11 | return f"""
12 | {"async" if isAsync else ""} {funcName}({arg}) {{
13 | {f'throw new Error("{funcName} not implemented");' if code == "" else code}
14 | }}
15 | """
16 |
17 |
18 | def builder(source: dict, itemType: ItemType):
19 | lines = []
20 |
21 | code = "const mangayomiSources = [<>];".replace("<>", str(jsonExt))
22 | lines.append("class DefaultExtension extends MProvider {")
23 | lines.append(
24 | createFunction(
25 | False, "constructor", [], "super();\n\t\tthis.client = new Client();"
26 | )
27 | )
28 | lines.append(
29 | createFunction(
30 | False, "getPreference", ["key"], "return new SharedPreferences().get(key);"
31 | )
32 | )
33 | lines.append(createFunction(False, "getHeaders", ["url"]))
34 | lines.append(createFunction(True, "getPopular", ["page"]))
35 | lines.append(createFunction(True, "getLatestUpdates", ["page"]))
36 | lines.append(createFunction(True, "search", ["query", "page", "filters"]))
37 | lines.append(createFunction(True, "getDetail", ["url"]))
38 |
39 | if itemType == ItemType.anime:
40 | lines.append(createFunction(True, "getVideoList", ["url"]))
41 | elif itemType == ItemType.manga:
42 | lines.append(createFunction(True, "getPageList", ["url"]))
43 | elif itemType == ItemType.novel:
44 | lines.append(createFunction(True, "getHtmlContent", ["name","url"]))
45 | lines.append(createFunction(True, "cleanHtmlContent", ["html"]))
46 |
47 | lines.append(createFunction(False, "getFilterList", []))
48 | lines.append(createFunction(False, "getSourcePreferences", []))
49 | lines.append("}")
50 | return code + "".join(lines)
51 |
52 |
53 | print("---------------------------------")
54 | print("--------Extension Builder--------")
55 | print("---------------------------------")
56 |
57 | name = input("Extension name: ")
58 | lang = input("Langauges (, seperated): ")
59 | baseUrl = input("Base url: ")
60 | apiUrl = input("API url: ")
61 | typeSource = input("Source type (s/m/t): ")
62 | iconUrl = "https://www.google.com/s2/favicons?sz=256&domain=" + baseUrl
63 | isManga = input("Is manga (0/1): ")
64 | if isManga == "1":
65 | itemType = "m"
66 | else:
67 | itemType = input("Type (m/a/n): ")
68 |
69 | name = name.title()
70 | langs = lang.split(",")
71 | baseUrl = baseUrl[:-1] if baseUrl[-1] == "/" else baseUrl
72 | apiUrl = apiUrl[:-1] if apiUrl != "" and apiUrl[-1] == "/" else apiUrl
73 | typeSource = (
74 | "single" if typeSource == "s" else "multi" if typeSource == "m" else "torrent"
75 | )
76 | isManga = True if int(isManga) else False
77 | itemType = (
78 | 0 if itemType == "m" else 1 if itemType == "a" else 2 if itemType == "n" else 0
79 | )
80 |
81 | itemType = ItemType(itemType)
82 |
83 | ext = Source(
84 | name=name,
85 | lang=langs[0],
86 | baseUrl=baseUrl,
87 | apiUrl=apiUrl,
88 | typeSource=typeSource,
89 | iconUrl=iconUrl,
90 | version="0.0.1",
91 | isManga=isManga,
92 | itemType=itemType,
93 | )
94 |
95 | jsonExt = ext.toJSON()
96 | if len(langs) > 1:
97 | jsonExt.pop("id")
98 | jsonExt.pop("lang")
99 |
100 | ids = {}
101 | for lang in langs:
102 | ids[lang] = generateHash(lang, name)
103 |
104 | jsonExt["ids"] = ids
105 | jsonExt["langs"] = langs
106 |
107 | pkgPath = f"{itemType.name}/src/{langs[0]}/{name.lower()}.js"
108 | jsonExt["pkgPath"] = pkgPath
109 |
110 | jsonExt = json.dumps(jsonExt)
111 | code = builder(jsonExt, itemType)
112 |
113 | filePath = getParentPath() / "javascript" / pkgPath
114 | dirname = os.path.dirname(filePath)
115 | if not os.path.exists(dirname):
116 | os.makedirs(dirname)
117 |
118 | writeFile(filePath, code)
119 | print(f"DONE: {filePath}")
120 |
--------------------------------------------------------------------------------
/index.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Weeb Central",
4 | "id": 693275080,
5 | "baseUrl": "https://weebcentral.com",
6 | "lang": "en",
7 | "typeSource": "single",
8 | "iconUrl": "https://www.google.com/s2/favicons?sz=128&domain=https://weebcentral.com",
9 | "dateFormat": "",
10 | "dateFormatLocale": "",
11 | "isNsfw": false,
12 | "hasCloudflare": false,
13 | "sourceCodeUrl": "https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/javascript/manga/src/en/weebcentral.js",
14 | "apiUrl": "",
15 | "version": "0.1.0",
16 | "isManga": true,
17 | "itemType": 0,
18 | "isFullData": false,
19 | "appMinVerReq": "0.5.0",
20 | "additionalParams": "",
21 | "sourceCodeLanguage": 1,
22 | "notes": ""
23 | },
24 | {
25 | "name": "ReadComicOnline",
26 | "id": 376287717,
27 | "baseUrl": "https://readcomiconline.li",
28 | "lang": "en",
29 | "typeSource": "single",
30 | "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://readcomiconline.li/",
31 | "dateFormat": "",
32 | "dateFormatLocale": "",
33 | "isNsfw": false,
34 | "hasCloudflare": false,
35 | "sourceCodeUrl": "https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/javascript/manga/src/en/readcomiconline.js",
36 | "apiUrl": "",
37 | "version": "0.3.0",
38 | "isManga": true,
39 | "itemType": 0,
40 | "isFullData": false,
41 | "appMinVerReq": "0.5.0",
42 | "additionalParams": "",
43 | "sourceCodeLanguage": 1,
44 | "notes": ""
45 | },
46 | {
47 | "name": "Mangapill",
48 | "id": 960321322,
49 | "baseUrl": "https://mangapill.com",
50 | "lang": "en",
51 | "typeSource": "single",
52 | "iconUrl": "https://www.google.com/s2/favicons?sz=64&domain=https://mangapill.com/",
53 | "dateFormat": "",
54 | "dateFormatLocale": "",
55 | "isNsfw": false,
56 | "hasCloudflare": false,
57 | "sourceCodeUrl": "https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/javascript/manga/src/en/mangapill.js",
58 | "apiUrl": "",
59 | "version": "1.0.4",
60 | "isManga": true,
61 | "itemType": 0,
62 | "isFullData": false,
63 | "appMinVerReq": "0.5.0",
64 | "additionalParams": "",
65 | "sourceCodeLanguage": 1,
66 | "notes": ""
67 | },
68 | {
69 | "name": "Mangapark",
70 | "id": 2302366102,
71 | "baseUrl": "https://mangapark.io",
72 | "lang": "en",
73 | "typeSource": "single",
74 | "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://mangapark.io",
75 | "dateFormat": "",
76 | "dateFormatLocale": "",
77 | "isNsfw": false,
78 | "hasCloudflare": false,
79 | "sourceCodeUrl": "https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/javascript/manga/src/en/mangapark.js",
80 | "apiUrl": "",
81 | "version": "1.0.1",
82 | "isManga": true,
83 | "itemType": 0,
84 | "isFullData": false,
85 | "appMinVerReq": "0.5.0",
86 | "additionalParams": "",
87 | "sourceCodeLanguage": 1,
88 | "notes": ""
89 | },
90 | {
91 | "name": "WeLoMa",
92 | "id": 1890238687,
93 | "baseUrl": "https://weloma.art",
94 | "lang": "ja",
95 | "typeSource": "single",
96 | "iconUrl": "https://raw.github.com/Swakshan/mangayomi-swak-extensions/main/javascript/icon/ja.weloma.jpg",
97 | "dateFormat": "",
98 | "dateFormatLocale": "",
99 | "isNsfw": false,
100 | "hasCloudflare": false,
101 | "sourceCodeUrl": "https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/javascript/manga/src/ja/weloma.js",
102 | "apiUrl": "",
103 | "version": "1.0.0",
104 | "isManga": true,
105 | "itemType": 0,
106 | "isFullData": false,
107 | "appMinVerReq": "0.5.0",
108 | "additionalParams": "",
109 | "sourceCodeLanguage": 1,
110 | "notes": ""
111 | }
112 | ]
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # UV
98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | #uv.lock
102 |
103 | # poetry
104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | # This is especially recommended for binary packages to ensure reproducibility, and is more
106 | # commonly ignored for libraries.
107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 |
110 | # pdm
111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | # in version control.
115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 |
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 |
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 |
127 | # SageMath parsed files
128 | *.sage.py
129 |
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 |
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 |
143 | # Rope project settings
144 | .ropeproject
145 |
146 | # mkdocs documentation
147 | /site
148 |
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 |
154 | # Pyre type checker
155 | .pyre/
156 |
157 | # pytype static type analyzer
158 | .pytype/
159 |
160 | # Cython debug symbols
161 | cython_debug/
162 |
163 | # PyCharm
164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | # and can be added to the global gitignore or merged into this file. For a more nuclear
167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 |
170 | # Ruff stuff:
171 | .ruff_cache/
172 |
173 | # PyPI configuration file
174 | .pypirc
175 |
176 | # Mine
177 | prev_*.json
178 |
179 | # Remove extension
180 | removed/
--------------------------------------------------------------------------------
/scripts/model.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from typing import Optional, Dict, Any, List
3 | from enum import IntEnum
4 | from common import generateHash
5 |
6 |
7 | class ItemType(IntEnum):
8 | manga = 0
9 | anime = 1
10 | novel = 2
11 |
12 |
13 | @dataclass
14 | class Source:
15 | id: Optional[int] = None
16 | name: str = ""
17 | baseUrl: str = ""
18 | lang: str = ""
19 | isNsfw: bool = False
20 | sourceCodeUrl: str = ""
21 | typeSource: str = ""
22 | iconUrl: str = ""
23 | hasCloudflare: bool = False
24 | dateFormat: str = ""
25 | dateFormatLocale: str = ""
26 | apiUrl: str = ""
27 | version: str = ""
28 | isManga: Optional[bool] = None
29 | itemType: ItemType = ItemType.manga
30 | isFullData: bool = False
31 | appMinVerReq: str = "0.5.0"
32 | additionalParams: str = ""
33 | sourceCodeLanguage: int = 1
34 | notes: str = ""
35 |
36 | def __post_init__(self):
37 | # Set isManga based on itemType if it's None
38 | if self.isManga is None:
39 | self.isManga = self.itemType == ItemType.manga
40 |
41 | @classmethod
42 | def fromJSON(self, json: Dict[str, Any]) -> "Source":
43 | source_code_lang = json.get("sourceCodeLanguage", 1)
44 |
45 | # Calculate id using hash if not provided
46 | if "id" not in json or json["id"] is None:
47 |
48 | id_value = generateHash(json.get("lang", ""), json.get("name", ""))
49 | else:
50 | id_value = json["id"]
51 |
52 | return self(
53 | id=id_value,
54 | name=json.get("name", ""),
55 | baseUrl=json.get("baseUrl", ""),
56 | lang=json.get("lang", ""),
57 | isNsfw=json.get("isNsfw", False),
58 | sourceCodeUrl=json.get("sourceCodeUrl", ""),
59 | typeSource=json.get("typeSource", ""),
60 | iconUrl=json.get("iconUrl", ""),
61 | hasCloudflare=json.get("hasCloudflare", False),
62 | dateFormat=json.get("dateFormat", ""),
63 | dateFormatLocale=json.get("dateFormatLocale", ""),
64 | apiUrl=json.get("apiUrl", ""),
65 | version=json.get("version", ""),
66 | isManga=json.get("isManga", json.get("itemType", 0) == 0),
67 | itemType=ItemType(json.get("itemType", 0)),
68 | isFullData=json.get("isFullData", False),
69 | appMinVerReq=json.get("appMinVerReq", "0.5.0"),
70 | additionalParams=json.get("additionalParams", ""),
71 | sourceCodeLanguage=source_code_lang,
72 | notes=json.get("notes", ""),
73 | )
74 |
75 | def toJSON(self) -> Dict[str, Any]:
76 | return {
77 | "name": self.name,
78 | "id": (
79 | self.id if self.id is not None else generateHash(self.lang, self.name)
80 | ),
81 | "baseUrl": self.baseUrl,
82 | "lang": self.lang,
83 | "typeSource": self.typeSource,
84 | "iconUrl": self.iconUrl,
85 | "dateFormat": self.dateFormat,
86 | "dateFormatLocale": self.dateFormatLocale,
87 | "isNsfw": self.isNsfw,
88 | "hasCloudflare": self.hasCloudflare,
89 | "sourceCodeUrl": self.sourceCodeUrl,
90 | "apiUrl": self.apiUrl,
91 | "version": self.version,
92 | "isManga": self.isManga,
93 | "itemType": self.itemType.value,
94 | "isFullData": self.isFullData,
95 | "appMinVerReq": self.appMinVerReq,
96 | "additionalParams": self.additionalParams,
97 | "sourceCodeLanguage": self.sourceCodeLanguage,
98 | "notes": self.notes,
99 | }
100 |
101 |
102 | @dataclass
103 | class UpdateInfo:
104 | name: str = ""
105 | version: str = "0.0.0"
106 | langs: List[str] = field(default_factory=list)
107 | lastUpd: int = 253370745000
108 | #"9999/01/01 00:00"
109 |
110 | def setLang(self, lang):
111 | self.langs.append(lang)
112 |
113 | @classmethod
114 | def fromJSON(self, json: Dict[str, Any]) -> "Source":
115 | return self(
116 | name=json["name"],
117 | version=json["version"],
118 | langs=json["langs"].split(", "),
119 | lastUpd=json["lastUpd"],
120 | )
121 |
122 | def toJSON(self) -> Dict[str, Any]:
123 | return {
124 | "name": self.name,
125 | "version": self.version,
126 | "langs": ", ".join(self.langs),
127 | "lastUpd": self.lastUpd,
128 | }
129 |
--------------------------------------------------------------------------------
/javascript/anime/src/en/subsplease.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "SubsPlease",
4 | "id": 2732508901,
5 | "baseUrl": "https://subsplease.org",
6 | "lang": "en",
7 | "typeSource": "single",
8 | "iconUrl":
9 | "https://raw.github.com/Swakshan/mangayomi-swak-extensions/main/javascript/icon/en.subsplease.jpg",
10 | "dateFormat": "",
11 | "dateFormatLocale": "",
12 | "isNsfw": false,
13 | "hasCloudflare": false,
14 | "sourceCodeUrl": "",
15 | "apiUrl": "https://subsplease.org/api",
16 | "version": "0.0.5",
17 | "isManga": false,
18 | "itemType": 1,
19 | "isFullData": false,
20 | "appMinVerReq": "0.5.0",
21 | "additionalParams": "",
22 | "sourceCodeLanguage": 1,
23 | "notes": "",
24 | "pkgPath": "anime/src/en/subsplease.js",
25 | },
26 | ];
27 | class DefaultExtension extends MProvider {
28 | constructor() {
29 | super();
30 | this.client = new Client();
31 | }
32 |
33 | getPreference(key) {
34 | return new SharedPreferences().get(key);
35 | }
36 |
37 | getHeaders(url) {
38 | throw new Error("getHeaders not implemented");
39 | }
40 | getBaseUrl() {
41 | return this.source.baseUrl;
42 | }
43 |
44 | async requestAPI(slug) {
45 | var apiUrl = this.source.apiUrl;
46 | var api = `${apiUrl}/?${slug}`;
47 | var res = await this.client.get(api);
48 | return JSON.parse(res.body) || {};
49 | }
50 |
51 | async animeList(slug) {
52 | var baseUrl = this.getBaseUrl();
53 | var body = await this.requestAPI(slug);
54 | var list = [];
55 | var hasNextPage = slug.includes("f=latest"); //Only latest has next page, Search doesnt.
56 | for (var ep in body) {
57 | var item = body[ep];
58 | var name = item.show;
59 | var imageUrl = baseUrl + item.image_url;
60 | var link = item.page;
61 | list.push({ name, imageUrl, link });
62 | }
63 |
64 | return { list, hasNextPage };
65 | }
66 |
67 | async getPopular(page) {
68 | var slug = `f=latest&tz=&p=${page - 1}`;
69 | return await this.animeList(slug);
70 | }
71 |
72 | async getLatestUpdates(page) {
73 | var slug = `f=latest&tz=&p=${page - 1}`;
74 | return await this.animeList(slug);
75 | }
76 |
77 | async search(query, page, filters) {
78 | var slug = `f=search&tz=&s=${query}`;
79 | return await this.animeList(slug);
80 | }
81 |
82 | async getDetail(url) {
83 | var baseUrl = this.getBaseUrl();
84 | var baseSlug = `${baseUrl}/shows/`;
85 | if (url.includes(baseUrl)) url = url.replace(baseSlug, "");
86 | var link = baseSlug + url;
87 |
88 | var doc = new Document((await this.client.get(link)).body);
89 | var sid = doc.selectFirst("#show-release-table").attr("sid");
90 | var description =
91 | doc.selectFirst("div.series-syn").selectFirst("p").text || "";
92 |
93 | var slug = `f=show&tz=&sid=${sid}`;
94 | var body = await this.requestAPI(slug);
95 |
96 | var episodes = body.episode || {};
97 | var chapters = [];
98 | for (var epItem in episodes) {
99 | var item = episodes[epItem];
100 | var dateUpload = new Date(item.release_date).valueOf().toString();
101 | var episodeNum = item.episode;
102 | var urls = {};
103 | item.downloads.forEach((download) => {
104 | delete download.torrent;
105 | delete download.xdcc;
106 | urls[download.res] = download.magnet;
107 | });
108 | chapters.push({
109 | name: `Episode ${episodeNum}`,
110 | url: JSON.stringify(urls),
111 | dateUpload,
112 | });
113 | }
114 |
115 | return { description, chapters };
116 | }
117 |
118 | sortQuality(qs) {
119 | var pref = this.getPreference("subsplease_pref_video_resolution");
120 | var sortedQ = qs.filter((q) => q != pref);
121 | if (qs.includes(pref)) sortedQ.unshift(pref);
122 | return sortedQ;
123 | }
124 |
125 | async getVideoList(url) {
126 | var data = JSON.parse(url);
127 | var sortedQ = this.sortQuality(Object.keys(data));
128 | var streams = [];
129 | sortedQ.forEach((item) => {
130 | var quality = `${item}p`;
131 | var magLink = data[item];
132 | streams.push({
133 | url: magLink,
134 | originalUrl: magLink,
135 | quality,
136 | });
137 | });
138 |
139 | return streams;
140 | }
141 |
142 | getFilterList() {
143 | throw new Error("getFilterList not implemented");
144 | }
145 |
146 | getSourcePreferences() {
147 | return [
148 | {
149 | key: "subsplease_pref_video_resolution",
150 | listPreference: {
151 | title: "Preferred video resolution",
152 | summary: "",
153 | valueIndex: 0,
154 | entries: ["1080p", "720p", "480p"],
155 | entryValues: ["1080", "720", "480"],
156 | },
157 | },
158 | ];
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/scripts/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "manga": {
3 | "WeLoMa": {
4 | "name": "WeLoMa",
5 | "version": "1.0.0",
6 | "langs": "ja",
7 | "lastUpd": 1761760611.599596
8 | },
9 | "Mangapill": {
10 | "name": "Mangapill",
11 | "version": "1.0.4",
12 | "langs": "en",
13 | "lastUpd": 1758079504.835998
14 | },
15 | "Mangapark": {
16 | "name": "Mangapark",
17 | "version": "1.0.1",
18 | "langs": "en",
19 | "lastUpd": 1758079504.835998
20 | },
21 | "ReadComicOnline": {
22 | "name": "ReadComicOnline",
23 | "version": "0.3.0",
24 | "langs": "en",
25 | "lastUpd": 1753526709.455681
26 | },
27 | "Weeb Central": {
28 | "name": "Weeb Central",
29 | "version": "0.1.0",
30 | "langs": "en",
31 | "lastUpd": 1741990440
32 | }
33 | },
34 | "anime": {
35 | "Moviesda": {
36 | "name": "Moviesda",
37 | "version": "1.2.2",
38 | "langs": "ta",
39 | "lastUpd": 1764609098.718991
40 | },
41 | "XPrime": {
42 | "name": "XPrime",
43 | "version": "2.3.4",
44 | "langs": "all",
45 | "lastUpd": 1764609098.718991
46 | },
47 | "yFlix": {
48 | "name": "yFlix",
49 | "version": "0.0.6",
50 | "langs": "all",
51 | "lastUpd": 1764609098.718991
52 | },
53 | "AnimeKai": {
54 | "name": "AnimeKai",
55 | "version": "0.5.1",
56 | "langs": "en",
57 | "lastUpd": 1762790154.654533
58 | },
59 | "Kaido": {
60 | "name": "Kaido",
61 | "version": "1.0.2",
62 | "langs": "en",
63 | "lastUpd": 1758797366.64915
64 | },
65 | "KickAssAnime": {
66 | "name": "KickAssAnime",
67 | "version": "1.2.5",
68 | "langs": "en",
69 | "lastUpd": 1757857275.260451
70 | },
71 | "Anixl": {
72 | "name": "Anixl",
73 | "version": "0.0.81",
74 | "langs": "en",
75 | "lastUpd": 1757071752.925684
76 | },
77 | "SubsPlease": {
78 | "name": "SubsPlease",
79 | "version": "0.0.5",
80 | "langs": "en",
81 | "lastUpd": 1753293069.296975
82 | },
83 | "Aniwatch": {
84 | "name": "Aniwatch",
85 | "version": "1.0.0",
86 | "langs": "en",
87 | "lastUpd": 1751305007.371692
88 | },
89 | "Anicrush": {
90 | "name": "Anicrush",
91 | "version": "0.0.5",
92 | "langs": "en",
93 | "lastUpd": 1751042518.408327
94 | },
95 | "AnimeParadise": {
96 | "name": "AnimeParadise",
97 | "version": "0.1.0",
98 | "langs": "en",
99 | "lastUpd": 1750509383.278437
100 | },
101 | "AnimeZZ": {
102 | "name": "AnimeZZ",
103 | "version": "1.1.1",
104 | "langs": "en",
105 | "lastUpd": 1750504671.044623
106 | },
107 | "Dramacool": {
108 | "name": "Dramacool",
109 | "version": "1.1.0",
110 | "langs": "all",
111 | "lastUpd": 1749795088.253707
112 | },
113 | "Autoembed": {
114 | "name": "Autoembed",
115 | "version": "1.3.3",
116 | "langs": "all",
117 | "lastUpd": 1748094760.694626
118 | },
119 | "AnimeGG": {
120 | "name": "AnimeGG",
121 | "version": "1.0.3",
122 | "langs": "en",
123 | "lastUpd": 1747738043.896285
124 | },
125 | "Sudatchi": {
126 | "name": "Sudatchi",
127 | "version": "1.1.1",
128 | "langs": "en",
129 | "lastUpd": 1747738043.896285
130 | },
131 | "Animeonsen": {
132 | "name": "Animeonsen",
133 | "version": "1.0.1",
134 | "langs": "en, ja",
135 | "lastUpd": 1747738043.896285
136 | },
137 | "Gojo": {
138 | "name": "Gojo",
139 | "version": "0.0.6",
140 | "langs": "en",
141 | "lastUpd": 1747738043.896285
142 | },
143 | "Aniwave": {
144 | "name": "Aniwave",
145 | "version": "0.0.7",
146 | "langs": "en",
147 | "lastUpd": 1746729660
148 | },
149 | "KissKH": {
150 | "name": "KissKH",
151 | "version": "0.1.6",
152 | "langs": "all",
153 | "lastUpd": 1746206220
154 | }
155 | },
156 | "novel": {
157 | "Novelbuddy": {
158 | "name": "Novelbuddy",
159 | "version": "0.0.9",
160 | "langs": "en",
161 | "lastUpd": 1749491688.879926
162 | }
163 | }
164 | }
--------------------------------------------------------------------------------
/scripts/README-temp.md:
--------------------------------------------------------------------------------
1 | # Mangayomi Swak Extensions
2 |
3 | This repository contains the available javascript extension catalogues for the Mangayomi app.
4 |
5 | {{Extension Table}}
6 |
7 | ## How to add the extensions
8 |
9 | Click on one of the buttons below to add the corresponding repository/repositories:
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | If you installed the app via Live Container, then use the following buttons instead:
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Or add them manually in the app (More -> Settings -> Browse):
30 |
31 | manga repo
32 | ```
33 | https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/index.json
34 | ```
35 |
36 | anime repo
37 | ```
38 | https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/anime_index.json
39 | ```
40 |
41 | novel repo
42 | ```
43 | https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/novel_index.json
44 | ```
45 |
46 | # Contributing
47 |
48 | Contributions are welcome!
49 |
50 | To get started with development, see [CONTRIBUTING-DART.md](./CONTRIBUTING-DART.md) for create sources in Dart or [CONTRIBUTING-JS.md](./CONTRIBUTING-JS.md) for create sources in JavaScript.
51 |
52 | ## License
53 |
54 | Copyright 2023 Moustapha Kodjo Amadou
55 |
56 | Licensed under the Apache License, Version 2.0 (the "License");
57 | you may not use this file except in compliance with the License.
58 | You may obtain a copy of the License at
59 |
60 | http://www.apache.org/licenses/LICENSE-2.0
61 |
62 | Unless required by applicable law or agreed to in writing, software
63 | distributed under the License is distributed on an "AS IS" BASIS,
64 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
65 | See the License for the specific language governing permissions and
66 | limitations under the License.
67 |
68 |
69 |
70 | ## Disclaimer
71 |
72 | We hereby issue this notice to inform you that these extensions just function like an ordinary browser (like your browser) that fetch video files from internet, and do not violate the provisions of the Digital Millennium Copyright Act (DMCA). The Content these extensions may access is not hosted by us or the Mangayomi application but the websites they are browsing in their autonomous mode. It is sole responsibility of the user and his/her countries' or states' law. If you think they are violating any intellectual property then please contact the actual file hosts not the owners of this repository or the Mangayomi app.
73 |
--------------------------------------------------------------------------------
/javascript/anime/src/en/animegg.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "AnimeGG",
4 | "lang": "en",
5 | "id": 209614032,
6 | "baseUrl": "https://www.animegg.org",
7 | "apiUrl": "",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=256&domain=https://www.animegg.org/",
10 | "typeSource": "single",
11 | "itemType": 1,
12 | "version": "1.0.3",
13 | "pkgPath": "anime/src/en/animegg.js"
14 | }
15 | ];
16 |
17 | // Authors: - Swakshan
18 |
19 | class DefaultExtension extends MProvider {
20 | constructor() {
21 | super();
22 | this.client = new Client();
23 | }
24 |
25 | getHeaders(url) {
26 | return {
27 | Referer: this.source.baseUrl,
28 | Origin: this.source.baseUrl,
29 | };
30 | }
31 |
32 | getPreference(key) {
33 | return parseInt(new SharedPreferences().get(key));
34 | }
35 |
36 | async requestText(slug) {
37 | var url = `${this.source.baseUrl}${slug}`;
38 | var res = await this.client.get(url, this.getHeaders());
39 | return res.body;
40 | }
41 | async request(slug) {
42 | return new Document(await this.requestText(slug));
43 | }
44 |
45 | async fetchPopularnLatest(slug) {
46 | var body = await this.request(slug);
47 | var items = body.select("li.fea");
48 | var list = [];
49 | var hasNextPage = true;
50 | if (items.length > 0) {
51 | for (var item of items) {
52 | var imageUrl = item.selectFirst("img").getSrc;
53 | var linkSection = item.selectFirst(".rightpop").selectFirst("a");
54 | var link = linkSection.getHref;
55 | var name = linkSection.text;
56 | list.push({
57 | name,
58 | imageUrl,
59 | link,
60 | });
61 | }
62 | } else {
63 | hasNextPage = false;
64 | }
65 | return { list, hasNextPage };
66 | }
67 |
68 | async getPopular(page) {
69 | var start = (page - 1) * 25;
70 | var limit = start + 25;
71 |
72 | var category = "";
73 | var pop = this.getPreference("animegg_popular_category");
74 | switch (pop) {
75 | case 1: {
76 | category = "sortBy=createdAt&sortDirection=DESC&";
77 | break;
78 | }
79 | case 2: {
80 | category = "ongoing=true&";
81 | break;
82 | }
83 | case 3: {
84 | category = "ongoing=false&";
85 | break;
86 | }
87 | case 4: {
88 | category = "sortBy=sortLetter&sortDirection=ASC&";
89 | break;
90 | }
91 | }
92 | var slug = `/popular-series?${category}start=${start}&limit=${limit}`;
93 | return await this.fetchPopularnLatest(slug);
94 | }
95 | get supportsLatest() {
96 | throw new Error("supportsLatest not implemented");
97 | }
98 | async getLatestUpdates(page) {
99 | var start = (page - 1) * 25;
100 | var limit = start + 25;
101 |
102 | var slug = `/releases?start=${start}&limit=${limit}`;
103 | return await this.fetchPopularnLatest(slug);
104 | }
105 | async search(query, page, filters) {
106 | var slug = `/search?q=${query}`;
107 | var body = await this.request(slug);
108 | var items = body.select(".moose.page > a");
109 | var list = [];
110 | for (var item of items) {
111 | var imageUrl = item.selectFirst("img").getSrc;
112 | var link = item.getHref;
113 | var name = item.selectFirst("h2").text;
114 | list.push({
115 | name,
116 | imageUrl,
117 | link,
118 | });
119 | }
120 |
121 | return { list, hasNextPage: false };
122 | }
123 |
124 | statusCode(status) {
125 | return (
126 | {
127 | Ongoing: 0,
128 | Completed: 1,
129 | }[status] ?? 5
130 | );
131 | }
132 |
133 | async getDetail(url) {
134 | var baseUrl = this.source.baseUrl;
135 | var slug = url.replace(baseUrl, "");
136 | var link = baseUrl + slug;
137 |
138 | var body = await this.request(slug);
139 |
140 | var media = body.selectFirst(".media");
141 | var title = media.selectFirst("h1").text;
142 | var spans = media.selectFirst("p.infoami").select("span");
143 | var statusText = spans[spans.length - 1].text.replace("Status: ", "");
144 | var status = this.statusCode(statusText);
145 |
146 | var tagscat = media.select(".tagscat > li");
147 | var genre = [];
148 | tagscat.forEach((tag) => genre.push(tag.text));
149 | var description = body.selectFirst("p.ptext").text;
150 | var chapters = [];
151 |
152 | var episodesList = body.select(".newmanga > li");
153 | episodesList.forEach((ep) => {
154 | var epTitle = ep.selectFirst("i.anititle").text;
155 | var epNumber = ep.selectFirst("strong").text.replace(title, "Episode");
156 | var epName = epNumber == epTitle ? epNumber : `${epNumber} - ${epTitle}`;
157 | var epUrl = ep.selectFirst("a").getHref;
158 |
159 | var scanlator = "";
160 | var type = ep.select("span.btn-xs");
161 | type.forEach((t) => {
162 | scanlator += t.text + ", ";
163 | });
164 | scanlator = scanlator.slice(0, -2);
165 |
166 | chapters.push({ name: epName, url: epUrl, scanlator });
167 | });
168 |
169 | return { description, status, genre, chapters, link };
170 | }
171 |
172 | async exxtractStreams(div, audio) {
173 | var slug = div.selectFirst("iframe").getSrc;
174 | var streams = [];
175 | if (slug.length < 1) {
176 | return streams;
177 | }
178 | var body = await this.requestText(slug);
179 | var sKey = "var videoSources = ";
180 | var eKey = "var httpProtocol";
181 | var start = body.indexOf(sKey) + sKey.length;
182 | var end = body.indexOf(eKey) - 8;
183 | var videoSourcesStr = body.substring(start, end);
184 | let videoSources = eval("(" + videoSourcesStr + ")");
185 | var headers = this.getHeaders();
186 | videoSources.forEach((videoSource) => {
187 | var url = this.source.baseUrl + videoSource.file;
188 | var quality = `${videoSource.label} - ${audio}`;
189 |
190 | streams.push({
191 | url,
192 | originalUrl: url,
193 | quality,
194 | headers,
195 | });
196 | });
197 | return streams.reverse();
198 | }
199 |
200 | // For anime episode video list
201 | async getVideoList(url) {
202 | var body = await this.request(url);
203 |
204 | var sub = body.selectFirst("#subbed-Animegg");
205 | var subStreams = await this.exxtractStreams(sub, "Sub");
206 |
207 | var dub = body.selectFirst("#dubbed-Animegg");
208 | var dubStreams = await this.exxtractStreams(dub, "Dub");
209 |
210 | var raw = body.selectFirst("#raw-Animegg");
211 | var rawStreams = await this.exxtractStreams(raw, "Raw");
212 |
213 | var pref = this.getPreference("animegg_stream_type_1");
214 | var streams = [];
215 | if (pref == 0) {
216 | streams = [...subStreams, ...dubStreams, ...rawStreams];
217 | } else if (pref == 1) {
218 | streams = [...dubStreams, ...subStreams, ...rawStreams];
219 | } else {
220 | streams = [...rawStreams, ...subStreams, ...dubStreams];
221 | }
222 |
223 | return streams;
224 | }
225 |
226 | getSourcePreferences() {
227 | return [
228 | {
229 | key: "animegg_popular_category",
230 | listPreference: {
231 | title: "Preferred popular category",
232 | summary: "",
233 | valueIndex: 0,
234 | entries: [
235 | "Popular",
236 | "Newest",
237 | "Ongoing",
238 | "Completed",
239 | "Alphabetical",
240 | ],
241 | entryValues: ["0", "1", "2", "3", "4"],
242 | },
243 | },
244 | {
245 | key: "animegg_stream_type_1",
246 | listPreference: {
247 | title: "Preferred stream type",
248 | summary: "",
249 | valueIndex: 0,
250 | entries: ["Sub", "Dub", "Raw"],
251 | entryValues: ["0", "1", "2"],
252 | },
253 | },
254 | ];
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mangayomi Swak Extensions
2 |
3 | This repository contains the available javascript extension catalogues for the Mangayomi app.
4 |
5 | ## Available Extensions List
6 |
7 | Expand list
8 |
9 | ## Manga
10 |
11 | | Name | Version | Language | Last Updated |
12 | |------|---------|----------|---------------|
13 | | WeLoMa | 1.0.0 | ja | 2025/10/29 23:26 IST |
14 | | Mangapill | 1.0.4 | en | 2025/09/17 08:55 IST |
15 | | Mangapark | 1.0.1 | en | 2025/09/17 08:55 IST |
16 | | ReadComicOnline | 0.3.0 | en | 2025/07/26 16:15 IST |
17 | | Weeb Central | 0.1.0 | en | 2025/03/15 03:44 IST |
18 |
19 | ## Anime
20 |
21 | | Name | Version | Language | Last Updated |
22 | |------|---------|----------|---------------|
23 | | Moviesda | 1.2.2 | ta | 2025/12/01 22:41 IST |
24 | | XPrime | 2.3.4 | all | 2025/12/01 22:41 IST |
25 | | yFlix | 0.0.6 | all | 2025/12/01 22:41 IST |
26 | | AnimeKai | 0.5.1 | en | 2025/11/10 21:25 IST |
27 | | Kaido | 1.0.2 | en | 2025/09/25 16:19 IST |
28 | | KickAssAnime | 1.2.5 | en | 2025/09/14 19:11 IST |
29 | | Anixl | 0.0.81 | en | 2025/09/05 16:59 IST |
30 | | SubsPlease | 0.0.5 | en | 2025/07/23 23:21 IST |
31 | | Aniwatch | 1.0.0 | en | 2025/06/30 23:06 IST |
32 | | Anicrush | 0.0.5 | en | 2025/06/27 22:11 IST |
33 | | AnimeParadise | 0.1.0 | en | 2025/06/21 18:06 IST |
34 | | AnimeZZ | 1.1.1 | en | 2025/06/21 16:47 IST |
35 | | Dramacool | 1.1.0 | all | 2025/06/13 11:41 IST |
36 | | Autoembed | 1.3.3 | all | 2025/05/24 19:22 IST |
37 | | AnimeGG | 1.0.3 | en | 2025/05/20 16:17 IST |
38 | | Sudatchi | 1.1.1 | en | 2025/05/20 16:17 IST |
39 | | Animeonsen | 1.0.1 | en, ja | 2025/05/20 16:17 IST |
40 | | Gojo | 0.0.6 | en | 2025/05/20 16:17 IST |
41 | | Aniwave | 0.0.7 | en | 2025/05/09 00:11 IST |
42 | | KissKH | 0.1.6 | all | 2025/05/02 22:47 IST |
43 |
44 | ## Novel
45 |
46 | | Name | Version | Language | Last Updated |
47 | |------|---------|----------|---------------|
48 | | Novelbuddy | 0.0.9 | en | 2025/06/09 23:24 IST |
49 |
50 |
51 |
52 | ## How to add the extensions
53 |
54 | Click on one of the buttons below to add the corresponding repository/repositories:
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | If you installed the app via Live Container, then use the following buttons instead:
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | Or add them manually in the app (More -> Settings -> Browse):
75 |
76 | manga repo
77 | ```
78 | https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/index.json
79 | ```
80 |
81 | anime repo
82 | ```
83 | https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/anime_index.json
84 | ```
85 |
86 | novel repo
87 | ```
88 | https://raw.githubusercontent.com/Swakshan/mangayomi-swak-extensions/refs/heads/main/novel_index.json
89 | ```
90 |
91 | # Contributing
92 |
93 | Contributions are welcome!
94 |
95 | To get started with development, see [CONTRIBUTING-DART.md](./CONTRIBUTING-DART.md) for create sources in Dart or [CONTRIBUTING-JS.md](./CONTRIBUTING-JS.md) for create sources in JavaScript.
96 |
97 | ## License
98 |
99 | Copyright 2023 Moustapha Kodjo Amadou
100 |
101 | Licensed under the Apache License, Version 2.0 (the "License");
102 | you may not use this file except in compliance with the License.
103 | You may obtain a copy of the License at
104 |
105 | http://www.apache.org/licenses/LICENSE-2.0
106 |
107 | Unless required by applicable law or agreed to in writing, software
108 | distributed under the License is distributed on an "AS IS" BASIS,
109 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
110 | See the License for the specific language governing permissions and
111 | limitations under the License.
112 |
113 |
114 |
115 | ## Disclaimer
116 |
117 | We hereby issue this notice to inform you that these extensions just function like an ordinary browser (like your browser) that fetch video files from internet, and do not violate the provisions of the Digital Millennium Copyright Act (DMCA). The Content these extensions may access is not hosted by us or the Mangayomi application but the websites they are browsing in their autonomous mode. It is sole responsibility of the user and his/her countries' or states' law. If you think they are violating any intellectual property then please contact the actual file hosts not the owners of this repository or the Mangayomi app.
118 |
--------------------------------------------------------------------------------
/javascript/anime/src/en/animeparadise.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "AnimeParadise",
4 | "id": 419768715,
5 | "lang": "en",
6 | "baseUrl": "https://animeparadise.moe",
7 | "apiUrl": "https://api.animeparadise.moe",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=128&domain=https://animeparadise.moe",
10 | "typeSource": "single",
11 | "itemType": 1,
12 | "version": "0.1.0",
13 | "pkgPath": "anime/src/en/animeparadise.js",
14 | },
15 | ];
16 |
17 | class DefaultExtension extends MProvider {
18 | getPreference(key) {
19 | const preferences = new SharedPreferences();
20 | return preferences.get(key);
21 | }
22 |
23 | async extractFromUrl(url) {
24 | var res = await new Client().get(this.source.baseUrl + url);
25 | var doc = new Document(res.body);
26 | var jsonData = doc.selectFirst("#__NEXT_DATA__").text;
27 | return JSON.parse(jsonData).props.pageProps;
28 | }
29 |
30 | async requestAPI(slug) {
31 | var api = `${this.source.apiUrl}/${slug}`;
32 | var response = await new Client().get(api);
33 | var body = JSON.parse(response.body);
34 | return body;
35 | }
36 |
37 | async formList(slug) {
38 | var jsonData = await this.requestAPI(slug);
39 | var list = [];
40 | if ("episodes" in jsonData) {
41 | jsonData.episodes.forEach((item) => {
42 | list.push({
43 | "name": item.origin.title,
44 | "link": item.origin.link,
45 | "imageUrl": item.image,
46 | });
47 | });
48 | } else {
49 | jsonData.data.forEach((item) => {
50 | list.push({
51 | "name": item.title,
52 | "link": item.link,
53 | "imageUrl": item.posterImage.original,
54 | });
55 | });
56 | }
57 |
58 | return {
59 | "list": list,
60 | "hasNextPage": false,
61 | };
62 | }
63 |
64 | async getPopular(page) {
65 | return await this.formList('?sort={"rate": -1 }');
66 | }
67 |
68 | async getLatestUpdates(page) {
69 | var slug = '?sort={"postDate": -1 }';
70 |
71 | var choice = this.getPreference("animeparadise_pref_latest_tab");
72 | if (choice === "recent_ep") slug = "ep/recently-added";
73 |
74 | return await this.formList(slug);
75 | }
76 | async search(query, page, filters) {
77 | var season = filters[0].values[filters[0].state].value;
78 | var year = filters[1].values[filters[1].state].value;
79 |
80 | var genre = "genre[]=";
81 | for (var filter of filters[2].state) {
82 | if (filter.state == true) genre += `${filter.value}&genre[]=`;
83 | }
84 | var slug = `search?q=${query}&year=${year}&season=${season}&${genre}`;
85 | return await this.formList(slug);
86 | }
87 | statusCode(status) {
88 | return (
89 | {
90 | "current": 0,
91 | "finished": 1,
92 | }[status] ?? 5
93 | );
94 | }
95 |
96 | async getDetail(url) {
97 | var linkSlug = this.source.baseUrl + `/anime/`;
98 | if (url.includes(linkSlug)) url = url.replace(linkSlug, "");
99 |
100 | var jsonData = await this.extractFromUrl(`/anime/${url}`);
101 | jsonData = jsonData.data;
102 | var details = {};
103 | var chapters = [];
104 | details.imageUrl = jsonData.posterImage.original;
105 | details.description = jsonData.synopsys;
106 | details.genre = jsonData.genres;
107 | details.status = this.statusCode(jsonData.status);
108 | var id = jsonData._id;
109 | var epAPI = await this.requestAPI(`anime/${id}/episode`);
110 | epAPI.data.forEach((ep) => {
111 | var epName = `E${ep.number}: ${ep.title}`;
112 | var epUrl = `${ep.uid}?origin=${ep.origin}`;
113 | chapters.push({ name: epName, url: epUrl });
114 | });
115 | details.link = `${linkSlug}${url}`;
116 | details.chapters = chapters.reverse();
117 | return details;
118 | }
119 | // Sorts streams based on user preference.
120 | async sortStreams(streams) {
121 | var sortedStreams = [];
122 | var copyStreams = streams.slice();
123 |
124 | var pref = await this.getPreference("animeparadise_pref_video_resolution");
125 | for (var stream of streams) {
126 | if (stream.quality.indexOf(pref) > -1) {
127 | sortedStreams.push(stream);
128 | var index = copyStreams.indexOf(stream);
129 | if (index > -1) {
130 | copyStreams.splice(index, 1);
131 | }
132 | break;
133 | }
134 | }
135 | return [...sortedStreams, ...copyStreams];
136 | }
137 |
138 | // Extracts the streams url for different resolutions from a hls stream.
139 | async extractStreams(url) {
140 | var proxyUrl = "https://stream.animeparadise.moe/";
141 | url = proxyUrl + "m3u8?url=" + url;
142 | var streams = [
143 | {
144 | url: url,
145 | originalUrl: url,
146 | quality: `Auto`,
147 | },
148 | ];
149 | const response = await new Client().get(url);
150 | if (response.statusCode == 200) {
151 | const body = response.body;
152 | const lines = body.split("\n");
153 |
154 | for (let i = 0; i < lines.length; i++) {
155 | if (lines[i].startsWith("#EXT-X-STREAM-INF:")) {
156 | var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1];
157 | var m3u8Url = proxyUrl + lines[i + 1].trim();
158 |
159 | streams.push({
160 | url: m3u8Url,
161 | originalUrl: m3u8Url,
162 | quality: resolution,
163 | });
164 | }
165 | }
166 | }
167 | return streams;
168 | }
169 |
170 | // For anime episode video list
171 | async getVideoList(url) {
172 | var streams = [];
173 | var jsonData = await this.extractFromUrl(`/watch/${url}`);
174 | var epData = jsonData.episode;
175 | streams = await this.extractStreams(epData.streamLink);
176 |
177 | var subtitles = [];
178 | epData.subData.forEach((sub) => {
179 | subtitles.push({
180 | "label": sub.label,
181 | "file": `${this.source.apiUrl}/stream/file/${sub.src}`,
182 | });
183 | });
184 |
185 | streams[0].subtitles = subtitles;
186 |
187 | return streams;
188 | }
189 |
190 | addCatogory(arr, typ) {
191 | arr = arr.map((x) => ({ type_name: typ, name: x, value: x }));
192 | arr.unshift({
193 | type_name: typ,
194 | name: "All",
195 | value: "",
196 | });
197 | return arr;
198 | }
199 |
200 | getFilterList() {
201 | var seasons = ["Winter", "Spring", "Summer", "Fall"];
202 |
203 | const currentYear = new Date().getFullYear();
204 | var years = Array.from({ length: currentYear - 1939 }, (_, i) =>
205 | (i + 1940).toString()
206 | ).reverse();
207 |
208 | var genres = [
209 | "Action",
210 | "Adventure",
211 | "Comedy",
212 | "Drama",
213 | "Ecchi",
214 | "Fantasy",
215 | "Horror",
216 | "Mahou Shojo",
217 | "Mecha",
218 | "Music",
219 | "Mystery",
220 | "Psychological",
221 | "Romance",
222 | "Sci-Fi",
223 | "Slice of Life",
224 | "Sports",
225 | "Supernatural",
226 | "Thriller",
227 | ].map((x) => ({ type_name: "CheckBox", name: x, value: x }));
228 |
229 | return [
230 | {
231 | type_name: "SelectFilter",
232 | name: "Season",
233 | state: 0,
234 | values: this.addCatogory(seasons, "SelectOption"),
235 | },
236 | {
237 | type_name: "SelectFilter",
238 | name: "Year",
239 | state: 0,
240 | values: this.addCatogory(years, "SelectOption"),
241 | },
242 | {
243 | type_name: "GroupFilter",
244 | name: "Genres",
245 | state: genres,
246 | },
247 | ];
248 | }
249 |
250 | getSourcePreferences() {
251 | return [
252 | {
253 | key: "animeparadise_pref_latest_tab",
254 | listPreference: {
255 | title: "Latest tab category",
256 | summary: "Anime list to be shown in latest tab",
257 | valueIndex: 0,
258 | entries: ["Recently added anime", "Recently added episode"],
259 | entryValues: ["recent_ani", "recent_ep"],
260 | },
261 | },
262 | {
263 | key: "animeparadise_pref_video_resolution",
264 | listPreference: {
265 | title: "Preferred video resolution",
266 | summary: "",
267 | valueIndex: 0,
268 | entries: ["Auto", "1080p", "720p", "360p"],
269 | entryValues: ["auto", "1080", "720", "360"],
270 | },
271 | },
272 | ];
273 | }
274 | }
275 |
--------------------------------------------------------------------------------
/CONTRIBUTING-JS.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | This guide have some instructions and tips on how to create a new Mangayomi extension on JavaScript extension.
4 |
5 | ## Prerequisites
6 |
7 | Before starting please have installed the recent desktop version of the mangayomi application preferably or if you want with a tablet too.
8 |
9 |
10 | ### Writing your extension
11 | 1. Open the app.
12 | 2. Go to extension tab :
13 | 
14 | 3. then click `+` and you will see :
15 | 
16 | 4. Fill in the fields with your new source that you would like to create,
17 | 
18 | NB: only the `ApiUrl` field is optional
19 | then click on save
20 | 5. you will see your new source in the extension list
21 | 
22 | click to open settings
23 | 6. After click on edit code
24 | 
25 | 7. Finally you can now write the extension
26 | 
27 | - This page contains three parts:
28 | - Code editor: where you will write your code
29 | - Fecth result: where you will test the different implemented methods by having a result in the expected format
30 | - Console: which will show you the logs
31 |
32 | Once extension is ready you can relocate your code into `mangayomi-extension` project in a `src` or `multisrc` package and create a Pull Request.
33 |
34 | ### Source
35 |
36 | | Field | Description |
37 | | ----- | ----------- |
38 | | `name` | Name displayed in the "Sources" tab in Mangayomi. |
39 | | `baseUrl` | Base URL of the source without any trailing slashes. |
40 | | `apiUrl` | (Optional, defaults is empty) Api URL of the source with trailing slashes. |
41 | | `lang` | An ISO 639-1 compliant language code (two letters in lower case in most cases, but can also include the country/dialect part by using a simple dash character). |
42 | | `id` | Identifier of your source, automatically set in `Source`. It should only be manually overriden if you need to copy an existing autogenerated ID. |
43 | | `isManga` | (Optional, defaults to `true`) specify source type (false for anime and true for manga)|
44 | | `dateFormat` | (Optional, defaults is empty) |
45 | | `iconUrl` | The extension icon URL |
46 | | `version` | The extension version code. This must be incremented with any change to the code. |
47 | | `dateFormatLocale` | (Optional, defaults is empty) |
48 | | `isNsfw` | (Optional, defaults to `false`) Flag to indicate that a source contains NSFW content. |
49 |
50 | ### Extension call flow
51 |
52 | #### Popular manga
53 |
54 | a.k.a. the Browse source entry point in the app (invoked by tapping on the source name).
55 |
56 | - The app calls `getPopular` which should return a JSON
57 | ```bash
58 | {
59 | 'list': array of {'url':string,'name':string,'link':string},
60 | hasNextPage: Boolean
61 | }
62 | ```
63 | - This method supports pagination. When user scrolls the manga list and more results must be fetched, the app calls it again with increasing `page` values(starting with `page=1`). This continues while `hasNextPage` is passed as `true` and `list` is not empty.
64 |
65 | #### Latest manga
66 |
67 | a.k.a. the Latest source entry point in the app (invoked by tapping on the "Latest" button beside the source name).
68 |
69 | - Similar to popular manga, but should be fetching the latest entries from a source.
70 |
71 | #### Search manga
72 |
73 | - When the user searches inside the app, `search` will be called and the rest of the flow is similar to what happens with `getPopular`.
74 | - `getFilterList` will be called to get all filters and filter types.
75 |
76 |
77 | #### Manga Details
78 |
79 | - When user taps on an manga, `getDetail` will be called and the results will be cached.
80 | - `getDetail` is called to update an manga's details from when it was initialized earlier.
81 | - `title` is a string containing title.
82 | - `description` is a string containing description.
83 | - `author` is a string containing author.
84 | - `genre` contain array of all genres.
85 | - `status` is an "integer" value.
86 | You can refer to this example to see the correspondence:
87 | ```bash
88 | 0=>"ongoing", 1=>"complete", 2=>"hiatus", 3=>"canceled", 4=>"publishingFinished", 5=>unknow
89 | ```
90 |
91 | - `chapters` or `episodes` contain all of all manga chapters or anime episodes.
92 | - `name` is a string containing a chapter name.
93 | - `url` is a string containing a chapter url.
94 | - `scanlator` is a string containing a chapter scanlator.
95 | - `dateUpload` is a string containing date **expressed in millisecondsSinceEpoch**.
96 | - If you don't pass `dateUpload` and leave it null, the app will use the default date instead, but it's recommended to always fill it if it's available.
97 |
98 | #### Chapter pages
99 |
100 | - When user opens an chapter, `getPageList` will be called and it will return an array of string or an array of map like `{ url:string,headers:map }` that are used by the reader.
101 |
102 | #### Episode Videos
103 |
104 | - When user opens an episode, `getVideoList` will be called and it will return a
105 | ```bash
106 | array of {'url':string,'originalUrl':string,'quality':string}
107 | ```
108 |
109 | that are used by the player.
110 |
111 | ## Example sources that can help you understand how to create your source
112 |
113 | - [Example](https://github.com/kodjodevf/mangayomi-extensions/blob/main/javascript/anime/src/de/aniworld.js)
114 | of HTML parsing using HTML DOM selector.
115 | - [Example](https://github.com/kodjodevf/mangayomi-extensions/blob/main/javascript/anime/src/en/allanime.js)
116 | of Json API usage.
117 |
118 |
119 | ## Some functions already available and usable
120 |
121 |
122 | ### http client
123 |
124 | Return Response
125 | ```bash
126 | - Simple request
127 |
128 | const client = new Client();
129 |
130 | const res = await client.get("http://example.com");
131 |
132 | console.log(res.body);
133 |
134 | - With headers
135 |
136 | const client = new Client();
137 |
138 | const res = await client.get("http://example.com",{"Referer": "http://example.com"});
139 |
140 | console.log(res.body);
141 |
142 | - With body
143 |
144 | const client = new Client();
145 |
146 | const res = await client.post("http://example.com",{"Referer": "http://example.com"},{'name':'test'});
147 |
148 | console.log(res.body);
149 |
150 | ```
151 |
152 | ### HTML DOM selector
153 |
154 | Example:
155 | ```bash
156 | const htmlString = `
157 |
158 |
159 |
160 | div head
161 |
162 |
163 |
164 |
165 | | 1 |
166 | 2 |
167 | 3 |
168 | 4 |
169 | one |
170 | two |
171 | three |
172 | four |
173 |
174 |
175 |
176 |
177 | end
178 |
179 | `
180 |
181 | const document = new Document(htmlString);
182 | console.log(document.selectFirst("a").attr("href")); // https://github.com/kodjodevf
183 | console.log(document.selectFirst("td").text); // 1
184 |
185 | ```
186 | See [`dom_selector`](https://github.com/kodjodevf/mangayomi/blob/main/lib/eval/javascript/dom_selector.dart) to see available methods.
187 |
188 |
189 | ### String utils
190 | - this.substringAfter(`string: pattern`)
191 | - this.substringAfterLast(`string: pattern`)
192 | - this.substringBefore(`string: pattern`)
193 | - this.substringBeforeLast(`string: pattern`)
194 | - this.substringBetween(`string: left`, `string: right`)
195 |
196 | ### Crypto utils
197 | - unpackJs(`string: code`);
198 | - deobfuscateJsPassword(`string: inputString`)
199 | - encryptAESCryptoJS(`string: plainText`, `string: passphrase`)
200 | - decryptAESCryptoJS(`string: encrypted`, `string: passphrase`)
201 | - cryptoHandler(`string: text`, `string: iv`, `string: secretKeyString`, `Boolean: encrypt`)
202 |
203 | ## Help
204 |
205 | If you need a help or have some questions, ask a community in our [Discord server](https://discord.com/invite/EjfBuYahsP).
206 |
--------------------------------------------------------------------------------
/javascript/anime/src/en/animeonsen.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [{
2 | "name": "Animeonsen",
3 | "id": 1004796349,
4 | "langs": ["en", "ja"],
5 | "baseUrl": "https://www.animeonsen.xyz",
6 | "apiUrl": "https://api.animeonsen.xyz",
7 | "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://www.animeonsen.xyz",
8 | "typeSource": "single",
9 | "itemType": 1,
10 | "version": "1.0.1",
11 | "pkgPath": "anime/src/all/animeonsen.js"
12 | }];
13 |
14 | class DefaultExtension extends MProvider {
15 | constructor() {
16 | super();
17 | this.client = new Client();
18 | }
19 |
20 | getPreference(key) {
21 | return new SharedPreferences().get(key);
22 | }
23 |
24 | async getToken() {
25 | const preferences = new SharedPreferences();
26 | var token_ts = parseInt(preferences.getString("animeosen_token_expiry_at", "0"))
27 | var now_ts = parseInt(new Date().getTime() / 1000);
28 |
29 | // token lasts for 7days but still checking after 6days
30 | if (now_ts - token_ts > 60 * 60 * 24 * 6) {
31 | var tokenBody = {
32 | client_id: "f296be26-28b5-4358-b5a1-6259575e23b7",
33 | client_secret: "349038c4157d0480784753841217270c3c5b35f4281eaee029de21cb04084235",
34 | grant_type: "client_credentials"
35 | }
36 | var res = await this.client.post("https://auth.animeonsen.xyz/oauth/token", {}, tokenBody)
37 | res = JSON.parse(res.body)
38 | var token = res.access_token
39 | preferences.setString("animeosen_token", token);
40 | preferences.setString("animeosen_token_expiry_at", "" + now_ts);
41 | return token
42 | } else {
43 | return preferences.getString("animeosen_token", "");
44 |
45 | }
46 | }
47 |
48 | async getHeaders(slug) {
49 | var brToken = ""
50 | if (slug.endsWith("/search")) {
51 | brToken = "0e36d0275d16b40d7cf153634df78bc229320d073f565db2aaf6d027e0c30b13"
52 | }
53 | else {
54 | brToken = await this.getToken()
55 | }
56 |
57 | return {
58 | 'Authorization': `Bearer ${brToken}`,
59 | 'content-type': "application/json"
60 | }
61 | }
62 |
63 | async request(slug, body = {}) {
64 |
65 | var headers = await this.getHeaders(slug)
66 |
67 | if (slug.endsWith("/search")) {
68 |
69 | var api = `https://search.animeonsen.xyz${slug}`
70 | var res = await this.client.post(api, headers, body)
71 | return JSON.parse(res.body)
72 | }
73 | var api = `${this.source.apiUrl}/v4/content${slug}`
74 | var res = await this.client.get(api, headers)
75 | return JSON.parse(res.body)
76 | }
77 |
78 | animeContent(anime, pref_name, imgRes) {
79 | var name_eng = anime.content_title_en
80 | var name_jp = anime.content_title
81 | var name = pref_name == "jpn" ? name_jp : name_eng;
82 | var link = anime.content_id
83 | var imageUrl = `${this.source.apiUrl}/v4/image/${imgRes}/${link}`
84 | return { name, imageUrl, link };
85 | }
86 |
87 | async getHome(page) {
88 | var limit = 20
89 | var start = (page - 1) * limit;
90 |
91 | var slug = `/index?start=${start}&limit=${limit}`
92 | var res = await this.request(slug)
93 |
94 | var pref_name = this.getPreference("animeonsen__pref_title_lang")
95 | var imgRes = this.getPreference("animeonsen__pref_img_res_1")
96 |
97 | var hasNextPage = res.cursor.next[0]
98 | var list = []
99 | for (var anime of res.content) {
100 | list.push(this.animeContent(anime, pref_name, imgRes));
101 | }
102 | return { list, hasNextPage }
103 | }
104 |
105 | async getPopular(page) {
106 | return await this.getHome(page)
107 | }
108 | get supportsLatest() {
109 | throw new Error("supportsLatest not implemented");
110 | }
111 |
112 |
113 | async search(query, page, filters) {
114 | var slug = "/indexes/content/search"
115 |
116 | var limit = 30;
117 | var offset = (page - 1) * limit;
118 | var nextOffset = offset + limit;
119 |
120 | var params = { limit, offset, q: query };
121 |
122 | var res = await this.request(slug, params);
123 |
124 | var estimatedTotalHits = res.estimatedTotalHits
125 | var hasNextPage = estimatedTotalHits > nextOffset;
126 |
127 | var list = []
128 | var hits = res.hits
129 | var pref_name = this.getPreference("animeonsen__pref_title_lang")
130 | var imgRes = this.getPreference("animeonsen__pref_img_res_1")
131 | if (hits.length > 0) {
132 | for (var anime of hits) {
133 | list.push(this.animeContent(anime, pref_name, imgRes));
134 | }
135 | }
136 | return { list, hasNextPage }
137 | }
138 |
139 | statusCode(status) {
140 | return {
141 | "currently_airing": 0,
142 | "finished_airing": 1,
143 | }[status] ?? 5;
144 | }
145 |
146 | async getDetail(url) {
147 | var linkSlug = `${this.source.baseUrl}/details/`
148 | url = url.replace(linkSlug, "")
149 | var link = `${linkSlug}${url}`
150 | var detailsApiSlug = `/${url}/extensive`
151 | var animeDetails = await this.request(detailsApiSlug);
152 |
153 | var pref_name = this.getPreference("animeonsen_pref_ep_title_lang")
154 | var imgRes = this.getPreference("animeonsen__pref_img_res_1")
155 |
156 | var name_eng = animeDetails.content_title_en
157 | var name_jp = animeDetails.content_title
158 | var name = pref_name == "jpn" ? name_jp : name_eng;
159 | var link = animeDetails.content_id
160 | var imageUrl = `${this.source.apiUrl}/v4/image/${imgRes}/${link}`
161 | var is_movie = animeDetails.is_movie
162 |
163 | var mal_data = animeDetails.mal_data
164 | var description = mal_data.synopsis
165 | var genre = []
166 | mal_data.genres.forEach(g => genre.push(g.name))
167 | var status = this.statusCode(mal_data.status);
168 |
169 | var chapters = [];
170 | var episodeAPISlug = `/${url}/episodes`
171 | var episodeDetails = await this.request(episodeAPISlug);
172 |
173 | Object.keys(episodeDetails).forEach(ep => {
174 | var ep_data = episodeDetails[ep]
175 |
176 | var ep_name_eng = ep_data.contentTitle_episode_en
177 | var ep_name_jp = ep_data.contentTitle_episode_jp
178 | var ep_name = pref_name == "jpn" ? ep_name_jp : ep_name_eng;
179 |
180 | chapters.push({
181 | name:`E${ep}: ${ep_name}`,
182 | url: `/${url}/video/${ep}`,
183 | })
184 | })
185 |
186 | chapters.reverse()
187 | return { name, imageUrl, status, description, genre,link, chapters }
188 | }
189 |
190 | // For anime episode video list
191 | async getVideoList(url) {
192 | var streamDetails = await this.request(url);
193 | var streamData = streamDetails.uri
194 |
195 | var streams = [
196 | {
197 | quality:`Default (720p)`,
198 | url: streamData.stream,
199 | originalUrl: streamData.stream
200 | }
201 | ];
202 |
203 | var subtitles = [];
204 | var subData = streamDetails.subtitles;
205 | Object.keys(subData).forEach(sub => {
206 | subtitles.push({
207 | label:sub,
208 | file: subData[url]
209 | })
210 | });
211 |
212 | streams[0].subtitles = subtitles
213 |
214 | return streams
215 | }
216 |
217 | getSourcePreferences() {
218 | return [{
219 | key: 'animeonsen__pref_title_lang',
220 | listPreference: {
221 | title: 'Preferred title language',
222 | summary: '',
223 | valueIndex: 0,
224 | entries: ["Japenese", "English"],
225 | entryValues: ["jpn", "en"]
226 | }
227 | }, {
228 | key: 'animeonsen_pref_ep_title_lang',
229 | listPreference: {
230 | title: 'Preferred episode title language',
231 | summary: '',
232 | valueIndex: 1,
233 | entries: ["Japenese", "English"],
234 | entryValues: ["jpn", "en"]
235 | }
236 | },{
237 | key: 'animeonsen__pref_img_res_1',
238 | listPreference: {
239 | title: 'Preferred image resolution',
240 | summary: '',
241 | valueIndex: 1,
242 | entries: ["Low", "Medium", "High"],
243 | entryValues: ["210x300", "420x600", "840x1200"]
244 | }
245 | }];
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/javascript/manga/src/en/weebcentral.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "id": 693275080,
4 | "name": "Weeb Central",
5 | "lang": "en",
6 | "baseUrl": "https://weebcentral.com",
7 | "apiUrl": "",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=128&domain=https://weebcentral.com",
10 | "typeSource": "single",
11 | "isManga": true,
12 | "itemType": 0,
13 | "version": "0.1.0",
14 | "pkgPath": "manga/src/en/weebcentral.js"
15 | }
16 | ];
17 |
18 | // Authors: - Swakshan, Schnitzel5, kodjodevf
19 |
20 |
21 | class DefaultExtension extends MProvider {
22 | constructor() {
23 | super();
24 | this.client = new Client();
25 | }
26 | getHeaders(url) {
27 | return { Referer: `${this.source.baseUrl}/` };
28 | }
29 |
30 | async request(slug) {
31 | var url = `${this.source.baseUrl}${slug}`;
32 | var res = await this.client.get(url);
33 | return new Document(res.body);
34 | }
35 |
36 | async getPopular(page) {
37 | const filters = this.getFilterList();
38 | filters[0].state = 2;
39 | return await this.search("", page, filters);
40 | }
41 |
42 | async getLatestUpdates(page) {
43 | const filters = this.getFilterList();
44 | filters[0].state = 5;
45 | return await this.search("", page, filters);
46 | }
47 |
48 | getImageUrl(id) {
49 | return `https://temp.compsci88.com/cover/normal/${id}.webp`;
50 | }
51 |
52 | async search(query, page, filters) {
53 | var offset = 32 * (parseInt(page) - 1);
54 | var sort = filters[0].values[filters[0].state].value;
55 | var order = filters[1].values[filters[1].state].value;
56 | var translation = filters[2].values[filters[2].state].value;
57 | var status = "";
58 | for (var filter of filters[3].state) {
59 | if (filter.state == true) status += `&included_status=${filter.value}`;
60 | }
61 | var type = "";
62 | for (var filter of filters[4].state) {
63 | if (filter.state == true) type += `&included_type=${filter.value}`;
64 | }
65 | var tags = "";
66 | for (var filter of filters[5].state) {
67 | if (filter.state == true) tags += `&included_tag=${filter.value}`;
68 | }
69 | var slug = `/search/data?limit=32&offset=${offset}&author=&text=${query}&sort=${sort}&order=${order}&official=${translation}${status}${type}${tags}&display_mode=Full%20Display`;
70 | var doc = await this.request(slug);
71 | var list = [];
72 | var mangaElements = doc.select("article:has(section)");
73 | for (var manga of mangaElements) {
74 | var imageUrl = manga.selectFirst("img").getSrc;
75 | var details = manga.selectFirst("section > a");
76 | var link = details.getHref;
77 | var name = manga.selectFirst("article > div > div > div").text;
78 | list.push({ name, imageUrl, link });
79 | }
80 |
81 | var hasNextPage = doc.selectFirst("button").text.length > 0;
82 | return { list, hasNextPage };
83 | }
84 | statusCode(status) {
85 | return (
86 | {
87 | Ongoing: 0,
88 | Complete: 1,
89 | Hiatus: 2,
90 | Canceled: 3,
91 | }[status] ?? 5
92 | );
93 | }
94 |
95 | async getDetail(url) {
96 | var urlSplits = url.split("/");
97 | var link = urlSplits[urlSplits.length - 2];
98 | var slug = url.startsWith("http") ? `/series/${link}` : `/series/${url}`;
99 | var doc = await this.request(slug);
100 | var imageUrl = url.startsWith("http")
101 | ? this.getImageUrl(link)
102 | : this.getImageUrl(url);
103 | var description = doc.selectFirst("p.whitespace-pre-wrap.break-words").text;
104 |
105 | var chapters = [];
106 | var ul = doc.select("ul.flex.flex-col.gap-4 > li");
107 | var author = "";
108 | var genre = [];
109 | var status = 5;
110 | for (var li of ul) {
111 | var strongTxt = li.selectFirst("strong").text;
112 | if (strongTxt.indexOf("Author(s):") != -1) {
113 | author = li.selectFirst("a").text;
114 | } else if (strongTxt.indexOf("Tags(s):") != -1) {
115 | li.select("a").forEach((a) => genre.push(a.text));
116 | } else if (strongTxt.indexOf("Status:") != -1) {
117 | status = this.statusCode(li.selectFirst("a").text);
118 | }
119 | }
120 |
121 | var chapSlug = `${slug}/full-chapter-list`;
122 | doc = await this.request(chapSlug);
123 | var chapList = doc.select("div.flex.items-center");
124 | for (var chap of chapList) {
125 | var name = chap
126 | .selectFirst("span.grow.flex.items-center.gap-2")
127 | .selectFirst("span").text;
128 | var dateUpload = new Date(chap.selectFirst("time.text-datetime").text)
129 | .valueOf()
130 | .toString();
131 | var url = chap.selectFirst("input").attr("value");
132 | chapters.push({ name, url, dateUpload });
133 | }
134 | return { description, imageUrl, author, genre, status, chapters };
135 | }
136 | async getPageList(url) {
137 | var slug = `/chapters/${url}/images?current_page=1&reading_style=long_strip`;
138 | var doc = await this.request(slug);
139 |
140 | var urls = [];
141 |
142 | doc.select("section > img").forEach((page) => urls.push(page.attr("src")));
143 |
144 | return urls.map((x) => ({
145 | url: x,
146 | headers: {
147 | Referer: `${this.source.baseUrl}/`,
148 | Accept: "image/avif,image/webp,*/*",
149 | Host: `${x.match(/^(?:https?:\/\/)?([^\/:]+)(:\d+)?/)[1]}`,
150 | },
151 | }));
152 | }
153 |
154 | getFilterList() {
155 | return [
156 | {
157 | type_name: "SelectFilter",
158 | name: "Sort",
159 | state: 0,
160 | values: [
161 | ["Best Match", "Best Match"],
162 | ["Alphabet", "Alphabet"],
163 | ["Popularity", "Popularity"],
164 | ["Subscribers", "Subscribers"],
165 | ["Recently Added", "Recently Added"],
166 | ["Latest Updates", "Latest Updates"],
167 | ].map((x) => ({ type_name: "SelectOption", name: x[0], value: x[1] })),
168 | },
169 | {
170 | type_name: "SelectFilter",
171 | name: "Order",
172 | state: 0,
173 | values: [
174 | ["Ascending", "Ascending"],
175 | ["Descending", "Descending"],
176 | ].map((x) => ({ type_name: "SelectOption", name: x[0], value: x[1] })),
177 | },
178 | {
179 | type_name: "SelectFilter",
180 | name: "Official Translation",
181 | state: 0,
182 | values: [
183 | ["Any", "Any"],
184 | ["True", "True"],
185 | ["False", "False"],
186 | ].map((x) => ({ type_name: "SelectOption", name: x[0], value: x[1] })),
187 | },
188 | {
189 | type_name: "GroupFilter",
190 | name: "Series Status",
191 | state: [
192 | ["Ongoing", "Ongoing"],
193 | ["Complete", "Complete"],
194 | ["Hiatus", "Hiatus"],
195 | ["Canceled", "Canceled"],
196 | ].map((x) => ({ type_name: "CheckBox", name: x[0], value: x[1] })),
197 | },
198 | {
199 | type_name: "GroupFilter",
200 | name: "Series Type",
201 | state: [
202 | ["Manga", "Manga"],
203 | ["Manhwa", "Manhwa"],
204 | ["Manhua", "Manhua"],
205 | ["OEL", "OEL"],
206 | ].map((x) => ({ type_name: "CheckBox", name: x[0], value: x[1] })),
207 | },
208 | {
209 | type_name: "GroupFilter",
210 | name: "Tags",
211 | state: [
212 | ["Action", "Action"],
213 | ["Adventure", "Adventure"],
214 | ["Adult", "Adult"],
215 | ["Comedy", "Comedy"],
216 | ["Doujinshi", "Doujinshi"],
217 | ["Drama", "Drama"],
218 | ["Ecchi", "Ecchi"],
219 | ["Fantasy", "Fantasy"],
220 | ["Gender Bender", "Gender Bender"],
221 | ["Harem", "Harem"],
222 | ["Hentai", "Hentai"],
223 | ["Historical", "Historical"],
224 | ["Horror", "Horror"],
225 | ["Isekai", "Isekai"],
226 | ["Josei", "Josei"],
227 | ["Lolicon", "Lolicon"],
228 | ["Martial Arts", "Martial Arts"],
229 | ["Mature", "Mature"],
230 | ["Mecha", "Mecha"],
231 | ["Mystery", "Mystery"],
232 | ["Psychological", "Psychological"],
233 | ["Romance", "Romance"],
234 | ["School Life", "School Life"],
235 | ["Sci-Fi", "Sci-Fi"],
236 | ["Seinen", "Seinen"],
237 | ["Shotacon", "Shotacon"],
238 | ["Shoujo", "Shoujo"],
239 | ["Shoujo Ai", "Shoujo Ai"],
240 | ["Shounen", "Shounen"],
241 | ["Slice of Life", "Slice of Life"],
242 | ["Smut", "Smut"],
243 | ["Sports", "Sports"],
244 | ["Supernatural", "Supernatural"],
245 | ["Tragedy", "Tragedy"],
246 | ["Yaoi", "Yaoi"],
247 | ["Yuri", "Yuri"],
248 | ["Other", "Other"],
249 | ].map((x) => ({ type_name: "CheckBox", name: x[0], value: x[1] })),
250 | },
251 | ];
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/javascript/anime/src/en/sudatchi.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "Sudatchi",
4 | "id": 398530136,
5 | "lang": "en",
6 | "baseUrl": "https://sudatchi.com",
7 | "apiUrl": "",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=128&domain=https://sudatchi.com",
10 | "typeSource": "single",
11 | "version": "1.1.1",
12 | "dateFormat": "",
13 | "dateFormatLocale": "",
14 | "itemType": 1,
15 | "pkgPath": "anime/src/en/sudatchi.js",
16 | },
17 | ];
18 |
19 | class DefaultExtension extends MProvider {
20 | getHeaders(url) {
21 | return {
22 | "Referer": this.source.baseUrl,
23 | };
24 | }
25 |
26 | getPreference(key) {
27 | const preferences = new SharedPreferences();
28 | return preferences.get(key);
29 | }
30 |
31 | getUrl(slug) {
32 | return `https://ipfs.sudatchi.com/ipfs/${slug}`;
33 | }
34 |
35 | async requestApi(slug) {
36 | var url = this.source.baseUrl + "/api" + slug;
37 |
38 | var res = await new Client().get(url, this.getHeaders());
39 |
40 | return JSON.parse(res.body);
41 | }
42 |
43 | async formListForAnilist(animes) {
44 | var list = [];
45 | var lang = this.getPreference("sudatchi_pref_lang");
46 | for (var item of animes) {
47 | var titles = item.title;
48 | var name = titles.romaji;
49 | switch (lang) {
50 | case "e": {
51 | name = titles.english != null ? titles.english : name;
52 | break;
53 | }
54 | case "j": {
55 | name = titles.native != null ? titles.native : name;
56 | break;
57 | }
58 | }
59 | var link = item.id;
60 | var coverImage = item.coverImage;
61 | var imageUrl =
62 | "large" in coverImage ? coverImage.large : coverImage.medium;
63 |
64 | list.push({
65 | name,
66 | imageUrl,
67 | link: `${link}`,
68 | });
69 | }
70 | return list;
71 | }
72 |
73 | async formList(animes) {
74 | var list = [];
75 | var lang = this.getPreference("sudatchi_pref_lang");
76 | for (var item of animes) {
77 | var details = "Anime" in item ? item.Anime : item;
78 | var name =
79 | "titleRomanji" in details ? details.titleRomanji : details.title;
80 | switch (lang) {
81 | case "e": {
82 | name = "titleEnglish" in details ? details.titleEnglish : name;
83 | break;
84 | }
85 | case "j": {
86 | name = "titleJapanese" in details ? details.titleJapanese : name;
87 | break;
88 | }
89 | }
90 | var link = "anilistId" in details ? details.anilistId : details.id;
91 | var imageUrl =
92 | "coverImage" in details
93 | ? details.coverImage
94 | : this.getUrl(details.imgUrl);
95 | list.push({
96 | name,
97 | imageUrl,
98 | link: `${link}`,
99 | });
100 | }
101 | return list;
102 | }
103 |
104 | async getPopular(page) {
105 | var pageProps = await this.requestApi("/fetchHomeData");
106 | // var = extract
107 | var latestEpisodes = await this.formList(pageProps.latestEpisodes);
108 | var latestAnimes = await this.formListForAnilist(pageProps.ongoingAnimes);
109 | var animeSpotlight = await this.formList(pageProps.AnimeSpotlight);
110 | var list = [...animeSpotlight, ...latestAnimes, ...latestEpisodes];
111 | return {
112 | list,
113 | hasNextPage: false,
114 | };
115 | }
116 | get supportsLatest() {
117 | throw new Error("supportsLatest not implemented");
118 | }
119 | async getLatestUpdates(page) {
120 | var pageProps = await this.requestApi("/fetchHomeData");
121 | var list = await this.formList(pageProps.latestEpisodes);
122 |
123 | return {
124 | list,
125 | hasNextPage: false,
126 | };
127 | }
128 | async search(query, page, filters) {
129 | var body = await this.requestApi("/fetchAnime");
130 |
131 | var url = this.source.baseUrl + "/api/fetchAnime";
132 |
133 | var res = await new Client().post(url, this.getHeaders(), {
134 | "query": query,
135 | });
136 | var body = JSON.parse(res.body);
137 |
138 | var list = await this.formListForAnilist(body.results);
139 | var hasNextPage = body.pages > page ? true : false;
140 |
141 | return {
142 | list,
143 | hasNextPage,
144 | };
145 | }
146 |
147 | statusCode(status) {
148 | return (
149 | {
150 | "Currently Airing": 0,
151 | "Finished Airing": 1,
152 | "Hiatus": 2,
153 | "Discontinued": 3,
154 | "Not Yet Released": 4,
155 | }[status] ?? 5
156 | );
157 | }
158 |
159 | async getDetail(url) {
160 | var linkSlug = "https://sudatchi.com/anime/";
161 | if (url.includes(linkSlug)) url = url.replace(linkSlug, "");
162 |
163 | var lang = this.getPreference("sudatchi_pref_lang");
164 | var link = `${linkSlug}${url}`;
165 | var details = await this.requestApi(`/anime/${url}`);
166 | var titles = details.title;
167 | var name = titles.romaji;
168 | switch (lang) {
169 | case "e": {
170 | name = titles.english != null ? titles.english : name;
171 | break;
172 | }
173 | case "j": {
174 | name = titles.native != null ? titles.native : name;
175 | break;
176 | }
177 | }
178 | var description = details.description;
179 | var status = this.statusCode(details.status);
180 | var imageUrl = details.coverImage;
181 | var genre = details.genres;
182 |
183 | var chapters = [];
184 | var episodes = details.episodes;
185 | if (episodes.length > 0) {
186 | var typeId = details.format;
187 | if (typeId == "MOVIE") {
188 | var number = episodes[0].number;
189 | var epUrl = `${url}/${number}`;
190 | chapters.push({ name: "Movie", url: epUrl });
191 | } else {
192 | for (var eObj of episodes) {
193 | var epName = eObj.title;
194 | var number = eObj.number;
195 | var epUrl = `${url}/${number}`;
196 | chapters.push({ name: epName, url: epUrl });
197 | }
198 | }
199 | }
200 |
201 | chapters.reverse();
202 |
203 | return { name, description, status, imageUrl, genre, chapters, link };
204 | }
205 | // For novel html content
206 | async getHtmlContent(url) {
207 | throw new Error("getHtmlContent not implemented");
208 | }
209 | // Clean html up for reader
210 | async cleanHtmlContent(html) {
211 | throw new Error("cleanHtmlContent not implemented");
212 | }
213 |
214 | async extractStreams(url) {
215 | const response = await new Client().get(url);
216 | const body = response.body;
217 | const lines = body.split("\n");
218 | var audios = [];
219 |
220 | var streams = [
221 | {
222 | url: url,
223 | originalUrl: url,
224 | quality: "auto",
225 | },
226 | ];
227 |
228 | for (let i = 0; i < lines.length; i++) {
229 | var currentLine = lines[i];
230 | if (currentLine.startsWith("#EXT-X-STREAM-INF:")) {
231 | var resolution = currentLine.match(/RESOLUTION=(\d+x\d+)/)[1];
232 | var m3u8Url = lines[i + 1].trim();
233 | m3u8Url = m3u8Url.replace("./", `${url}/`);
234 | streams.push({
235 | url: m3u8Url,
236 | originalUrl: m3u8Url,
237 | quality: resolution,
238 | });
239 | } else if (currentLine.startsWith("#EXT-X-MEDIA:TYPE=AUDIO")) {
240 | var attributesString = currentLine.split(",");
241 | var attributeRegex = /([A-Z-]+)=("([^"]*)"|[^,]*)/g;
242 | let match;
243 | var trackInfo = {};
244 | while ((match = attributeRegex.exec(attributesString)) !== null) {
245 | var key = match[1];
246 | var value = match[3] || match[2];
247 | if (key === "NAME") {
248 | trackInfo.label = value;
249 | } else if (key === "URI") {
250 | trackInfo.file = value;
251 | }
252 | }
253 | audios.push(trackInfo);
254 | }
255 | }
256 | streams[0].audios = audios;
257 | return streams;
258 | }
259 |
260 | // For anime episode video list
261 | async getVideoList(url) {
262 | var jsonData = await this.requestApi(`/episode/${url}`);
263 | var episodeData = jsonData.episode;
264 | var epId = episodeData.id;
265 |
266 | var epLink = `https://sudatchi.com/videos/m3u8/episode-${epId}.m3u8`;
267 | var streams = await this.extractStreams(epLink);
268 |
269 | var subs = JSON.parse(jsonData.subtitlesJson);
270 | var subtitles = [];
271 | for (var sub of subs) {
272 | var file = `https://ipfs.sudatchi.com${sub.url}`;
273 | var label = sub.SubtitlesName.name;
274 | subtitles.push({ file: file, label: label });
275 | }
276 | streams[0].subtitles = subtitles;
277 |
278 | return streams;
279 | }
280 | // For manga chapter pages
281 | async getPageList() {
282 | throw new Error("getPageList not implemented");
283 | }
284 | getFilterList() {
285 | throw new Error("getFilterList not implemented");
286 | }
287 |
288 | getSourcePreferences() {
289 | return [
290 | {
291 | key: "sudatchi_pref_lang",
292 | listPreference: {
293 | title: "Preferred title language",
294 | summary: "",
295 | valueIndex: 0,
296 | entries: ["Romanji", "English", "Japanese"],
297 | entryValues: ["r", "e", "j"],
298 | },
299 | },
300 | ];
301 | }
302 | }
303 |
--------------------------------------------------------------------------------
/javascript/anime/src/en/animez.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "AnimeZZ",
4 | "id": 492689523,
5 | "lang": "en",
6 | "baseUrl": "https://animeyy.com",
7 | "apiUrl": "",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=256&domain=https://animeyy.com",
10 | "typeSource": "multi",
11 | "itemType": 1,
12 | "version": "1.1.1",
13 | "pkgPath": "anime/src/en/animez.js",
14 | },
15 | ];
16 |
17 | class DefaultExtension extends MProvider {
18 | constructor() {
19 | super();
20 | this.client = new Client();
21 | }
22 |
23 | getBaseUrl() {
24 | return "https://animeyy.com";
25 | }
26 |
27 | getHeaders() {
28 | return {
29 | "Referer": this.getBaseUrl(),
30 | };
31 | }
32 |
33 | getPreference(key) {
34 | return new SharedPreferences().get(key);
35 | }
36 |
37 | async request(slug) {
38 | var url = this.getBaseUrl() + slug;
39 | var res = await this.client.get(url, this.getHeaders());
40 | return new Document(res.body);
41 | }
42 | async page(slug) {
43 | var body = await this.request(slug);
44 | var list = [];
45 | var hasNextPage = false;
46 |
47 | var animes = body.select("li.TPostMv");
48 | animes.forEach((anime) => {
49 | var link = anime.selectFirst("a").getHref;
50 | var name = anime.selectFirst("h2.Title").text;
51 | var imageUrl = this.getBaseUrl() + "/" + anime.selectFirst("img").getSrc;
52 |
53 | list.push({ name, link, imageUrl });
54 | });
55 |
56 | var paginations = body.select(".pagination > li");
57 | hasNextPage =
58 | paginations[paginations.length - 1].text == "Last" ? true : false;
59 |
60 | return { list, hasNextPage };
61 | }
62 |
63 | sortByPref(key) {
64 | var sort = parseInt(this.getPreference(key));
65 | var sortBy = "hot";
66 | switch (sort) {
67 | case 1: {
68 | sortBy = "lastest-chap";
69 | break;
70 | }
71 | case 2: {
72 | sortBy = "hot";
73 | break;
74 | }
75 | case 3: {
76 | sortBy = "lastest-manga";
77 | break;
78 | }
79 | case 4: {
80 | sortBy = "top-manga";
81 | break;
82 | }
83 | case 5: {
84 | sortBy = "top-month";
85 | break;
86 | }
87 | case 6: {
88 | sortBy = "top-week";
89 | break;
90 | }
91 | case 7: {
92 | sortBy = "top-day";
93 | break;
94 | }
95 | case 8: {
96 | sortBy = "follow";
97 | break;
98 | }
99 | case 9: {
100 | sortBy = "comment";
101 | break;
102 | }
103 | case 10: {
104 | sortBy = "num-chap";
105 | break;
106 | }
107 | }
108 | return sortBy;
109 | }
110 |
111 | async getPopular(page) {
112 | var sortBy = this.sortByPref("animez_pref_popular_section");
113 | var slug = `/?act=search&f[status]=all&f[sortby]=${sortBy}&&pageNum=${page}`;
114 | return await this.page(slug);
115 | }
116 | get supportsLatest() {
117 | throw new Error("supportsLatest not implemented");
118 | }
119 | async getLatestUpdates(page) {
120 | var sortBy = this.sortByPref("animez_pref_latest_section");
121 | var slug = `/?act=search&f[status]=all&f[sortby]=${sortBy}&&pageNum=${page}`;
122 | return await this.page(slug);
123 | }
124 | async search(query, page, filters) {
125 | var slug = `/?act=search&f[status]=all&f[keyword]=${query}&&pageNum=${page}`;
126 | return await this.page(slug);
127 | }
128 | async getDetail(url) {
129 | var baseUrl = this.getBaseUrl();
130 | if (url.includes(baseUrl)) url = url.replace(baseUrl, "");
131 | var link = +url;
132 | var body = await this.request(url);
133 | var name = body.selectFirst("#title-detail-manga").text;
134 | var animeId = body.selectFirst("#title-detail-manga").attr("data-manga");
135 | var genre = [];
136 | body
137 | .select("li.AAIco-adjust")[3]
138 | .select("a")
139 | .forEach((g) => genre.push(g.text));
140 | var description = body.selectFirst("#summary_shortened").text;
141 |
142 | var chapters = [];
143 | var chapLen = 0;
144 | var pageNum = 1;
145 | var hasNextPage = true;
146 | while (hasNextPage) {
147 | var pageSlug = `?act=ajax&code=load_list_chapter&manga_id=${animeId}&page_num=${pageNum}&chap_id=0&keyword=`;
148 | var pageBody = await this.request(pageSlug);
149 | var parsedBody = JSON.parse(pageBody.html);
150 | var nav = parsedBody.nav;
151 | if (nav == null) {
152 | // if "nav" doesnt exists there is no next page
153 | hasNextPage = false;
154 | } else {
155 | var navLi = new Document(nav).select(".page-link.next").length;
156 | if (navLi > 0) {
157 | // if "nav" exists and has li.next then there is next page
158 | pageNum++;
159 | } else {
160 | // if "nav" exists and doesn't have li.next then there is no next page
161 | hasNextPage = false;
162 | }
163 | }
164 |
165 | var list_chap = new Document(parsedBody.list_chap).select(
166 | "li.wp-manga-chapter"
167 | );
168 |
169 | list_chap.forEach((chapter) => {
170 | var a = chapter.selectFirst("a");
171 | var title = a.text;
172 | var epLink = a.getHref;
173 | var scanlator = "Sub";
174 | if (title.indexOf("Dub") > 0) {
175 | title = title.replace("-Dub", "");
176 | scanlator = "Dub";
177 | }
178 | title = title.indexOf("Movie") > -1 ? title : `Episode ${title}`;
179 | var epData = {
180 | name: title,
181 | url: epLink,
182 | scanlator,
183 | };
184 | if (chapLen > 0) {
185 | var pos = chapLen - 1;
186 | var lastEntry = chapters[pos];
187 | if (lastEntry.name == epData.name) {
188 | // if last entries name is same then append url and scanlator to last entry
189 | chapters.pop(); // remove the last entry
190 | epData.url = `${epData.url}||${lastEntry.url}`;
191 | epData.scanlator = `${lastEntry.scanlator}, ${epData.scanlator}`;
192 | chapLen = pos; // since the last entry is removed the chapLen will decrease
193 | }
194 | }
195 |
196 | chapters.push(epData);
197 | chapLen++;
198 | });
199 | }
200 |
201 | return {
202 | link,
203 | description,
204 | chapters,
205 | genre,
206 | };
207 | }
208 |
209 | // Sorts streams based on user preference.
210 | sortStreams(streams) {
211 | var sortedStreams = [];
212 |
213 | var copyStreams = streams.slice();
214 | var pref = this.getPreference("animez_pref_stream_audio");
215 | for (var stream of streams) {
216 | if (stream.quality.indexOf(pref) > -1) {
217 | sortedStreams.push(stream);
218 | var index = copyStreams.indexOf(stream);
219 | if (index > -1) {
220 | copyStreams.splice(index, 1);
221 | }
222 | break;
223 | }
224 | }
225 | return [...sortedStreams, ...copyStreams];
226 | }
227 |
228 | // For anime episode video list
229 | async getVideoList(url) {
230 | var linkSlugs = url.split("||");
231 | var streams = [];
232 | var hdr = this.getHeaders();
233 | for (var slug of linkSlugs) {
234 | var body = await this.request(slug);
235 | var iframeSrc = body.selectFirst("iframe").getSrc;
236 | var streamLink = iframeSrc
237 | .replace("/embed/", "/anime/")
238 | .replace("\n", "");
239 | var audio = slug.indexOf("dub-") > -1 ? "Dub" : "Sub";
240 |
241 | streams.push({
242 | url: streamLink,
243 | originalUrl: streamLink,
244 | quality: audio,
245 | headers: hdr,
246 | });
247 | }
248 |
249 | return this.sortStreams(streams);
250 | }
251 |
252 | getSourcePreferences() {
253 | return [
254 | {
255 | key: "animez_pref_popular_section",
256 | listPreference: {
257 | title: "Preferred popular content",
258 | summary: "",
259 | valueIndex: 1,
260 | entries: [
261 | "Latest update",
262 | "Hot",
263 | "New releases",
264 | "Top all",
265 | "Top month",
266 | "Top week",
267 | "Top day",
268 | "Top follow",
269 | "Top comments",
270 | "Number of episodes",
271 | ],
272 | entryValues: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
273 | },
274 | },
275 | {
276 | key: "animez_pref_latest_section",
277 | listPreference: {
278 | title: "Preferred latest content",
279 | summary: "",
280 | valueIndex: 0,
281 | entries: [
282 | "Latest update",
283 | "Hot",
284 | "New releases",
285 | "Top all",
286 | "Top month",
287 | "Top week",
288 | "Top day",
289 | "Top follow",
290 | "Top comments",
291 | "Number of episodes",
292 | ],
293 | entryValues: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
294 | },
295 | },
296 | {
297 | key: "animez_pref_stream_audio",
298 | listPreference: {
299 | title: "Preferred stream audio",
300 | summary: "",
301 | valueIndex: 0,
302 | entries: ["Sub", "Dub"],
303 | entryValues: ["Sub", "Dub"],
304 | },
305 | },
306 | ];
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/javascript/manga/src/en/mangapill.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "Mangapill",
4 | "id": 960321322,
5 | "lang": "en",
6 | "baseUrl": "https://mangapill.com",
7 | "apiUrl": "",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=64&domain=https://mangapill.com/",
10 | "typeSource": "single",
11 | "isManga": true,
12 | "version": "1.0.4",
13 | "dateFormat": "",
14 | "dateFormatLocale": "",
15 | "pkgPath": "manga/src/en/mangapill.js",
16 | },
17 | ];
18 |
19 | class DefaultExtension extends MProvider {
20 | getHeaders(url) {
21 | return {
22 | "Referer": this.source.baseUrl,
23 | };
24 | }
25 |
26 | statusCode(status) {
27 | return (
28 | {
29 | "publishing": 0,
30 | "finished": 1,
31 | "on hiatus": 2,
32 | "discontinued": 3,
33 | "not yet published": 4,
34 | }[status] ?? 5
35 | );
36 | }
37 |
38 | async getPreference(key) {
39 | const preferences = new SharedPreferences();
40 | return parseInt(preferences.get(key));
41 | }
42 |
43 | async getMangaList(slug) {
44 | var lang = await this.getPreference("pref_title_lang");
45 |
46 | var url = `${this.source.baseUrl}/${slug}`;
47 | var res = await new Client().get(url, this.getHeaders());
48 | var doc = new Document(res.body);
49 | var list = [];
50 | var mangaElements = doc.select("div.grid.gap-3.lg > div");
51 | for (var manga of mangaElements) {
52 | var details = manga.selectFirst("div").select("a");
53 | var detLen = details.length;
54 | details = details[detLen - 1];
55 |
56 | var imageUrl = manga.selectFirst("img").getSrc;
57 | var link = details.getHref;
58 | var nameSection = details.select("div");
59 |
60 | var name =
61 | nameSection[1] && lang == 2 ? nameSection[1].text : nameSection[0].text;
62 |
63 | list.push({ name, imageUrl, link });
64 | }
65 | var hasNextPage = false;
66 | if (slug.includes("search?q")) {
67 | hasNextPage = doc.selectFirst(".container.py-3 a.btn.btn-sm").className
68 | ? true
69 | : false;
70 | }
71 | return { list, hasNextPage };
72 | }
73 |
74 | async getNavPage(prefKey) {
75 | var val = await this.getPreference(prefKey);
76 | var slug = "";
77 | switch (val) {
78 | case 1: {
79 | slug = "mangas/new";
80 | break;
81 | }
82 | case 2: {
83 | slug = "chapters";
84 | break;
85 | }
86 | }
87 | return await this.getMangaList(slug);
88 | }
89 |
90 | async getPopular(page) {
91 | return await this.getNavPage("pref_popular_content");
92 | }
93 | get supportsLatest() {
94 | throw new Error("supportsLatest not implemented");
95 | }
96 |
97 | async getLatestUpdates(page) {
98 | return await this.getNavPage("pref_latest_content");
99 | }
100 |
101 | async searchManga(query, status, type, genre, page) {
102 | var slug = `search?q=${query}&status=${status}&type=${type}${genre}&page=${page}`;
103 | return await this.getMangaList(slug);
104 | }
105 |
106 | async search(query, page, filters) {
107 | var type = filters[0]?.values[filters[0].state].value ?? "";
108 | var status = filters[1]?.values[filters[1].state].value ?? "";
109 |
110 | var genre = "";
111 | if (filters && filters[2]) {
112 | for (var filter of filters[2].state) {
113 | if (filter.state == true) genre += `&genre=${filter.value}`;
114 | }
115 | }
116 | return await this.searchManga(query, status, type, genre, page);
117 | }
118 |
119 | async getMangaDetail(slug) {
120 | var lang = await this.getPreference("pref_title_lang");
121 | var baseUrl = this.source.baseUrl;
122 | if (slug.includes(baseUrl)) slug = slug.replace(baseUrl, "");
123 |
124 | var link = `${baseUrl}${slug}`;
125 | var res = await new Client().get(link, this.getHeaders());
126 | var doc = new Document(res.body);
127 |
128 | var mangaName = doc.selectFirst(".mb-3 .font-bold.text-lg").text;
129 | if (doc.selectFirst(".mb-3 .text-sm.text-secondary") && lang == 2)
130 | mangaName = doc.selectFirst(".mb-3 .text-sm.text-secondary").text;
131 | var description = doc
132 | .selectFirst("meta[name='description']")
133 | .attr("content");
134 | var imageUrl = doc.selectFirst(".w-full.h-full").getSrc;
135 | var statusText = doc
136 | .select(".grid.grid-cols-1 > div")[1]
137 | .selectFirst("div").text;
138 | var status = this.statusCode(statusText);
139 |
140 | var genre = [];
141 | var genreList = doc.select("a.mr-1");
142 | for (var gen of genreList) {
143 | genre.push(gen.text);
144 | }
145 |
146 | var chapters = [];
147 | var chapList = doc.select("div.my-3.grid > a");
148 | for (var chap of chapList) {
149 | var name = chap.text;
150 | var url = chap.getHref;
151 | chapters.push({ name, url });
152 | }
153 | return {
154 | name: mangaName,
155 | description,
156 | link,
157 | imageUrl,
158 | status,
159 | genre,
160 | chapters,
161 | };
162 | }
163 |
164 | async getDetail(url) {
165 | return await this.getMangaDetail(url);
166 | }
167 | // For anime episode video list
168 | async getVideoList(url) {
169 | throw new Error("getVideoList not implemented");
170 | }
171 |
172 | // For manga chapter pages
173 | async getPageList(url) {
174 | var link = `${this.source.baseUrl}${url}`;
175 |
176 | var res = await new Client().get(link, this.getHeaders());
177 | var doc = new Document(res.body);
178 |
179 | var urls = [];
180 |
181 | var pages = doc.select("chapter-page");
182 | for (var page of pages) {
183 | var img = page.selectFirst("img").getSrc;
184 | if (img != null) urls.push(img);
185 | }
186 |
187 | return urls;
188 | }
189 |
190 | getFilterList() {
191 | return [
192 | {
193 | type_name: "SelectFilter",
194 | name: "Type",
195 | state: 0,
196 | values: [
197 | ["All", ""],
198 | ["Manga", "manga"],
199 | ["Novel", "novel"],
200 | ["One-Shot", "one-shot"],
201 | ["Doujinshi", "doujinshi"],
202 | ["Manhwa", "manhwa"],
203 | ["Manhua", "manhua"],
204 | ["Oel", "oel"],
205 | ].map((x) => ({ type_name: "SelectOption", name: x[0], value: x[1] })),
206 | },
207 | {
208 | type_name: "SelectFilter",
209 | name: "Status",
210 | state: 0,
211 | values: [
212 | ["All", ""],
213 | ["Publishing", "publishing"],
214 | ["Finished", "finished"],
215 | ["On hiatus", "on hiatus"],
216 | ["Discontinued", "discontinued"],
217 | ["Not yet published", "not yet published"],
218 | ].map((x) => ({ type_name: "SelectOption", name: x[0], value: x[1] })),
219 | },
220 | {
221 | type_name: "GroupFilter",
222 | name: "Genre",
223 | state: [
224 | ["Action", "Action"],
225 | ["Adventure", "Adventure"],
226 | ["Cars", "Cars"],
227 | ["Comedy", "Comedy"],
228 | ["Dementia", "Dementia"],
229 | ["Demons", "Demons"],
230 | ["Doujinshi", "Doujinshi"],
231 | ["Drama", "Drama"],
232 | ["Ecchi", "Ecchi"],
233 | ["Fantasy", "Fantasy"],
234 | ["Game", "Game"],
235 | ["Gender Bender", "Gender Bender"],
236 | ["Harem", "Harem"],
237 | ["Historical", "Historical"],
238 | ["Horror", "Horror"],
239 | ["Isekai", "Isekai"],
240 | ["Josei", "Josei"],
241 | ["Kids", "Kids"],
242 | ["Magic", "Magic"],
243 | ["Martial Arts", "Martial Arts"],
244 | ["Mecha", "Mecha"],
245 | ["Military", "Military"],
246 | ["Music", "Music"],
247 | ["Mystery", "Mystery"],
248 | ["Parody", "Parody"],
249 | ["Police", "Police"],
250 | ["Psychological", "Psychological"],
251 | ["Romance", "Romance"],
252 | ["Samurai", "Samurai"],
253 | ["School", "School"],
254 | ["Sci-Fi", "Sci-Fi"],
255 | ["Seinen", "Seinen"],
256 | ["Shoujo", "Shoujo"],
257 | ["Shoujo Ai", "Shoujo Ai"],
258 | ["Shounen", "Shounen"],
259 | ["Shounen Ai", "Shounen Ai"],
260 | ["Slice of Life", "Slice of Life"],
261 | ["Space", "Space"],
262 | ["Sports", "Sports"],
263 | ["Super Power", "Super Power"],
264 | ["Supernatural", "Supernatural"],
265 | ["Thriller", "Thriller"],
266 | ["Tragedy", "Tragedy"],
267 | ["Vampire", "Vampire"],
268 | ["Yaoi", "Yaoi"],
269 | ["Yuri", "Yuri"],
270 | ].map((x) => ({ type_name: "CheckBox", name: x[0], value: x[1] })),
271 | },
272 | ];
273 | }
274 |
275 | getSourcePreferences() {
276 | return [
277 | {
278 | key: "pref_popular_content",
279 | listPreference: {
280 | title: "Preferred popular content",
281 | summary: "",
282 | valueIndex: 0,
283 | entries: ["New Mangas", "Recent Chapters"],
284 | entryValues: ["1", "2"],
285 | },
286 | },
287 | {
288 | key: "pref_latest_content",
289 | listPreference: {
290 | title: "Preferred latest content",
291 | summary: "",
292 | valueIndex: 1,
293 | entries: ["New Mangas", "Recent Chapters"],
294 | entryValues: ["1", "2"],
295 | },
296 | },
297 | {
298 | key: "pref_title_lang",
299 | listPreference: {
300 | title: "Preferred title language",
301 | summary: "",
302 | valueIndex: 0,
303 | entries: ["Romaji", "English"],
304 | entryValues: ["1", "2"],
305 | },
306 | },
307 | ];
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/javascript/anime/src/en/gojo.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "Gojo",
4 | "id": 1018827104,
5 | "lang": "en",
6 | "baseUrl": "https://gojo.wtf",
7 | "apiUrl": "",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=128&domain=https://gojo.wtf/",
10 | "typeSource": "multi",
11 | "itemType": 1,
12 | "version": "0.0.6",
13 | "pkgPath": "anime/src/en/gojo.js",
14 | },
15 | ];
16 |
17 | class DefaultExtension extends MProvider {
18 | getHeaders() {
19 | return {
20 | "Referer": this.source.baseUrl,
21 | "Origin": this.source.baseUrl,
22 | "User-Agent":
23 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4084.56 Safari/537.3",
24 | };
25 | }
26 |
27 | constructor() {
28 | super();
29 | this.client = new Client();
30 | }
31 |
32 | getPreference(key) {
33 | const preferences = new SharedPreferences();
34 | return preferences.get(key);
35 | }
36 |
37 | async gojoAPI(slug) {
38 | var url = `https://backend.gojo.wtf/api/anime${slug}`;
39 | var res = await this.client.post(url, this.getHeaders());
40 | if (res.statusCode != 200) return null;
41 | return JSON.parse(res.body);
42 | }
43 |
44 | getTitle(data) {
45 | var pref = this.getPreference("gojo_pref_title");
46 | if (data.hasOwnProperty(pref)) {
47 | return data[pref];
48 | }
49 | return data["romaji"];
50 | }
51 |
52 | formatList(animeList) {
53 | var list = [];
54 | //
55 | animeList.forEach((anime) => {
56 | var name = this.getTitle(anime.title);
57 | var image = anime.coverImage;
58 | var imageUrl = "";
59 | if (typeof image == "object" && image.hasOwnProperty("large")) {
60 | imageUrl = image.large;
61 | } else {
62 | imageUrl = image;
63 | }
64 | var link = "" + anime.id;
65 |
66 | list.push({ name, imageUrl, link });
67 | });
68 | return list;
69 | }
70 |
71 | async getPopular(page) {
72 | var list = [];
73 | var res = await this.gojoAPI("/home");
74 | if (res != null) {
75 | list.push(...this.formatList(res.popular));
76 | list.push(...this.formatList(res.trending));
77 | list.push(...this.formatList(res.seasonal));
78 | list.push(...this.formatList(res.top));
79 | }
80 | return { list, hasNextPage: true };
81 | }
82 |
83 | async getLatestUpdates(page) {
84 | var list = [];
85 | var res = await this.gojoAPI(`/recent?type=anime&page=${page}&perPage=30`);
86 | if (res != null) {
87 | list.push(...this.formatList(res));
88 | }
89 | var hasNextPage = true;
90 | if (list.length < 30) hasNextPage = false;
91 |
92 | return { list, hasNextPage };
93 | }
94 |
95 | async search(query, page, filters) {
96 | var list = [];
97 | var hasNextPage = false;
98 |
99 | var res = await this.gojoAPI(
100 | `/search?query=${query}&page=${page}&perPage=30`
101 | );
102 | if (res != null) {
103 | list.push(...this.formatList(res.results));
104 | if (res.lastPage < page) hasNextPage = true;
105 | }
106 |
107 | return { list, hasNextPage };
108 | }
109 |
110 | async getDetail(url) {
111 | var linkSlug = `${this.source.baseUrl}/watch/`;
112 | if (url.includes(linkSlug)) url = url.replace(linkSlug, "");
113 |
114 | var anilistId = url;
115 | var res = await this.gojoAPI(`/info/${anilistId}`);
116 | if (res == null) {
117 | throw new Error("Error on getDetail");
118 | }
119 | var name = this.getTitle(res.title);
120 | var imageUrl = res.coverImage.large;
121 | var description = res.description;
122 | var link = `${linkSlug}${anilistId}`;
123 | var genres = res.genres;
124 | var status = (() => {
125 | switch (res.status) {
126 | case "RELEASING":
127 | return 0;
128 | case "FINISHED":
129 | return 1;
130 | case "HIATUS":
131 | return 2;
132 | case "NOT_YET_RELEASED":
133 | return 3;
134 | default:
135 | return 5;
136 | }
137 | })();
138 |
139 | var chapters = [];
140 |
141 | var body = await this.gojoAPI(`/episodes/${anilistId}`);
142 | if (body != null && body.length > 0) {
143 | // Find the maximum episodes as some providers may not have all.
144 | var maxEpisodes = 0;
145 | for (var prd of body) {
146 | if (prd["episodes"].length > maxEpisodes) {
147 | maxEpisodes = prd["episodes"].length;
148 | }
149 | }
150 |
151 | for (var i = 0; i < maxEpisodes; i++) {
152 | var chapNum = -1;
153 | var chapName = "";
154 | var chapLink = {};
155 | var chapScan = "Sub";
156 |
157 | for (var prd of body) {
158 | var chap = prd.episodes[i];
159 |
160 | // Check if the current provider episode is the same as the previous one.
161 | // If not, break out of the loop.
162 | var epNum = chap.number;
163 | if (chapNum == -1) {
164 | chapNum = epNum;
165 | }
166 |
167 | if (chapNum != epNum) continue;
168 |
169 | // Episode Name is stored only once.
170 | if (chapName.length == 0) {
171 | chapName = `E${epNum}`;
172 | if (chap.hasOwnProperty("title")) {
173 | if (chap.title != null) chapName += ": " + chap.title;
174 | }
175 | }
176 |
177 | // If Dub is available, add it to the scanlator list.
178 | if (chap.hasOwnProperty("hasDub")) {
179 | if (!chapScan.includes("Dub") && chap.hasDub) {
180 | chapScan += ", Dub";
181 | }
182 | }
183 |
184 | // If isFiller is available, add it to the scanlator list.
185 | if (chap.hasOwnProperty("isFiller")) {
186 | if (
187 | !chapScan.includes("Filler") &&
188 | chap.isFiller &&
189 | this.getPreference("gojo_pref_mark_filler")
190 | ) {
191 | chapScan = "Filler, " + chapScan;
192 | }
193 | }
194 |
195 | // Delete unnecessary properties from the chapter object.
196 | delete chap.image;
197 | delete chap.description;
198 | delete chap.isFiller;
199 | delete chap.title;
200 |
201 | var prdName = prd.providerId;
202 | chapLink[prdName] = chap;
203 | }
204 |
205 | chapters.push({
206 | name: chapName,
207 | url: `${anilistId}||` + JSON.stringify(chapLink),
208 | scanlator: chapScan,
209 | });
210 | }
211 | }
212 | chapters.reverse();
213 |
214 | return { name, imageUrl, description, link, chapters, genres, status };
215 | }
216 |
217 | strixNzazaExtractor(res, prvd, type) {
218 | if (res == null) return {};
219 |
220 | var src = res.sources[0];
221 | var url = src.url;
222 | var quality = `${prvd} - ${src.quality} - ${type.toUpperCase()}`;
223 | return {
224 | url: url,
225 | quality,
226 | originalUrl: url,
227 | };
228 | }
229 |
230 | paheExtractor(res, type) {
231 | var streams = [];
232 | if (res != null) {
233 | var srcs = res.sources;
234 | var hdr = this.getHeaders();
235 | for (var src of srcs) {
236 | var url = src.url;
237 | var quality = `Pahe - ${src.quality} - ${type.toUpperCase()}`;
238 | streams.push({
239 | url: url,
240 | headers: hdr,
241 | quality,
242 | originalUrl: url,
243 | });
244 | }
245 | }
246 | return streams;
247 | }
248 |
249 | async getStream(prvd, anilistId, epNum, subType, id, dub_id) {
250 | var slug = `/tiddies?provider=${prvd}&id=${anilistId}&num=${epNum}&subType=${subType}&watchId=${id}&dub_id=${dub_id}`;
251 | return await this.gojoAPI(slug);
252 | }
253 |
254 | // For anime episode video list
255 | async getVideoList(url) {
256 | var split = url.split("||");
257 | var anilistId = split[0];
258 | var info = JSON.parse(split[1]);
259 | var streams = [];
260 | var extractDubs = this.getPreference("gojo_extract_dub_streams");
261 |
262 | for (var prvd in info) {
263 | var prd = info[prvd];
264 | var epNum = prd.number;
265 | var subType = "sub";
266 | var id = prd.id;
267 | var dub_id = null;
268 |
269 | var res = await this.getStream(
270 | prvd,
271 | anilistId,
272 | epNum,
273 | subType,
274 | id,
275 | dub_id
276 | );
277 | if (prvd != "pahe") {
278 | streams.push(this.strixNzazaExtractor(res, prvd, subType));
279 | } else {
280 | streams.push(...this.paheExtractor(res, subType));
281 | }
282 |
283 | if (!extractDubs) continue;
284 | subType = "dub";
285 | if (prd.hasOwnProperty("dub_id")) dub_id = prd.dub_id;
286 |
287 | var res = await this.getStream(
288 | prvd,
289 | anilistId,
290 | epNum,
291 | subType,
292 | id,
293 | dub_id
294 | );
295 | if (prvd != "pahe") {
296 | streams.push(this.strixNzazaExtractor(res, prvd, subType));
297 | } else {
298 | streams.push(...this.paheExtractor(res, subType));
299 | }
300 | }
301 | return streams;
302 | }
303 | getSourcePreferences() {
304 | return [
305 | {
306 | key: "gojo_pref_title",
307 | listPreference: {
308 | title: "Preferred Title",
309 | summary: "",
310 | valueIndex: 0,
311 | entries: ["Romaji", "English", "Native"],
312 | entryValues: ["romaji", "english", "native"],
313 | },
314 | },
315 | {
316 | key: "gojo_pref_mark_filler",
317 | switchPreferenceCompat: {
318 | title: "Mark filler episodes",
319 | summary: "",
320 | value: true,
321 | },
322 | },
323 | {
324 | key: "gojo_extract_dub_streams",
325 | switchPreferenceCompat: {
326 | title: "Extract dub streams",
327 | summary: "",
328 | value: false,
329 | },
330 | },
331 | ];
332 | }
333 | }
334 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 |
--------------------------------------------------------------------------------
/javascript/manga/src/ja/weloma.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "WeLoMa",
4 | "id": 1890238687,
5 | "baseUrl": "https://weloma.art",
6 | "lang": "ja",
7 | "typeSource": "single",
8 | "iconUrl":
9 | "https://raw.github.com/Swakshan/mangayomi-swak-extensions/main/javascript/icon/ja.weloma.jpg",
10 | "dateFormat": "",
11 | "dateFormatLocale": "",
12 | "isNsfw": false,
13 | "hasCloudflare": false,
14 | "sourceCodeUrl": "",
15 | "apiUrl": "",
16 | "version": "1.0.0",
17 | "isManga": true,
18 | "itemType": 0,
19 | "isFullData": false,
20 | "appMinVerReq": "0.5.0",
21 | "additionalParams": "",
22 | "sourceCodeLanguage": 1,
23 | "notes": "",
24 | "pkgPath": "manga/src/ja/weloma.js",
25 | },
26 | ];
27 | class DefaultExtension extends MProvider {
28 | constructor() {
29 | super();
30 | this.client = new Client();
31 | }
32 |
33 | getPreference(key) {
34 | return new SharedPreferences().get(key);
35 | }
36 |
37 | getHeaders(url) {
38 | return {
39 | "user-agent":
40 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
41 | "Cookie": "smartlink_shown=1;",
42 | };
43 | }
44 |
45 | async request(slug) {
46 | var url = `${this.source.baseUrl}${slug}`;
47 | var body = (await this.client.get(url, this.getHeaders())).body;
48 | return new Document(body);
49 | }
50 |
51 | async searchPage({
52 | query = "",
53 | sort = "",
54 | sort_type = "",
55 | genres = [],
56 | status = "",
57 | page = 1,
58 | } = {}) {
59 | function addSlug(para, value) {
60 | if (value.length > 0) return `&${para}=${value}`;
61 | else return "";
62 | }
63 | var slug = "/manga-list.html?";
64 | slug += `name=${query}`;
65 | slug += addSlug("sort", sort);
66 | slug += addSlug("genre", genres.length > 0 ? genres.join(",") : "");
67 | slug += addSlug("sort_type", sort_type);
68 | slug += addSlug("m_status", status);
69 | slug += addSlug("page", `${page}`);
70 |
71 | var doc = await this.request(slug);
72 |
73 | var list = [];
74 | doc.select(".thumb-item-flow").forEach((item) => {
75 | var linkSection = item.selectFirst(".series-title").selectFirst("a");
76 | var link = linkSection.getHref;
77 | var name = linkSection.text.trim();
78 |
79 | var imgStyle = item
80 | .selectFirst(".img-in-ratio")
81 | .attr("style")
82 | .trim()
83 | .substring(23, 150);
84 | var imageUrl = imgStyle.substring(0, imgStyle.indexOf("')"));
85 | list.push({ name, link, imageUrl });
86 | });
87 |
88 | var lastPage = doc
89 | .selectFirst("ul.pagination.pagination-v4")
90 | .select("a")
91 | .slice(-1)[0];
92 | var hasNextPage = !lastPage.className.includes("disabled");
93 |
94 | return { list, hasNextPage };
95 | }
96 |
97 | async getPopular(page) {
98 | return await this.searchPage({ sort: "views", page: page });
99 | }
100 |
101 | async getLatestUpdates(page) {
102 | return await this.searchPage({ sort: "last_update", page: page });
103 | }
104 |
105 | async search(query, page, filters) {
106 | function checkBox(state) {
107 | var rd = [];
108 | state.forEach((item) => {
109 | if (item.state) {
110 | rd.push(item.value);
111 | }
112 | });
113 | return rd;
114 | }
115 | function selectFiler(filter) {
116 | return filter.values[filter.state].value;
117 | }
118 |
119 | var isFiltersAvailable = !filters || filters.length != 0;
120 | var sort = isFiltersAvailable ? selectFiler(filters[0]) : "";
121 | var genres = isFiltersAvailable ? checkBox(filters[1].state) : [];
122 | var status = isFiltersAvailable ? selectFiler(filters[2]) : "";
123 | var sortOrder = isFiltersAvailable ? selectFiler(filters[3]) : "";
124 |
125 | return await this.searchPage({
126 | query,
127 | sort,
128 | sortOrder,
129 | genres,
130 | status,
131 | page,
132 | });
133 | }
134 |
135 | async getDetail(url) {
136 | function statusCode(status) {
137 | return (
138 | {
139 | "On going": 0,
140 | "Completed": 1,
141 | "Dropped": 3,
142 | }[status] ?? 5
143 | );
144 | }
145 | function uploadTime(time) {
146 | var ts = 0;
147 | var timeSplit = time.split(" ");
148 | var unit = parseInt(timeSplit[0]);
149 | var unitName = timeSplit[1];
150 | switch (unitName) {
151 | case "seconds": {
152 | ts += unit;
153 | break;
154 | }
155 | case "minutes": {
156 | ts += unit * 60;
157 | break;
158 | }
159 | case "hours": {
160 | ts += unit * (60 * 60);
161 | break;
162 | }
163 | case "days": {
164 | ts += unit * (24 * 60 * 60);
165 | break;
166 | }
167 | case "weeks": {
168 | ts += unit * (7 * 24 * 60 * 60);
169 | break;
170 | }
171 | case "months": {
172 | ts += unit * (30 * 24 * 60 * 60);
173 | break;
174 | }
175 | case "years": {
176 | ts += unit * (12 * 30 * 24 * 60 * 60);
177 | break;
178 | }
179 | }
180 | return parseInt(new Date().valueOf()) - ts * 1000;
181 | }
182 | var baseUrl = this.source.baseUrl;
183 | var slug = url.replace(baseUrl, "");
184 | var link = baseUrl + url;
185 |
186 | var doc = await this.request(slug);
187 |
188 | var mangaInfo = doc.selectFirst(".manga-info");
189 | var name = mangaInfo.selectFirst("h3").text;
190 | var imageUrl = doc.selectFirst("img.thumbnail").getSrc;
191 | var description = doc.selectFirst(".summary-content").text.trim();
192 | var infoList = mangaInfo.select("li");
193 | var genre = [];
194 | infoList[2].select("a").forEach((a) => genre.push(a.text.trim()));
195 | var statusText = infoList[3].selectFirst("a").text;
196 | var status = statusCode(statusText);
197 | var chapters = [];
198 | doc
199 | .selectFirst(".list-chapters")
200 | .select("a")
201 | .forEach((item) => {
202 | var chapName = item.selectFirst(".chapter-name").text;
203 | var chapLink = item.getHref;
204 | var dateUpload = item.selectFirst(".chapter-time").text;
205 |
206 | chapters.push({
207 | name: chapName,
208 | url: chapLink,
209 | dateUpload: "" + uploadTime(dateUpload),
210 | });
211 | });
212 |
213 | return {
214 | name,
215 | imageUrl,
216 | description,
217 | link,
218 | status,
219 | genre,
220 | chapters,
221 | };
222 | }
223 |
224 | decodeBase64(b64Text) {
225 | var g = {},
226 | b = 65,
227 | d = 0,
228 | a,
229 | c = 0,
230 | h,
231 | e = "",
232 | k = String.fromCharCode,
233 | l = b64Text.length;
234 | for (a = ""; 91 > b; ) a += k(b++);
235 | a += a.toLowerCase() + "0123456789+/";
236 | for (b = 0; 64 > b; b++) g[a.charAt(b)] = b;
237 | for (a = 0; a < l; a++)
238 | for (b = g[b64Text.charAt(a)], d = (d << 6) + b, c += 6; 8 <= c; )
239 | ((h = (d >>> (c -= 8)) & 255) || a < l - 2) && (e += k(h));
240 | return e;
241 | }
242 | async getPageList(url) {
243 | var urls = [];
244 | var body = await this.request(url);
245 | body.select(".chapter-img").forEach((item) => {
246 | var imgB64 = item.attr("data-img");
247 | urls.push(this.decodeBase64(imgB64));
248 | });
249 | return urls;
250 | }
251 |
252 | getFilterList() {
253 | function formateState(type_name, items, values) {
254 | var state = [];
255 | for (var i = 0; i < items.length; i++) {
256 | state.push({ type_name: type_name, name: items[i], value: values[i] });
257 | }
258 | return state;
259 | }
260 |
261 | var filters = [];
262 | var items = [];
263 | var values = [];
264 |
265 | // Sort
266 | items = ["Any", "Alphabetical order", "Most views", "Last updated"];
267 | values = ["", "name", "views", "last_update"];
268 | filters.push({
269 | type_name: "SelectFilter",
270 | name: "Sort",
271 | state: 0,
272 | values: formateState("SelectOption", items, values),
273 | });
274 |
275 | // Genres
276 | items = [
277 | "Action",
278 | "Adult",
279 | "Adventure",
280 | "Comedy",
281 | "Drama",
282 | "Ecchi",
283 | "Fantasy",
284 | "Gender Bender",
285 | "Harem",
286 | "Historical",
287 | "Horror",
288 | "Martial Art",
289 | "Mature",
290 | "Mecha",
291 | "Mystery",
292 | "Psychological",
293 | "Romance",
294 | "School Life",
295 | "Sci-fi",
296 | "Seinen",
297 | "Shoujo",
298 | "Shojou Ai",
299 | "Shounen",
300 | "Shounen Ai",
301 | "Slice of Life",
302 | "Sports",
303 | "Supernatural",
304 | "Tragedy",
305 | "Yuri",
306 | "Josei",
307 | "Smut",
308 | "One Shot",
309 | "Shotacon",
310 | ];
311 | values = [
312 | "action",
313 | "adult",
314 | "adventure",
315 | "comedy",
316 | "drama",
317 | "ecchi",
318 | "fantasy",
319 | "gender-bender",
320 | "harem",
321 | "historical",
322 | "horror",
323 | "martial-art",
324 | "mature",
325 | "mecha",
326 | "mystery",
327 | "psychological",
328 | "romance",
329 | "school-life",
330 | "sci-fi",
331 | "seinen",
332 | "shoujo",
333 | "shojou-ai",
334 | "shounen",
335 | "shounen-ai",
336 | "slice-of-life",
337 | "sports",
338 | "supernatural",
339 | "tragedy",
340 | "yuri",
341 | "josei",
342 | "smut",
343 | "one-shot",
344 | "shotacon",
345 | ];
346 | filters.push({
347 | type_name: "GroupFilter",
348 | name: "Genres",
349 | state: formateState("CheckBox", items, values),
350 | });
351 |
352 | // Status
353 | items = ["Any", "Completed", "Ongoing", "dropped"];
354 | values = ["", "2", "1", "3"];
355 | filters.push({
356 | type_name: "SelectFilter",
357 | name: "Status",
358 | state: 0,
359 | values: formateState("SelectOption", items, values),
360 | });
361 |
362 | // Sort order
363 | items = ["Ascending", "Descending"];
364 | values = ["ASC", "DESC"];
365 | filters.push({
366 | type_name: "SelectFilter",
367 | name: "Sort order",
368 | state: 0,
369 | values: formateState("SelectOption", items, values),
370 | });
371 |
372 | return filters;
373 | }
374 |
375 | getSourcePreferences() {
376 | throw new Error("getSourcePreferences not implemented");
377 | }
378 | }
379 |
--------------------------------------------------------------------------------
/javascript/anime/src/en/kaido.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "Kaido",
4 | "id": 2457624982,
5 | "baseUrl": "https://kaido.to",
6 | "lang": "en",
7 | "typeSource": "single",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=256&domain=https://kaido.to/",
10 | "dateFormat": "",
11 | "dateFormatLocale": "",
12 | "isNsfw": false,
13 | "hasCloudflare": false,
14 | "sourceCodeUrl": "",
15 | "apiUrl": "",
16 | "version": "1.0.2",
17 | "isManga": false,
18 | "itemType": 1,
19 | "isFullData": false,
20 | "appMinVerReq": "0.5.0",
21 | "additionalParams": "",
22 | "sourceCodeLanguage": 1,
23 | "notes": "",
24 | "pkgPath": "anime/src/en/kaido.js",
25 | },
26 | ];
27 | class DefaultExtension extends MProvider {
28 | constructor() {
29 | super();
30 | this.client = new Client();
31 | }
32 |
33 | getPreference(key) {
34 | return new SharedPreferences().get(key);
35 | }
36 |
37 | getHeaders() {
38 | return {'referer':"https://rapid-cloud.co/"}
39 | }
40 |
41 | async request(slug) {
42 | var url = this.source.baseUrl + slug;
43 | var res = await this.client.get(url);
44 | return new Document(res.body);
45 | }
46 |
47 | async filter({ keyword = "", sort = "default", page = "1" }) {
48 | var titlePref = this.getPreference("kaido_title_lang");
49 |
50 | var slug = keyword == "" ? "/filter?" : `/search?keyword=${keyword}&`;
51 | slug += `sort=${sort}&page=${page}`;
52 |
53 | var doc = await this.request(slug);
54 | var list = [];
55 |
56 | doc.select(".flw-item").forEach((item) => {
57 | var name = item.selectFirst("h3").selectFirst("a").attr(titlePref);
58 | var link = item.selectFirst("a").attr("href");
59 | var imageUrl = item.selectFirst("img").attr("data-src");
60 | list.push({
61 | name,
62 | link,
63 | imageUrl,
64 | });
65 | });
66 |
67 | var page_item = doc.select(".page-item");
68 | var hasNextPage =
69 | page_item.length > 0 && page_item.at(-1).text != `${page}` ? true : false;
70 |
71 | return { list, hasNextPage };
72 | }
73 |
74 | async getPopular(page) {
75 | return await this.filter({ "sort": "score", "page": page });
76 | }
77 |
78 | async getLatestUpdates(page) {
79 | return await this.filter({ "sort": "recently_updated", "page": page });
80 | }
81 |
82 | async search(query, page, filters) {
83 | return await this.filter({ "keyword": query, "page": page });
84 | }
85 |
86 | async ajaxRequest(slug) {
87 | var url = this.source.baseUrl + "/ajax/episode" + slug;
88 | var res = await this.client.get(url);
89 | var json = JSON.parse(res.body);
90 | return json["html"] || json["link"];
91 | }
92 |
93 | async getDetail(url) {
94 | function statusCode(status) {
95 | return (
96 | {
97 | "Currently Airing": 0,
98 | "Finished Airing": 1,
99 | }[status] ?? 5
100 | );
101 | }
102 |
103 | var epTitlePref = this.getPreference("kaido_ep_title_lang");
104 | var baseUrl = this.source.baseUrl + "/watch";
105 | var slug = url.replace(baseUrl, "");
106 | var link = baseUrl + slug
107 |
108 | var doc = await this.request(slug);
109 | var anisc_info = doc.selectFirst(".anisc-info");
110 | var description = anisc_info.selectFirst(".text").text.trim();
111 | var genre = [];
112 | anisc_info
113 | .selectFirst(".item-list")
114 | .select("a")
115 | .forEach((item) => genre.push(item.text));
116 |
117 | var status = 5;
118 | for (var item of anisc_info.select(".item-title")) {
119 | var head = item.selectFirst(".item-head").text;
120 | if (head.includes("Status")) {
121 | status = statusCode(item.selectFirst(".name").text);
122 | break;
123 | }
124 | }
125 |
126 | var totalSub = parseInt(doc.selectFirst(".tick-sub").text);
127 | var totalDub = parseInt(doc.selectFirst(".tick-dub").text);
128 | var statsItem = doc.selectFirst(".film-stats").select("span.item");
129 | var animeType = statsItem[0].text.trim();
130 | var duration = statsItem[statsItem.length - 1].text.trim();
131 |
132 | var data_id = doc.selectFirst("#wrapper").attr("data-id");
133 | var epiRes = await this.ajaxRequest(`/list/${data_id}`);
134 | var epiDoc = new Document(epiRes);
135 |
136 | var chapters = [];
137 | epiDoc.select("a.ep-item").forEach((item) => {
138 | var isFiller = item.className.includes("ssl-item-filler");
139 | var episodeNum = item.attr("data-number");
140 | var episodeTitle = item.selectFirst(".ep-name").attr(epTitlePref);
141 | var episodeId = item.attr("data-id");
142 | var episodeTitle = `E${episodeNum}: ${episodeTitle}`;
143 | if (animeType == "Movie") {
144 | episodeTitle = animeType;
145 | } else {
146 | duration = null;
147 | }
148 | var scanlator = "";
149 |
150 | if (parseInt(episodeId) <= totalSub) scanlator += "SUB";
151 | if (parseInt(episodeId) <= totalDub) scanlator += ", DUB";
152 | chapters.push({
153 | name: episodeTitle,
154 | url: episodeId,
155 | scanlator,
156 | isFiller,
157 | duration,
158 | });
159 | });
160 | chapters.reverse();
161 | return { link, status, description, genre, chapters };
162 | }
163 |
164 | async getVideoList(url) {
165 | function serverName(serId) {
166 | return {
167 | "1": "Vidcloud",
168 | "4": "Vidstreaming",
169 | }[serId];
170 | }
171 | var streams = [];
172 | var prefServer = this.getPreference("kaido_stream_server");
173 | // If no server is chosen, use the default server 1
174 | if (prefServer.length < 1) prefServer.push("1");
175 |
176 | var prefDubType = this.getPreference("kaido_stream_subdub_type");
177 | // If no dubtype is chosen, use the default dubtype sub
178 | if (prefDubType.length < 1) prefDubType.push("sub");
179 |
180 | var serRes = await this.ajaxRequest(`/servers?episodeId=${url}`);
181 | var serDoc = new Document(serRes);
182 |
183 | for (var serData of serDoc.select(".server-item")) {
184 | var serId = serData.attr("data-server-id");
185 | if (!prefServer.includes(serId)) continue;
186 |
187 | var serDubType = serData.attr("data-type");
188 | if (!prefDubType.includes(serDubType)) continue;
189 |
190 | var dataId = serData.attr("data-id");
191 | var streamData = await this.serverData(
192 | dataId,
193 | serverName(serId),
194 | serDubType.toUpperCase()
195 | );
196 | if (streamData != null) streams = [...streams, ...streamData];
197 | }
198 | return streams;
199 | }
200 |
201 | getFilterList() {
202 | throw new Error("getFilterList not implemented");
203 | }
204 |
205 | getSourcePreferences() {
206 | return [
207 | {
208 | key: "kaido_title_lang",
209 | listPreference: {
210 | title: "Preferred title language",
211 | summary: "Choose in which language anime title should be shown",
212 | valueIndex: 0,
213 | entries: ["English", "Romaji"],
214 | entryValues: ["title", "data-jname"],
215 | },
216 | },
217 | {
218 | key: "kaido_ep_title_lang",
219 | listPreference: {
220 | title: "Preferred episode title language",
221 | summary: "Choose in which language episode title should be shown",
222 | valueIndex: 0,
223 | entries: ["English", "Romaji"],
224 | entryValues: ["title", "data-jname"],
225 | },
226 | },
227 | {
228 | key: "kaido_stream_subdub_type",
229 | multiSelectListPreference: {
230 | title: "Preferred stream sub/dub type",
231 | summary: "",
232 | values: ["sub", "dub"],
233 | entries: ["Soft Sub", "Dub"],
234 | entryValues: ["sub", "dub"],
235 | },
236 | },
237 | {
238 | key: "kaido_stream_server",
239 | multiSelectListPreference: {
240 | title: "Preferred server",
241 | summary: "Choose the server/s you want to extract streams from",
242 | values: ["1", "4"],
243 | entries: ["Vidcloud", "Vidstreaming"],
244 | entryValues: ["1", "4"],
245 | },
246 | },
247 | {
248 | key: "kaido_extract_streams",
249 | switchPreferenceCompat: {
250 | title: "Split stream into different quality streams",
251 | summary: "Split stream Auto into 360p/720p/1080p",
252 | value: true,
253 | },
254 | },
255 | ];
256 | }
257 |
258 | //----------- Server -----------------
259 | formatSubtitles(subtitles, dubType) {
260 | var subs = [];
261 | subtitles.forEach((sub) => {
262 | if (!sub.kind.includes("thumbnail")) {
263 | subs.push({
264 | file: sub.file,
265 | label: `${sub.label} - ${dubType}`,
266 | });
267 | }
268 | });
269 |
270 | return subs;
271 | }
272 |
273 | async formatStreams(sUrl, serverName, dubType) {
274 | function streamNamer(res) {
275 | return `${res} - ${dubType} : ${serverName}`;
276 | }
277 |
278 | var hdr = this.getHeaders()
279 |
280 | var streams = [
281 | {
282 | url: sUrl,
283 | originalUrl: sUrl,
284 | quality: streamNamer("Auto"),
285 | headers:hdr
286 | },
287 | ];
288 |
289 | var pref = this.getPreference("kaido_extract_streams");
290 | if (!pref) return streams;
291 |
292 | var baseUrl = sUrl.replace("master.m3u8","")
293 |
294 | const response = await new Client().get(sUrl);
295 | const body = response.body;
296 | const lines = body.split("\n");
297 |
298 | for (let i = 0; i < lines.length; i++) {
299 | if (lines[i].startsWith("#EXT-X-STREAM-INF:")) {
300 | var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1];
301 | var qUrl = lines[i + 1].trim();
302 | var m3u8Url = `${baseUrl}${qUrl}`;
303 | streams.push({
304 | url: m3u8Url,
305 | originalUrl: m3u8Url,
306 | quality: streamNamer(resolution),
307 | headers:hdr
308 | });
309 | }
310 | }
311 | return streams;
312 | }
313 |
314 | async serverData(dataId, serverName, dubType) {
315 | var streamLink = await this.ajaxRequest(`/sources?id=${dataId}`);
316 | var streamId = streamLink.split("/").pop().slice(0, -3);
317 |
318 | var res = await this.client.get(
319 | `https://rapid-cloud.co/embed-2/v2/e-1/getSources?id=${streamId}`
320 | );
321 | if (res.statusCode != 200) return null;
322 |
323 | var streamData = JSON.parse(res.body);
324 |
325 | var url = streamData.sources[0].file;
326 | var streams = await this.formatStreams(url, serverName, dubType);
327 | var subtitles = streamData.tracks;
328 | streams[0].subtitles = this.formatSubtitles(subtitles, dubType);
329 | return streams;
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/javascript/anime/src/all/dramacool.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "Dramacool",
4 | "lang": "all",
5 | "id": 883645892,
6 | "baseUrl": "https://dramacool.com.tr",
7 | "apiUrl": "",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=128&domain=https://dramacool.com.tr",
10 | "typeSource": "multi",
11 | "itemType": 1,
12 | "version": "1.1.0",
13 | "pkgPath": "anime/src/all/dramacool.js",
14 | },
15 | ];
16 |
17 | // Authors: - Swakshan
18 |
19 | class DefaultExtension extends MProvider {
20 | getHeaders(url) {
21 | return {
22 | Referer: url,
23 | "User-Agent":
24 | "Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6788.76 Safari/537.36",
25 | };
26 | }
27 |
28 | getPreference(key) {
29 | return new SharedPreferences().get(key);
30 | }
31 |
32 | getBaseUrl() {
33 | return this.source.baseUrl;
34 | }
35 |
36 | async request(slug) {
37 | const baseUrl = this.getBaseUrl();
38 | var url = `${baseUrl}${slug}`;
39 | var res = await new Client().get(url, this.getHeaders(baseUrl));
40 | var doc = new Document(res.body);
41 | return doc;
42 | }
43 |
44 | async getList(slug) {
45 | var body = await this.request(slug);
46 | var list = [];
47 | var hasNextPage =
48 | body.selectFirst("a.next.page-numbers").text.length > 0 ? true : false;
49 | var items = body.select(".switch-block.list-episode-item > li");
50 | items.forEach((item) => {
51 | var a = item.selectFirst("a");
52 | var link = a.getHref.replace(this.getBaseUrl(), "");
53 | var imageUrl = a.selectFirst("img").getSrc;
54 | var name = a.selectFirst("h3").text;
55 |
56 | list.push({ name, link, imageUrl });
57 | });
58 |
59 | return { list, hasNextPage };
60 | }
61 |
62 | async getPopular(page) {
63 | var slug = "/most-popular-drama";
64 | return await this.getList(`${slug}/page/${page}/`);
65 | }
66 |
67 | get supportsLatest() {
68 | throw new Error("supportsLatest not implemented");
69 | }
70 | async getLatestUpdates(page) {
71 | var slug = this.getPreference("dramacool_latest_list");
72 | return await this.getList(`/${slug}/page/${page}/`);
73 | }
74 |
75 | statusFromString(status) {
76 | return (
77 | {
78 | Ongoing: 0,
79 | Completed: 1,
80 | }[status] ?? 5
81 | );
82 | }
83 |
84 | async search(query, page, filters) {
85 | var slug = `/page/${page}/?type=movies&s=${query}`;
86 | return await this.getList(slug);
87 | }
88 |
89 | formatReleaseDate(str) {
90 | var timeSplit = str.split(" ");
91 | var t = parseInt(timeSplit[0]);
92 | var unit = timeSplit[1];
93 |
94 | var mins = 0;
95 | var mons = 0;
96 | if (unit.includes("minute")) {
97 | mins = t;
98 | } else if (unit.includes("hour")) {
99 | mins = t * 60;
100 | } else if (unit.includes("day")) {
101 | mins = t * 60 * 24;
102 | } else if (unit.includes("week")) {
103 | mins = t * 60 * 24 * 7;
104 | } else if (unit.includes("month")) {
105 | mons = t;
106 | }
107 | var now = new Date();
108 | now.setMinutes(now.getMinutes() - mins);
109 | now.setMinutes(now.getMonth() - mons);
110 | var pastDate = new Date(now);
111 | return "" + pastDate.valueOf();
112 | }
113 |
114 | async getDetail(url) {
115 | if (url.includes("-episode")) {
116 | url = "/series" + url.split("-episode")[0] + "/";
117 | } else if (url.includes("-full-movie")) {
118 | url = "/series" + url.split("-full-movie")[0] + "/";
119 | }
120 |
121 | var body = await this.request(url);
122 | var infos = body.select(".info > p");
123 |
124 | var name = body.selectFirst("h1").text.trim();
125 | var imageUrl = body.selectFirst(".img").selectFirst("img").getSrc;
126 | var isDescription = infos[1].text.includes("Description");
127 | var description = isDescription ? infos[2].text.trim() : "";
128 | var link = `${this.getBaseUrl()}${url}`;
129 | var statusIndex = infos.at(-3).text.includes("Status:") ? -3 : -2;
130 | var status = this.statusFromString(
131 | infos.at(statusIndex).selectFirst("a").text
132 | );
133 | var genre = [];
134 | infos
135 | .at(-1)
136 | .select("a")
137 | .forEach((a) => genre.push(a.text.trim()));
138 |
139 | var chapters = [];
140 | var epLists = body.select("ul.list-episode-item-2.all-episode > li");
141 | for (var ep of epLists) {
142 | var a = ep.selectFirst("a");
143 | var epLink = a.getHref.replace(this.getBaseUrl(), "");
144 | var epName = a.selectFirst("h3").text.replace(name + " ", "");
145 | var scanlator = a.selectFirst("span.type").text;
146 | var dateUpload = this.formatReleaseDate(a.selectFirst("span.time").text);
147 | chapters.push({
148 | name: epName,
149 | url: epLink,
150 | scanlator,
151 | dateUpload,
152 | });
153 | }
154 |
155 | return { name, imageUrl, description, link, status, genre, chapters };
156 | }
157 |
158 | async splitStreams(streams, server) {
159 | var pref = this.getPreference("dramacool_split_stream_quality");
160 | if (!pref) return streams;
161 | var autoStream = streams[0];
162 | var autoStreamUrl = autoStream.url;
163 | var hdr = autoStream.headers;
164 | var hostUrl = "";
165 | if (server == "Asianload") {
166 | hostUrl = autoStreamUrl.substring(0, autoStreamUrl.indexOf("/media"));
167 | } else {
168 | hostUrl = autoStreamUrl.substring(
169 | 0,
170 | autoStreamUrl.indexOf("master.m3u8")
171 | );
172 | }
173 |
174 | var response = await new Client().get(autoStreamUrl, hdr);
175 | var body = response.body;
176 | var lines = body.split("\n");
177 |
178 | for (let i = 0; i < lines.length; i++) {
179 | if (lines[i].startsWith("#EXT-X-STREAM-INF:")) {
180 | var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1];
181 | resolution = `${server} - ${resolution}`;
182 | var m3u8Url = lines[i + 1].trim();
183 | m3u8Url = hostUrl + m3u8Url;
184 | streams.push({
185 | url: m3u8Url,
186 | originalUrl: m3u8Url,
187 | quality: resolution,
188 | headers: hdr,
189 | });
190 | }
191 | }
192 |
193 | return streams;
194 | }
195 |
196 | decodeBase64(f) {
197 | var g = {},
198 | b = 65,
199 | d = 0,
200 | a,
201 | c = 0,
202 | h,
203 | e = "",
204 | k = String.fromCharCode,
205 | l = f.length;
206 | for (a = ""; 91 > b; ) a += k(b++);
207 | a += a.toLowerCase() + "0123456789+/";
208 | for (b = 0; 64 > b; b++) g[a.charAt(b)] = b;
209 | for (a = 0; a < l; a++)
210 | for (b = g[f.charAt(a)], d = (d << 6) + b, c += 6; 8 <= c; )
211 | ((h = (d >>> (c -= 8)) & 255) || a < l - 2) && (e += k(h));
212 | return e;
213 | }
214 |
215 | getUnPackJs(doc) {
216 | var skey = "eval(function(p,a,c,k,e,d)";
217 | var eKey = "";
218 | var start = doc.indexOf(skey);
219 | var end = doc.indexOf(eKey, start);
220 | var js = doc.substring(start, end);
221 | return unpackJs(js);
222 | }
223 |
224 | async extractDramacoolEmbed(doc) {
225 | var streams = [];
226 | var unpack = this.getUnPackJs(doc);
227 |
228 | var skey = 'hls2":"';
229 | var eKey = '"};jwplayer';
230 | var start = unpack.indexOf(skey) + skey.length;
231 | var end = unpack.indexOf(eKey, start);
232 | var track = unpack.substring(start, end);
233 |
234 | streams.push({
235 | url: track,
236 | originalUrl: track,
237 | quality: "Dramacool - Auto",
238 | });
239 |
240 | streams = await this.splitStreams(streams, "Dramacool");
241 |
242 | return streams;
243 | }
244 |
245 | async extractAsianLoadEmbed(doc) {
246 | var streams = [];
247 | var unpack = this.getUnPackJs(doc);
248 |
249 | // Download track
250 | var skey = 'window.open(window.atob("';
251 | var eKey = '"),"_blank")';
252 | var start = unpack.indexOf(skey) + skey.length;
253 | var end = unpack.indexOf(eKey, start);
254 | var track = unpack.substring(start, end);
255 | var downUrl = this.decodeBase64(track);
256 |
257 | // Tracks
258 | var streamUrl = downUrl
259 | .replace(".mp4", "/master.m3u8")
260 | .replace("dl2.", "sv4.");
261 |
262 | streams.push({
263 | url: streamUrl,
264 | originalUrl: streamUrl,
265 | quality: "Asianload - Auto",
266 | });
267 |
268 | streams = await this.splitStreams(streams, "Asianload");
269 |
270 | streams.push({
271 | url: downUrl,
272 | originalUrl: downUrl,
273 | quality: "Asianload - Download",
274 | });
275 |
276 | return streams;
277 | }
278 |
279 | // Sorts streams based on user preference.
280 | async sortStreams(streams) {
281 | var sortedStreams = [];
282 |
283 | var copyStreams = streams.slice();
284 | var pref = this.getPreference("dramacool_video_resolution");
285 | for (var i in streams) {
286 | var stream = streams[i];
287 | if (stream.quality.indexOf(pref) > -1) {
288 | sortedStreams.push(stream);
289 | var index = copyStreams.indexOf(stream);
290 | if (index > -1) {
291 | copyStreams.splice(index, 1);
292 | }
293 | break;
294 | }
295 | }
296 | return [...sortedStreams, ...copyStreams];
297 | }
298 |
299 | // For anime episode video list
300 | async getVideoList(url) {
301 | var res = await this.request(url);
302 | var iframe = res.selectFirst("iframe").attr("src").trim();
303 | if (iframe == "") {
304 | throw new Error("No iframe found");
305 | }
306 |
307 | var streams = [];
308 |
309 | res = await new Client().get(iframe);
310 | var doc = res.body;
311 |
312 | if (iframe.includes("//dramacool")) {
313 | streams = await this.extractDramacoolEmbed(doc);
314 | } else if (iframe.includes("//asianload")) {
315 | streams = await this.extractAsianLoadEmbed(doc);
316 | }
317 |
318 | return this.sortStreams(streams);
319 | }
320 |
321 | getSourcePreferences() {
322 | return [
323 | {
324 | key: "dramacool_latest_list",
325 | listPreference: {
326 | title: "Preferred latest list",
327 | summary: 'Choose which type of content to be shown "Lastest"',
328 | valueIndex: 0,
329 | entries: ["Drama", "Movie", "KShow"],
330 | entryValues: [
331 | "recently-added-drama",
332 | "recently-added-movie",
333 | "recently-added-kshow",
334 | ],
335 | },
336 | },
337 | {
338 | key: "dramacool_split_stream_quality",
339 | switchPreferenceCompat: {
340 | title: "Split stream into different quality streams",
341 | summary: "Split stream Auto into 360p/720p/1080p",
342 | value: true,
343 | },
344 | },
345 | {
346 | key: "dramacool_video_resolution",
347 | listPreference: {
348 | title: "Preferred video resolution",
349 | summary: "",
350 | valueIndex: 0,
351 | entries: ["Auto", "Direct download", "720p", "480", "360p"],
352 | entryValues: ["Auto", "download", "720", "480", "360"],
353 | },
354 | },
355 | ];
356 | }
357 | }
358 |
--------------------------------------------------------------------------------
/javascript/anime/src/en/anixl.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "Anixl",
4 | "id": 2448777672,
5 | "baseUrl": "https://anixl.to",
6 | "lang": "en",
7 | "typeSource": "multi",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=256&domain=https://anixl.to/",
10 | "dateFormat": "",
11 | "dateFormatLocale": "",
12 | "isNsfw": false,
13 | "hasCloudflare": false,
14 | "sourceCodeUrl": "",
15 | "apiUrl": "",
16 | "version": "0.0.81",
17 | "isManga": false,
18 | "itemType": 1,
19 | "isFullData": false,
20 | "appMinVerReq": "0.5.0",
21 | "additionalParams": "",
22 | "sourceCodeLanguage": 1,
23 | "notes": "",
24 | "pkgPath": "anime/src/en/anixl.js",
25 | },
26 | ];
27 | class DefaultExtension extends MProvider {
28 | constructor() {
29 | super();
30 | this.client = new Client();
31 | }
32 |
33 | getBaseUrl() {
34 | return this.source.baseUrl;
35 | }
36 |
37 | baseUrl = this.getBaseUrl();
38 |
39 | getPreference(key) {
40 | return new SharedPreferences().get(key);
41 | }
42 |
43 | getHeaders() {
44 | return { "content-type": "application/json" };
45 | }
46 |
47 | async request(query, variables) {
48 | var body = { query, variables: variables };
49 | var req = await this.client.post(
50 | this.baseUrl + "/apo/",
51 | this.getHeaders(),
52 | body
53 | );
54 | return JSON.parse(req.body);
55 | }
56 |
57 | listing(res, page) {
58 | var result = res["data"]["result"];
59 | var items = result["items"];
60 | var maxPage = result["paging"]["pages"];
61 | var list = [];
62 |
63 | items.forEach((item) => {
64 | var data = item["data"];
65 | list.push({
66 | "link": data.ani_id,
67 | "name": data.info_title,
68 | "imageUrl": `${this.baseUrl}${data.urlCover300}`,
69 | });
70 | });
71 | var hasNextPage = page < maxPage;
72 | return { list, hasNextPage };
73 | }
74 |
75 | async searchList({
76 | keyword = "",
77 | incGenres = null,
78 | excGenres = null,
79 | sortBy = "field_score",
80 | page = 1,
81 | } = {}) {
82 | var query = `query ($page: Int = 1,$sortby:String,$query:String,$incGenres:[String],$excGenres:[String]) {
83 | result: get_searchAnime(
84 | select: {
85 | word: $query
86 | page: $page
87 | size: 20
88 | sortby: $sortby
89 | incGenres:$incGenres
90 | excGenres:$excGenres
91 | }
92 | ) {
93 | items {
94 | data {
95 | ani_id
96 | info_title
97 | urlCover300
98 | }
99 | }paging {
100 | pages
101 | }
102 | }
103 | }`;
104 | var variables = {
105 | "query": keyword,
106 | "page": parseInt(page),
107 | "sortby": sortBy,
108 | "incGenres": incGenres,
109 | "excGenres": excGenres,
110 | };
111 | var res = await this.request(query, variables);
112 | return this.listing(res, page);
113 | }
114 |
115 | async getPopular(page) {
116 | return await this.searchList({ page: page });
117 | }
118 |
119 | async getLatestUpdates(page) {
120 | var query = `query ($page: Int!) {
121 | result: get_latest_animes(select: {
122 | page: $page
123 | }) {
124 | items {
125 | data {
126 | ani_id
127 | info_title
128 | urlCover300
129 | }
130 | } paging {
131 | pages
132 | }
133 | }
134 | }`;
135 | var variables = {
136 | "page": parseInt(page),
137 | };
138 | var res = await this.request(query, variables);
139 | return this.listing(res, page);
140 | }
141 |
142 | async search(query, page, filters) {
143 | return await this.searchList({ keyword: query, page: page });
144 | }
145 |
146 | async getAnimeDetail(ani_id) {
147 | function statusCode(status) {
148 | return (
149 | {
150 | "currently_airing": 0,
151 | "finished_airing": 1,
152 | }[status] ?? 5
153 | );
154 | }
155 | var query = `query ($ani_id: String!) {
156 | get_animesNode(id: $ani_id) {
157 | data {
158 | info_title
159 | info_filmdesc
160 | info_meta_genre
161 | info_meta_status
162 | }
163 | }
164 | }`;
165 | var variables = {
166 | "ani_id": ani_id,
167 | };
168 | var res = await this.request(query, variables);
169 | var data = res["data"]["get_animesNode"]["data"];
170 | var name = data.info_title;
171 | var description = data.info_filmdesc;
172 | var genre = data.info_meta_genre;
173 | var status = statusCode(data.info_meta_status);
174 | var link = this.baseUrl + "/title/" + ani_id;
175 |
176 | return { name, status, description, genre, link };
177 | }
178 |
179 | async getEpisodeDetail(ani_id, page) {
180 | var wholeItems = [];
181 | var query = `query ($ani_id: String!, $page: Int!){
182 | get_animesEpisodesList(select: {
183 | ani_id: $ani_id,
184 | page: $page
185 | size: 150
186 | }) {
187 | items {
188 | data {
189 | ep_index
190 | ep_title
191 | date_create
192 | sourcesNode_list {
193 | data {
194 | src_type
195 | sou_id
196 | }
197 | }
198 | }
199 | }
200 | paging {
201 | pages
202 | }
203 | }
204 | }`;
205 | var variables = {
206 | "ani_id": ani_id,
207 | "page": page,
208 | };
209 | var res = await this.request(query, variables);
210 | var animesEpisodesList = res["data"]["get_animesEpisodesList"];
211 | var items = animesEpisodesList["items"];
212 |
213 | items.reverse();
214 | if (items.length > 0) {
215 | wholeItems = [...items, ...wholeItems];
216 | }
217 |
218 | var maxPage = animesEpisodesList["paging"]["pages"];
219 | if (page < maxPage) {
220 | return this.getEpisodeDetail(ani_id, page + 1);
221 | }
222 |
223 | return wholeItems;
224 | }
225 |
226 | async getDetail(url) {
227 | var ani_id = url;
228 | if (url.includes(this.baseUrl)) {
229 | ani_id = url.split("/title/")[1];
230 | }
231 | var details = await this.getAnimeDetail(ani_id);
232 |
233 | var epDetails = await this.getEpisodeDetail(ani_id, 1);
234 | var chapters = [];
235 | epDetails.forEach((item) => {
236 | var data = item.data;
237 | var title = `E${data.ep_index}: ${data.ep_title}`;
238 | var dateUpload = new Date(data.date_create).valueOf().toString();
239 | // var link = JSON.stringify(data.sourcesNode_list)
240 | var sourcesNode_list = data.sourcesNode_list;
241 |
242 | var scanlator = "";
243 | var links = {};
244 | sourcesNode_list.forEach((node) => {
245 | var src_type = node.data.src_type;
246 | links[src_type] = node.data.sou_id;
247 | scanlator += src_type.toUpperCase() + " ";
248 | });
249 |
250 | chapters.push({
251 | name: title,
252 | dateUpload: dateUpload,
253 | scanlator: scanlator.trim(),
254 | url: JSON.stringify(links),
255 | });
256 | });
257 |
258 | details.chapters = chapters;
259 | return details;
260 | }
261 |
262 | splitStreams(streamUrl, srcType, m3u8List) {
263 | var pref = this.getPreference("anixl_split_streams");
264 | var streams = [
265 | {
266 | url: streamUrl,
267 | originalUrl: streamUrl,
268 | quality: `Auto - ${srcType}`,
269 | },
270 | ];
271 | if (!pref || m3u8List.length < 1) return streams;
272 |
273 | m3u8List.forEach((item) => {
274 | var segment = item.name;
275 | var url = streamUrl.replace("master.m3u8", segment);
276 | var quality = "Highest";
277 | if (segment.includes("index-f2")) {
278 | quality = "Medium";
279 | } else if (segment.includes("index-f3")) {
280 | quality = "Lowest";
281 | }
282 | streams.push({
283 | url: url,
284 | originalUrl: url,
285 | quality: `${quality} - ${srcType}`,
286 | });
287 | });
288 | return streams.reverse();
289 | }
290 |
291 | async getVideoList(url) {
292 | var query = `query ($sou_id: String!){
293 | get_sourcesNode(id:$sou_id) {
294 | id
295 | data{
296 | m3u8_lists{
297 | name
298 | }
299 | souPath
300 | src_name
301 | track{
302 | trackPath
303 | kind
304 | label
305 | }
306 | }
307 | }
308 | }`;
309 |
310 | var jsonData = JSON.parse(url);
311 | var prefSrcType = this.getPreference("anixl_pref_stream_type");
312 | if (prefSrcType.length == 0) prefSrcType.push("sub");
313 | var streams = [];
314 |
315 | var noSubMode = this.getPreference("anixl_no_sub");
316 | for (var srcType of prefSrcType) {
317 | if (jsonData.hasOwnProperty(srcType)) {
318 | var srcId = jsonData[srcType];
319 | srcType = srcType.toUpperCase();
320 |
321 | var variables = { "sou_id": srcId };
322 |
323 | var res = await this.request(query, variables);
324 | var resData = res.data.get_sourcesNode.data;
325 | var streamUrl = resData.souPath;
326 | var m3u8_lists = resData.m3u8_lists;
327 | var splitStr = this.splitStreams(streamUrl, srcType, m3u8_lists);
328 | streams = [...splitStr,...streams];
329 |
330 | var subtitles = [];
331 |
332 | if (!noSubMode) {
333 | resData.track.forEach((item) =>
334 | subtitles.push({
335 | file: item.trackPath,
336 | label: `${item.label} : ${srcType}`,
337 | })
338 | );
339 | }
340 |
341 | streams[0].subtitles = subtitles;
342 | }
343 | }
344 |
345 | return streams.reverse();;
346 | }
347 |
348 | getFilterList() {
349 | throw new Error("getFilterList not implemented");
350 | }
351 |
352 | getSourcePreferences() {
353 | return [
354 | {
355 | key: "anixl_pref_stream_type",
356 | multiSelectListPreference: {
357 | title: "Preferred stream sub/dub type",
358 | summary: "",
359 | values: ["sub", "dub", "raw"],
360 | entries: ["Sub", "Dub", "Raw"],
361 | entryValues: ["sub", "dub", "raw"],
362 | },
363 | },
364 | {
365 | key: "anixl_split_streams",
366 | switchPreferenceCompat: {
367 | title: "Split stream into different quality streams",
368 | summary: "Split stream Auto into 360p/720p/1080p",
369 | value: true,
370 | },
371 | },
372 | {
373 | key: "anixl_no_sub",
374 | switchPreferenceCompat: {
375 | title: "No subs mode",
376 | summary: "This is req for downloading video. Might remove it later",
377 | value: false,
378 | },
379 | },
380 | ];
381 | }
382 | }
383 |
--------------------------------------------------------------------------------
/javascript/novel/src/en/novelbuddy.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "Novelbuddy",
4 | "id": 2507947282,
5 | "baseUrl": "https://novelbuddy.com",
6 | "lang": "en",
7 | "typeSource": "single",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=256&domain=https://novelbuddy.com/",
10 | "dateFormat": "",
11 | "dateFormatLocale": "",
12 | "isNsfw": false,
13 | "hasCloudflare": false,
14 | "sourceCodeUrl": "",
15 | "apiUrl": "",
16 | "version": "0.0.9",
17 | "isManga": false,
18 | "itemType": 2,
19 | "isFullData": false,
20 | "appMinVerReq": "0.5.0",
21 | "additionalParams": "",
22 | "sourceCodeLanguage": 1,
23 | "notes": "",
24 | "pkgPath": "novel/src/en/novelbuddy.js",
25 | },
26 | ];
27 | class DefaultExtension extends MProvider {
28 | constructor() {
29 | super();
30 | this.client = new Client();
31 | }
32 |
33 | getPreference(key) {
34 | return new SharedPreferences().get(key);
35 | }
36 |
37 | getHeaders(url) {
38 | throw new Error("getHeaders not implemented");
39 | }
40 |
41 | async request(slug) {
42 | var url = `${this.source.baseUrl}${slug}`;
43 | var body = (await this.client.get(url)).body;
44 | return new Document(body);
45 | }
46 |
47 | async searchPage({
48 | query = "",
49 | genres = [],
50 | status = "all",
51 | sort = "views",
52 | page = 1,
53 | } = {}) {
54 | function addSlug(para, value) {
55 | return `&${para}=${value}`;
56 | }
57 | function bundleSlug(category, items) {
58 | var rd = "";
59 | for (var item of items) {
60 | rd += `&${category}[]=${item.toLowerCase()}`;
61 | }
62 | return rd;
63 | }
64 |
65 | var slug = "/search?";
66 | slug += `q=${query}`;
67 | slug += bundleSlug("genre", genres);
68 | slug += addSlug("status", status);
69 | slug += addSlug("sort", sort);
70 | slug += addSlug("page", `${page}`);
71 |
72 | var doc = await this.request(slug);
73 |
74 | var list = [];
75 | var hasNextPage = false;
76 | doc.select(".book-item").forEach((item) => {
77 | var linkSection = item.selectFirst("a");
78 | var link = linkSection.getHref;
79 | var name = linkSection.attr("title");
80 |
81 | var imageUrl = "https:" + linkSection.selectFirst("img").attr("data-src");
82 | list.push({ name, link, imageUrl });
83 | });
84 |
85 | var lastPage = doc.selectFirst(".paginator").select("a");
86 | if (lastPage.length) {
87 | lastPage = lastPage.slice(-1)[0];
88 | hasNextPage = !lastPage.className.includes("active");
89 | }
90 |
91 | return { list, hasNextPage };
92 | }
93 |
94 | async getPopular(page) {
95 | return await this.searchPage({ sort: "views", page: page });
96 | }
97 |
98 | async getLatestUpdates(page) {
99 | return await this.searchPage({ sort: "updated_at", page: page });
100 | }
101 |
102 | async search(query, page, filters) {
103 | function checkBox(state) {
104 | var rd = [];
105 | state.forEach((item) => {
106 | if (item.state) {
107 | rd.push(item.value);
108 | }
109 | });
110 | return rd;
111 | }
112 | function selectFiler(filter) {
113 | return filter.values[filter.state].value;
114 | }
115 |
116 | var isFiltersAvailable = !filters || filters.length != 0;
117 | var genres = isFiltersAvailable ? checkBox(filters[0].state) : [];
118 | var status = isFiltersAvailable ? selectFiler(filters[1]) : "all";
119 | var sort = isFiltersAvailable ? selectFiler(filters[2]) : "views";
120 |
121 | return await this.searchPage({ query, genres, status, sort, page });
122 | }
123 |
124 | async getDetail(url) {
125 | function statusCode(status) {
126 | return (
127 | {
128 | "OnGoing": 0,
129 | "Completed": 1,
130 | }[status] ?? 5
131 | );
132 | }
133 | var baseUrl = this.source.baseUrl;
134 | var slug = url.replace(baseUrl, "");
135 | var link = baseUrl + url;
136 |
137 | var doc = await this.request(slug);
138 |
139 | var detail = doc.selectFirst(".detail");
140 | var name = detail.selectFirst("h1").text;
141 | var imageUrl =
142 | "https:" +
143 | doc.selectFirst(".img-cover").selectFirst("img").attr("data-src");
144 | var meta = detail.selectFirst(".meta");
145 | var genre = [];
146 | var status = 5;
147 | meta.select("p").forEach((item) => {
148 | var title = item.selectFirst("strong").text;
149 | if (title.includes("Genres")) {
150 | item
151 | .select("a")
152 | .forEach((a) => genre.push(a.text.replace(",", "").trim()));
153 | } else if (title.includes("Status")) {
154 | var statusText = item.selectFirst("a").text.trim();
155 | status = statusCode(statusText);
156 | }
157 | });
158 | var description = doc
159 | .selectFirst(".section-body.summary")
160 | .selectFirst("p")
161 | .text.trim();
162 |
163 | var chapters = [];
164 | var html = doc.html;
165 | var sKey = "bookId = ";
166 | var start = html.indexOf(sKey) + sKey.length;
167 | var end = html.indexOf(";", start);
168 | var bookId = html.substring(start, end).trim();
169 | var chapDoc = await this.request(
170 | `/api/manga/${bookId}/chapters?source=detail`
171 | );
172 | chapDoc
173 | .selectFirst("#chapter-list")
174 | .select("li")
175 | .forEach((item) => {
176 | var chapLink = item.selectFirst("a").getHref;
177 | var chapName = item.selectFirst("strong").text.trim();
178 | var dateString = item.selectFirst("time").text.trim();
179 | var dateUpload = new Date(dateString).valueOf().toString();
180 | chapters.push({
181 | name: chapName,
182 | url: chapLink,
183 | dateUpload,
184 | });
185 | });
186 |
187 | return {
188 | name,
189 | imageUrl,
190 | description,
191 | link,
192 | status,
193 | genre,
194 | chapters,
195 | };
196 | }
197 |
198 | async getHtmlContent(name, url) {
199 | var doc = await this.request(url);
200 | return this.cleanHtmlContent(doc);
201 | }
202 |
203 | async cleanHtmlContent(html) {
204 | var para = html.selectFirst(".content-inner").select("p");
205 | var title = para[0].text.trim();
206 | var content = "";
207 | para.slice(1).forEach((item) => {
208 | content += item.text.trim() + "
";
209 | });
210 | return `${title}
${content}`;
211 | }
212 |
213 | getFilterList() {
214 | function formateState(type_name, items, values) {
215 | var state = [];
216 | for (var i = 0; i < items.length; i++) {
217 | state.push({ type_name: type_name, name: items[i], value: values[i] });
218 | }
219 | return state;
220 | }
221 |
222 | var filters = [];
223 | var items = [];
224 | var values = [];
225 |
226 | // Genres
227 | items = [
228 | "Action",
229 | "Action Adventure",
230 | "ActionAdventure",
231 | "Adult",
232 | "Adventcure",
233 | "Adventure",
234 | "Adventurer",
235 | "Anime u0026 Comics",
236 | "Bender",
237 | "Booku0026Literature",
238 | "Chinese",
239 | "Comed",
240 | "Comedy",
241 | "Cultivation",
242 | "Drama",
243 | "dventure",
244 | "Eastern",
245 | "Ecchi",
246 | "Ecchi Fantasy",
247 | "Fan-Fiction",
248 | "Fanfiction",
249 | "Fantas",
250 | "Fantasy",
251 | "FantasyAction",
252 | "Game",
253 | "Games",
254 | "Gender",
255 | "Gender Bender",
256 | "Harem",
257 | "HaremAction",
258 | "Haremv",
259 | "Historica",
260 | "Historical",
261 | "History",
262 | "Horror",
263 | "Isekai",
264 | "Josei",
265 | "Light Novel",
266 | "Litrpg",
267 | "Lolicon",
268 | "Magic",
269 | "Martial",
270 | "Martial Arts",
271 | "Mature",
272 | "Mecha",
273 | "Military",
274 | "Modern Life",
275 | "Movies",
276 | "Myster",
277 | "Mystery",
278 | "Mystery.Adventure",
279 | "Psychologic",
280 | "Psychological",
281 | "Reincarnatio",
282 | "Reincarnation",
283 | "Romanc",
284 | "Romance",
285 | "Romance.Adventure",
286 | "Romance.Harem",
287 | "Romance.Smut",
288 | "RomanceAction",
289 | "Romancem",
290 | "School Life",
291 | "Sci-fi",
292 | "Seinen",
293 | "Seinen Wuxia",
294 | "Shoujo",
295 | "Shoujo Ai",
296 | "Shounen",
297 | "Shounen Ai",
298 | "Slice of Lif",
299 | "Slice Of Life",
300 | "Slice of Lifel",
301 | "Smut",
302 | "Sports",
303 | "Superna",
304 | "Supernatural",
305 | "System",
306 | "Thriller",
307 | "Tragedy",
308 | "Urban",
309 | "Urban Life",
310 | "Wuxia",
311 | "Xianxia",
312 | "Xuanhuan",
313 | "Yaoi",
314 | "Yuri",
315 | ];
316 |
317 | values = [
318 | "action",
319 | "action-adventure",
320 | "actionadventure",
321 | "adult",
322 | "adventcure",
323 | "adventure",
324 | "adventurer",
325 | "anime-u0026-comics",
326 | "bender",
327 | "booku0026literature",
328 | "chinese",
329 | "comed",
330 | "comedy",
331 | "cultivation",
332 | "drama",
333 | "dventure",
334 | "eastern",
335 | "ecchi",
336 | "ecchi-fantasy",
337 | "fan-fiction",
338 | "fanfiction",
339 | "fantas",
340 | "fantasy",
341 | "fantasyaction",
342 | "game",
343 | "games",
344 | "gender",
345 | "gender-bender",
346 | "harem",
347 | "haremaction",
348 | "haremv",
349 | "historica",
350 | "historical",
351 | "history",
352 | "horror",
353 | "isekai",
354 | "josei",
355 | "light-novel",
356 | "litrpg",
357 | "lolicon",
358 | "magic",
359 | "martial",
360 | "martial-arts",
361 | "mature",
362 | "mecha",
363 | "military",
364 | "modern-life",
365 | "movies",
366 | "myster",
367 | "mystery",
368 | "mystery-adventure",
369 | "psychologic",
370 | "psychological",
371 | "reincarnatio",
372 | "reincarnation",
373 | "romanc",
374 | "romance",
375 | "romance-adventure",
376 | "romance-harem",
377 | "romance-smut",
378 | "romanceaction",
379 | "romancem",
380 | "school-life",
381 | "sci-fi",
382 | "seinen",
383 | "seinen-wuxia",
384 | "shoujo",
385 | "shoujo-ai",
386 | "shounen",
387 | "shounen-ai",
388 | "slice-of-lif",
389 | "slice-of-life",
390 | "slice-of-lifel",
391 | "smut",
392 | "sports",
393 | "superna",
394 | "supernatural",
395 | "system",
396 | "thriller",
397 | "tragedy",
398 | "urban",
399 | "urban-life",
400 | "wuxia",
401 | "xianxia",
402 | "xuanhuan",
403 | "yaoi",
404 | "yuri",
405 | ];
406 | filters.push({
407 | type_name: "GroupFilter",
408 | name: "Genres",
409 | state: formateState("CheckBox", items, values),
410 | });
411 |
412 | // Status
413 | items = ["All", "Ongoing", "Completed"];
414 | values = ["all", "ongoing", "completed"];
415 | filters.push({
416 | type_name: "SelectFilter",
417 | name: "Status",
418 | state: 0,
419 | values: formateState("SelectOption", items, values),
420 | });
421 |
422 | // Sort order
423 | items = ["Views", "Updated", "Created", "Name A-Z", "Rating"];
424 | values = ["views", "updated_at", "created_at", "name", "rating"];
425 | filters.push({
426 | type_name: "SelectFilter",
427 | name: "Order by",
428 | state: 0,
429 | values: formateState("SelectOption", items, values),
430 | });
431 |
432 | return filters;
433 | }
434 |
435 | getSourcePreferences() {
436 | throw new Error("getSourcePreferences not implemented");
437 | }
438 | }
439 |
--------------------------------------------------------------------------------
/javascript/manga/src/en/readcomiconline.js:
--------------------------------------------------------------------------------
1 | const mangayomiSources = [
2 | {
3 | "name": "ReadComicOnline",
4 | "lang": "en",
5 | "id": 376287717,
6 | "baseUrl": "https://readcomiconline.li",
7 | "apiUrl": "",
8 | "iconUrl":
9 | "https://www.google.com/s2/favicons?sz=256&domain=https://readcomiconline.li/",
10 | "typeSource": "single",
11 | "isManga": true,
12 | "itemType": 0,
13 | "version": "0.3.0",
14 | "pkgPath": "manga/src/en/readcomiconline.js",
15 | },
16 | ];
17 |
18 | // Authors: - Swakshan, kodjodevf
19 |
20 | class DefaultExtension extends MProvider {
21 | constructor() {
22 | super();
23 | this.client = new Client();
24 | }
25 |
26 | getPreference(key) {
27 | return new SharedPreferences().get(key);
28 | }
29 |
30 | getHeaders() {
31 | return {
32 | "User-Agent":
33 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6832.64 Safari/537.36",
34 | Referer: this.source.baseUrl,
35 | Origin: this.source.baseUrl,
36 | };
37 | }
38 |
39 | async request(slug) {
40 | var url = slug;
41 | var baseUrl = this.source.baseUrl;
42 | if (!slug.includes(baseUrl)) url = baseUrl + slug;
43 | var res = await this.client.get(url, this.getHeaders());
44 | return new Document(res.body);
45 | }
46 |
47 | async getListPage(slug, page) {
48 | var url = `${slug}page=${page}`;
49 | var doc = await this.request(url);
50 | var baseUrl = this.source.baseUrl;
51 | var list = [];
52 |
53 | var comicList = doc.select(".list-comic > .item");
54 | comicList.forEach((item) => {
55 | var name = item.selectFirst(".title").text;
56 | var link = item.selectFirst("a").getHref;
57 | var imageSlug = item.selectFirst("img").getSrc;
58 | var imageUrl = imageSlug.includes("http")
59 | ? imageSlug
60 | : `${baseUrl}${imageSlug}`;
61 | list.push({ name, link, imageUrl });
62 | });
63 |
64 | var pager = doc.select("ul.pager > li");
65 |
66 | var hasNextPage = false;
67 | if (pager.length > 0)
68 | hasNextPage = pager[pager.length - 1].text.includes("Last")
69 | ? true
70 | : false;
71 |
72 | return { list, hasNextPage };
73 | }
74 |
75 | async getPopular(page) {
76 | return await this.getListPage("/ComicList/MostPopular?", page);
77 | }
78 | get supportsLatest() {
79 | throw new Error("supportsLatest not implemented");
80 | }
81 | async getLatestUpdates(page) {
82 | return await this.getListPage("/ComicList/LatestUpdate?", page);
83 | }
84 | async search(query, page, filters) {
85 | function getFilter(state) {
86 | var rd = "";
87 | state.forEach((item) => {
88 | if (item.state) {
89 | rd += `${item.value},`;
90 | }
91 | });
92 | return rd.slice(0, -1);
93 | }
94 |
95 | var isFiltersAvailable = !filters || filters.length != 0;
96 | var genre = isFiltersAvailable ? getFilter(filters[0].state) : [];
97 | var status = isFiltersAvailable
98 | ? filters[1].values[filters[1].state].value
99 | : "";
100 | var year = isFiltersAvailable
101 | ? filters[2].values[filters[2].state].value
102 | : "";
103 |
104 | var slug = `/AdvanceSearch?comicName=${query}&ig=${encodeURIComponent(
105 | genre
106 | )}&status=${status}&pubDate=${year}&`;
107 |
108 | return await this.getListPage(slug, page);
109 | }
110 |
111 | async getDetail(url) {
112 | function statusCode(status) {
113 | return (
114 | {
115 | Ongoing: 0,
116 | Completed: 1,
117 | }[status] ?? 5
118 | );
119 | }
120 | var baseUrl = this.source.baseUrl;
121 | if (url.includes(baseUrl)) url = url.replace(baseUrl, "");
122 | var link = baseUrl + link;
123 | var doc = await this.request(url);
124 |
125 | var detailsSection = doc.selectFirst(".barContent");
126 | var name = detailsSection.selectFirst("a").text;
127 | var imageSlug = doc.selectFirst(".rightBox").selectFirst("img").getSrc;
128 | var imageUrl = imageSlug.includes("http")
129 | ? imageSlug
130 | : `${this.source.baseUrl}${imageSlug}`;
131 | var pTag = detailsSection.select("p");
132 |
133 | var description = pTag[pTag.length - 2].text;
134 |
135 | var status = 5;
136 | var genre = [];
137 | var author = "";
138 | var artist = "";
139 |
140 | pTag.forEach((p) => {
141 | var itemText = p.text.trim();
142 |
143 | if (itemText.includes("Genres")) {
144 | genre = itemText.replace("Genres:", "").trim().split(", ");
145 | } else if (itemText.includes("Status")) {
146 | var sts = itemText.replace("Status: ", "").trim().split("\n")[0];
147 | status = statusCode(sts);
148 | } else if (itemText.includes("Writer")) {
149 | author = itemText.replace("Writer: ", "");
150 | } else if (itemText.includes("Artist")) {
151 | artist = itemText.replace("Artist: ", "");
152 | }
153 | });
154 | var chapters = [];
155 | var tr = doc.selectFirst("table").select("tr");
156 | tr.splice(0, 2); // 1st item in the table is headers & 2nd item is a line break
157 | tr.forEach((item) => {
158 | var tds = item.select("td");
159 | var aTag = tds[0].selectFirst("a");
160 | var chapLink = aTag.getHref;
161 |
162 | var chapTitle = aTag.text.trim().replace(`${name} `, "");
163 | chapTitle = chapTitle[0] == "_" ? chapTitle.substring(1) : chapTitle;
164 |
165 | var uploadDate = tds[1].text.trim();
166 | var date = new Date(uploadDate);
167 | var dateUpload = date.getTime().toString();
168 |
169 | chapters.push({ url: chapLink, name: chapTitle, dateUpload });
170 | });
171 |
172 | return {
173 | name,
174 | link,
175 | imageUrl,
176 | description,
177 | genre,
178 | status,
179 | author,
180 | artist,
181 | chapters,
182 | };
183 | }
184 |
185 | // For manga chapter pages
186 | async getPageList(url) {
187 | var pages = [];
188 | var hdr = this.getHeaders();
189 | let match;
190 | var imageQuality = this.getPreference("readcomiconline_image_quality");
191 | var prefServer = this.getPreference("readcomiconline_server");
192 |
193 | var url = `${url}&s=${prefServer}&quality=${imageQuality}`;
194 | var doc = await this.request(url);
195 | var html = doc.html;
196 |
197 | // Find host url for images
198 | var baseUrlOverride = "";
199 | const hostRegex = /return\s+baeu\s*\(\s*l\s*,\s*'([^']+?)'\s*\);?/g;
200 | match = hostRegex.exec(html);
201 | if (match != null && match.length > 0) {
202 | baseUrlOverride = match[1];
203 | if (baseUrlOverride.slice(-1) == "/")
204 | baseUrlOverride = baseUrlOverride.substring(0, -1);
205 | }
206 |
207 | var sKey = "l = l.replace(/";
208 | var eKey = "/g";
209 | var s = html.indexOf(sKey) + sKey.length;
210 | var e = html.indexOf(eKey, s);
211 | var replaceKey = html.substring(s, e).trim()
212 | var replaceRegex = new RegExp(replaceKey, "g");
213 |
214 | sKey = "var pth = ";
215 | eKey = "=";
216 | s = html.indexOf(sKey) + sKey. length;
217 | e = html.indexOf(eKey, s);
218 | var variableTag = html.substring(s, e).trim().split(" ")[1];
219 |
220 | eKey = "//beau";
221 | var lines = html.substring(eKey, html.indexOf(eKey, e)).split("\n");
222 |
223 | lines.forEach((line) => {
224 | line = line.trim();
225 | if (line.startsWith(variableTag)) {
226 | var encodedImageUrl = line
227 | .replace(`${variableTag} = '`, "")
228 | .slice(0, -2);
229 | var decodedImageUrl = this.decodeImageUrl(
230 | replaceRegex,
231 | encodedImageUrl,
232 | baseUrlOverride
233 | );
234 | pages.push({
235 | url: decodedImageUrl,
236 | headers: hdr,
237 | });
238 | }
239 | });
240 | return pages;
241 | }
242 | getFilterList() {
243 | function formateState(type_name, items, values) {
244 | var state = [];
245 | for (var i = 0; i < items.length; i++) {
246 | state.push({ type_name: type_name, name: items[i], value: values[i] });
247 | }
248 | return state;
249 | }
250 |
251 | var filters = [];
252 |
253 | // Genre
254 | var items = [
255 | "Action",
256 | "Adventure",
257 | "Anthology",
258 | "Anthropomorphic",
259 | "Biography",
260 | "Children",
261 | "Comedy",
262 | "Crime",
263 | "Drama",
264 | "Family",
265 | "Fantasy",
266 | "Fighting",
267 | "Graphic Novels",
268 | "Historical",
269 | "Horror",
270 | "Leading Ladies",
271 | "LGBTQ",
272 | "Literature",
273 | "Manga",
274 | "Martial Arts",
275 | "Mature",
276 | "Military",
277 | "Mini-Series",
278 | "Movies & TV",
279 | "Music",
280 | "Mystery",
281 | "Mythology",
282 | "Personal",
283 | "Political",
284 | "Post-Apocalyptic",
285 | "Psychological",
286 | "Pulp",
287 | "Religious",
288 | "Robots",
289 | "Romance",
290 | "School Life",
291 | "Sci-Fi",
292 | "Slice of Life",
293 | "Sport",
294 | "Spy",
295 | "Superhero",
296 | "Supernatural",
297 | "Suspense",
298 | "Teen",
299 | "Thriller",
300 | "Vampires",
301 | "Video Games",
302 | "War",
303 | "Western",
304 | "Zombies",
305 | ];
306 |
307 | var values = [
308 | "1",
309 | "2",
310 | "38",
311 | "46",
312 | "41",
313 | "49",
314 | "3",
315 | "17",
316 | "19",
317 | "25",
318 | "20",
319 | "31",
320 | "5",
321 | "28",
322 | "15",
323 | "35",
324 | "51",
325 | "44",
326 | "40",
327 | "4",
328 | "8",
329 | "33",
330 | "56",
331 | "47",
332 | "55",
333 | "23",
334 | "21",
335 | "48",
336 | "42",
337 | "43",
338 | "27",
339 | "39",
340 | "53",
341 | "9",
342 | "32",
343 | "52",
344 | "16",
345 | "50",
346 | "54",
347 | "30",
348 | "22",
349 | "24",
350 | "29",
351 | "57",
352 | "18",
353 | "34",
354 | "37",
355 | "26",
356 | "45",
357 | "36",
358 | ];
359 | filters.push({
360 | type_name: "GroupFilter",
361 | name: "Genres",
362 | state: formateState("CheckBox", items, values),
363 | });
364 |
365 | // Status
366 | items = ["Any", "Ongoing", "Completed"];
367 | values = ["", "Ongoing", "Completed"];
368 | filters.push({
369 | type_name: "SelectFilter",
370 | name: "Status",
371 | state: 0,
372 | values: formateState("SelectOption", items, values),
373 | });
374 |
375 | // Years
376 | const currentYear = new Date().getFullYear();
377 | items = Array.from({ length: currentYear - 1919 }, (_, i) =>
378 | (1920 + i).toString()
379 | ).reverse();
380 | items = ["All", ...items];
381 | values = ["", ...items];
382 | filters.push({
383 | type_name: "SelectFilter",
384 | name: "Year",
385 | state: 0,
386 | values: formateState("SelectOption", items, values),
387 | });
388 |
389 | return filters;
390 | }
391 | getSourcePreferences() {
392 | return [
393 | {
394 | key: "readcomiconline_image_quality",
395 | listPreference: {
396 | title: "Preferred image quality",
397 | summary: "",
398 | valueIndex: 1,
399 | entries: ["Low", "High"],
400 | entryValues: ["lq", "hq"],
401 | },
402 | },
403 | {
404 | key: "readcomiconline_server",
405 | listPreference: {
406 | title: "Preferred server",
407 | summary: "",
408 | valueIndex: 0,
409 | entries: ["Server 1", "Server 2"],
410 | entryValues: ["s1", "s2"],
411 | },
412 | },
413 | ];
414 | }
415 |
416 | // -------- ReadComicOnline Image Decoder --------
417 | // Source:- https://readcomiconline.li/Scripts/rguard.min.js
418 |
419 | base64UrlDecode(input) {
420 | let base64 = input.replace(/-/g, "+").replace(/_/g, "/");
421 |
422 | while (base64.length % 4 !== 0) {
423 | base64 += "=";
424 | }
425 |
426 | const base64abc =
427 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
428 | const outputBytes = [];
429 |
430 | for (let i = 0; i < base64.length; i += 4) {
431 | const c1 = base64abc.indexOf(base64[i]);
432 | const c2 = base64abc.indexOf(base64[i + 1]);
433 | const c3 = base64abc.indexOf(base64[i + 2]);
434 | const c4 = base64abc.indexOf(base64[i + 3]);
435 |
436 | const triplet = (c1 << 18) | (c2 << 12) | ((c3 & 63) << 6) | (c4 & 63);
437 |
438 | outputBytes.push((triplet >> 16) & 0xff);
439 | if (base64[i + 2] !== "=") outputBytes.push((triplet >> 8) & 0xff);
440 | if (base64[i + 3] !== "=") outputBytes.push(triplet & 0xff);
441 | }
442 |
443 | // Convert bytes to ISO-8859-1 string
444 | return String.fromCharCode(...outputBytes);
445 | }
446 |
447 | extractAndConcatParts(input) {
448 | return input.substring(15, 33) + input.substring(50);
449 | }
450 |
451 | trimAndAppendChars(input) {
452 | return (
453 | input.substring(0, input.length - 11) +
454 | input[input.length - 2] +
455 | input[input.length - 1]
456 | );
457 | }
458 |
459 | decodeImageUrl(replaceRegex,obfuscatedUrl, customDomain) {
460 | var decodedUrl = "";
461 | obfuscatedUrl = obfuscatedUrl
462 | .replace(replaceRegex, "d")
463 | .replace(/b/g, "b")
464 | .replace(/h/g, "h");
465 |
466 | // If URL does not already start with HTTPS
467 | if (!obfuscatedUrl.startsWith("https")) {
468 | let queryParams = obfuscatedUrl.substring(obfuscatedUrl.indexOf("?"));
469 |
470 | // Trim to base URL, removing size suffix
471 | if (obfuscatedUrl.includes("=s0?")) {
472 | decodedUrl = obfuscatedUrl.substring(0, obfuscatedUrl.indexOf("=s0?"));
473 | } else {
474 | decodedUrl = obfuscatedUrl.substring(
475 | 0,
476 | obfuscatedUrl.indexOf("=s1600?")
477 | );
478 | }
479 |
480 | // Extract and decode URL
481 | decodedUrl = this.extractAndConcatParts(decodedUrl);
482 | decodedUrl = this.trimAndAppendChars(decodedUrl);
483 | decodedUrl = this.base64UrlDecode(decodedUrl);
484 |
485 | // Remove 4 characters from index 13 to 17
486 | decodedUrl = decodedUrl.substring(0, 13) + decodedUrl.substring(17);
487 |
488 | // Append size indicator
489 | if (obfuscatedUrl.includes("=s0")) {
490 | decodedUrl = decodedUrl.substring(0, decodedUrl.length - 2) + "=s0";
491 | } else {
492 | decodedUrl = decodedUrl.substring(0, decodedUrl.length - 2) + "=s1600";
493 | }
494 |
495 | // Combine decoded base URL with original query params
496 | decodedUrl = "https://2.bp.blogspot.com/" + decodedUrl + queryParams;
497 | } else {
498 | decodedUrl = obfuscatedUrl;
499 | }
500 |
501 | // Optionally replace domain with a custom one
502 | if (customDomain && customDomain !== "") {
503 | decodedUrl = decodedUrl.replace(
504 | "https://2.bp.blogspot.com",
505 | customDomain
506 | );
507 | }
508 |
509 | return decodedUrl;
510 | }
511 | }
512 |
--------------------------------------------------------------------------------