quasarr 0.1.6__py3-none-any.whl → 1.23.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of quasarr might be problematic. Click here for more details.
- quasarr/__init__.py +316 -42
- quasarr/api/__init__.py +187 -0
- quasarr/api/arr/__init__.py +387 -0
- quasarr/api/captcha/__init__.py +1189 -0
- quasarr/api/config/__init__.py +23 -0
- quasarr/api/sponsors_helper/__init__.py +166 -0
- quasarr/api/statistics/__init__.py +196 -0
- quasarr/downloads/__init__.py +319 -256
- quasarr/downloads/linkcrypters/__init__.py +0 -0
- quasarr/downloads/linkcrypters/al.py +237 -0
- quasarr/downloads/linkcrypters/filecrypt.py +444 -0
- quasarr/downloads/linkcrypters/hide.py +123 -0
- quasarr/downloads/packages/__init__.py +476 -0
- quasarr/downloads/sources/al.py +697 -0
- quasarr/downloads/sources/by.py +106 -0
- quasarr/downloads/sources/dd.py +76 -0
- quasarr/downloads/sources/dj.py +7 -0
- quasarr/downloads/sources/dl.py +199 -0
- quasarr/downloads/sources/dt.py +66 -0
- quasarr/downloads/sources/dw.py +14 -7
- quasarr/downloads/sources/he.py +112 -0
- quasarr/downloads/sources/mb.py +47 -0
- quasarr/downloads/sources/nk.py +54 -0
- quasarr/downloads/sources/nx.py +42 -83
- quasarr/downloads/sources/sf.py +159 -0
- quasarr/downloads/sources/sj.py +7 -0
- quasarr/downloads/sources/sl.py +90 -0
- quasarr/downloads/sources/wd.py +110 -0
- quasarr/downloads/sources/wx.py +127 -0
- quasarr/providers/cloudflare.py +204 -0
- quasarr/providers/html_images.py +22 -0
- quasarr/providers/html_templates.py +211 -104
- quasarr/providers/imdb_metadata.py +108 -3
- quasarr/providers/log.py +19 -0
- quasarr/providers/myjd_api.py +201 -40
- quasarr/providers/notifications.py +99 -11
- quasarr/providers/obfuscated.py +65 -0
- quasarr/providers/sessions/__init__.py +0 -0
- quasarr/providers/sessions/al.py +286 -0
- quasarr/providers/sessions/dd.py +78 -0
- quasarr/providers/sessions/dl.py +175 -0
- quasarr/providers/sessions/nx.py +76 -0
- quasarr/providers/shared_state.py +656 -79
- quasarr/providers/statistics.py +154 -0
- quasarr/providers/version.py +60 -1
- quasarr/providers/web_server.py +1 -1
- quasarr/search/__init__.py +144 -15
- quasarr/search/sources/al.py +448 -0
- quasarr/search/sources/by.py +204 -0
- quasarr/search/sources/dd.py +135 -0
- quasarr/search/sources/dj.py +213 -0
- quasarr/search/sources/dl.py +354 -0
- quasarr/search/sources/dt.py +265 -0
- quasarr/search/sources/dw.py +94 -67
- quasarr/search/sources/fx.py +89 -33
- quasarr/search/sources/he.py +196 -0
- quasarr/search/sources/mb.py +195 -0
- quasarr/search/sources/nk.py +188 -0
- quasarr/search/sources/nx.py +75 -21
- quasarr/search/sources/sf.py +374 -0
- quasarr/search/sources/sj.py +213 -0
- quasarr/search/sources/sl.py +246 -0
- quasarr/search/sources/wd.py +208 -0
- quasarr/search/sources/wx.py +337 -0
- quasarr/storage/config.py +39 -10
- quasarr/storage/setup.py +269 -97
- quasarr/storage/sqlite_database.py +6 -1
- quasarr-1.23.0.dist-info/METADATA +306 -0
- quasarr-1.23.0.dist-info/RECORD +77 -0
- {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/WHEEL +1 -1
- quasarr/arr/__init__.py +0 -423
- quasarr/captcha_solver/__init__.py +0 -284
- quasarr-0.1.6.dist-info/METADATA +0 -81
- quasarr-0.1.6.dist-info/RECORD +0 -31
- {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/entry_points.txt +0 -0
- {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info/licenses}/LICENSE +0 -0
- {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
from bs4 import BeautifulSoup
|
|
12
|
+
|
|
13
|
+
from quasarr.providers.log import info, debug
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_by_download_links(shared_state, url, mirror, title): # signature must align with other download link functions!
|
|
17
|
+
by = shared_state.values["config"]("Hostnames").get("by")
|
|
18
|
+
headers = {
|
|
19
|
+
'User-Agent': shared_state.values["user_agent"],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
mirror_lower = mirror.lower() if mirror else None
|
|
23
|
+
links = []
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
resp = requests.get(url, headers=headers, timeout=10)
|
|
27
|
+
page_content = resp.text
|
|
28
|
+
soup = BeautifulSoup(page_content, "html.parser")
|
|
29
|
+
frames = [iframe.get("src") for iframe in soup.find_all("iframe") if iframe.get("src")]
|
|
30
|
+
|
|
31
|
+
frame_urls = [src for src in frames if f'https://{by}' in src]
|
|
32
|
+
if not frame_urls:
|
|
33
|
+
debug(f"No iframe hosts found on {url} for {title}.")
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
async_results = []
|
|
37
|
+
|
|
38
|
+
def fetch(url):
|
|
39
|
+
try:
|
|
40
|
+
r = requests.get(url, headers=headers, timeout=10)
|
|
41
|
+
return r.text, url
|
|
42
|
+
except Exception:
|
|
43
|
+
info(f"Error fetching iframe URL: {url}")
|
|
44
|
+
return None, url
|
|
45
|
+
|
|
46
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
|
47
|
+
future_to_url = {executor.submit(fetch, url): url for url in frame_urls}
|
|
48
|
+
for future in concurrent.futures.as_completed(future_to_url):
|
|
49
|
+
content, source = future.result()
|
|
50
|
+
if content:
|
|
51
|
+
async_results.append((content, source))
|
|
52
|
+
|
|
53
|
+
url_hosters = []
|
|
54
|
+
for content, source in async_results:
|
|
55
|
+
host_soup = BeautifulSoup(content, "html.parser")
|
|
56
|
+
link = host_soup.find("a", href=re.compile(
|
|
57
|
+
r"https?://(?:www\.)?(?:hide\.cx|filecrypt\.(?:cc|co|to))/container/"))
|
|
58
|
+
|
|
59
|
+
# Fallback to the old format
|
|
60
|
+
if not link:
|
|
61
|
+
link = host_soup.find("a", href=re.compile(r"/go\.php\?"))
|
|
62
|
+
|
|
63
|
+
if not link:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
href = link["href"]
|
|
67
|
+
hostname = link.text.strip().replace(" ", "")
|
|
68
|
+
hostname_lower = hostname.lower()
|
|
69
|
+
|
|
70
|
+
if mirror_lower and mirror_lower not in hostname_lower:
|
|
71
|
+
debug(f'Skipping link from "{hostname}" (not the desired mirror "{mirror}")!')
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
url_hosters.append((href, hostname))
|
|
75
|
+
|
|
76
|
+
def resolve_redirect(href_hostname):
|
|
77
|
+
href, hostname = href_hostname
|
|
78
|
+
try:
|
|
79
|
+
r = requests.get(href, headers=headers, timeout=10, allow_redirects=True)
|
|
80
|
+
if "/404.html" in r.url:
|
|
81
|
+
info(f"Link leads to 404 page for {hostname}: {r.url}")
|
|
82
|
+
return None
|
|
83
|
+
time.sleep(1)
|
|
84
|
+
return r.url
|
|
85
|
+
except Exception as e:
|
|
86
|
+
info(f"Error resolving link for {hostname}: {e}")
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
for pair in url_hosters:
|
|
90
|
+
resolved_url = resolve_redirect(pair)
|
|
91
|
+
hostname = pair[1]
|
|
92
|
+
|
|
93
|
+
if not hostname:
|
|
94
|
+
hostname = urlparse(resolved_url).hostname
|
|
95
|
+
|
|
96
|
+
if resolved_url and hostname and hostname.startswith(("ddownload", "rapidgator", "turbobit", "filecrypt")):
|
|
97
|
+
if "rapidgator" in hostname:
|
|
98
|
+
links.insert(0, [resolved_url, hostname])
|
|
99
|
+
else:
|
|
100
|
+
links.append([resolved_url, hostname])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
info(f"Error loading BY download links: {e}")
|
|
105
|
+
|
|
106
|
+
return links
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
from quasarr.providers.log import info, debug
|
|
6
|
+
from quasarr.providers.sessions.dd import create_and_persist_session, retrieve_and_validate_session
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_dd_download_links(shared_state, url, mirror, title): # signature must align with other download link functions!
|
|
10
|
+
dd = shared_state.values["config"]("Hostnames").get("dd")
|
|
11
|
+
|
|
12
|
+
dd_session = retrieve_and_validate_session(shared_state)
|
|
13
|
+
if not dd_session:
|
|
14
|
+
info(f"Could not retrieve valid session for {dd}")
|
|
15
|
+
return []
|
|
16
|
+
|
|
17
|
+
links = []
|
|
18
|
+
|
|
19
|
+
qualities = [
|
|
20
|
+
"disk-480p",
|
|
21
|
+
"web-480p",
|
|
22
|
+
"movie-480p-x265",
|
|
23
|
+
"disk-1080p-x265",
|
|
24
|
+
"web-1080p",
|
|
25
|
+
"web-1080p-x265",
|
|
26
|
+
"web-2160p-x265-hdr",
|
|
27
|
+
"movie-1080p-x265",
|
|
28
|
+
"movie-2160p-webdl-x265-hdr"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
headers = {
|
|
32
|
+
'User-Agent': shared_state.values["user_agent"],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
release_list = []
|
|
37
|
+
for page in range(0, 100, 20):
|
|
38
|
+
url = f'https://{dd}/index/search/keyword/{title}/qualities/{','.join(qualities)}/from/{page}/search'
|
|
39
|
+
|
|
40
|
+
releases_on_page = dd_session.get(url, headers=headers, timeout=10).json()
|
|
41
|
+
if releases_on_page:
|
|
42
|
+
release_list.extend(releases_on_page)
|
|
43
|
+
|
|
44
|
+
for release in release_list:
|
|
45
|
+
try:
|
|
46
|
+
if release.get("fake"):
|
|
47
|
+
debug(f"Release {release.get('release')} marked as fake. Invalidating DD session...")
|
|
48
|
+
create_and_persist_session(shared_state)
|
|
49
|
+
return []
|
|
50
|
+
elif release.get("release") == title:
|
|
51
|
+
filtered_links = []
|
|
52
|
+
for link in release["links"]:
|
|
53
|
+
if mirror and mirror not in link["hostname"]:
|
|
54
|
+
debug(f'Skipping link from "{link["hostname"]}" (not the desired mirror "{mirror}")!')
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
if any(
|
|
58
|
+
existing_link["hostname"] == link["hostname"] and
|
|
59
|
+
existing_link["url"].endswith(".mkv") and
|
|
60
|
+
link["url"].endswith(".mkv")
|
|
61
|
+
for existing_link in filtered_links
|
|
62
|
+
):
|
|
63
|
+
debug(f"Skipping duplicate `.mkv` link from {link['hostname']}")
|
|
64
|
+
continue # Skip adding duplicate `.mkv` links from the same hostname
|
|
65
|
+
filtered_links.append(link)
|
|
66
|
+
|
|
67
|
+
links = [link["url"] for link in filtered_links]
|
|
68
|
+
break
|
|
69
|
+
except Exception as e:
|
|
70
|
+
info(f"Error parsing DD download: {e}")
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
info(f"Error loading DD download: {e}")
|
|
75
|
+
|
|
76
|
+
return links
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
|
|
9
|
+
from quasarr.providers.log import info, debug
|
|
10
|
+
from quasarr.providers.sessions.dl import retrieve_and_validate_session, fetch_via_requests_session, invalidate_session
|
|
11
|
+
|
|
12
|
+
hostname = "dl"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def extract_password_from_post(soup, host):
|
|
16
|
+
"""
|
|
17
|
+
Extract password from forum post using multiple strategies.
|
|
18
|
+
Returns empty string if no password found or if explicitly marked as 'no password'.
|
|
19
|
+
"""
|
|
20
|
+
# Get flattened text from the post - collapse whitespace to single spaces
|
|
21
|
+
post_text = soup.get_text()
|
|
22
|
+
post_text = re.sub(r'\s+', ' ', post_text).strip()
|
|
23
|
+
|
|
24
|
+
# Strategy 1: Look for password label followed by the password value
|
|
25
|
+
# Pattern: "Passwort:" followed by optional separators, then the password
|
|
26
|
+
password_pattern = r'(?:passwort|password|pass|pw)[\s:]+([a-zA-Z0-9._-]{2,50})'
|
|
27
|
+
match = re.search(password_pattern, post_text, re.IGNORECASE)
|
|
28
|
+
|
|
29
|
+
if match:
|
|
30
|
+
password = match.group(1).strip()
|
|
31
|
+
# Skip if it looks like a section header or common word
|
|
32
|
+
if not re.match(r'^(?:download|mirror|link|episode|info|mediainfo|spoiler|hier|click|klick|kein|none|no)',
|
|
33
|
+
password, re.IGNORECASE):
|
|
34
|
+
debug(f"Found password: {password}")
|
|
35
|
+
return password
|
|
36
|
+
|
|
37
|
+
# Strategy 2: Look for explicit "no password" indicators (only if no valid password found)
|
|
38
|
+
no_password_patterns = [
|
|
39
|
+
r'(?:passwort|password|pass|pw)[\s:]*(?:kein(?:es)?|none|no|nicht|not|nein|-|–|—)',
|
|
40
|
+
r'(?:kein(?:es)?|none|no|nicht|not|nein)\s*(?:passwort|password|pass|pw)',
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
for pattern in no_password_patterns:
|
|
44
|
+
if re.search(pattern, post_text, re.IGNORECASE):
|
|
45
|
+
debug("No password required (explicitly stated)")
|
|
46
|
+
return ""
|
|
47
|
+
|
|
48
|
+
# Strategy 3: Default to hostname-based password
|
|
49
|
+
default_password = f"www.{host}"
|
|
50
|
+
debug(f"No password found, using default: {default_password}")
|
|
51
|
+
return default_password
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def extract_mirror_name_from_link(link_element):
|
|
55
|
+
"""
|
|
56
|
+
Extract the mirror/hoster name from the link text or nearby text.
|
|
57
|
+
Returns the extracted name or None.
|
|
58
|
+
"""
|
|
59
|
+
# Get the link text
|
|
60
|
+
link_text = link_element.get_text(strip=True)
|
|
61
|
+
|
|
62
|
+
# Try to extract a meaningful name from the link text
|
|
63
|
+
# Look for text that looks like a hoster name (alphanumeric, may contain numbers/dashes)
|
|
64
|
+
# Filter out common non-hoster words
|
|
65
|
+
common_non_hosters = {'download', 'mirror', 'link', 'hier', 'click', 'klick', 'code', 'spoiler'}
|
|
66
|
+
|
|
67
|
+
# Clean and extract potential mirror name
|
|
68
|
+
if link_text and len(link_text) > 2:
|
|
69
|
+
# Remove common symbols and whitespace
|
|
70
|
+
cleaned = re.sub(r'[^\w\s-]', '', link_text).strip().lower()
|
|
71
|
+
|
|
72
|
+
# If it's a single word or hyphenated word and not in common non-hosters
|
|
73
|
+
if cleaned and cleaned not in common_non_hosters:
|
|
74
|
+
# Extract the main part (first word if multiple)
|
|
75
|
+
main_part = cleaned.split()[0] if ' ' in cleaned else cleaned
|
|
76
|
+
if len(main_part) > 2: # Must be at least 3 characters
|
|
77
|
+
return main_part
|
|
78
|
+
|
|
79
|
+
# Check if there's a bold tag or nearby text in parent
|
|
80
|
+
parent = link_element.parent
|
|
81
|
+
if parent:
|
|
82
|
+
parent_text = parent.get_text(strip=True)
|
|
83
|
+
# Look for text before the link that might be the mirror name
|
|
84
|
+
for sibling in link_element.previous_siblings:
|
|
85
|
+
if hasattr(sibling, 'get_text'):
|
|
86
|
+
sibling_text = sibling.get_text(strip=True).lower()
|
|
87
|
+
if sibling_text and len(sibling_text) > 2 and sibling_text not in common_non_hosters:
|
|
88
|
+
cleaned = re.sub(r'[^\w\s-]', '', sibling_text).strip()
|
|
89
|
+
if cleaned:
|
|
90
|
+
return cleaned.split()[0] if ' ' in cleaned else cleaned
|
|
91
|
+
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_links_and_password_from_post(post_content, host):
|
|
96
|
+
"""
|
|
97
|
+
Extract download links and password from a forum post.
|
|
98
|
+
Only filecrypt and hide are supported - other link crypters will cause an error.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
tuple of (links, password) where:
|
|
102
|
+
- links: list of [url, mirror_name] pairs where mirror_name is the actual hoster
|
|
103
|
+
- password: extracted password string
|
|
104
|
+
"""
|
|
105
|
+
links = []
|
|
106
|
+
soup = BeautifulSoup(post_content, 'html.parser')
|
|
107
|
+
|
|
108
|
+
for link in soup.find_all('a', href=True):
|
|
109
|
+
href = link.get('href')
|
|
110
|
+
|
|
111
|
+
# Skip internal forum links
|
|
112
|
+
if href.startswith('/') or host in href:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
# Check supported link crypters
|
|
116
|
+
if re.search(r'filecrypt\.', href, re.IGNORECASE):
|
|
117
|
+
crypter_type = "filecrypt"
|
|
118
|
+
elif re.search(r'hide\.', href, re.IGNORECASE):
|
|
119
|
+
crypter_type = "hide"
|
|
120
|
+
elif re.search(r'keeplinks\.', href, re.IGNORECASE):
|
|
121
|
+
crypter_type = "keeplinks"
|
|
122
|
+
elif re.search(r'tolink\.', href, re.IGNORECASE):
|
|
123
|
+
crypter_type = "tolink"
|
|
124
|
+
else:
|
|
125
|
+
debug(f"Unsupported link crypter/hoster found: {href}")
|
|
126
|
+
debug(f"Currently only filecrypt and hide are supported. Other crypters may be added later.")
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
# Extract mirror name from link text or nearby context
|
|
130
|
+
mirror_name = extract_mirror_name_from_link(link)
|
|
131
|
+
|
|
132
|
+
# Use mirror name if found, otherwise fall back to crypter type
|
|
133
|
+
identifier = mirror_name if mirror_name else crypter_type
|
|
134
|
+
|
|
135
|
+
# Avoid duplicates
|
|
136
|
+
if [href, identifier] not in links:
|
|
137
|
+
links.append([href, identifier])
|
|
138
|
+
if mirror_name:
|
|
139
|
+
debug(f"Found {crypter_type} link for mirror: {mirror_name}")
|
|
140
|
+
else:
|
|
141
|
+
debug(f"Found {crypter_type} link (no mirror name detected)")
|
|
142
|
+
|
|
143
|
+
# Only extract password if we found links
|
|
144
|
+
password = ""
|
|
145
|
+
if links:
|
|
146
|
+
password = extract_password_from_post(soup, host)
|
|
147
|
+
|
|
148
|
+
return links, password
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_dl_download_links(shared_state, url, mirror, title):
|
|
152
|
+
"""
|
|
153
|
+
Get download links from a thread.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
tuple of (links, password) where:
|
|
157
|
+
- links: list of [url, mirror_name] pairs
|
|
158
|
+
- password: extracted password string
|
|
159
|
+
"""
|
|
160
|
+
host = shared_state.values["config"]("Hostnames").get(hostname)
|
|
161
|
+
|
|
162
|
+
sess = retrieve_and_validate_session(shared_state)
|
|
163
|
+
if not sess:
|
|
164
|
+
info(f"Could not retrieve valid session for {host}")
|
|
165
|
+
return [], ""
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
response = fetch_via_requests_session(shared_state, method="GET", target_url=url, timeout=30)
|
|
169
|
+
|
|
170
|
+
if response.status_code != 200:
|
|
171
|
+
info(f"Failed to load thread page: {url} (Status: {response.status_code})")
|
|
172
|
+
return [], ""
|
|
173
|
+
|
|
174
|
+
soup = BeautifulSoup(response.text, 'html.parser')
|
|
175
|
+
|
|
176
|
+
first_post = soup.select_one('article.message--post')
|
|
177
|
+
if not first_post:
|
|
178
|
+
info(f"Could not find first post in thread: {url}")
|
|
179
|
+
return [], ""
|
|
180
|
+
|
|
181
|
+
post_content = first_post.select_one('div.bbWrapper')
|
|
182
|
+
if not post_content:
|
|
183
|
+
info(f"Could not find post content in thread: {url}")
|
|
184
|
+
return [], ""
|
|
185
|
+
|
|
186
|
+
# Extract both links and password from the same post content
|
|
187
|
+
links, password = extract_links_and_password_from_post(str(post_content), host)
|
|
188
|
+
|
|
189
|
+
if not links:
|
|
190
|
+
info(f"No supported download links found in thread: {url}")
|
|
191
|
+
return [], ""
|
|
192
|
+
|
|
193
|
+
debug(f"Found {len(links)} download link(s) for: {title} (password: {password})")
|
|
194
|
+
return links, password
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
info(f"Error extracting download links from {url}: {e}")
|
|
198
|
+
invalidate_session(shared_state)
|
|
199
|
+
return [], ""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import requests
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
from quasarr.providers.log import info
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_dt_download_links(shared_state, url, mirror, title): # signature must align with other download link functions!
|
|
12
|
+
headers = {"User-Agent": shared_state.values["user_agent"]}
|
|
13
|
+
session = requests.Session()
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
resp = session.get(url, headers=headers, timeout=10)
|
|
17
|
+
soup = BeautifulSoup(resp.text, "html.parser")
|
|
18
|
+
|
|
19
|
+
article = soup.find("article")
|
|
20
|
+
if not article:
|
|
21
|
+
info(f"Could not find article block on DT page for {title}")
|
|
22
|
+
return False
|
|
23
|
+
body = article.find("div", class_="card-body")
|
|
24
|
+
if not body:
|
|
25
|
+
info(f"Could not find download section for {title}")
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
# grab all <a href="…">
|
|
29
|
+
anchors = body.find_all("a", href=True)
|
|
30
|
+
|
|
31
|
+
except Exception as e:
|
|
32
|
+
info(f"DT site has been updated. Grabbing download links for {title} not possible! ({e})")
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
# first do your normal filtering
|
|
36
|
+
filtered = []
|
|
37
|
+
for a in anchors:
|
|
38
|
+
href = a["href"].strip()
|
|
39
|
+
|
|
40
|
+
if not href.lower().startswith(("http://", "https://")):
|
|
41
|
+
continue
|
|
42
|
+
lower = href.lower()
|
|
43
|
+
if "imdb.com" in lower or "?ref=" in lower:
|
|
44
|
+
continue
|
|
45
|
+
if mirror and mirror not in href:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
filtered.append(href)
|
|
49
|
+
|
|
50
|
+
# if after filtering you got nothing, fall back to regex
|
|
51
|
+
if not filtered:
|
|
52
|
+
text = body.get_text(separator="\n")
|
|
53
|
+
urls = re.findall(r'https?://[^\s<>"\']+', text)
|
|
54
|
+
# de-dupe preserving order
|
|
55
|
+
seen = set()
|
|
56
|
+
for u in urls:
|
|
57
|
+
u = u.strip()
|
|
58
|
+
if u not in seen:
|
|
59
|
+
seen.add(u)
|
|
60
|
+
# apply same filters
|
|
61
|
+
low = u.lower()
|
|
62
|
+
if low.startswith(("http://", "https://")) and "imdb.com" not in low and "?ref=" not in low:
|
|
63
|
+
if not mirror or mirror in u:
|
|
64
|
+
filtered.append(u)
|
|
65
|
+
|
|
66
|
+
return filtered
|
quasarr/downloads/sources/dw.py
CHANGED
|
@@ -7,8 +7,10 @@ import re
|
|
|
7
7
|
import requests
|
|
8
8
|
from bs4 import BeautifulSoup
|
|
9
9
|
|
|
10
|
+
from quasarr.providers.log import info, debug
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
def get_dw_download_links(shared_state, url, mirror, title): # signature must align with other download link functions!
|
|
12
14
|
dw = shared_state.values["config"]("Hostnames").get("dw")
|
|
13
15
|
ajax_url = "https://" + dw + "/wp-admin/admin-ajax.php"
|
|
14
16
|
|
|
@@ -19,11 +21,11 @@ def get_dw_download_links(shared_state, url, title):
|
|
|
19
21
|
session = requests.Session()
|
|
20
22
|
|
|
21
23
|
try:
|
|
22
|
-
request = session.get(url, headers=headers)
|
|
24
|
+
request = session.get(url, headers=headers, timeout=10)
|
|
23
25
|
content = BeautifulSoup(request.text, "html.parser")
|
|
24
|
-
download_buttons = content.
|
|
26
|
+
download_buttons = content.find_all("button", {"class": "show_link"})
|
|
25
27
|
except:
|
|
26
|
-
|
|
28
|
+
info(f"DW site has been updated. Grabbing download links for {title} not possible!")
|
|
27
29
|
return False
|
|
28
30
|
|
|
29
31
|
download_links = []
|
|
@@ -35,9 +37,9 @@ def get_dw_download_links(shared_state, url, title):
|
|
|
35
37
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
response = session.post(ajax_url, payload, headers=headers)
|
|
40
|
+
response = session.post(ajax_url, payload, headers=headers, timeout=10)
|
|
39
41
|
if response.status_code != 200:
|
|
40
|
-
|
|
42
|
+
info(f"DW site has been updated. Grabbing download links for {title} not possible!")
|
|
41
43
|
continue
|
|
42
44
|
else:
|
|
43
45
|
response = response.json()
|
|
@@ -50,9 +52,14 @@ def get_dw_download_links(shared_state, url, title):
|
|
|
50
52
|
f'.html{match.group(2) if match.group(2) else ""}')
|
|
51
53
|
|
|
52
54
|
hoster = button.nextSibling.img["src"].split("/")[-1].replace(".png", "")
|
|
55
|
+
hoster = f"1fichier" if hoster.startswith("fichier") else hoster # align with expected mirror name
|
|
56
|
+
if mirror and mirror.lower() not in hoster.lower():
|
|
57
|
+
debug(f'Skipping link from "{hoster}" (not the desired mirror "{mirror}")!')
|
|
58
|
+
continue
|
|
59
|
+
|
|
53
60
|
download_links.append([link, hoster])
|
|
54
61
|
except:
|
|
55
|
-
|
|
62
|
+
info(f"DW site has been updated. Parsing download links for {title} not possible!")
|
|
56
63
|
pass
|
|
57
64
|
|
|
58
65
|
return download_links
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from urllib.parse import urlparse, urljoin
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from bs4 import BeautifulSoup
|
|
10
|
+
|
|
11
|
+
from quasarr.providers.log import info, debug
|
|
12
|
+
|
|
13
|
+
hostname = "he"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_he_download_links(shared_state, url, mirror, title):
|
|
17
|
+
headers = {
|
|
18
|
+
'User-Agent': shared_state.values["user_agent"],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
session = requests.Session()
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
resp = session.get(url, headers=headers, timeout=30)
|
|
25
|
+
soup = BeautifulSoup(resp.text, 'html.parser')
|
|
26
|
+
except Exception as e:
|
|
27
|
+
info(f"{hostname}: could not fetch release for {title}: {e}")
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
imdb_id = None
|
|
31
|
+
try:
|
|
32
|
+
imdb_link = soup.find('a', href=re.compile(r"imdb\.com/title/tt\d+", re.IGNORECASE))
|
|
33
|
+
if imdb_link:
|
|
34
|
+
href = imdb_link['href'].strip()
|
|
35
|
+
m = re.search(r"(tt\d{4,7})", href)
|
|
36
|
+
if m:
|
|
37
|
+
imdb_id = m.group(1)
|
|
38
|
+
else:
|
|
39
|
+
debug(f"{hostname}: imdb_id not found for title {title} in link href.")
|
|
40
|
+
else:
|
|
41
|
+
debug(f"{hostname}: imdb_id link href not found for title {title}.")
|
|
42
|
+
except Exception:
|
|
43
|
+
debug(f"{hostname}: failed to extract imdb_id for title {title}.")
|
|
44
|
+
|
|
45
|
+
anchors = []
|
|
46
|
+
for retries in range(10):
|
|
47
|
+
form = soup.find('form', id=re.compile(r'content-protector-access-form'))
|
|
48
|
+
if not form:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
action = form.get('action') or url
|
|
52
|
+
action_url = urljoin(resp.url, action)
|
|
53
|
+
|
|
54
|
+
payload = {}
|
|
55
|
+
for inp in form.find_all('input'):
|
|
56
|
+
name = inp.get('name')
|
|
57
|
+
if not name:
|
|
58
|
+
continue
|
|
59
|
+
value = inp.get('value', '')
|
|
60
|
+
payload[name] = value
|
|
61
|
+
|
|
62
|
+
append_patt = re.compile(r"append\(\s*[\'\"](?P<key>[^\'\"]+)[\'\"]\s*,\s*[\'\"](?P<val>[^\'\"]+)[\'\"]\s*\)",
|
|
63
|
+
re.IGNORECASE)
|
|
64
|
+
|
|
65
|
+
for script in soup.find_all('script'):
|
|
66
|
+
txt = script.string if script.string is not None else script.get_text()
|
|
67
|
+
if not txt:
|
|
68
|
+
continue
|
|
69
|
+
for m in append_patt.finditer(txt):
|
|
70
|
+
payload[m.group('key')] = m.group('val')
|
|
71
|
+
|
|
72
|
+
post_headers = headers.copy()
|
|
73
|
+
post_headers.update({'Referer': resp.url})
|
|
74
|
+
try:
|
|
75
|
+
resp = session.post(action_url, data=payload, headers=post_headers, timeout=30)
|
|
76
|
+
soup = BeautifulSoup(resp.text, 'html.parser')
|
|
77
|
+
except Exception as e:
|
|
78
|
+
info(f"{hostname}: could not submit protector form for {title}: {e}")
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
unlocked = soup.select('.content-protector-access-form')
|
|
82
|
+
if unlocked:
|
|
83
|
+
for u in unlocked:
|
|
84
|
+
anchors.extend(u.find_all('a', href=True))
|
|
85
|
+
|
|
86
|
+
if anchors:
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
links = []
|
|
90
|
+
for a in anchors:
|
|
91
|
+
try:
|
|
92
|
+
href = a['href'].strip()
|
|
93
|
+
|
|
94
|
+
netloc = urlparse(href).netloc
|
|
95
|
+
hoster = netloc.split(':')[0].lower()
|
|
96
|
+
parts = hoster.split('.')
|
|
97
|
+
if len(parts) >= 2:
|
|
98
|
+
hoster = parts[-2]
|
|
99
|
+
|
|
100
|
+
links.append([href, hoster])
|
|
101
|
+
except Exception:
|
|
102
|
+
debug(f"{hostname}: could not resolve download link hoster for {title}")
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
if not links:
|
|
106
|
+
info(f"No external download links found on {hostname} page for {title}")
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
"links": links,
|
|
111
|
+
"imdb_id": imdb_id,
|
|
112
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from bs4 import BeautifulSoup
|
|
9
|
+
|
|
10
|
+
from quasarr.providers.log import info, debug
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_mb_download_links(shared_state, url, mirror, title): # signature must align with other download link functions!
|
|
14
|
+
headers = {
|
|
15
|
+
'User-Agent': shared_state.values["user_agent"],
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
response = requests.get(url, headers=headers, timeout=10)
|
|
20
|
+
response.raise_for_status()
|
|
21
|
+
except Exception as e:
|
|
22
|
+
info(f"Failed to fetch page for {title or url}: {e}")
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
soup = BeautifulSoup(response.text, "html.parser")
|
|
26
|
+
|
|
27
|
+
download_links = []
|
|
28
|
+
|
|
29
|
+
pattern = re.compile(r'https?://(?:www\.)?filecrypt\.[^/]+/Container/', re.IGNORECASE)
|
|
30
|
+
for a in soup.find_all('a', href=pattern):
|
|
31
|
+
try:
|
|
32
|
+
link = a['href']
|
|
33
|
+
hoster = a.get_text(strip=True).lower()
|
|
34
|
+
|
|
35
|
+
if mirror and mirror.lower() not in hoster.lower():
|
|
36
|
+
debug(f'Skipping link from "{hoster}" (not the desired mirror "{mirror}")!')
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
download_links.append([link, hoster])
|
|
40
|
+
except Exception as e:
|
|
41
|
+
debug(f"Error parsing MB download links: {e}")
|
|
42
|
+
|
|
43
|
+
if not download_links:
|
|
44
|
+
info(f"No download links found for {title}. Site structure may have changed. - {url}")
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
return download_links
|