Fixed s.to

This commit is contained in:
JMcrafter26 2025-12-08 19:27:48 +01:00
parent da75042805
commit dcd7c03ef2
10 changed files with 2682 additions and 19 deletions

View file

@ -5,7 +5,7 @@
"name": "Cufiy", "name": "Cufiy",
"icon": "https://files.catbox.moe/ttj4fc.gif" "icon": "https://files.catbox.moe/ttj4fc.gif"
}, },
"version": "0.3.15", "version": "0.3.16",
"language": "English (DUB)", "language": "English (DUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "720p", "quality": "720p",

View file

@ -6,7 +6,8 @@ async function searchResults(keyword) {
try { try {
const encodedKeyword = encodeURIComponent(keyword); const encodedKeyword = encodeURIComponent(keyword);
const searchApiUrl = `https://s.to/ajax/seriesSearch?keyword=${encodedKeyword}`; const searchApiUrl = `https://s.to/ajax/seriesSearch?keyword=${encodedKeyword}`;
const responseText = await fetch(searchApiUrl); const response = await soraFetch(searchApiUrl);
const responseText = await response?.text() ?? response;
const data = await JSON.parse(responseText); const data = await JSON.parse(responseText);
@ -27,7 +28,7 @@ async function searchResults(keyword) {
async function extractDetails(url) { async function extractDetails(url) {
try { try {
const fetchUrl = `${url}`; const fetchUrl = `${url}`;
const response = await fetch(fetchUrl); const response = await soraFetch(fetchUrl);
const text = response.text ? await response.text() : response; const text = response.text ? await response.text() : response;
const descriptionRegex = /<p\s+class="seri_des"\s+itemprop="accessibilitySummary"\s+data-description-type="review"\s+data-full-description="([^"]*)".*?>(.*?)<\/p>/s; const descriptionRegex = /<p\s+class="seri_des"\s+itemprop="accessibilitySummary"\s+data-description-type="review"\s+data-full-description="([^"]*)".*?>(.*?)<\/p>/s;
@ -40,11 +41,15 @@ async function extractDetails(url) {
} }
const descriptionMatch = descriptionRegex.exec(text) || []; const descriptionMatch = descriptionRegex.exec(text) || [];
// sanitize description by removing HTML tags
let description = descriptionMatch[1] || '';
description = description.replace(/<[^>]+>/g, '').trim();
const airdateMatch = "Unknown"; // TODO: Implement airdate extraction const airdateMatch = "Unknown"; // TODO: Implement airdate extraction
const transformedResults = [{ const transformedResults = [{
description: descriptionMatch[1] || 'No description available', description: description || 'No description available',
aliases: aliasesArray[0] || 'No aliases available', aliases: aliasesArray[0] || 'No aliases available',
airdate: airdateMatch airdate: airdateMatch
}]; }];
@ -64,16 +69,18 @@ async function extractEpisodes(url) {
try { try {
const baseUrl = 'https://s.to'; const baseUrl = 'https://s.to';
const fetchUrl = `${url}`; const fetchUrl = `${url}`;
const response = await fetch(fetchUrl); const response = await soraFetch(fetchUrl);
const html = response.text ? await response.text() : response; const html = response.text ? await response.text() : response;
const finishedList = []; const finishedList = [];
const seasonLinks = getSeasonLinks(html); const seasonLinks = getSeasonLinks(html);
console.log("Season Links: " + JSON.stringify(seasonLinks));
for (const seasonLink of seasonLinks) { for (const seasonLink of seasonLinks) {
const seasonEpisodes = await fetchSeasonEpisodes(`${baseUrl}${seasonLink}`); const seasonEpisodes = await fetchSeasonEpisodes(`${baseUrl}${seasonLink}`);
finishedList.push(...seasonEpisodes); finishedList.push(...seasonEpisodes);
} }
console.log("Finished Episode List: " + JSON.stringify(finishedList));
// Replace the field "number" with the current index of each item, starting from 1 // Replace the field "number" with the current index of each item, starting from 1
finishedList.forEach((item, index) => { finishedList.forEach((item, index) => {
@ -94,7 +101,7 @@ async function extractStreamUrl(url) {
try { try {
const baseUrl = 'https://s.to'; const baseUrl = 'https://s.to';
const fetchUrl = `${url}`; const fetchUrl = `${url}`;
const response = await fetch(fetchUrl); const response = await soraFetch(fetchUrl);
const text = response.text ? await response.text() : response; const text = response.text ? await response.text() : response;
const finishedList = []; const finishedList = [];
@ -125,7 +132,7 @@ async function extractStreamUrl(url) {
const providerName = value; const providerName = value;
// fetch the provider link and extract the stream URL // fetch the provider link and extract the stream URL
const streamUrl = await fetch(providerLink); const streamUrl = await soraFetch(providerLink);
console.log("Stream URL: " + streamUrl); console.log("Stream URL: " + streamUrl);
const winLocRegex = /window\.location\.href\s*=\s*['"]([^'"]+)['"]/; const winLocRegex = /window\.location\.href\s*=\s*['"]([^'"]+)['"]/;
const winLocMatch = await winLocRegex.exec(streamUrl); const winLocMatch = await winLocRegex.exec(streamUrl);
@ -195,9 +202,11 @@ function selectHoster(finishedList) {
// "https://speedfiles.net/82346fs": "speedfiles", // "https://speedfiles.net/82346fs": "speedfiles",
// }; // };
console.log("Hoster List: " + JSON.stringify(finishedList));
// Define the preferred providers and languages // Define the preferred providers and languages
const providerList = ["VOE", "SpeedFiles", "Vidmoly", "DoodStream", "Vidoza", "MP4Upload"]; const providerList = ["VOE", "SpeedFiles", "Filemoon", "Vidmoly", "DoodStream", "Vidoza", "MP4Upload"];
const languageList = ["Englisch", "mit Untertitel Englisch", "Deutsch", "mit Untertitel Deutsch"]; const languageList = ["English", "mit Untertitel Deutsch", "mit Untertitel Englisch"];
@ -263,10 +272,14 @@ async function fetchSeasonEpisodes(url) {
try { try {
const baseUrl = 'https://s.to'; const baseUrl = 'https://s.to';
const fetchUrl = `${url}`; const fetchUrl = `${url}`;
const text = await fetch(fetchUrl); const response = await soraFetch(fetchUrl);
const text = await response?.text() ?? response;
// Updated regex to allow empty <strong> content // Updated regex to allow empty <strong> content
const regex = /<td class="seasonEpisodeTitle">\s*<a[^>]*href="([^"]+)"[^>]*>.*?<strong>([^<]*)<\/strong>.*?<span>([^<]+)<\/span>.*?<\/a>/g; const regex = /<td class="seasonEpisodeTitle">\s*<a[^>]*href="([^"]+)"[^>]*>.*?<strong>([^<]*)<\/strong>.*?<span>([^<]+)<\/span>.*?<\/a>/g;
const regex2 =
/<td[^>]*seasonEpisodeTitle[^>]*>\s*<a[^>]*href=["']([^"']+)["'][^>]*>[\s\S]*?<strong>\s*([^<]*?)\s*<\/strong>[\s\S]*?(?:<span[^>]*>\s*([^<]*?)\s*<\/span>)?[\s\S]*?<\/a>/gi;
const matches = []; const matches = [];
let match; let match;
@ -277,6 +290,16 @@ async function fetchSeasonEpisodes(url) {
matches.push({ number: holderNumber, href: `${baseUrl}${link}` }); matches.push({ number: holderNumber, href: `${baseUrl}${link}` });
} }
// If no matches found with the first regex, try the second one
if (matches.length === 0) {
console.log("No matches found with first regex, trying second regex.");
while ((match = regex2.exec(text)) !== null) {
const [_, link] = match;
console.log("Match found with second regex: " + link);
matches.push({ number: holderNumber, href: `${baseUrl}${link}` });
}
}
return matches; return matches;
} catch (error) { } catch (error) {
@ -348,6 +371,7 @@ function base64Decode(str) {
} }
// ⚠️ DO NOT EDIT BELOW THIS LINE ⚠️ // ⚠️ DO NOT EDIT BELOW THIS LINE ⚠️
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR

View file

@ -5,7 +5,7 @@
"name": "Hamzo & Cufiy", "name": "Hamzo & Cufiy",
"icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024" "icon": "https://cdn.discordapp.com/avatars/623644371819954226/591ecab10b0b4535e859bb0b9bbe62e5?size=1024"
}, },
"version": "0.3.15", "version": "0.3.16",
"language": "German (DUB)", "language": "German (DUB)",
"streamType": "HLS", "streamType": "HLS",
"quality": "720p", "quality": "720p",

View file

@ -6,7 +6,8 @@ async function searchResults(keyword) {
try { try {
const encodedKeyword = encodeURIComponent(keyword); const encodedKeyword = encodeURIComponent(keyword);
const searchApiUrl = `https://s.to/ajax/seriesSearch?keyword=${encodedKeyword}`; const searchApiUrl = `https://s.to/ajax/seriesSearch?keyword=${encodedKeyword}`;
const responseText = await fetch(searchApiUrl); const response = await soraFetch(searchApiUrl);
const responseText = await response?.text() ?? response;
const data = await JSON.parse(responseText); const data = await JSON.parse(responseText);
@ -27,7 +28,7 @@ async function searchResults(keyword) {
async function extractDetails(url) { async function extractDetails(url) {
try { try {
const fetchUrl = `${url}`; const fetchUrl = `${url}`;
const response = await fetch(fetchUrl); const response = await soraFetch(fetchUrl);
const text = response.text ? await response.text() : response; const text = response.text ? await response.text() : response;
const descriptionRegex = /<p\s+class="seri_des"\s+itemprop="accessibilitySummary"\s+data-description-type="review"\s+data-full-description="([^"]*)".*?>(.*?)<\/p>/s; const descriptionRegex = /<p\s+class="seri_des"\s+itemprop="accessibilitySummary"\s+data-description-type="review"\s+data-full-description="([^"]*)".*?>(.*?)<\/p>/s;
@ -40,11 +41,15 @@ async function extractDetails(url) {
} }
const descriptionMatch = descriptionRegex.exec(text) || []; const descriptionMatch = descriptionRegex.exec(text) || [];
// sanitize description by removing HTML tags
let description = descriptionMatch[1] || '';
description = description.replace(/<[^>]+>/g, '').trim();
const airdateMatch = "Unknown"; // TODO: Implement airdate extraction const airdateMatch = "Unknown"; // TODO: Implement airdate extraction
const transformedResults = [{ const transformedResults = [{
description: descriptionMatch[1] || 'No description available', description: description || 'No description available',
aliases: aliasesArray[0] || 'No aliases available', aliases: aliasesArray[0] || 'No aliases available',
airdate: airdateMatch airdate: airdateMatch
}]; }];
@ -64,16 +69,18 @@ async function extractEpisodes(url) {
try { try {
const baseUrl = 'https://s.to'; const baseUrl = 'https://s.to';
const fetchUrl = `${url}`; const fetchUrl = `${url}`;
const response = await fetch(fetchUrl); const response = await soraFetch(fetchUrl);
const html = response.text ? await response.text() : response; const html = response.text ? await response.text() : response;
const finishedList = []; const finishedList = [];
const seasonLinks = getSeasonLinks(html); const seasonLinks = getSeasonLinks(html);
console.log("Season Links: " + JSON.stringify(seasonLinks));
for (const seasonLink of seasonLinks) { for (const seasonLink of seasonLinks) {
const seasonEpisodes = await fetchSeasonEpisodes(`${baseUrl}${seasonLink}`); const seasonEpisodes = await fetchSeasonEpisodes(`${baseUrl}${seasonLink}`);
finishedList.push(...seasonEpisodes); finishedList.push(...seasonEpisodes);
} }
console.log("Finished Episode List: " + JSON.stringify(finishedList));
// Replace the field "number" with the current index of each item, starting from 1 // Replace the field "number" with the current index of each item, starting from 1
finishedList.forEach((item, index) => { finishedList.forEach((item, index) => {
@ -94,7 +101,7 @@ async function extractStreamUrl(url) {
try { try {
const baseUrl = 'https://s.to'; const baseUrl = 'https://s.to';
const fetchUrl = `${url}`; const fetchUrl = `${url}`;
const response = await fetch(fetchUrl); const response = await soraFetch(fetchUrl);
const text = response.text ? await response.text() : response; const text = response.text ? await response.text() : response;
const finishedList = []; const finishedList = [];
@ -125,7 +132,7 @@ async function extractStreamUrl(url) {
const providerName = value; const providerName = value;
// fetch the provider link and extract the stream URL // fetch the provider link and extract the stream URL
const streamUrl = await fetch(providerLink); const streamUrl = await soraFetch(providerLink);
console.log("Stream URL: " + streamUrl); console.log("Stream URL: " + streamUrl);
const winLocRegex = /window\.location\.href\s*=\s*['"]([^'"]+)['"]/; const winLocRegex = /window\.location\.href\s*=\s*['"]([^'"]+)['"]/;
const winLocMatch = await winLocRegex.exec(streamUrl); const winLocMatch = await winLocRegex.exec(streamUrl);
@ -195,8 +202,10 @@ function selectHoster(finishedList) {
// "https://speedfiles.net/82346fs": "speedfiles", // "https://speedfiles.net/82346fs": "speedfiles",
// }; // };
console.log("Hoster List: " + JSON.stringify(finishedList));
// Define the preferred providers and languages // Define the preferred providers and languages
const providerList = ["VOE", "SpeedFiles", "Vidmoly", "DoodStream", "Vidoza", "MP4Upload"]; const providerList = ["VOE", "SpeedFiles", "Filemoon", "Vidmoly", "DoodStream", "Vidoza", "MP4Upload"];
const languageList = ["Deutsch", "mit Untertitel Deutsch", "mit Untertitel Englisch"]; const languageList = ["Deutsch", "mit Untertitel Deutsch", "mit Untertitel Englisch"];
@ -263,10 +272,14 @@ async function fetchSeasonEpisodes(url) {
try { try {
const baseUrl = 'https://s.to'; const baseUrl = 'https://s.to';
const fetchUrl = `${url}`; const fetchUrl = `${url}`;
const text = await fetch(fetchUrl); const response = await soraFetch(fetchUrl);
const text = await response?.text() ?? response;
// Updated regex to allow empty <strong> content // Updated regex to allow empty <strong> content
const regex = /<td class="seasonEpisodeTitle">\s*<a[^>]*href="([^"]+)"[^>]*>.*?<strong>([^<]*)<\/strong>.*?<span>([^<]+)<\/span>.*?<\/a>/g; const regex = /<td class="seasonEpisodeTitle">\s*<a[^>]*href="([^"]+)"[^>]*>.*?<strong>([^<]*)<\/strong>.*?<span>([^<]+)<\/span>.*?<\/a>/g;
const regex2 =
/<td[^>]*seasonEpisodeTitle[^>]*>\s*<a[^>]*href=["']([^"']+)["'][^>]*>[\s\S]*?<strong>\s*([^<]*?)\s*<\/strong>[\s\S]*?(?:<span[^>]*>\s*([^<]*?)\s*<\/span>)?[\s\S]*?<\/a>/gi;
const matches = []; const matches = [];
let match; let match;
@ -277,6 +290,16 @@ async function fetchSeasonEpisodes(url) {
matches.push({ number: holderNumber, href: `${baseUrl}${link}` }); matches.push({ number: holderNumber, href: `${baseUrl}${link}` });
} }
// If no matches found with the first regex, try the second one
if (matches.length === 0) {
console.log("No matches found with first regex, trying second regex.");
while ((match = regex2.exec(text)) !== null) {
const [_, link] = match;
console.log("Match found with second regex: " + link);
matches.push({ number: holderNumber, href: `${baseUrl}${link}` });
}
}
return matches; return matches;
} catch (error) { } catch (error) {
@ -348,6 +371,7 @@ function base64Decode(str) {
} }
// ⚠️ DO NOT EDIT BELOW THIS LINE ⚠️ // ⚠️ DO NOT EDIT BELOW THIS LINE ⚠️
// EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR // EDITING THIS FILE COULD BREAK THE UPDATER AND CAUSE ISSUES WITH THE EXTRACTOR

2
test/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.json
.venv

1555
test/debug_viewer.html Normal file

File diff suppressed because it is too large Load diff

736
test/host.py Normal file
View file

@ -0,0 +1,736 @@
import requests
import sys
import rich
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.layout import Layout
from rich.text import Text
from rich import box
import os
import json
import subprocess
import inquirer
import webbrowser
console = Console()
module = {}
temp_dir = os.path.join(os.getcwd(), "temp_modules")
moduleHostExe = os.path.join(os.getcwd(), "modulehost.exe")
script_path = ""
current_search_results = []
current_search_query = ""
debug_mode = False
def setup():
# make sure temp directory exists
if not debug_mode:
os.makedirs(temp_dir, exist_ok=True)
if not os.path.exists(moduleHostExe):
console.print(f"[red]Error: {moduleHostExe} not found. Please ensure it is in the current directory.[/red]")
exit(1)
def get_module_info(manifest_url):
try:
response = requests.get(manifest_url)
response.raise_for_status()
manifest = response.json()
if not sanitize_manifest(manifest):
return None
module['manifest'] = manifest
# save manifest to a temporary file
with open(f"{temp_dir}/{module['filename']}_manifest.json", "w", encoding="utf-8") as manifest_file:
json.dump(manifest, manifest_file, ensure_ascii=False, indent=4)
return module
except requests.RequestException as e:
console.print(f"[red]Error fetching manifest:[/red] {e}")
return None
except ValueError:
console.print("[red]Error parsing JSON from manifest.[/red]")
return None
def sanitize_manifest(manifest):
required_fields = ['sourceName', 'scriptUrl', 'iconUrl', 'version']
for field in required_fields:
if field not in manifest:
console.print(f"[red]Manifest missing required field:[/red] {field}")
return False
# create safe filename (only alphanumeric, underscores, () and hyphens)
module['filename'] = ''.join(c for c in manifest['sourceName'] if c.isalnum() or c in (' ', '_', '-', '(', ')')).rstrip().replace(' ', '_')
return True
def get_module_script(module):
script_url = module['manifest']['scriptUrl']
try:
response = requests.get(script_url)
response.raise_for_status()
# Save script to a temporary file utf-8 encoded
with open(f"{temp_dir}/{module['filename']}.js", "w", encoding="utf-8") as script_file:
script_file.write(response.text)
return f"{temp_dir}/{module['filename']}.js"
except requests.RequestException as e:
console.print(f"[red]Error fetching script:[/red] {e}")
return None
def get_module_from_id(module_id):
modules_api = "https://library.cufiy.net/api/modules/get?id="
# validate module ID format (at least 3 characters, alphanumeric, case sensitive)
if not (3 <= len(module_id) <= 50 and module_id.isalnum()):
console.print("[red]Invalid Module ID format.[/red]")
return None
try:
response = requests.get(modules_api + module_id)
response.raise_for_status()
data = response.json()
if not data['data']:
console.print("[red]Module ID not found.[/red]")
return None
if 'manifestUrl' not in data['data']:
console.print("[red]Module ID not found or invalid response.[/red]")
return None
module['manifest'] = data['data']
return get_module_info(data['data']['manifestUrl'])
except requests.RequestException as e:
console.print(f"[red]Error fetching module by ID:[/red] {e}")
return None
def run_command(command, args, silent=True):
commands = {
"search": "searchResults",
"details": "extractDetails",
"episodes": "extractEpisodes",
"stream": "extractStreamUrl"
}
if command not in commands:
if not silent:
console.print(f"[red]Unknown command:[/red] {command}")
return None
if not silent:
console.print(f"[blue]Running command:[/blue] {command}")
# run the command using subprocess or other methods as needed and await the result
# {"status":"error","message":"Usage: node run_module.js --path <file> --function <fnName> [--param <jsonOrString>]","data":null,"debug":[]}
process_args = [moduleHostExe, "--path", args[0], "--function", commands[command]]
if debug_mode:
process_args.append("--debug")
process_args.append("--debug-full")
process_args.append("--trace-warnings")
if len(args) > 1:
process_args += ["--param", args[1]]
try:
result = subprocess.run(process_args, capture_output=True, text=False)
if debug_mode:
# load last command output and merge with current debug
debug_output = []
if os.path.exists("last_command_output.json"):
with open("last_command_output.json", "r", encoding="utf-8") as f:
try:
debug_output = json.load(f)
except Exception:
debug_output = []
# append current debug
try:
current_debug = json.loads(result.stdout.decode('utf-8', errors='replace'))
if isinstance(current_debug, list):
debug_output.extend(current_debug)
else:
debug_output.append(current_debug)
except Exception:
pass
# save merged debug output
with open("last_command_output.json", "w", encoding="utf-8") as f:
json.dump(debug_output, f, ensure_ascii=False, indent=4)
except Exception as e:
if not silent:
console.print(f"[red]Error running command:[/red] {e}")
return None
# decode output bytes to text with sensible fallbacks
stdout_bytes = result.stdout if result.stdout is not None else result.stderr
try:
stdout_text = stdout_bytes.decode('utf-8') if isinstance(stdout_bytes, (bytes, bytearray)) else str(stdout_bytes)
except Exception:
# fallback replace mode
try:
stdout_text = stdout_bytes.decode('utf-8', errors='replace') if isinstance(stdout_bytes, (bytes, bytearray)) else str(stdout_bytes)
except Exception:
stdout_text = ''
if result.returncode == 0:
if not silent:
console.print(f"[green]Command output:[/green] {stdout_text}")
# try to json parse the output (utf-8)
try:
output = json.loads(stdout_text)
# if status is success, return data
if output.get('status') == 'success':
return output.get('data')
else:
if not silent:
console.print(f"[red]Command failed:[/red] {output.get('message')}")
return None
except json.JSONDecodeError:
if not silent:
console.print("[red]Error parsing JSON output.[/red]")
return None
else:
if not silent:
console.print(f"[red]Command error:[/red] {result.stderr}")
return None
def cls():
os.system('cls' if os.name=='nt' else 'clear')
def show_header():
"""Display app header"""
header = Text("MODULE HOST", style="bold cyan", justify="center")
if 'manifest' in module:
subheader = Text(f"📦 {module['manifest']['sourceName']} v{module['manifest']['version']}",
style="dim cyan", justify="center")
console.print(Panel.fit(header, subtitle=subheader, border_style="cyan"))
else:
console.print(Panel.fit(header, border_style="cyan"))
console.print()
def app():
cls()
show_header()
# check if modules are saved in temp directory, if yes list them and ask if user wants to load one
existing_modules = [f[:-14] for f in os.listdir(temp_dir) if f.endswith("_manifest.json")]
if existing_modules:
console.print("[green]📚 Existing modules found[/green]")
console.print()
# add option to skip loading existing module
existing_modules.append(" Add new module")
questions = [
inquirer.List('module',
message="Select a module to load",
choices=existing_modules,
),
]
answers = inquirer.prompt(questions)
if answers:
if answers['module'] == " Add new module":
pass # Continue to add new module
else:
selected_module = answers['module']
# load manifest
with open(f"{temp_dir}/{selected_module}_manifest.json", "r", encoding="utf-8") as manifest_file:
manifest = json.load(manifest_file)
module['manifest'] = manifest
module['filename'] = selected_module
console.print(f"[green]✓ Module '{module['manifest']['sourceName']}' loaded successfully![/green]")
console.print()
if 'manifest' not in module:
cls()
show_header()
user_input = console.input("[cyan]🔗 Enter module manifest URL or module ID:[/cyan] ").strip()
if user_input.startswith("http"):
# If the input is a URL, fetch the module info directly
get_module_info(user_input)
else:
# Otherwise, treat it as a module ID
get_module_from_id(user_input)
if 'manifest' in module:
console.print(f"[green]✓ Module '{module['manifest']['sourceName']}' loaded successfully![/green]")
else:
console.print("[red]✗ Failed to load module.[/red]")
return
# if file already exists, skip downloading
if os.path.exists(f"{temp_dir}/{module['filename']}.js"):
script_path = f"{temp_dir}/{module['filename']}.js"
else:
script_path = get_module_script(module)
if script_path:
console.print(f"[green]✓ Module script saved[/green]")
# goto search
show_search(script_path)
def show_search(script_path, search_query=None):
"""Search page with navigation"""
global current_search_results, current_search_query
cls()
show_header()
if search_query is None:
search_query = console.input("[cyan]🔍 Enter search query:[/cyan] ").strip()
if not search_query:
search_query = "naruto"
current_search_query = search_query
console.print(f"\n[dim]Searching for '[cyan]{search_query}[/cyan]'...[/dim]")
search_results = run_command("search", [script_path, search_query], silent=True)
if search_results is None or len(search_results) == 0:
console.print("[red]✗ No search results found.[/red]\n")
input("Press Enter to search again...")
show_search(script_path)
return
current_search_results = search_results
cls()
show_header()
# Display search results in a nice panel
console.print(Panel(
f"[cyan]Search Results[/cyan]\n[dim]Found {len(search_results)} results for '{search_query}'[/dim]",
border_style="cyan"
))
console.print()
# Create choices with navigation options
choices = [f" {item['title']}" for item in search_results]
choices.append("" * 50)
choices.append("🔍 New Search")
choices.append("🚪 Exit")
questions = [
inquirer.List('result',
message="Select an option",
choices=choices,
),
]
answers = inquirer.prompt(questions)
if not answers:
return
selection = answers['result']
if selection == "🔍 New Search":
show_search(script_path)
return
elif selection == "🚪 Exit":
return
elif selection.startswith(""):
show_search(script_path, current_search_query)
return
# Find selected item
selected_index = None
for i, item in enumerate(search_results):
if f" {item['title']}" == selection:
selected_index = i
break
if selected_index is not None:
show_details(script_path, search_results[selected_index])
else:
show_search(script_path, current_search_query)
def show_details(script_path, item):
"""Details page with beautiful layout"""
cls()
show_header()
console.print(f"[dim]Loading details...[/dim]\n")
# Fetch details and merge with the original item data
details = run_command("details", [script_path, item['href']], silent=True)
if details:
item = {**item, **details[0]}
cls()
show_header()
# Display title in a prominent panel
title_panel = Panel(
Text(item['title'], style="bold cyan", justify="center"),
border_style="cyan",
box=box.DOUBLE
)
console.print(title_panel)
console.print()
# Display details in a nice table
details_table = Table(show_header=False, box=box.SIMPLE, padding=(0, 2))
details_table.add_column("Field", style="cyan", width=15)
details_table.add_column("Value", style="white")
if 'description' in item and item['description']:
details_table.add_row("📝 Description", item['description'][:200] + "..." if len(item.get('description', '')) > 200 else item.get('description', 'N/A'))
if 'aliases' in item and item['aliases']:
details_table.add_row("⏱️ Duration", item['aliases'])
if 'airdate' in item and item['airdate']:
details_table.add_row("📅 Air Date", item['airdate'])
console.print(Panel(details_table, title="[cyan]Details[/cyan]", border_style="dim"))
console.print()
else:
console.print("[red]✗ Failed to fetch details.[/red]\n")
input("Press Enter to go back...")
show_search(script_path, current_search_query)
return
console.print("[dim]Loading episodes...[/dim]\n")
episodes = run_command("episodes", [script_path, item['href']], silent=True)
if episodes:
# Display episodes in a panel
episodes_panel = Panel(
f"[cyan]Episodes[/cyan]\n[dim]{len(episodes)} episodes available[/dim]",
border_style="cyan"
)
console.print(episodes_panel)
console.print()
# Create episode choices with navigation
choices = [f" Episode {ep['number']}" for ep in episodes]
choices.append("" * 50)
choices.append("⬅️ Back to Search")
choices.append("🚪 Exit")
questions = [
inquirer.List('episode',
message="Select an episode",
choices=choices,
),
]
answers = inquirer.prompt(questions)
if not answers:
show_search(script_path, current_search_query)
return
selection = answers['episode']
if selection == "⬅️ Back to Search":
show_search(script_path, current_search_query)
return
elif selection == "🚪 Exit":
return
elif selection.startswith(""):
show_details(script_path, item)
return
# Extract episode number and find the episode
episode_num = int(selection.split("Episode ")[1])
episode_item = next((ep for ep in episodes if ep['number'] == episode_num), None)
if episode_item:
href = episode_item['href']
if not href.startswith("http"):
# prepend the base url from the module manifest if exists
base_url = module['manifest'].get('baseUrl', '')
href = f"{base_url}/{href.lstrip('/')}"
console.print(f"\n[dim]Fetching stream URL for Episode {episode_item['number']}...[/dim]")
stream_url = run_command("stream", [script_path, href], silent=True)
if stream_url:
# Handle different stream formats
stream_data = None
final_stream_url = None
streams = []
# Parse the response
if isinstance(stream_url, str):
# Check if it's a direct URL (starts with http)
if stream_url.startswith('http'):
final_stream_url = stream_url
else:
# Try to parse as JSON
try:
stream_data = json.loads(stream_url)
except:
# Not JSON, might be just text - skip it
console.print(f"[yellow]⚠ Unexpected stream format: {stream_url}[/yellow]\n")
input("Press Enter to continue...")
show_details(script_path, item)
return
elif isinstance(stream_url, dict):
stream_data = stream_url
elif isinstance(stream_url, list):
# Direct list of streams
streams = stream_url
# Extract URL from parsed data
if stream_data:
if 'streams' in stream_data and isinstance(stream_data['streams'], list) and len(stream_data['streams']) > 0:
# Handle flat list format: ['DUB', 'url1', 'SUB', 'url2']
raw_streams = stream_data['streams']
# Parse the flat list into structured streams
parsed_streams = []
i = 0
while i < len(raw_streams):
item_val = raw_streams[i]
# If it's not a URL, it's likely a label
if isinstance(item_val, str) and not item_val.startswith('http'):
# Next item should be the URL
if i + 1 < len(raw_streams) and isinstance(raw_streams[i + 1], str) and raw_streams[i + 1].startswith('http'):
parsed_streams.append({
'title': item_val,
'streamUrl': raw_streams[i + 1]
})
i += 2
else:
i += 1
elif isinstance(item_val, str) and item_val.startswith('http'):
# URL without label
parsed_streams.append({
'title': f'Server {len(parsed_streams) + 1}',
'streamUrl': item_val
})
i += 1
elif isinstance(item_val, dict):
# Already structured
parsed_streams.append(item_val)
i += 1
else:
i += 1
streams = parsed_streams
if len(streams) == 1:
final_stream_url = streams[0].get('streamUrl')
elif len(streams) > 1:
# Let user choose server
cls()
show_header()
console.print(Panel(
f"[cyan]Select Server for Episode {episode_item['number']}[/cyan]\n[dim]{len(streams)} servers available[/dim]",
border_style="cyan"
))
console.print()
server_choices = [f" {s.get('title', f'Server {i+1}')}" for i, s in enumerate(streams)]
server_choices.append("" * 50)
server_choices.append("⬅️ Back")
questions = [
inquirer.List('server',
message="Select a server",
choices=server_choices,
),
]
answers = inquirer.prompt(questions)
if answers and not answers['server'].startswith("") and answers['server'] != "⬅️ Back":
selected_server_idx = None
for i, s in enumerate(streams):
if f" {s.get('title', f'Server {i+1}')}" == answers['server']:
selected_server_idx = i
break
if selected_server_idx is not None:
final_stream_url = streams[selected_server_idx].get('streamUrl')
else:
show_details(script_path, item)
return
else:
# No valid streams found
console.print("[red]✗ No valid stream URLs found.[/red]\n")
input("Press Enter to continue...")
show_details(script_path, item)
return
elif 'streamUrl' in stream_data:
final_stream_url = stream_data['streamUrl']
else:
# Assume it's a direct URL
final_stream_url = str(stream_data) if stream_data else None
if final_stream_url:
cls()
show_header()
# Display stream URL
stream_panel = Panel(
f"[green]{final_stream_url}[/green]",
title=f"[cyan]Stream URL - Episode {episode_item['number']}[/cyan]",
border_style="green"
)
console.print(stream_panel)
console.print()
# Offer options to open/download
action_choices = [
"▶️ Open in VLC",
"🌐 Open in Browser",
"📺 Open in MPV",
"💾 Download with Browser",
"📋 Copy URL to Clipboard",
"" * 50,
"⬅️ Back to Episodes"
]
questions = [
inquirer.List('action',
message="What would you like to do?",
choices=action_choices,
),
]
answers = inquirer.prompt(questions)
if answers:
action = answers['action']
if action == "▶️ Open in VLC":
console.print(f"\n[dim]Opening in VLC...[/dim]")
try:
subprocess.Popen(['vlc', final_stream_url])
console.print("[green]✓ Opened in VLC[/green]\n")
except FileNotFoundError:
console.print("[yellow]⚠ VLC not found. Please install VLC or add it to PATH.[/yellow]\n")
input("Press Enter to continue...")
show_details(script_path, item)
elif action == "🌐 Open in Browser":
console.print(f"\n[dim]Opening in browser...[/dim]")
import webbrowser
webbrowser.open(final_stream_url)
console.print("[green]✓ Opened in browser[/green]\n")
input("Press Enter to continue...")
show_details(script_path, item)
elif action == "📺 Open in MPV":
console.print(f"\n[dim]Opening in MPV...[/dim]")
try:
subprocess.Popen(['mpv', final_stream_url])
console.print("[green]✓ Opened in MPV[/green]\n")
except FileNotFoundError:
console.print("[yellow]⚠ MPV not found. Please install MPV or add it to PATH.[/yellow]\n")
input("Press Enter to continue...")
show_details(script_path, item)
elif action == "💾 Download with Browser":
console.print(f"\n[dim]Opening download in browser...[/dim]")
import webbrowser
webbrowser.open(final_stream_url)
console.print("[green]✓ Download started in browser[/green]")
console.print("[dim]Note: Some streams may require a download manager or may not be directly downloadable.[/dim]\n")
input("Press Enter to continue...")
show_details(script_path, item)
elif action == "📋 Copy URL to Clipboard":
try:
import pyperclip
pyperclip.copy(final_stream_url)
console.print("[green]✓ URL copied to clipboard[/green]\n")
except ImportError:
console.print("[yellow]⚠ pyperclip not installed. Install with: pip install pyperclip[/yellow]")
console.print(f"\n[cyan]URL:[/cyan] {final_stream_url}\n")
input("Press Enter to continue...")
show_details(script_path, item)
elif action == "⬅️ Back to Episodes":
show_details(script_path, item)
else:
show_details(script_path, item)
else:
console.print("[red]✗ Failed to extract stream URL.[/red]\n")
input("Press Enter to continue...")
show_details(script_path, item)
else:
console.print("[red]✗ Failed to fetch stream URL.[/red]\n")
input("Press Enter to continue...")
show_details(script_path, item)
else:
console.print("[red]✗ Failed to fetch episodes.[/red]\n")
input("Press Enter to go back...")
show_search(script_path, current_search_query)
return
def debug_module(modulePath, debug_function = None, query = None):
"""Function to debug module in debug viewer"""
cls()
show_header()
# clear last debug output
if os.path.exists("last_command_output.json"):
os.remove("last_command_output.json")
if debug_function is not None:
console.print(f"[dim]Debugging module function '{debug_function}' at '{modulePath}' with query '{query}'...[/dim]\n")
debug_data = run_command(debug_function, [modulePath, query], silent=False)
else:
console.print(f"[dim]Running all tests for module at '{modulePath}'...[/dim]\n")
console.print("[dim]Use --function <search|details|episodes|stream> to debug a specific function and provide --query <query> for input.[/dim]\n")
if query is None:
query = "naruto"
print("[dim]1. Search Test[/dim]")
debug_data = run_command("search", [modulePath, query], silent=False)
print("\n[dim]2. Details Test[/dim]")
if debug_data and len(debug_data) > 0:
first_item = debug_data[0]
details_data = run_command("details", [modulePath, first_item['href']], silent=False)
print("\n[dim]3. Episodes Test[/dim]")
episodes_data = run_command("episodes", [modulePath, first_item['href']], silent=False)
if episodes_data and len(episodes_data) > 0:
first_episode = episodes_data[0]
print("\n[dim]4. Stream URL Test[/dim]")
stream_data = run_command("stream", [modulePath, first_episode['href']], silent=False)
console.print("\n[dim]Debugging complete and saved. Reload debug viewer to see changes.[/dim]")
exit(0)
if __name__ == "__main__":
# if --debug or -d is passed, run debug mode
if "--debug" in sys.argv or "-d" in sys.argv:
debug_mode = True
setup()
# expect --path <modulePath> --function <functionName> [--query <query>]
modulePath = None
functionName = None
query = None
if "--path" in sys.argv or "-p" in sys.argv:
path_index = sys.argv.index("--path") + 1
if path_index < len(sys.argv):
modulePath = sys.argv[path_index]
if "--function" in sys.argv or "-f" in sys.argv:
func_index = sys.argv.index("--function") + 1
if func_index < len(sys.argv):
functionName = sys.argv[func_index]
if "--query" in sys.argv or "-q" in sys.argv:
query_index = sys.argv.index("--query") + 1
if query_index < len(sys.argv):
query = sys.argv[query_index]
if modulePath is not None:
debug_module(modulePath, functionName, query)
else:
console.print("[red]✗ Please provide a module path with --path <modulePath>[/red]")
exit(1)
if "--help" in sys.argv or "-h" in sys.argv:
console.print("Usage: python host.py [--debug|-d] [--help|-h]")
console.print("--debug/-d : Run in debug mode to test a module")
console.print("--help/-h : Show this help message")
exit(0)
setup()
app()

BIN
test/modulehost.exe Normal file

Binary file not shown.

3
test/requirements.txt Normal file
View file

@ -0,0 +1,3 @@
requests
rich
inquirer

319
test/run_module.beta.js Normal file
View file

@ -0,0 +1,319 @@
#!/usr/bin/env node
// Simple CLI to load a module JS file and run one of its functions.
// Made by Cufiy
// Works with CommonJS exports or files that define top-level global functions (executed in a VM sandbox).
// Usage examples (PowerShell-safe quoting):
// node run_module.js --path 'temp_modules/AniCrush_(ENG_SUB).js' --function searchResults --param '"naruto"'
// node run_module.js --path 'temp_modules/AniCrush_(ENG_SUB).js' --function extractStreamUrl --param '"_movieId=123&ep=1&sv=4&sc=sub"'
const fs = require('fs');
const path = require('path');
const vm = require('vm');
function parseArgs() {
const args = process.argv.slice(2);
const out = {};
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === '--path' && args[i+1]) { out.path = args[++i]; }
else if (a === '--function' && args[i+1]) { out.function = args[++i]; }
else if (a === '--param' && args[i+1]) { out.param = args[++i]; }
else if (a === '--quiet' || a === '-q') { out.quiet = true; }
else if (a === '--debug' || a === '-d') { out.debug = true; }
else if (a === '--debug-full') { out.debugFull = true; }
else if (a === '--sandbox') { out.sandbox = true; }
else if (a === '--help') { out.help = true; }
}
return out;
}
async function loadModuleWithSandbox(filePath, debugLog, opts = {}) {
// Read file and execute in a VM context so top-level functions become properties on the sandbox.
const code = await fs.promises.readFile(filePath, 'utf8');
// Provide fetchv2 as a wrapper that mimics the module's expected behavior
// helper for conditional logging based on flags
const pushDebug = (msg, level = 'info', source = 'RUNNER') => {
if (opts.quiet && level !== 'error') return;
if (!opts.debug && level === 'debug') return;
const tag = `[${String(level || 'info').toUpperCase()}]`;
const src = `[${String(source || 'RUNNER').toUpperCase()}]`;
const entry = `${tag} ${src} ${String(msg)}`;
debugLog.push(entry);
};
const fetchv2 = async (url, options) => {
// fetch calls initiated by the module — mark as MODULE
pushDebug('[FETCH REQUEST] ' + url + ' ' + formatArg(options), 'debug', 'MODULE');
try {
const response = await fetch(url, options);
// Try to read a clone of the body for transparent logging without consuming the original.
// Attach an 'error' handler to the clone stream to avoid unhandled stream errors
let bodyText = null;
try {
if (response && typeof response.clone === 'function') {
const clone = response.clone();
// If the clone exposes a Node.js stream, capture errors to avoid crashing the process
try {
if (clone && clone.body && typeof clone.body.on === 'function') {
clone.body.on('error', (err) => {
pushDebug('[FETCH RESPONSE BODY STREAM ERROR] ' + (err && err.message), 'error', 'MODULE');
});
}
} catch (e) {
// ignore
}
try {
bodyText = await clone.text();
} catch (e) {
// fallback to arrayBuffer -> string conversion
try {
const buf = await clone.arrayBuffer();
bodyText = Buffer.from(buf).toString('utf8');
} catch (e2) {
bodyText = '[unreadable body]';
}
}
} else if (response && typeof response.text === 'function') {
// fallback (may consume body)
try {
bodyText = await response.text();
} catch (e) {
bodyText = '[unreadable body]';
}
}
} catch (e) {
bodyText = '[unreadable body]';
}
// truncate response body for logs unless debug+debugFull are set
let displayBody = bodyText;
try {
const isFull = Boolean(opts && opts.debug && opts.debugFull);
if (typeof bodyText === 'string' && !isFull) {
displayBody = bodyText.length > 100 ? bodyText.slice(0,100) + '…' : bodyText;
}
} catch (e) {
// ignore
}
pushDebug('[FETCH RESPONSE] ' + url + ' status=' + (response && response.status) + ' body=' + formatArg(displayBody), 'debug', 'MODULE');
return response;
} catch (error) {
pushDebug('[FETCH ERROR] ' + url + ' ' + (error && error.message), 'error', 'MODULE');
throw error;
}
};
const formatArg = (arg) => {
if (typeof arg === 'object' && arg !== null) {
try {
return JSON.stringify(arg);
} catch (e) {
return String(arg);
}
}
return String(arg);
};
const sandbox = {
console: {
log: (...args) => pushDebug(args.map(formatArg).join(' '), 'info', 'MODULE'),
error: (...args) => pushDebug('[ERROR] ' + args.map(formatArg).join(' '), 'error', 'MODULE'),
warn: (...args) => pushDebug('[WARN] ' + args.map(formatArg).join(' '), 'warn', 'MODULE'),
info: (...args) => pushDebug('[INFO] ' + args.map(formatArg).join(' '), 'info', 'MODULE'),
debug: (...args) => pushDebug('[DEBUG] ' + args.map(formatArg).join(' '), 'debug', 'MODULE')
},
// allow access to require and module/exports in case the script uses CommonJS patterns
require: require,
module: { exports: {} },
exports: {},
// override fetch with our logging wrapper (and also expose fetchv2)
fetch: fetchv2,
fetchv2: fetchv2,
// Provide soraFetch as an alias in case modules use it
soraFetch: fetchv2,
// Inject small obfuscated helper if modules expect it
_0xB4F2: function() { return "accinrxxxxxxxxxx"; },
// Encoding functions
encodeURIComponent: encodeURIComponent,
decodeURIComponent: decodeURIComponent,
// JSON operations
JSON: JSON,
// Promise support
Promise: Promise,
// small helper to avoid accidental infinite loops
setTimeout,
setInterval,
clearTimeout,
clearInterval
};
const context = vm.createContext(sandbox);
try {
const script = new vm.Script(code, { filename: filePath });
script.runInContext(context, { timeout: 5000 });
} catch (e) {
// running the file might fail if it expects browser globals — that's okay, we still return what we have
// silently continue
}
// Merge module.exports/exports into sandbox for easier lookup
const mod = Object.assign({}, sandbox.module && sandbox.module.exports ? sandbox.module.exports : {}, sandbox.exports || {});
// Also attach any top-level functions that were defined (e.g., function searchResults() { ... })
// We need to iterate over sandbox properties, not context
for (const k of Object.keys(sandbox)) {
if (!(k in mod) && typeof sandbox[k] === 'function') {
mod[k] = sandbox[k];
}
}
return mod;
}
async function main() {
const opts = parseArgs();
const debugLog = [];
if (opts.help || !opts.path || !opts.function) {
console.log(JSON.stringify({
status: 'error',
message: 'Usage: node run_module.js --path <file> --function <fnName> [--param <jsonOrString>]',
data: null,
debug: []
}));
process.exit(opts.help ? 0 : 1);
}
const filePath = path.resolve(opts.path);
if (!fs.existsSync(filePath)) {
console.log(JSON.stringify({
status: 'error',
message: 'File not found: ' + filePath,
data: null,
debug: []
}));
process.exit(2);
}
let mod = {};
// Optionally prefer sandbox execution (avoids require side-effects and lets us hook internal functions)
// Load module: prefer sandbox first (so we can hook top-level functions). If sandbox doesn't expose the requested
// function, fall back to require(). If --sandbox is set, only use sandbox.
if (opts.sandbox) {
mod = await loadModuleWithSandbox(filePath, debugLog, opts);
} else {
try {
mod = await loadModuleWithSandbox(filePath, debugLog, opts);
} catch (e) {
// ignore sandbox errors
}
if (!mod || !(opts.function in mod)) {
try {
delete require.cache[require.resolve(filePath)];
const required = require(filePath);
if (required && (typeof required === 'object' || typeof required === 'function')) {
mod = required;
}
} catch (e) {
// ignore require errors
}
}
}
// helper for main-scope debug logging (used by wrappers)
const pushMainDebug = (msg, level = 'info') => {
if (opts.quiet && level !== 'error') return;
if (!opts.debug && level === 'debug') return;
const tag = `[${String(level || 'info').toUpperCase()}]`;
const src = `[RUNNER]`;
debugLog.push(`${tag} ${src} ${String(msg)}`);
};
// If module exposes multiExtractor, wrap it to log providers when --debug
const wrapMulti = (fn) => {
const _orig = fn;
return async function(providers) {
if (opts && opts.debug) {
try {
pushMainDebug('[multiExtractor providers] ' + (typeof providers === 'object' ? JSON.stringify(providers) : String(providers)), 'debug');
} catch (e) {
pushMainDebug('[multiExtractor providers] [unserializable]', 'debug');
}
}
return await _orig.apply(this, arguments);
};
};
if (mod) {
if (typeof mod.multiExtractor === 'function') mod.multiExtractor = wrapMulti(mod.multiExtractor);
if (typeof mod === 'function' && mod.name === 'multiExtractor') mod = { multiExtractor: wrapMulti(mod) };
if (mod.default && typeof mod.default === 'function' && mod.default.name === 'multiExtractor') {
mod.default = wrapMulti(mod.default);
mod.multiExtractor = mod.default;
}
}
let fn = mod[opts.function];
// also check default export
if (!fn && mod.default && typeof mod.default === 'object') fn = mod.default[opts.function];
if (typeof fn !== 'function') {
console.log(JSON.stringify({
status: 'error',
message: `Function '${opts.function}' not found in module`,
data: null,
debug: debugLog
}));
process.exit(4);
}
let param = opts.param;
// try parse JSON, fallback to string
try {
if (param) param = JSON.parse(param);
} catch (e) {
// leave as string
}
try {
const maybeResult = fn.length === 0 ? fn() : fn(param);
// Check if it's a Promise - handle cross-context Promises
const result = (maybeResult && typeof maybeResult.then === 'function') ? await maybeResult : maybeResult;
// Parse the result if it's a JSON string (modules return JSON.stringify'd data)
let data = result;
if (typeof result === 'string') {
try {
data = JSON.parse(result);
} catch (e) {
// if it's not valid JSON, keep it as string
data = result;
}
}
// Prepare debug output: if quiet -> empty; if debug -> all entries; otherwise filter out DEBUG-level entries
let outDebug = [];
if (opts.quiet) outDebug = [];
else if (opts.debug) outDebug = debugLog;
else outDebug = debugLog.filter(d => !String(d).startsWith('[DEBUG]'));
console.log(JSON.stringify({
status: 'success',
message: 'Fetch successful',
data: data,
debug: outDebug
}));
} catch (e) {
console.log(JSON.stringify({
status: 'error',
message: 'Error executing function: ' + e.message,
data: null,
debug: debugLog
}));
process.exit(5);
}
}
main();