├── 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 | Add all repositories 12 | 13 | Add manga repository 14 | 15 | Add anime repository 16 | 17 | Add novel repository 18 | 19 | If you installed the app via Live Container, then use the following buttons instead: 20 | 21 | Add all repositories 22 | 23 | Add manga repository 24 | 25 | Add anime repository 26 | 27 | Add novel repository 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 | Add all repositories 57 | 58 | Add manga repository 59 | 60 | Add anime repository 61 | 62 | Add novel repository 63 | 64 | If you installed the app via Live Container, then use the following buttons instead: 65 | 66 | Add all repositories 67 | 68 | Add manga repository 69 | 70 | Add anime repository 71 | 72 | Add novel repository 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 | ![1](https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/screenshots/1.png) 14 | 3. then click `+` and you will see : 15 | ![2](https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/screenshots/2.png) 16 | 4. Fill in the fields with your new source that you would like to create, 17 | ![3](https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/screenshots/3.png) 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 | ![4](https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/screenshots/4.png) 22 | click to open settings 23 | 6. After click on edit code 24 | ![5](https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/screenshots/5.png) 25 | 7. Finally you can now write the extension 26 | ![6](https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/screenshots/6.png) 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 |
author
160 |
div head
161 |
162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 |
1234onetwothreefour
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 | --------------------------------------------------------------------------------