mirror of
https://github.com/Arrowar/StreamingCommunity.git
synced 2025-06-07 20:15:24 +00:00
Compare commits
No commits in common. "main" and "v3.0.7" have entirely different histories.
360
.github/.domain/domain_update.py
vendored
360
.github/.domain/domain_update.py
vendored
@ -1,360 +0,0 @@
|
|||||||
# 20.04.2024
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
from urllib.parse import urlparse, unquote
|
|
||||||
|
|
||||||
|
|
||||||
# External libraries
|
|
||||||
import httpx
|
|
||||||
import tldextract
|
|
||||||
import ua_generator
|
|
||||||
import dns.resolver
|
|
||||||
|
|
||||||
|
|
||||||
# Variables
|
|
||||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
JSON_FILE_PATH = os.path.join(SCRIPT_DIR, "domains.json")
|
|
||||||
ua = ua_generator.generate(device='desktop', browser=('chrome', 'edge'))
|
|
||||||
|
|
||||||
|
|
||||||
def get_headers():
|
|
||||||
return ua.headers.get()
|
|
||||||
|
|
||||||
def get_tld(url_str):
|
|
||||||
try:
|
|
||||||
parsed = urlparse(unquote(url_str))
|
|
||||||
domain = parsed.netloc.lower().lstrip('www.')
|
|
||||||
parts = domain.split('.')
|
|
||||||
return parts[-1] if len(parts) >= 2 else None
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_base_domain(url_str):
|
|
||||||
try:
|
|
||||||
parsed = urlparse(url_str)
|
|
||||||
domain = parsed.netloc.lower().lstrip('www.')
|
|
||||||
parts = domain.split('.')
|
|
||||||
return '.'.join(parts[:-1]) if len(parts) > 2 else parts[0]
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_base_url(url_str):
|
|
||||||
try:
|
|
||||||
parsed = urlparse(url_str)
|
|
||||||
return f"{parsed.scheme}://{parsed.netloc}"
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def log(msg, level='INFO'):
|
|
||||||
levels = {
|
|
||||||
'INFO': '[ ]',
|
|
||||||
'SUCCESS': '[+]',
|
|
||||||
'WARNING': '[!]',
|
|
||||||
'ERROR': '[-]'
|
|
||||||
}
|
|
||||||
entry = f"{levels.get(level, '[?]')} {msg}"
|
|
||||||
print(entry)
|
|
||||||
|
|
||||||
def load_json_data(file_path):
|
|
||||||
if not os.path.exists(file_path):
|
|
||||||
log(f"Error: The file {file_path} was not found.", "ERROR")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log(f"Error reading the file {file_path}: {e}", "ERROR")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def save_json_data(file_path, data):
|
|
||||||
try:
|
|
||||||
with open(file_path, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
||||||
log(f"Data successfully saved to {file_path}", "SUCCESS")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log(f"Error saving the file {file_path}: {e}", "ERROR")
|
|
||||||
|
|
||||||
def parse_url(url):
|
|
||||||
if not url.startswith(('http://', 'https://')):
|
|
||||||
url = 'https://' + url
|
|
||||||
|
|
||||||
try:
|
|
||||||
extracted = tldextract.extract(url)
|
|
||||||
parsed = urlparse(url)
|
|
||||||
clean_url = f"{parsed.scheme}://{parsed.netloc}/"
|
|
||||||
full_domain = f"{extracted.domain}.{extracted.suffix}" if extracted.domain else extracted.suffix
|
|
||||||
domain_tld = extracted.suffix
|
|
||||||
result = {
|
|
||||||
'url': clean_url,
|
|
||||||
'full_domain': full_domain,
|
|
||||||
'domain': domain_tld,
|
|
||||||
'suffix': extracted.suffix,
|
|
||||||
'subdomain': extracted.subdomain or None
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log(f"Error parsing URL: {e}", "ERROR")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def check_dns_resolution(domain):
|
|
||||||
try:
|
|
||||||
resolver = dns.resolver.Resolver()
|
|
||||||
resolver.timeout = 2
|
|
||||||
resolver.lifetime = 2
|
|
||||||
|
|
||||||
try:
|
|
||||||
answers = resolver.resolve(domain, 'A')
|
|
||||||
return str(answers[0])
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
answers = resolver.resolve(domain, 'AAAA')
|
|
||||||
return str(answers[0])
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def find_new_domain(input_url, output_file=None, verbose=True, json_output=False):
|
|
||||||
log_buffer = []
|
|
||||||
original_info = parse_url(input_url)
|
|
||||||
|
|
||||||
if not original_info:
|
|
||||||
log(f"Could not parse original URL: {input_url}", "ERROR")
|
|
||||||
if json_output:
|
|
||||||
return {'full_url': input_url, 'domain': None}
|
|
||||||
return None
|
|
||||||
|
|
||||||
log(f"Starting analysis for: {original_info['full_domain']}")
|
|
||||||
orig_ip = check_dns_resolution(original_info['full_domain'])
|
|
||||||
if orig_ip:
|
|
||||||
log(f"Original domain resolves to: {orig_ip}", "SUCCESS")
|
|
||||||
else:
|
|
||||||
log(f"Original domain does not resolve to an IP address", "WARNING")
|
|
||||||
|
|
||||||
headers = get_headers()
|
|
||||||
new_domains = []
|
|
||||||
redirects = []
|
|
||||||
final_url = None
|
|
||||||
final_domain_info = None
|
|
||||||
url_to_test_in_loop = None
|
|
||||||
|
|
||||||
for protocol in ['https://', 'http://']:
|
|
||||||
try:
|
|
||||||
url_to_test_in_loop = f"{protocol}{original_info['full_domain']}"
|
|
||||||
log(f"Testing connectivity to {url_to_test_in_loop}")
|
|
||||||
redirect_chain = []
|
|
||||||
current_url = url_to_test_in_loop
|
|
||||||
max_redirects = 10
|
|
||||||
redirect_count = 0
|
|
||||||
|
|
||||||
while redirect_count < max_redirects:
|
|
||||||
with httpx.Client(verify=False, follow_redirects=False, timeout=5) as client:
|
|
||||||
response = client.get(current_url, headers=headers)
|
|
||||||
|
|
||||||
redirect_info = {'url': current_url, 'status_code': response.status_code}
|
|
||||||
redirect_chain.append(redirect_info)
|
|
||||||
log(f"Request to {current_url} - Status: {response.status_code}")
|
|
||||||
|
|
||||||
if response.status_code in (301, 302, 303, 307, 308):
|
|
||||||
if 'location' in response.headers:
|
|
||||||
next_url = response.headers['location']
|
|
||||||
if next_url.startswith('/'):
|
|
||||||
parsed_current = urlparse(current_url)
|
|
||||||
next_url = f"{parsed_current.scheme}://{parsed_current.netloc}{next_url}"
|
|
||||||
|
|
||||||
log(f"Redirect found: {next_url} (Status: {response.status_code})")
|
|
||||||
current_url = next_url
|
|
||||||
redirect_count += 1
|
|
||||||
redirect_domain_info_val = parse_url(next_url)
|
|
||||||
if redirect_domain_info_val and redirect_domain_info_val['full_domain'] != original_info['full_domain']:
|
|
||||||
new_domains.append({'domain': redirect_domain_info_val['full_domain'], 'url': next_url, 'source': 'redirect'})
|
|
||||||
|
|
||||||
else:
|
|
||||||
log(f"Redirect status code but no Location header", "WARNING")
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
if redirect_chain:
|
|
||||||
final_url = redirect_chain[-1]['url']
|
|
||||||
final_domain_info = parse_url(final_url)
|
|
||||||
redirects.extend(redirect_chain)
|
|
||||||
log(f"Final URL after redirects: {final_url}", "SUCCESS")
|
|
||||||
if final_domain_info and final_domain_info['full_domain'] != original_info['full_domain']:
|
|
||||||
new_domains.append({'domain': final_domain_info['full_domain'], 'url': final_url, 'source': 'final_url'})
|
|
||||||
|
|
||||||
final_status = redirect_chain[-1]['status_code'] if redirect_chain else None
|
|
||||||
|
|
||||||
if final_status and final_status < 400 and final_status != 403:
|
|
||||||
break
|
|
||||||
|
|
||||||
if final_status == 403 and redirect_chain and len(redirect_chain) > 1:
|
|
||||||
log(f"Got 403 Forbidden, but captured {len(redirect_chain)-1} redirects before that", "SUCCESS")
|
|
||||||
break
|
|
||||||
|
|
||||||
except httpx.RequestError as e:
|
|
||||||
log(f"Error connecting to {protocol}{original_info['full_domain']}: {str(e)}", "ERROR")
|
|
||||||
|
|
||||||
url_for_auto_redirect = input_url
|
|
||||||
if url_to_test_in_loop:
|
|
||||||
url_for_auto_redirect = url_to_test_in_loop
|
|
||||||
elif original_info and original_info.get('url'):
|
|
||||||
url_for_auto_redirect = original_info['url']
|
|
||||||
|
|
||||||
if not redirects or not new_domains:
|
|
||||||
log("Trying alternate method with automatic redirect following")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with httpx.Client(verify=False, follow_redirects=True, timeout=5) as client:
|
|
||||||
response_auto = client.get(url_for_auto_redirect, headers=headers)
|
|
||||||
|
|
||||||
log(f"Connected with auto-redirects: Status {response_auto.status_code}")
|
|
||||||
|
|
||||||
if response_auto.history:
|
|
||||||
log(f"Found {len(response_auto.history)} redirects with auto-following", "SUCCESS")
|
|
||||||
|
|
||||||
for r_hist in response_auto.history:
|
|
||||||
redirect_info_auto = {'url': str(r_hist.url), 'status_code': r_hist.status_code}
|
|
||||||
redirects.append(redirect_info_auto)
|
|
||||||
log(f"Auto-redirect: {r_hist.url} (Status: {r_hist.status_code})")
|
|
||||||
|
|
||||||
final_url = str(response_auto.url)
|
|
||||||
final_domain_info = parse_url(final_url)
|
|
||||||
for redirect_hist_item in response_auto.history:
|
|
||||||
redirect_domain_val = parse_url(str(redirect_hist_item.url))
|
|
||||||
if redirect_domain_val and original_info and redirect_domain_val['full_domain'] != original_info['full_domain']:
|
|
||||||
new_domains.append({'domain': redirect_domain_val['full_domain'], 'url': str(redirect_hist_item.url), 'source': 'auto-redirect'})
|
|
||||||
|
|
||||||
current_final_url_info = parse_url(str(response_auto.url))
|
|
||||||
|
|
||||||
if current_final_url_info and original_info and current_final_url_info['full_domain'] != original_info['full_domain']:
|
|
||||||
is_already_added = any(d['domain'] == current_final_url_info['full_domain'] and d['source'] == 'auto-redirect' for d in new_domains)
|
|
||||||
if not is_already_added:
|
|
||||||
new_domains.append({'domain': current_final_url_info['full_domain'], 'url': str(response_auto.url), 'source': 'final_url_auto'})
|
|
||||||
final_url = str(response_auto.url)
|
|
||||||
final_domain_info = current_final_url_info
|
|
||||||
log(f"Final URL from auto-redirect: {final_url}", "SUCCESS")
|
|
||||||
|
|
||||||
except httpx.RequestError as e:
|
|
||||||
log(f"Error with auto-redirect attempt: {str(e)}", "ERROR")
|
|
||||||
except NameError:
|
|
||||||
log(f"Error: URL for auto-redirect attempt was not defined.", "ERROR")
|
|
||||||
|
|
||||||
unique_domains = []
|
|
||||||
seen_domains = set()
|
|
||||||
for domain_info_item in new_domains:
|
|
||||||
if domain_info_item['domain'] not in seen_domains:
|
|
||||||
seen_domains.add(domain_info_item['domain'])
|
|
||||||
unique_domains.append(domain_info_item)
|
|
||||||
|
|
||||||
if not final_url:
|
|
||||||
final_url = input_url
|
|
||||||
if not final_domain_info:
|
|
||||||
final_domain_info = original_info
|
|
||||||
|
|
||||||
if final_domain_info:
|
|
||||||
parsed_final_url_info = parse_url(final_url)
|
|
||||||
if parsed_final_url_info:
|
|
||||||
final_url = parsed_final_url_info['url']
|
|
||||||
final_domain_info = parsed_final_url_info
|
|
||||||
else:
|
|
||||||
final_domain_info = original_info
|
|
||||||
final_url = original_info['url'] if original_info else input_url
|
|
||||||
|
|
||||||
results_original_domain = original_info['full_domain'] if original_info else None
|
|
||||||
results_final_domain_tld = final_domain_info['domain'] if final_domain_info and 'domain' in final_domain_info else None
|
|
||||||
|
|
||||||
results = {
|
|
||||||
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
'original_url': input_url,
|
|
||||||
'original_domain': results_original_domain,
|
|
||||||
'original_ip': orig_ip,
|
|
||||||
'new_domains': unique_domains,
|
|
||||||
'redirects': redirects,
|
|
||||||
'log': log_buffer
|
|
||||||
}
|
|
||||||
simplified_json_output = {'full_url': final_url, 'domain': results_final_domain_tld}
|
|
||||||
|
|
||||||
if verbose:
|
|
||||||
log(f"DEBUG - Simplified output: {simplified_json_output}", "INFO")
|
|
||||||
|
|
||||||
if output_file:
|
|
||||||
try:
|
|
||||||
with open(output_file, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(results, f, indent=2, ensure_ascii=False)
|
|
||||||
log(f"Results saved to {output_file}", "SUCCESS")
|
|
||||||
except Exception as e:
|
|
||||||
log(f"Error writing to output file: {str(e)}", "ERROR")
|
|
||||||
|
|
||||||
if json_output:
|
|
||||||
return simplified_json_output
|
|
||||||
else:
|
|
||||||
return results
|
|
||||||
|
|
||||||
def update_site_entry(site_name: str, all_domains_data: dict):
|
|
||||||
site_config = all_domains_data.get(site_name, {})
|
|
||||||
log(f"Processing site: {site_name}", "INFO")
|
|
||||||
if not site_config.get('full_url'):
|
|
||||||
log(f"Site {site_name} has no full_url in config. Skipping.", "WARNING")
|
|
||||||
return False
|
|
||||||
|
|
||||||
current_full_url = site_config.get('full_url')
|
|
||||||
current_domain_tld = site_config.get('domain')
|
|
||||||
found_domain_info = find_new_domain(current_full_url, verbose=False, json_output=True)
|
|
||||||
|
|
||||||
if found_domain_info and found_domain_info.get('full_url') and found_domain_info.get('domain'):
|
|
||||||
new_full_url = found_domain_info['full_url']
|
|
||||||
new_domain_tld = found_domain_info['domain']
|
|
||||||
|
|
||||||
if new_full_url != current_full_url or new_domain_tld != current_domain_tld:
|
|
||||||
log(f"Update found for {site_name}: URL '{current_full_url}' -> '{new_full_url}', TLD '{current_domain_tld}' -> '{new_domain_tld}'", "SUCCESS")
|
|
||||||
updated_entry = site_config.copy()
|
|
||||||
updated_entry['full_url'] = new_full_url
|
|
||||||
updated_entry['domain'] = new_domain_tld
|
|
||||||
if new_domain_tld != current_domain_tld :
|
|
||||||
updated_entry['old_domain'] = current_domain_tld if current_domain_tld else ""
|
|
||||||
|
|
||||||
updated_entry['time_change'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
all_domains_data[site_name] = updated_entry
|
|
||||||
return True
|
|
||||||
|
|
||||||
else:
|
|
||||||
log(f"No changes detected for {site_name}.", "INFO")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
log(f"Could not reliably find new domain info for {site_name} from URL: {current_full_url}. No search fallback.", "WARNING")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
|
||||||
log("Starting domain update script...")
|
|
||||||
all_domains_data = load_json_data(JSON_FILE_PATH)
|
|
||||||
if not all_domains_data:
|
|
||||||
log("Cannot proceed: Domain data is missing or could not be loaded.", "ERROR")
|
|
||||||
log("Script finished.")
|
|
||||||
return
|
|
||||||
|
|
||||||
any_updates_made = False
|
|
||||||
for site_name_key in list(all_domains_data.keys()):
|
|
||||||
if update_site_entry(site_name_key, all_domains_data):
|
|
||||||
any_updates_made = True
|
|
||||||
print("\n")
|
|
||||||
|
|
||||||
if any_updates_made:
|
|
||||||
save_json_data(JSON_FILE_PATH, all_domains_data)
|
|
||||||
log("Update complete. Some entries were modified.", "SUCCESS")
|
|
||||||
else:
|
|
||||||
log("Update complete. No domains were modified.", "INFO")
|
|
||||||
log("Script finished.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
62
.github/.domain/domains.json
vendored
62
.github/.domain/domains.json
vendored
@ -1,62 +0,0 @@
|
|||||||
{
|
|
||||||
"1337xx": {
|
|
||||||
"domain": "to",
|
|
||||||
"full_url": "https://www.1337xx.to/",
|
|
||||||
"old_domain": "to",
|
|
||||||
"time_change": "2025-03-19 12:20:19"
|
|
||||||
},
|
|
||||||
"cb01new": {
|
|
||||||
"domain": "digital",
|
|
||||||
"full_url": "https://cb01net.digital/",
|
|
||||||
"old_domain": "life",
|
|
||||||
"time_change": "2025-06-07 07:18:34"
|
|
||||||
},
|
|
||||||
"animeunity": {
|
|
||||||
"domain": "so",
|
|
||||||
"full_url": "https://www.animeunity.so/",
|
|
||||||
"old_domain": "so",
|
|
||||||
"time_change": "2025-03-19 12:20:23"
|
|
||||||
},
|
|
||||||
"animeworld": {
|
|
||||||
"domain": "ac",
|
|
||||||
"full_url": "https://www.animeworld.ac/",
|
|
||||||
"old_domain": "ac",
|
|
||||||
"time_change": "2025-03-21 12:20:27"
|
|
||||||
},
|
|
||||||
"guardaserie": {
|
|
||||||
"domain": "meme",
|
|
||||||
"full_url": "https://guardaserie.meme/",
|
|
||||||
"old_domain": "meme",
|
|
||||||
"time_change": "2025-03-19 12:20:24"
|
|
||||||
},
|
|
||||||
"ddlstreamitaly": {
|
|
||||||
"domain": "co",
|
|
||||||
"full_url": "https://ddlstreamitaly.co/",
|
|
||||||
"old_domain": "co",
|
|
||||||
"time_change": "2025-03-19 12:20:26"
|
|
||||||
},
|
|
||||||
"streamingwatch": {
|
|
||||||
"domain": "org",
|
|
||||||
"full_url": "https://www.streamingwatch.org/",
|
|
||||||
"old_domain": "org",
|
|
||||||
"time_change": "2025-04-29 12:30:30"
|
|
||||||
},
|
|
||||||
"altadefinizione": {
|
|
||||||
"domain": "spa",
|
|
||||||
"full_url": "https://altadefinizione.spa/",
|
|
||||||
"old_domain": "locker",
|
|
||||||
"time_change": "2025-05-26 23:22:45"
|
|
||||||
},
|
|
||||||
"streamingcommunity": {
|
|
||||||
"domain": "art",
|
|
||||||
"full_url": "https://streamingunity.art/",
|
|
||||||
"old_domain": "bid",
|
|
||||||
"time_change": "2025-06-05 11:18:33"
|
|
||||||
},
|
|
||||||
"altadefinizionegratis": {
|
|
||||||
"domain": "cc",
|
|
||||||
"full_url": "https://altadefinizionegratis.cc/",
|
|
||||||
"old_domain": "icu",
|
|
||||||
"time_change": "2025-06-02 10:35:25"
|
|
||||||
}
|
|
||||||
}
|
|
525
.github/.site/css/style.css
vendored
525
.github/.site/css/style.css
vendored
@ -38,11 +38,14 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
header {
|
||||||
max-width: 1400px;
|
background-color: var(--header-bg);
|
||||||
margin: 0 auto;
|
backdrop-filter: blur(10px);
|
||||||
padding: 20px;
|
position: fixed;
|
||||||
flex: 1;
|
width: 100%;
|
||||||
|
padding: 15px 0;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 12px var(--shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-container {
|
.header-container {
|
||||||
@ -85,6 +88,13 @@ body {
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.site-grid {
|
.site-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
@ -156,6 +166,78 @@ body {
|
|||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-content {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain {
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-item a {
|
||||||
|
margin-top: 1rem;
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-item a:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-title {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
text-align: center;
|
||||||
|
width: 80%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-item:hover .site-title {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-item::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-item:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.site-info {
|
.site-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -182,211 +264,6 @@ body {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-status {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #4CAF50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-status.offline {
|
|
||||||
background: #f44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background: var(--card-background);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 15px 20px;
|
|
||||||
box-shadow: 0 4px 20px var(--shadow-color);
|
|
||||||
z-index: 1001;
|
|
||||||
min-width: 280px;
|
|
||||||
max-width: 400px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.hidden {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-20px);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: 2px solid var(--primary-color);
|
|
||||||
border-radius: 50%;
|
|
||||||
border-top-color: transparent;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-icon.ready {
|
|
||||||
border: none;
|
|
||||||
background: #4CAF50;
|
|
||||||
animation: none;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-icon.ready::after {
|
|
||||||
content: '✓';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
color: white;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checking-sites {
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
background: var(--background-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.checking-site {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: between;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--card-background);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checking-site.completed {
|
|
||||||
opacity: 0.6;
|
|
||||||
background: var(--card-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.checking-site.online {
|
|
||||||
border-left: 3px solid #4CAF50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checking-site.offline {
|
|
||||||
border-left: 3px solid #f44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checking-site .site-name {
|
|
||||||
flex: 1;
|
|
||||||
font-weight: 500;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checking-site .site-status-icon {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checking-site .site-status-icon.checking {
|
|
||||||
background: var(--primary-color);
|
|
||||||
animation: pulse 1s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checking-site .site-status-icon.online {
|
|
||||||
background: #4CAF50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checking-site .site-status-icon.offline {
|
|
||||||
background: #f44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.5; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 6px;
|
|
||||||
background: var(--background-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
|
|
||||||
width: 0%;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border: 3px solid var(--primary-color);
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
animation: rotation 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
box-sizing: border-box;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 3px solid transparent;
|
|
||||||
border-bottom-color: var(--accent-color);
|
|
||||||
animation: rotationBack 0.5s linear infinite;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotation {
|
|
||||||
0% { transform: rotate(0deg) }
|
|
||||||
100% { transform: rotate(360deg) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotationBack {
|
|
||||||
0% { transform: rotate(0deg) }
|
|
||||||
100% { transform: rotate(-360deg) }
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background: var(--card-background);
|
background: var(--card-background);
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
@ -478,6 +355,26 @@ footer {
|
|||||||
transform: scale(1.2);
|
transform: scale(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.github-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-badge {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-badge i {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
.footer-description {
|
.footer-description {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@ -486,13 +383,103 @@ footer {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.update-info {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 30px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
.update-note {
|
.update-note {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsiveness */
|
.theme-toggle {
|
||||||
|
position: relative;
|
||||||
|
top: unset;
|
||||||
|
right: unset;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle label {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--background-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 0 10px var(--shadow-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle label:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle .fa-sun {
|
||||||
|
display: none;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle .fa-moon {
|
||||||
|
color: #8c52ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle input:checked ~ label .fa-sun {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle input:checked ~ label .fa-moon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 3px solid var(--primary-color);
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: rotation 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-bottom-color: var(--accent-color);
|
||||||
|
animation: rotationBack 0.5s linear infinite;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotation {
|
||||||
|
0% { transform: rotate(0deg) }
|
||||||
|
100% { transform: rotate(360deg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotationBack {
|
||||||
|
0% { transform: rotate(0deg) }
|
||||||
|
100% { transform: rotate(-360deg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improved Responsiveness */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.site-grid {
|
.site-grid {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
@ -509,7 +496,11 @@ footer {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
text-align: center;
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-container {
|
.header-container {
|
||||||
@ -526,6 +517,27 @@ footer {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.site-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-item {
|
||||||
|
min-height: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.footer-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.footer-title::after {
|
.footer-title::after {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -545,16 +557,83 @@ footer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
.time-change {
|
||||||
.site-grid {
|
color: var(--text-color);
|
||||||
grid-template-columns: 1fr;
|
opacity: 0.7;
|
||||||
}
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.site-item {
|
.label {
|
||||||
min-height: 220px;
|
color: var(--accent-color);
|
||||||
}
|
font-weight: 500;
|
||||||
|
}
|
||||||
.container {
|
|
||||||
padding: 10px;
|
.controls-container {
|
||||||
}
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: var(--card-background);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-controls label {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-controls select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-controls select:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sites-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-sites, .last-update-global {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-sites i, .last-update-global i {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-status.offline {
|
||||||
|
background: #f44336;
|
||||||
}
|
}
|
180
.github/.site/js/script.js
vendored
180
.github/.site/js/script.js
vendored
@ -1,82 +1,32 @@
|
|||||||
document.documentElement.setAttribute('data-theme', 'dark');
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
|
||||||
let statusIndicator = null;
|
function initGridControls() {
|
||||||
let checkingSites = new Map();
|
const gridSize = document.getElementById('grid-size');
|
||||||
let totalSites = 0;
|
const siteGrid = document.querySelector('.site-grid');
|
||||||
let completedSites = 0;
|
|
||||||
|
gridSize.addEventListener('change', function() {
|
||||||
|
switch(this.value) {
|
||||||
|
case 'small':
|
||||||
|
siteGrid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(200px, 1fr))';
|
||||||
|
break;
|
||||||
|
case 'medium':
|
||||||
|
siteGrid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(300px, 1fr))';
|
||||||
|
break;
|
||||||
|
case 'large':
|
||||||
|
siteGrid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(400px, 1fr))';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
localStorage.setItem('preferredGridSize', this.value);
|
||||||
|
});
|
||||||
|
|
||||||
function createStatusIndicator() {
|
const savedSize = localStorage.getItem('preferredGridSize');
|
||||||
statusIndicator = document.createElement('div');
|
if (savedSize) {
|
||||||
statusIndicator.className = 'status-indicator';
|
gridSize.value = savedSize;
|
||||||
statusIndicator.innerHTML = `
|
gridSize.dispatchEvent(new Event('change'));
|
||||||
<div class="status-header">
|
|
||||||
<div class="status-icon"></div>
|
|
||||||
<span class="status-title">Loading Sites...</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-text">Initializing site checks...</div>
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="progress-fill"></div>
|
|
||||||
</div>
|
|
||||||
<div class="checking-sites"></div>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(statusIndicator);
|
|
||||||
return statusIndicator;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateStatusIndicator(status, text, progress = 0) {
|
|
||||||
if (!statusIndicator) return;
|
|
||||||
|
|
||||||
const statusIcon = statusIndicator.querySelector('.status-icon');
|
|
||||||
const statusTitle = statusIndicator.querySelector('.status-title');
|
|
||||||
const statusText = statusIndicator.querySelector('.status-text');
|
|
||||||
const progressFill = statusIndicator.querySelector('.progress-fill');
|
|
||||||
|
|
||||||
statusTitle.textContent = status;
|
|
||||||
statusText.textContent = text;
|
|
||||||
progressFill.style.width = `${progress}%`;
|
|
||||||
|
|
||||||
if (status === 'Ready') {
|
|
||||||
statusIcon.classList.add('ready');
|
|
||||||
setTimeout(() => {
|
|
||||||
statusIndicator.classList.add('hidden');
|
|
||||||
setTimeout(() => statusIndicator.remove(), 300);
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSiteToCheck(siteName, siteUrl) {
|
async function checkSiteStatus(url) {
|
||||||
if (!statusIndicator) return;
|
|
||||||
|
|
||||||
const checkingSitesContainer = statusIndicator.querySelector('.checking-sites');
|
|
||||||
const siteElement = document.createElement('div');
|
|
||||||
siteElement.className = 'checking-site';
|
|
||||||
siteElement.innerHTML = `
|
|
||||||
<span class="site-name">${siteName}</span>
|
|
||||||
<div class="site-status-icon checking"></div>
|
|
||||||
`;
|
|
||||||
checkingSitesContainer.appendChild(siteElement);
|
|
||||||
checkingSites.set(siteName, siteElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSiteStatus(siteName, isOnline) {
|
|
||||||
const siteElement = checkingSites.get(siteName);
|
|
||||||
if (!siteElement) return;
|
|
||||||
|
|
||||||
const statusIcon = siteElement.querySelector('.site-status-icon');
|
|
||||||
statusIcon.classList.remove('checking');
|
|
||||||
statusIcon.classList.add(isOnline ? 'online' : 'offline');
|
|
||||||
siteElement.classList.add('completed', isOnline ? 'online' : 'offline');
|
|
||||||
|
|
||||||
completedSites++;
|
|
||||||
const progress = (completedSites / totalSites) * 100;
|
|
||||||
updateStatusIndicator(
|
|
||||||
'Checking Sites...',
|
|
||||||
`Checked ${completedSites}/${totalSites} sites`,
|
|
||||||
progress
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkSiteStatus(url, siteName) {
|
|
||||||
try {
|
try {
|
||||||
console.log(`Checking status for: ${url}`);
|
console.log(`Checking status for: ${url}`);
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@ -96,75 +46,66 @@ async function checkSiteStatus(url, siteName) {
|
|||||||
|
|
||||||
const isOnline = response.type === 'opaque';
|
const isOnline = response.type === 'opaque';
|
||||||
console.log(`Site ${url} is ${isOnline ? 'online' : 'offline'} (Type: ${response.type})`);
|
console.log(`Site ${url} is ${isOnline ? 'online' : 'offline'} (Type: ${response.type})`);
|
||||||
|
|
||||||
if (siteName) {
|
|
||||||
updateSiteStatus(siteName, isOnline);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isOnline;
|
return isOnline;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Error checking ${url}:`, error.message);
|
console.log(`Error checking ${url}:`, error.message);
|
||||||
|
|
||||||
if (siteName) {
|
|
||||||
updateSiteStatus(siteName, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const domainsJsonUrl = 'https://raw.githubusercontent.com/Arrowar/StreamingCommunity/refs/heads/main/.github/.domain/domains.json';
|
const supabaseUrl = 'https://zvfngpoxwrgswnzytadh.supabase.co';
|
||||||
|
const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp2Zm5ncG94d3Jnc3duenl0YWRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDAxNTIxNjMsImV4cCI6MjA1NTcyODE2M30.FNTCCMwi0QaKjOu8gtZsT5yQttUW8QiDDGXmzkn89QE';
|
||||||
|
|
||||||
async function loadSiteData() {
|
async function loadSiteData() {
|
||||||
try {
|
try {
|
||||||
console.log('Starting to load site data from GitHub...');
|
console.log('Starting to load site data...');
|
||||||
|
|
||||||
createStatusIndicator();
|
|
||||||
updateStatusIndicator('Loading...', 'Fetching site data from GitHub repository...', 0);
|
|
||||||
|
|
||||||
const siteList = document.getElementById('site-list');
|
const siteList = document.getElementById('site-list');
|
||||||
|
siteList.innerHTML = '<div class="loader"></div>';
|
||||||
console.log(`Fetching from GitHub: ${domainsJsonUrl}`);
|
|
||||||
const response = await fetch(domainsJsonUrl);
|
const headers = {
|
||||||
|
'accept': '*/*',
|
||||||
|
'accept-language': 'it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||||
|
'apikey': supabaseKey,
|
||||||
|
'authorization': `Bearer ${supabaseKey}`,
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'cache-control': 'no-cache',
|
||||||
|
'pragma': 'no-cache',
|
||||||
|
'range': '0-9'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Fetching from Supabase with headers:', headers);
|
||||||
|
const response = await fetch(`${supabaseUrl}/rest/v1/public?select=*`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: headers
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
|
||||||
const configSite = await response.json(); // Directly get the site data object
|
const data = await response.json();
|
||||||
|
|
||||||
siteList.innerHTML = '';
|
siteList.innerHTML = ''; if (data && data.length > 0) {
|
||||||
|
console.log('Raw data from Supabase:', data);
|
||||||
if (configSite && Object.keys(configSite).length > 0) { // Check if configSite is a non-empty object
|
const configSite = data[0].data;
|
||||||
totalSites = Object.keys(configSite).length;
|
console.log('Parsed config site:', configSite);
|
||||||
completedSites = 0;
|
let totalSites = Object.keys(configSite).length;
|
||||||
let latestUpdate = new Date(0);
|
let latestUpdate = new Date(0);
|
||||||
|
|
||||||
document.getElementById('sites-count').textContent = totalSites;
|
document.getElementById('sites-count').textContent = totalSites;
|
||||||
|
|
||||||
updateStatusIndicator('Checking Sites...', `Starting checks for ${totalSites} sites...`, 0);
|
|
||||||
|
|
||||||
Object.entries(configSite).forEach(([siteName, site]) => {
|
|
||||||
addSiteToCheck(siteName, site.full_url);
|
|
||||||
});
|
|
||||||
|
|
||||||
const statusChecks = Object.entries(configSite).map(async ([siteName, site]) => {
|
for (const siteName in configSite) {
|
||||||
const isOnline = await checkSiteStatus(site.full_url, siteName);
|
const site = configSite[siteName];
|
||||||
return { siteName, site, isOnline };
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(statusChecks);
|
|
||||||
|
|
||||||
updateStatusIndicator('Ready', 'All sites checked successfully!', 100);
|
|
||||||
|
|
||||||
results.forEach(({ siteName, site, isOnline }) => {
|
|
||||||
const siteItem = document.createElement('div');
|
const siteItem = document.createElement('div');
|
||||||
siteItem.className = 'site-item';
|
siteItem.className = 'site-item';
|
||||||
siteItem.style.cursor = 'pointer';
|
siteItem.style.cursor = 'pointer';
|
||||||
|
|
||||||
|
// Add status indicator
|
||||||
const statusDot = document.createElement('div');
|
const statusDot = document.createElement('div');
|
||||||
statusDot.className = 'site-status';
|
statusDot.className = 'site-status';
|
||||||
|
const isOnline = await checkSiteStatus(site.full_url);
|
||||||
if (!isOnline) statusDot.classList.add('offline');
|
if (!isOnline) statusDot.classList.add('offline');
|
||||||
siteItem.appendChild(statusDot);
|
siteItem.appendChild(statusDot);
|
||||||
|
|
||||||
|
// Update latest update time
|
||||||
const updateTime = new Date(site.time_change);
|
const updateTime = new Date(site.time_change);
|
||||||
if (updateTime > latestUpdate) {
|
if (updateTime > latestUpdate) {
|
||||||
latestUpdate = updateTime;
|
latestUpdate = updateTime;
|
||||||
@ -192,9 +133,7 @@ async function loadSiteData() {
|
|||||||
oldDomain.className = 'old-domain';
|
oldDomain.className = 'old-domain';
|
||||||
oldDomain.innerHTML = `<i class="fas fa-history"></i> ${site.old_domain}`;
|
oldDomain.innerHTML = `<i class="fas fa-history"></i> ${site.old_domain}`;
|
||||||
siteInfo.appendChild(oldDomain);
|
siteInfo.appendChild(oldDomain);
|
||||||
}
|
} siteItem.addEventListener('click', function() {
|
||||||
|
|
||||||
siteItem.addEventListener('click', function() {
|
|
||||||
window.open(site.full_url, '_blank', 'noopener,noreferrer');
|
window.open(site.full_url, '_blank', 'noopener,noreferrer');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -211,7 +150,7 @@ async function loadSiteData() {
|
|||||||
siteItem.appendChild(siteTitle);
|
siteItem.appendChild(siteTitle);
|
||||||
siteItem.appendChild(siteInfo);
|
siteItem.appendChild(siteInfo);
|
||||||
siteList.appendChild(siteItem);
|
siteList.appendChild(siteItem);
|
||||||
});
|
}
|
||||||
|
|
||||||
const formattedDate = latestUpdate.toLocaleDateString('it-IT', {
|
const formattedDate = latestUpdate.toLocaleDateString('it-IT', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@ -223,7 +162,6 @@ async function loadSiteData() {
|
|||||||
document.getElementById('last-update-time').textContent = formattedDate;
|
document.getElementById('last-update-time').textContent = formattedDate;
|
||||||
} else {
|
} else {
|
||||||
siteList.innerHTML = '<div class="no-sites">No sites available</div>';
|
siteList.innerHTML = '<div class="no-sites">No sites available</div>';
|
||||||
updateStatusIndicator('Ready', 'No sites found in the JSON file.', 100);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore:', error);
|
console.error('Errore:', error);
|
||||||
@ -233,10 +171,6 @@ async function loadSiteData() {
|
|||||||
<button onclick="loadSiteData()" class="retry-button">Riprova</button>
|
<button onclick="loadSiteData()" class="retry-button">Riprova</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
if (statusIndicator) {
|
|
||||||
updateStatusIndicator('Error', `Failed to load: ${error.message}`, 0);
|
|
||||||
statusIndicator.querySelector('.status-icon').style.background = '#f44336';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@ -75,24 +75,9 @@ jobs:
|
|||||||
executable: StreamingCommunity_linux_previous
|
executable: StreamingCommunity_linux_previous
|
||||||
separator: ':'
|
separator: ':'
|
||||||
|
|
||||||
# ARM64 build
|
|
||||||
- os: ubuntu-latest
|
|
||||||
artifact_name: StreamingCommunity_linux_arm64
|
|
||||||
executable: StreamingCommunity_linux_arm64
|
|
||||||
separator: ':'
|
|
||||||
architecture: arm64
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
# For ARM64, set architecture if present
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set up QEMU (for ARM64)
|
|
||||||
if: ${{ matrix.architecture == 'arm64' }}
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@ -109,7 +94,6 @@ jobs:
|
|||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
architecture: ${{ matrix.architecture || 'x64' }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@ -138,8 +122,6 @@ jobs:
|
|||||||
--hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding \
|
--hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding \
|
||||||
--hidden-import=Cryptodome.Random \
|
--hidden-import=Cryptodome.Random \
|
||||||
--hidden-import=telebot \
|
--hidden-import=telebot \
|
||||||
--hidden-import=curl_cffi --hidden-import=_cffi_backend \
|
|
||||||
--collect-all curl_cffi \
|
|
||||||
--additional-hooks-dir=pyinstaller/hooks \
|
--additional-hooks-dir=pyinstaller/hooks \
|
||||||
--add-data "StreamingCommunity${{ matrix.separator }}StreamingCommunity" \
|
--add-data "StreamingCommunity${{ matrix.separator }}StreamingCommunity" \
|
||||||
--name=${{ matrix.artifact_name }} test_run.py
|
--name=${{ matrix.artifact_name }} test_run.py
|
||||||
|
4
.github/workflows/update-loc.yml
vendored
4
.github/workflows/update-loc.yml
vendored
@ -16,12 +16,12 @@ jobs:
|
|||||||
- name: Count Lines of Code
|
- name: Count Lines of Code
|
||||||
run: |
|
run: |
|
||||||
LOC=$(cloc . --json | jq '.SUM.code')
|
LOC=$(cloc . --json | jq '.SUM.code')
|
||||||
echo "{\"schemaVersion\": 1, \"label\": \"Lines of Code\", \"message\": \"$LOC\", \"color\": \"green\"}" > .github/.domain/loc-badge.json
|
echo "{\"schemaVersion\": 1, \"label\": \"Lines of Code\", \"message\": \"$LOC\", \"color\": \"green\"}" > .github/media/loc-badge.json
|
||||||
|
|
||||||
- name: Commit and Push LOC Badge
|
- name: Commit and Push LOC Badge
|
||||||
run: |
|
run: |
|
||||||
git config --local user.name "GitHub Actions"
|
git config --local user.name "GitHub Actions"
|
||||||
git config --local user.email "actions@github.com"
|
git config --local user.email "actions@github.com"
|
||||||
git add .github/.domain/loc-badge.json
|
git add .github/media/loc-badge.json
|
||||||
git commit -m "Update lines of code badge" || echo "No changes to commit"
|
git commit -m "Update lines of code badge" || echo "No changes to commit"
|
||||||
git push
|
git push
|
50
.github/workflows/update_domain.yml
vendored
50
.github/workflows/update_domain.yml
vendored
@ -1,50 +0,0 @@
|
|||||||
name: Update domains
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 7-21 * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-domains:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.12'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
pip install httpx tldextract ua-generator dnspython
|
|
||||||
|
|
||||||
pip install --upgrade pip setuptools wheel
|
|
||||||
|
|
||||||
- name: Configure DNS
|
|
||||||
run: |
|
|
||||||
sudo sh -c 'echo "nameserver 9.9.9.9" > /etc/resolv.conf'
|
|
||||||
cat /etc/resolv.conf
|
|
||||||
|
|
||||||
- name: Execute domain update script
|
|
||||||
run: python .github/.domain/domain_update.py
|
|
||||||
|
|
||||||
- name: Commit and push changes (if any)
|
|
||||||
run: |
|
|
||||||
git config --global user.name 'github-actions[bot]'
|
|
||||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
|
||||||
|
|
||||||
# Check if domains.json was modified
|
|
||||||
if ! git diff --quiet .github/.domain/domains.json; then
|
|
||||||
git add .github/.domain/domains.json
|
|
||||||
git commit -m "Automatic domain update [skip ci]"
|
|
||||||
echo "Changes committed. Attempting to push..."
|
|
||||||
git push
|
|
||||||
else
|
|
||||||
echo "No changes to .github/.domain/domains.json to commit."
|
|
||||||
fi
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -52,4 +52,5 @@ cmd.txt
|
|||||||
bot_config.json
|
bot_config.json
|
||||||
scripts.json
|
scripts.json
|
||||||
active_requests.json
|
active_requests.json
|
||||||
|
domains.json
|
||||||
working_proxies.json
|
working_proxies.json
|
@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://i.ibb.co/v6RnT0wY/s2.jpg" alt="Project Logo" width="450"/>
|
<img src="https://i.ibb.co/v6RnT0wY/s2.jpg" alt="Project Logo" width="600"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@ -25,7 +25,7 @@
|
|||||||
<img src="https://img.shields.io/pypi/dm/streamingcommunity?style=for-the-badge" alt="PyPI Downloads"/>
|
<img src="https://img.shields.io/pypi/dm/streamingcommunity?style=for-the-badge" alt="PyPI Downloads"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Arrowar/StreamingCommunity">
|
<a href="https://github.com/Arrowar/StreamingCommunity">
|
||||||
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Arrowar/StreamingCommunity/main/.github/.domain/loc-badge.json&style=for-the-badge" alt="Lines of Code"/>
|
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Arrowar/StreamingCommunity/main/.github/media/loc-badge.json&style=for-the-badge" alt="Lines of Code"/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -518,7 +518,7 @@ To enable qBittorrent integration, follow the setup guide [here](https://github.
|
|||||||
"download_subtitle": true,
|
"download_subtitle": true,
|
||||||
"merge_subs": true,
|
"merge_subs": true,
|
||||||
"specific_list_subtitles": [
|
"specific_list_subtitles": [
|
||||||
"ita", // Specify language codes or use ["*"] to download all available subtitles
|
"ita",
|
||||||
"eng"
|
"eng"
|
||||||
],
|
],
|
||||||
"cleanup_tmp_folder": true
|
"cleanup_tmp_folder": true
|
||||||
@ -544,8 +544,6 @@ To enable qBittorrent integration, follow the setup guide [here](https://github.
|
|||||||
- `download_subtitle`: Whether to download subtitles
|
- `download_subtitle`: Whether to download subtitles
|
||||||
- `merge_subs`: Whether to merge subtitles with video
|
- `merge_subs`: Whether to merge subtitles with video
|
||||||
- `specific_list_subtitles`: List of subtitle languages to download
|
- `specific_list_subtitles`: List of subtitle languages to download
|
||||||
* Use `["*"]` to download all available subtitles
|
|
||||||
* Or specify individual languages like `["ita", "eng"]`
|
|
||||||
* Can be changed with `--specific_list_subtitles ita,eng`
|
* Can be changed with `--specific_list_subtitles ita,eng`
|
||||||
|
|
||||||
#### Cleanup
|
#### Cleanup
|
||||||
|
@ -5,9 +5,9 @@ import logging
|
|||||||
|
|
||||||
|
|
||||||
# External libraries
|
# External libraries
|
||||||
|
import httpx
|
||||||
import jsbeautifier
|
import jsbeautifier
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from curl_cffi import requests
|
|
||||||
|
|
||||||
|
|
||||||
# Internal utilities
|
# Internal utilities
|
||||||
@ -28,6 +28,7 @@ class VideoSource:
|
|||||||
- url (str): The URL of the video source.
|
- url (str): The URL of the video source.
|
||||||
"""
|
"""
|
||||||
self.headers = get_headers()
|
self.headers = get_headers()
|
||||||
|
self.client = httpx.Client()
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
def make_request(self, url: str) -> str:
|
def make_request(self, url: str) -> str:
|
||||||
@ -41,10 +42,8 @@ class VideoSource:
|
|||||||
- str: The response content if successful, None otherwise.
|
- str: The response content if successful, None otherwise.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=self.headers, timeout=MAX_TIMEOUT, impersonate="chrome110")
|
response = self.client.get(url, headers=self.headers, timeout=MAX_TIMEOUT, follow_redirects=True)
|
||||||
if response.status_code >= 400:
|
response.raise_for_status()
|
||||||
logging.error(f"Request failed with status code: {response.status_code}")
|
|
||||||
return None
|
|
||||||
return response.text
|
return response.text
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -39,7 +39,6 @@ class VideoSource:
|
|||||||
self.is_series = is_series
|
self.is_series = is_series
|
||||||
self.media_id = media_id
|
self.media_id = media_id
|
||||||
self.iframe_src = None
|
self.iframe_src = None
|
||||||
self.window_parameter = None
|
|
||||||
|
|
||||||
def get_iframe(self, episode_id: int) -> None:
|
def get_iframe(self, episode_id: int) -> None:
|
||||||
"""
|
"""
|
||||||
@ -110,45 +109,41 @@ class VideoSource:
|
|||||||
# Parse script to get video information
|
# Parse script to get video information
|
||||||
self.parse_script(script_text=script)
|
self.parse_script(script_text=script)
|
||||||
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
if e.response.status_code == 404:
|
|
||||||
console.print("[yellow]This content will be available soon![/yellow]")
|
|
||||||
return
|
|
||||||
|
|
||||||
logging.error(f"Error getting content: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting content: {e}")
|
logging.error(f"Error getting content: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def get_playlist(self) -> str | None:
|
def get_playlist(self) -> str:
|
||||||
"""
|
"""
|
||||||
Generate authenticated playlist URL.
|
Generate authenticated playlist URL.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: Fully constructed playlist URL with authentication parameters, or None if content unavailable
|
str: Fully constructed playlist URL with authentication parameters
|
||||||
"""
|
"""
|
||||||
if not self.window_parameter:
|
|
||||||
return None
|
|
||||||
|
|
||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
|
# Add 'h' parameter if video quality is 1080p
|
||||||
if self.canPlayFHD:
|
if self.canPlayFHD:
|
||||||
params['h'] = 1
|
params['h'] = 1
|
||||||
|
|
||||||
|
# Parse the original URL
|
||||||
parsed_url = urlparse(self.window_parameter.url)
|
parsed_url = urlparse(self.window_parameter.url)
|
||||||
query_params = parse_qs(parsed_url.query)
|
query_params = parse_qs(parsed_url.query)
|
||||||
|
|
||||||
|
# Check specifically for 'b=1' in the query parameters
|
||||||
if 'b' in query_params and query_params['b'] == ['1']:
|
if 'b' in query_params and query_params['b'] == ['1']:
|
||||||
params['b'] = 1
|
params['b'] = 1
|
||||||
|
|
||||||
|
# Add authentication parameters (token and expiration)
|
||||||
params.update({
|
params.update({
|
||||||
"token": self.window_parameter.token,
|
"token": self.window_parameter.token,
|
||||||
"expires": self.window_parameter.expires
|
"expires": self.window_parameter.expires
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Build the updated query string
|
||||||
query_string = urlencode(params)
|
query_string = urlencode(params)
|
||||||
|
|
||||||
|
# Construct the new URL with updated query parameters
|
||||||
return urlunparse(parsed_url._replace(query=query_string))
|
return urlunparse(parsed_url._replace(query=query_string))
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,22 +61,16 @@ def download_film(select_title: MediaItem) -> str:
|
|||||||
# Extract mostraguarda URL
|
# Extract mostraguarda URL
|
||||||
try:
|
try:
|
||||||
response = httpx.get(select_title.url, headers=get_headers(), timeout=10)
|
response = httpx.get(select_title.url, headers=get_headers(), timeout=10)
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
soup = BeautifulSoup(response.text, 'html.parser')
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
iframes = soup.find_all('iframe')
|
iframes = soup.find_all('iframe')
|
||||||
mostraguarda = iframes[0]['src']
|
mostraguarda = iframes[0]['src']
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"[red]Site: {site_constant.SITE_NAME}, request error: {e}, get mostraguarda")
|
console.print(f"[red]Site: {site_constant.SITE_NAME}, request error: {e}, get mostraguarda")
|
||||||
return None
|
|
||||||
|
|
||||||
# Extract supervideo URL
|
# Extract supervideo URL
|
||||||
supervideo_url = None
|
|
||||||
try:
|
try:
|
||||||
response = httpx.get(mostraguarda, headers=get_headers(), timeout=10)
|
response = httpx.get(mostraguarda, headers=get_headers(), timeout=10)
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
soup = BeautifulSoup(response.text, 'html.parser')
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
pattern = r'//supervideo\.[^/]+/[a-z]/[a-zA-Z0-9]+'
|
pattern = r'//supervideo\.[^/]+/[a-z]/[a-zA-Z0-9]+'
|
||||||
supervideo_match = re.search(pattern, response.text)
|
supervideo_match = re.search(pattern, response.text)
|
||||||
@ -84,9 +78,7 @@ def download_film(select_title: MediaItem) -> str:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"[red]Site: {site_constant.SITE_NAME}, request error: {e}, get supervideo URL")
|
console.print(f"[red]Site: {site_constant.SITE_NAME}, request error: {e}, get supervideo URL")
|
||||||
console.print("[yellow]This content will be available soon![/yellow]")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Init class
|
# Init class
|
||||||
video_source = VideoSource(supervideo_url)
|
video_source = VideoSource(supervideo_url)
|
||||||
master_playlist = video_source.get_playlist()
|
master_playlist = video_source.get_playlist()
|
||||||
|
@ -38,52 +38,38 @@ class GetSerieInfo:
|
|||||||
soup = BeautifulSoup(response.text, "html.parser")
|
soup = BeautifulSoup(response.text, "html.parser")
|
||||||
self.series_name = soup.find("title").get_text(strip=True).split(" - ")[0]
|
self.series_name = soup.find("title").get_text(strip=True).split(" - ")[0]
|
||||||
|
|
||||||
# Find all season dropdowns
|
# Process all seasons
|
||||||
seasons_dropdown = soup.find('div', class_='dropdown seasons')
|
season_items = soup.find_all('div', class_='accordion-item')
|
||||||
if not seasons_dropdown:
|
|
||||||
return
|
for season_idx, season_item in enumerate(season_items, 1):
|
||||||
|
season_header = season_item.find('div', class_='accordion-header')
|
||||||
# Get all season items
|
if not season_header:
|
||||||
season_items = seasons_dropdown.find_all('span', {'data-season': True})
|
continue
|
||||||
|
|
||||||
for season_item in season_items:
|
season_name = season_header.get_text(strip=True)
|
||||||
season_num = int(season_item['data-season'])
|
|
||||||
season_name = season_item.get_text(strip=True)
|
|
||||||
|
|
||||||
# Create a new season
|
# Create a new season and get a reference to it
|
||||||
current_season = self.seasons_manager.add_season({
|
current_season = self.seasons_manager.add_season({
|
||||||
'number': season_num,
|
'number': season_idx,
|
||||||
'name': season_name
|
'name': season_name
|
||||||
})
|
})
|
||||||
|
|
||||||
# Find all episodes for this season
|
# Find episodes for this season
|
||||||
episodes_container = soup.find('div', {'class': 'dropdown mirrors', 'data-season': str(season_num)})
|
episode_divs = season_item.find_all('div', class_='down-episode')
|
||||||
if not episodes_container:
|
for ep_idx, ep_div in enumerate(episode_divs, 1):
|
||||||
continue
|
episode_name_tag = ep_div.find('b')
|
||||||
|
if not episode_name_tag:
|
||||||
# Get all episode mirrors for this season
|
|
||||||
episode_mirrors = soup.find_all('div', {'class': 'dropdown mirrors',
|
|
||||||
'data-season': str(season_num)})
|
|
||||||
|
|
||||||
for mirror in episode_mirrors:
|
|
||||||
episode_data = mirror.get('data-episode', '').split('-')
|
|
||||||
if len(episode_data) != 2:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ep_num = int(episode_data[1])
|
episode_name = episode_name_tag.get_text(strip=True)
|
||||||
|
link_tag = ep_div.find('a', string=lambda text: text and "Supervideo" in text)
|
||||||
# Find supervideo link
|
episode_url = link_tag['href'] if link_tag else None
|
||||||
supervideo_span = mirror.find('span', {'data-id': 'supervideo'})
|
|
||||||
if not supervideo_span:
|
|
||||||
continue
|
|
||||||
|
|
||||||
episode_url = supervideo_span.get('data-link', '')
|
|
||||||
|
|
||||||
# Add episode to the season
|
# Add episode to the season
|
||||||
if current_season:
|
if current_season:
|
||||||
current_season.episodes.add({
|
current_season.episodes.add({
|
||||||
'number': ep_num,
|
'number': ep_idx,
|
||||||
'name': f"Episodio {ep_num}",
|
'name': episode_name,
|
||||||
'url': episode_url
|
'url': episode_url
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -31,8 +31,7 @@ class ScrapSerie:
|
|||||||
self.client = httpx.Client(
|
self.client = httpx.Client(
|
||||||
cookies={"sessionId": self.session_id},
|
cookies={"sessionId": self.session_id},
|
||||||
headers={"User-Agent": get_userAgent(), "csrf-token": self.csrf_token},
|
headers={"User-Agent": get_userAgent(), "csrf-token": self.csrf_token},
|
||||||
base_url=full_url,
|
base_url=full_url
|
||||||
verify=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -21,7 +21,7 @@ from .film import download_film
|
|||||||
# Variable
|
# Variable
|
||||||
indice = 5
|
indice = 5
|
||||||
_useFor = "Film_&_Serie"
|
_useFor = "Film_&_Serie"
|
||||||
_priority = 0
|
_priority = 1 # NOTE: Site search need the use of tmbd obj
|
||||||
_engineDownload = "hls"
|
_engineDownload = "hls"
|
||||||
_deprecate = False
|
_deprecate = False
|
||||||
|
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
# 21.05.24
|
# 21.05.24
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
|
||||||
|
|
||||||
# External libraries
|
# External libraries
|
||||||
import httpx
|
import httpx
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
@ -9,9 +13,12 @@ from rich.console import Console
|
|||||||
from StreamingCommunity.Util.config_json import config_manager
|
from StreamingCommunity.Util.config_json import config_manager
|
||||||
from StreamingCommunity.Util.headers import get_userAgent
|
from StreamingCommunity.Util.headers import get_userAgent
|
||||||
from StreamingCommunity.Util.table import TVShowManager
|
from StreamingCommunity.Util.table import TVShowManager
|
||||||
|
from StreamingCommunity.Lib.TMBD.tmdb import tmdb
|
||||||
|
|
||||||
|
|
||||||
|
# Logic class
|
||||||
from StreamingCommunity.Api.Template.config_loader import site_constant
|
from StreamingCommunity.Api.Template.config_loader import site_constant
|
||||||
from StreamingCommunity.Api.Template.Class.SearchType import MediaManager
|
from StreamingCommunity.Api.Template.Class.SearchType import MediaManager
|
||||||
from .util.ScrapeSerie import GetSerieInfo
|
|
||||||
|
|
||||||
|
|
||||||
# Variable
|
# Variable
|
||||||
@ -19,33 +26,76 @@ console = Console()
|
|||||||
media_search_manager = MediaManager()
|
media_search_manager = MediaManager()
|
||||||
table_show_manager = TVShowManager()
|
table_show_manager = TVShowManager()
|
||||||
max_timeout = config_manager.get_int("REQUESTS", "timeout")
|
max_timeout = config_manager.get_int("REQUESTS", "timeout")
|
||||||
|
MAX_THREADS = 12
|
||||||
|
|
||||||
|
|
||||||
def determine_media_type(item):
|
def determine_media_type(title):
|
||||||
"""
|
"""
|
||||||
Determine if the item is a film or TV series by checking actual seasons count
|
Use TMDB to determine if a title is a movie or TV show.
|
||||||
using GetSerieInfo.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Extract program name from path_id
|
# First search as a movie
|
||||||
program_name = None
|
movie_results = tmdb._make_request("search/movie", {"query": title})
|
||||||
if item.get('path_id'):
|
movie_count = len(movie_results.get("results", []))
|
||||||
parts = item['path_id'].strip('/').split('/')
|
|
||||||
if len(parts) >= 2:
|
# Then search as a TV show
|
||||||
program_name = parts[-1].split('.')[0]
|
tv_results = tmdb._make_request("search/tv", {"query": title})
|
||||||
|
tv_count = len(tv_results.get("results", []))
|
||||||
if not program_name:
|
|
||||||
|
# If results found in only one category, use that
|
||||||
|
if movie_count > 0 and tv_count == 0:
|
||||||
return "film"
|
return "film"
|
||||||
|
elif tv_count > 0 and movie_count == 0:
|
||||||
|
return "tv"
|
||||||
|
|
||||||
|
# If both have results, compare popularity
|
||||||
|
if movie_count > 0 and tv_count > 0:
|
||||||
|
top_movie = movie_results["results"][0]
|
||||||
|
top_tv = tv_results["results"][0]
|
||||||
|
|
||||||
|
return "film" if top_movie.get("popularity", 0) > top_tv.get("popularity", 0) else "tv"
|
||||||
|
|
||||||
scraper = GetSerieInfo(program_name)
|
return "film"
|
||||||
scraper.collect_info_title()
|
|
||||||
return "tv" if scraper.getNumberSeason() > 0 else "film"
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"[red]Error determining media type: {e}[/red]")
|
console.log(f"Error determining media type with TMDB: {e}")
|
||||||
return "film"
|
return "film"
|
||||||
|
|
||||||
|
|
||||||
|
def worker_determine_type(work_queue, result_dict, worker_id):
|
||||||
|
"""
|
||||||
|
Worker function to process items from queue and determine media types.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- work_queue: Queue containing items to process
|
||||||
|
- result_dict: Dictionary to store results
|
||||||
|
- worker_id: ID of the worker thread
|
||||||
|
"""
|
||||||
|
while not work_queue.empty():
|
||||||
|
try:
|
||||||
|
index, item = work_queue.get(block=False)
|
||||||
|
title = item.get('titolo', '')
|
||||||
|
media_type = determine_media_type(title)
|
||||||
|
|
||||||
|
result_dict[index] = {
|
||||||
|
'id': item.get('id', ''),
|
||||||
|
'name': title,
|
||||||
|
'type': media_type,
|
||||||
|
'path_id': item.get('path_id', ''),
|
||||||
|
'url': f"https://www.raiplay.it{item.get('url', '')}",
|
||||||
|
'image': f"https://www.raiplay.it{item.get('immagine', '')}",
|
||||||
|
}
|
||||||
|
|
||||||
|
work_queue.task_done()
|
||||||
|
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
console.log(f"Worker {worker_id} error: {e}")
|
||||||
|
work_queue.task_done()
|
||||||
|
|
||||||
|
|
||||||
def title_search(query: str) -> int:
|
def title_search(query: str) -> int:
|
||||||
"""
|
"""
|
||||||
Search for titles based on a search query.
|
Search for titles based on a search query.
|
||||||
@ -91,15 +141,33 @@ def title_search(query: str) -> int:
|
|||||||
data = response.json().get('agg').get('titoli').get('cards')
|
data = response.json().get('agg').get('titoli').get('cards')
|
||||||
data = data[:15] if len(data) > 15 else data
|
data = data[:15] if len(data) > 15 else data
|
||||||
|
|
||||||
# Process each item and add to media manager
|
# Use multithreading to determine media types in parallel
|
||||||
for item in data:
|
work_queue = queue.Queue()
|
||||||
media_search_manager.add_media({
|
result_dict = {}
|
||||||
'id': item.get('id', ''),
|
|
||||||
'name': item.get('titolo', ''),
|
# Add items to the work queue
|
||||||
'type': determine_media_type(item),
|
for i, item in enumerate(data):
|
||||||
'path_id': item.get('path_id', ''),
|
work_queue.put((i, item))
|
||||||
'url': f"https://www.raiplay.it{item.get('url', '')}",
|
|
||||||
'image': f"https://www.raiplay.it{item.get('immagine', '')}",
|
# Create and start worker threads
|
||||||
})
|
threads = []
|
||||||
|
for i in range(min(MAX_THREADS, len(data))):
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=worker_determine_type,
|
||||||
|
args=(work_queue, result_dict, i),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
threads.append(thread)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
# Wait for all threads to complete
|
||||||
|
for thread in threads:
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
# Add all results to media manager in correct order
|
||||||
|
for i in range(len(data)):
|
||||||
|
if i in result_dict:
|
||||||
|
media_search_manager.add_media(result_dict[i])
|
||||||
|
|
||||||
|
# Return the number of titles found
|
||||||
return media_search_manager.get_length()
|
return media_search_manager.get_length()
|
@ -30,48 +30,28 @@ class GetSerieInfo:
|
|||||||
try:
|
try:
|
||||||
program_url = f"{self.base_url}/programmi/{self.program_name}.json"
|
program_url = f"{self.base_url}/programmi/{self.program_name}.json"
|
||||||
response = httpx.get(url=program_url, headers=get_headers(), timeout=max_timeout)
|
response = httpx.get(url=program_url, headers=get_headers(), timeout=max_timeout)
|
||||||
|
|
||||||
# If 404, content is not yet available
|
|
||||||
if response.status_code == 404:
|
|
||||||
logging.info(f"Content not yet available: {self.program_name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
json_data = response.json()
|
json_data = response.json()
|
||||||
|
|
||||||
# Look for seasons in the 'blocks' property
|
# Look for seasons in the 'blocks' property
|
||||||
for block in json_data.get('blocks', []):
|
for block in json_data.get('blocks'):
|
||||||
|
if block.get('type') == 'RaiPlay Multimedia Block' and block.get('name', '').lower() == 'episodi':
|
||||||
|
self.publishing_block_id = block.get('id')
|
||||||
|
|
||||||
|
# Extract seasons from sets array
|
||||||
|
for season_set in block.get('sets', []):
|
||||||
|
if 'stagione' in season_set.get('name', '').lower():
|
||||||
|
self.seasons_manager.add_season({
|
||||||
|
'id': season_set.get('id', ''),
|
||||||
|
'number': len(self.seasons_manager.seasons) + 1,
|
||||||
|
'name': season_set.get('name', ''),
|
||||||
|
'path': season_set.get('path_id', ''),
|
||||||
|
'episodes_count': season_set.get('episode_size', {}).get('number', 0)
|
||||||
|
})
|
||||||
|
|
||||||
# Check if block is a season block or episodi block
|
|
||||||
if block.get('type') == 'RaiPlay Multimedia Block':
|
|
||||||
if block.get('name', '').lower() == 'episodi':
|
|
||||||
self.publishing_block_id = block.get('id')
|
|
||||||
|
|
||||||
# Extract seasons from sets array
|
|
||||||
for season_set in block.get('sets', []):
|
|
||||||
if 'stagione' in season_set.get('name', '').lower():
|
|
||||||
self._add_season(season_set, block.get('id'))
|
|
||||||
|
|
||||||
elif 'stagione' in block.get('name', '').lower():
|
|
||||||
self.publishing_block_id = block.get('id')
|
|
||||||
|
|
||||||
# Extract season directly from block's sets
|
|
||||||
for season_set in block.get('sets', []):
|
|
||||||
self._add_season(season_set, block.get('id'))
|
|
||||||
|
|
||||||
except httpx.HTTPError as e:
|
|
||||||
logging.error(f"Error collecting series info: {e}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Unexpected error collecting series info: {e}")
|
logging.error(f"Error collecting series info: {e}")
|
||||||
|
|
||||||
def _add_season(self, season_set: dict, block_id: str):
|
|
||||||
self.seasons_manager.add_season({
|
|
||||||
'id': season_set.get('id', ''),
|
|
||||||
'number': len(self.seasons_manager.seasons) + 1,
|
|
||||||
'name': season_set.get('name', ''),
|
|
||||||
'path': season_set.get('path_id', ''),
|
|
||||||
'episodes_count': season_set.get('episode_size', {}).get('number', 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
def collect_info_season(self, number_season: int) -> None:
|
def collect_info_season(self, number_season: int) -> None:
|
||||||
"""Get episodes for a specific season."""
|
"""Get episodes for a specific season."""
|
||||||
|
@ -121,16 +121,14 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_
|
|||||||
if site_constant.TELEGRAM_BOT:
|
if site_constant.TELEGRAM_BOT:
|
||||||
bot = get_bot_instance()
|
bot = get_bot_instance()
|
||||||
|
|
||||||
# Check proxy if not already set
|
|
||||||
finder = ProxyFinder(site_constant.FULL_URL)
|
|
||||||
proxy = finder.find_fast_proxy()
|
|
||||||
|
|
||||||
if direct_item:
|
if direct_item:
|
||||||
select_title_obj = MediaItem(**direct_item)
|
select_title_obj = MediaItem(**direct_item)
|
||||||
process_search_result(select_title_obj, selections, proxy)
|
process_search_result(select_title_obj, selections, proxy)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check proxy if not already set
|
||||||
|
finder = ProxyFinder(site_constant.FULL_URL)
|
||||||
|
proxy = finder.find_fast_proxy()
|
||||||
|
|
||||||
actual_search_query = get_user_input(string_to_search)
|
actual_search_query = get_user_input(string_to_search)
|
||||||
|
|
||||||
@ -144,7 +142,7 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_
|
|||||||
# Perform search on the database using the obtained query
|
# Perform search on the database using the obtained query
|
||||||
finder = ProxyFinder(site_constant.FULL_URL)
|
finder = ProxyFinder(site_constant.FULL_URL)
|
||||||
proxy = finder.find_fast_proxy()
|
proxy = finder.find_fast_proxy()
|
||||||
len_database = title_search(actual_search_query, proxy)
|
len_database = title_search(string_to_search, proxy)
|
||||||
|
|
||||||
# If only the database object (media_search_manager populated by title_search) is needed
|
# If only the database object (media_search_manager populated by title_search) is needed
|
||||||
if get_onlyDatabase:
|
if get_onlyDatabase:
|
||||||
|
@ -62,10 +62,6 @@ def download_film(select_title: MediaItem, proxy: str = None) -> str:
|
|||||||
video_source.get_content()
|
video_source.get_content()
|
||||||
master_playlist = video_source.get_playlist()
|
master_playlist = video_source.get_playlist()
|
||||||
|
|
||||||
if master_playlist is None:
|
|
||||||
console.print(f"[red]Site: {site_constant.SITE_NAME}, error: No master playlist found[/red]")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Define the filename and path for the downloaded film
|
# Define the filename and path for the downloaded film
|
||||||
title_name = os_manager.get_sanitize_file(select_title.name) + ".mp4"
|
title_name = os_manager.get_sanitize_file(select_title.name) + ".mp4"
|
||||||
mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(".mp4", ""))
|
mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(".mp4", ""))
|
||||||
|
@ -180,14 +180,10 @@ class M3U8Manager:
|
|||||||
|
|
||||||
self.sub_streams = []
|
self.sub_streams = []
|
||||||
if ENABLE_SUBTITLE:
|
if ENABLE_SUBTITLE:
|
||||||
if "*" in DOWNLOAD_SPECIFIC_SUBTITLE:
|
self.sub_streams = [
|
||||||
self.sub_streams = self.parser._subtitle.get_all_uris_and_names() or []
|
s for s in (self.parser._subtitle.get_all_uris_and_names() or [])
|
||||||
|
if s.get('language') in DOWNLOAD_SPECIFIC_SUBTITLE
|
||||||
else:
|
]
|
||||||
self.sub_streams = [
|
|
||||||
s for s in (self.parser._subtitle.get_all_uris_and_names() or [])
|
|
||||||
if s.get('language') in DOWNLOAD_SPECIFIC_SUBTITLE
|
|
||||||
]
|
|
||||||
|
|
||||||
def log_selection(self):
|
def log_selection(self):
|
||||||
tuple_available_resolution = self.parser._video.get_list_resolution()
|
tuple_available_resolution = self.parser._video.get_list_resolution()
|
||||||
@ -213,13 +209,9 @@ class M3U8Manager:
|
|||||||
f"[red]Set:[/red] {set_codec_info}"
|
f"[red]Set:[/red] {set_codec_info}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get available subtitles and their languages
|
|
||||||
available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
|
available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
|
||||||
available_sub_languages = [sub.get('language') for sub in available_subtitles]
|
available_sub_languages = [sub.get('language') for sub in available_subtitles]
|
||||||
|
downloadable_sub_languages = list(set(available_sub_languages) & set(DOWNLOAD_SPECIFIC_SUBTITLE))
|
||||||
# If "*" is in DOWNLOAD_SPECIFIC_SUBTITLE, all languages are downloadable
|
|
||||||
downloadable_sub_languages = available_sub_languages if "*" in DOWNLOAD_SPECIFIC_SUBTITLE else list(set(available_sub_languages) & set(DOWNLOAD_SPECIFIC_SUBTITLE))
|
|
||||||
|
|
||||||
if available_sub_languages:
|
if available_sub_languages:
|
||||||
console.print(
|
console.print(
|
||||||
f"[cyan bold]Subtitle [/cyan bold] [green]Available:[/green] [purple]{', '.join(available_sub_languages)}[/purple] | "
|
f"[cyan bold]Subtitle [/cyan bold] [green]Available:[/green] [purple]{', '.join(available_sub_languages)}[/purple] | "
|
||||||
|
@ -4,7 +4,6 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import asyncio
|
import asyncio
|
||||||
import importlib.metadata
|
|
||||||
|
|
||||||
# External library
|
# External library
|
||||||
import httpx
|
import httpx
|
||||||
@ -12,7 +11,7 @@ from rich.console import Console
|
|||||||
|
|
||||||
|
|
||||||
# Internal utilities
|
# Internal utilities
|
||||||
from .version import __version__ as source_code_version, __author__, __title__
|
from .version import __version__, __author__, __title__
|
||||||
from StreamingCommunity.Util.config_json import config_manager
|
from StreamingCommunity.Util.config_json import config_manager
|
||||||
from StreamingCommunity.Util.headers import get_userAgent
|
from StreamingCommunity.Util.headers import get_userAgent
|
||||||
|
|
||||||
@ -76,11 +75,7 @@ def update():
|
|||||||
percentual_stars = 0
|
percentual_stars = 0
|
||||||
|
|
||||||
# Get the current version (installed version)
|
# Get the current version (installed version)
|
||||||
try:
|
current_version = __version__
|
||||||
current_version = importlib.metadata.version(__title__)
|
|
||||||
except importlib.metadata.PackageNotFoundError:
|
|
||||||
#console.print(f"[yellow]Warning: Could not determine installed version for '{__title__}' via importlib.metadata. Falling back to source version.[/yellow]")
|
|
||||||
current_version = source_code_version
|
|
||||||
|
|
||||||
# Get commit details
|
# Get commit details
|
||||||
latest_commit = response_commits[0] if response_commits else None
|
latest_commit = response_commits[0] if response_commits else None
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
__title__ = 'StreamingCommunity'
|
__title__ = 'StreamingCommunity'
|
||||||
__version__ = '3.0.9'
|
__version__ = '3.0.7'
|
||||||
__author__ = 'Arrowar'
|
__author__ = 'Arrowar'
|
||||||
__description__ = 'A command-line program to download film'
|
__description__ = 'A command-line program to download film'
|
||||||
__copyright__ = 'Copyright 2025'
|
__copyright__ = 'Copyright 2024'
|
||||||
|
@ -39,6 +39,9 @@ class ConfigManager:
|
|||||||
|
|
||||||
# Get the actual path of the module file
|
# Get the actual path of the module file
|
||||||
current_file_path = os.path.abspath(__file__)
|
current_file_path = os.path.abspath(__file__)
|
||||||
|
# Navigate upwards to find the project root
|
||||||
|
# Assuming this file is in a package structure like StreamingCommunity/Util/config_json.py
|
||||||
|
# We need to go up 2 levels to reach the project root
|
||||||
base_path = os.path.dirname(os.path.dirname(os.path.dirname(current_file_path)))
|
base_path = os.path.dirname(os.path.dirname(os.path.dirname(current_file_path)))
|
||||||
|
|
||||||
# Initialize file paths
|
# Initialize file paths
|
||||||
@ -268,32 +271,33 @@ class ConfigManager:
|
|||||||
self._load_site_data_from_file()
|
self._load_site_data_from_file()
|
||||||
|
|
||||||
def _load_site_data_from_api(self) -> None:
|
def _load_site_data_from_api(self) -> None:
|
||||||
"""Load site data from GitHub."""
|
"""Load site data from API."""
|
||||||
domains_github_url = "https://raw.githubusercontent.com/Arrowar/StreamingCommunity/refs/heads/main/.github/.domain/domains.json"
|
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": get_userAgent()
|
"apikey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp2Zm5ncG94d3Jnc3duenl0YWRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDAxNTIxNjMsImV4cCI6MjA1NTcyODE2M30.FNTCCMwi0QaKjOu8gtZsT5yQttUW8QiDDGXmzkn89QE",
|
||||||
|
"Authorization": f"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp2Zm5ncG94d3Jnc3duenl0YWRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDAxNTIxNjMsImV4cCI6MjA1NTcyODE2M30.FNTCCMwi0QaKjOu8gtZsT5yQttUW8QiDDGXmzkn89QE",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": get_userAgent()
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
console.print(f"[bold cyan]Retrieving site data from GitHub:[/bold cyan] [green]{domains_github_url}[/green]")
|
console.print("[bold cyan]Retrieving site data from API...[/bold cyan]")
|
||||||
response = requests.get(domains_github_url, timeout=8, headers=headers)
|
response = requests.get("https://zvfngpoxwrgswnzytadh.supabase.co/rest/v1/public", timeout=8, headers=headers)
|
||||||
|
|
||||||
if response.ok:
|
if response.ok:
|
||||||
self.configSite = response.json()
|
data = response.json()
|
||||||
|
if data and len(data) > 0:
|
||||||
site_count = len(self.configSite) if isinstance(self.configSite, dict) else 0
|
self.configSite = data[0]['data']
|
||||||
console.print(f"[bold green]Site data loaded from GitHub:[/bold green] {site_count} streaming services found.")
|
|
||||||
|
site_count = len(self.configSite) if isinstance(self.configSite, dict) else 0
|
||||||
|
|
||||||
|
else:
|
||||||
|
console.print("[bold yellow]API returned an empty data set[/bold yellow]")
|
||||||
else:
|
else:
|
||||||
console.print(f"[bold red]GitHub request failed:[/bold red] HTTP {response.status_code}, {response.text[:100]}")
|
console.print(f"[bold red]API request failed:[/bold red] HTTP {response.status_code}, {response.text[:100]}")
|
||||||
self._handle_site_data_fallback()
|
self._handle_site_data_fallback()
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
console.print(f"[bold red]Error parsing JSON from GitHub:[/bold red] {str(e)}")
|
|
||||||
self._handle_site_data_fallback()
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"[bold red]GitHub connection error:[/bold red] {str(e)}")
|
console.print(f"[bold red]API connection error:[/bold red] {str(e)}")
|
||||||
self._handle_site_data_fallback()
|
self._handle_site_data_fallback()
|
||||||
|
|
||||||
def _load_site_data_from_file(self) -> None:
|
def _load_site_data_from_file(self) -> None:
|
||||||
@ -558,6 +562,7 @@ class ConfigManager:
|
|||||||
return section in config_source
|
return section in config_source
|
||||||
|
|
||||||
|
|
||||||
|
# Helper function to check the platform
|
||||||
def get_use_large_bar():
|
def get_use_large_bar():
|
||||||
"""
|
"""
|
||||||
Determine if the large bar feature should be enabled.
|
Determine if the large bar feature should be enabled.
|
||||||
|
@ -12,7 +12,7 @@ import inspect
|
|||||||
import subprocess
|
import subprocess
|
||||||
import contextlib
|
import contextlib
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import socket
|
|
||||||
|
|
||||||
# External library
|
# External library
|
||||||
from unidecode import unidecode
|
from unidecode import unidecode
|
||||||
@ -283,61 +283,43 @@ class InternManager():
|
|||||||
else:
|
else:
|
||||||
return f"{bytes / (1024 * 1024):.2f} MB/s"
|
return f"{bytes / (1024 * 1024):.2f} MB/s"
|
||||||
|
|
||||||
# def check_dns_provider(self):
|
def check_dns_provider(self):
|
||||||
# """
|
|
||||||
# Check if the system's current DNS server matches any known DNS providers.
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# bool: True if the current DNS server matches a known provider,
|
|
||||||
# False if no match is found or in case of errors
|
|
||||||
# """
|
|
||||||
# dns_providers = {
|
|
||||||
# "Cloudflare": ["1.1.1.1", "1.0.0.1"],
|
|
||||||
# "Google": ["8.8.8.8", "8.8.4.4"],
|
|
||||||
# "OpenDNS": ["208.67.222.222", "208.67.220.220"],
|
|
||||||
# "Quad9": ["9.9.9.9", "149.112.112.112"],
|
|
||||||
# "AdGuard": ["94.140.14.14", "94.140.15.15"],
|
|
||||||
# "Comodo": ["8.26.56.26", "8.20.247.20"],
|
|
||||||
# "Level3": ["209.244.0.3", "209.244.0.4"],
|
|
||||||
# "Norton": ["199.85.126.10", "199.85.127.10"],
|
|
||||||
# "CleanBrowsing": ["185.228.168.9", "185.228.169.9"],
|
|
||||||
# "Yandex": ["77.88.8.8", "77.88.8.1"]
|
|
||||||
# }
|
|
||||||
|
|
||||||
# try:
|
|
||||||
# resolver = dns.resolver.Resolver()
|
|
||||||
# nameservers = resolver.nameservers
|
|
||||||
|
|
||||||
# if not nameservers:
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# for server in nameservers:
|
|
||||||
# for provider, ips in dns_providers.items():
|
|
||||||
# if server in ips:
|
|
||||||
# return True
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# except Exception:
|
|
||||||
# return False
|
|
||||||
|
|
||||||
def check_dns_resolve(self):
|
|
||||||
"""
|
"""
|
||||||
Check if the system's current DNS server can resolve a domain name.
|
Check if the system's current DNS server matches any known DNS providers.
|
||||||
Works on both Windows and Unix-like systems.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the current DNS server can resolve a domain name,
|
bool: True if the current DNS server matches a known provider,
|
||||||
False if can't resolve or in case of errors
|
False if no match is found or in case of errors
|
||||||
"""
|
"""
|
||||||
test_domains = ["github.com", "google.com", "microsoft.com", "amazon.com"]
|
dns_providers = {
|
||||||
|
"Cloudflare": ["1.1.1.1", "1.0.0.1"],
|
||||||
|
"Google": ["8.8.8.8", "8.8.4.4"],
|
||||||
|
"OpenDNS": ["208.67.222.222", "208.67.220.220"],
|
||||||
|
"Quad9": ["9.9.9.9", "149.112.112.112"],
|
||||||
|
"AdGuard": ["94.140.14.14", "94.140.15.15"],
|
||||||
|
"Comodo": ["8.26.56.26", "8.20.247.20"],
|
||||||
|
"Level3": ["209.244.0.3", "209.244.0.4"],
|
||||||
|
"Norton": ["199.85.126.10", "199.85.127.10"],
|
||||||
|
"CleanBrowsing": ["185.228.168.9", "185.228.169.9"],
|
||||||
|
"Yandex": ["77.88.8.8", "77.88.8.1"]
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for domain in test_domains:
|
resolver = dns.resolver.Resolver()
|
||||||
# socket.gethostbyname() works consistently across all platforms
|
nameservers = resolver.nameservers
|
||||||
socket.gethostbyname(domain)
|
|
||||||
return True
|
if not nameservers:
|
||||||
except (socket.gaierror, socket.error):
|
return False
|
||||||
|
|
||||||
|
for server in nameservers:
|
||||||
|
for provider, ips in dns_providers.items():
|
||||||
|
if server in ips:
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class OsSummary:
|
class OsSummary:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -157,7 +157,7 @@ def global_search(search_terms: str = None, selected_sites: list = None):
|
|||||||
|
|
||||||
# Display progress information
|
# Display progress information
|
||||||
console.print(f"\n[bold green]Searching for:[/bold green] [yellow]{search_terms}[/yellow]")
|
console.print(f"\n[bold green]Searching for:[/bold green] [yellow]{search_terms}[/yellow]")
|
||||||
console.print(f"[bold green]Searching across:[/bold green] {len(selected_sites)} sites \n")
|
console.print(f"[bold green]Searching across:[/bold green] {len(selected_sites)} sites")
|
||||||
|
|
||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
search_task = progress.add_task("[cyan]Searching...", total=len(selected_sites))
|
search_task = progress.add_task("[cyan]Searching...", total=len(selected_sites))
|
||||||
@ -188,7 +188,7 @@ def global_search(search_terms: str = None, selected_sites: list = None):
|
|||||||
item_dict['source_alias'] = alias
|
item_dict['source_alias'] = alias
|
||||||
all_results[alias].append(item_dict)
|
all_results[alias].append(item_dict)
|
||||||
|
|
||||||
console.print(f"\n[green]Found {len(database.media_list)} results from {site_name}")
|
console.print(f"[green]Found {len(database.media_list)} results from {site_name}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"[bold red]Error searching {site_name}:[/bold red] {str(e)}")
|
console.print(f"[bold red]Error searching {site_name}:[/bold red] {str(e)}")
|
||||||
|
@ -210,19 +210,7 @@ def main(script_id = 0):
|
|||||||
log_not = Logger()
|
log_not = Logger()
|
||||||
initialize()
|
initialize()
|
||||||
|
|
||||||
# if not internet_manager.check_dns_provider():
|
if not internet_manager.check_dns_provider():
|
||||||
# print()
|
|
||||||
# console.print("[red]❌ ERROR: DNS configuration is required!")
|
|
||||||
# console.print("[red]The program cannot function correctly without proper DNS settings.")
|
|
||||||
# console.print("[yellow]Please configure one of these DNS servers:")
|
|
||||||
# console.print("[blue]• Cloudflare (1.1.1.1) 'https://developers.cloudflare.com/1.1.1.1/setup/windows/'")
|
|
||||||
# console.print("[blue]• Quad9 (9.9.9.9) 'https://docs.quad9.net/Setup_Guides/Windows/Windows_10/'")
|
|
||||||
# console.print("\n[yellow]⚠️ The program will not work until you configure your DNS settings.")
|
|
||||||
|
|
||||||
# time.sleep(2)
|
|
||||||
# msg.ask("[yellow]Press Enter to continue ...")
|
|
||||||
|
|
||||||
if not internet_manager.check_dns_resolve():
|
|
||||||
print()
|
print()
|
||||||
console.print("[red]❌ ERROR: DNS configuration is required!")
|
console.print("[red]❌ ERROR: DNS configuration is required!")
|
||||||
console.print("[red]The program cannot function correctly without proper DNS settings.")
|
console.print("[red]The program cannot function correctly without proper DNS settings.")
|
||||||
@ -231,7 +219,8 @@ def main(script_id = 0):
|
|||||||
console.print("[blue]• Quad9 (9.9.9.9) 'https://docs.quad9.net/Setup_Guides/Windows/Windows_10/'")
|
console.print("[blue]• Quad9 (9.9.9.9) 'https://docs.quad9.net/Setup_Guides/Windows/Windows_10/'")
|
||||||
console.print("\n[yellow]⚠️ The program will not work until you configure your DNS settings.")
|
console.print("\n[yellow]⚠️ The program will not work until you configure your DNS settings.")
|
||||||
|
|
||||||
os._exit(0)
|
time.sleep(2)
|
||||||
|
msg.ask("[yellow]Press Enter to continue ...")
|
||||||
|
|
||||||
# Load search functions
|
# Load search functions
|
||||||
search_functions = load_search_functions()
|
search_functions = load_search_functions()
|
||||||
|
@ -6,7 +6,6 @@ m3u8
|
|||||||
certifi
|
certifi
|
||||||
psutil
|
psutil
|
||||||
unidecode
|
unidecode
|
||||||
curl_cffi
|
|
||||||
dnspython
|
dnspython
|
||||||
jsbeautifier
|
jsbeautifier
|
||||||
pathvalidate
|
pathvalidate
|
||||||
@ -14,4 +13,3 @@ pycryptodomex
|
|||||||
ua-generator
|
ua-generator
|
||||||
qbittorrent-api
|
qbittorrent-api
|
||||||
pyTelegramBotAPI
|
pyTelegramBotAPI
|
||||||
beautifulsoup4
|
|
17
setup.py
17
setup.py
@ -1,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
def read_readme():
|
def read_readme():
|
||||||
@ -9,21 +8,9 @@ def read_readme():
|
|||||||
with open(os.path.join(os.path.dirname(__file__), "requirements.txt"), "r", encoding="utf-8-sig") as f:
|
with open(os.path.join(os.path.dirname(__file__), "requirements.txt"), "r", encoding="utf-8-sig") as f:
|
||||||
required_packages = f.read().splitlines()
|
required_packages = f.read().splitlines()
|
||||||
|
|
||||||
def get_version():
|
|
||||||
try:
|
|
||||||
import pkg_resources
|
|
||||||
return pkg_resources.get_distribution('StreamingCommunity').version
|
|
||||||
except:
|
|
||||||
version_file_path = os.path.join(os.path.dirname(__file__), "StreamingCommunity", "Upload", "version.py")
|
|
||||||
with open(version_file_path, "r", encoding="utf-8") as f:
|
|
||||||
version_match = re.search(r"^__version__\s*=\s*['\"]([^'\"]*)['\"]", f.read(), re.M)
|
|
||||||
if version_match:
|
|
||||||
return version_match.group(1)
|
|
||||||
raise RuntimeError("Unable to find version string in StreamingCommunity/Upload/version.py.")
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="StreamingCommunity",
|
name="StreamingCommunity",
|
||||||
version=get_version(),
|
version="3.0.7",
|
||||||
long_description=read_readme(),
|
long_description=read_readme(),
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
author="Lovi-0",
|
author="Lovi-0",
|
||||||
@ -42,4 +29,4 @@ setup(
|
|||||||
"Bug Reports": "https://github.com/Lovi-0/StreamingCommunity/issues",
|
"Bug Reports": "https://github.com/Lovi-0/StreamingCommunity/issues",
|
||||||
"Source": "https://github.com/Lovi-0/StreamingCommunity",
|
"Source": "https://github.com/Lovi-0/StreamingCommunity",
|
||||||
}
|
}
|
||||||
)
|
)
|
Loading…
x
Reference in New Issue
Block a user