Skip to content

Commit

Permalink
Merge pull request #11 from rix1337/dev
Browse files Browse the repository at this point in the history
Added site: DW
  • Loading branch information
rix1337 authored Aug 25, 2024
2 parents f178741 + 071f151 commit 4c49334
Show file tree
Hide file tree
Showing 16 changed files with 399 additions and 87 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Quasarr includes a solution to quickly and easily decrypt protected links.
Just follow the link from the console output (or discord notification) and solve the CAPTCHA.
Quasarr will confidently handle the rest.

**Warning: this project is still in the proof-of-concept stage.
It is only tested with Radarr and the three currently supported hostnames.**

# Instructions

* Follow instructions to set up at least one hostname for Quasarr
Expand All @@ -24,9 +27,6 @@ Quasarr will confidently handle the rest.
* Use this API key: `quasarr`
* As with other download clients, you must ensure the download path used by JDownloader is accessible to *arr.

**Warning: this project is still in the proof-of-concept stage.
It is only tested with Radarr and only two hostname are currently supported.**

# Setup

`pip install quasarr`
Expand Down
8 changes: 4 additions & 4 deletions quasarr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
import time

from quasarr.arr import api
from quasarr.persistence.config import Config, get_clean_hostnames
from quasarr.persistence.sqlite_database import DataBase
from quasarr.storage.config import Config, get_clean_hostnames
from quasarr.storage.sqlite_database import DataBase
from quasarr.providers import shared_state, version
from quasarr.providers.setup import path_config, hostnames_config, nx_credentials_config, jdownloader_config
from quasarr.storage.setup import path_config, hostnames_config, nx_credentials_config, jdownloader_config


def run():
Expand Down Expand Up @@ -45,7 +45,7 @@ def run():
config_path = "/config"
if not arguments.internal_address:
print(
"You must set the INTERNAL_ADDRESS variable to a locally reachable URL, e.g. http://localhost:8080")
"You must set the INTERNAL_ADDRESS variable to a locally reachable URL, e.g. http://192.168.1.1:8080")
print("The local URL will be used by Radarr/Sonarr to connect to Quasarr")
print("Stopping Quasarr...")
sys.exit(1)
Expand Down
83 changes: 63 additions & 20 deletions quasarr/arr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Quasarr
# Project by https://github.com/rix1337

import json
import re
import traceback
from base64 import urlsafe_b64decode
Expand All @@ -25,30 +26,72 @@ def api(shared_state_dict, shared_state_lock):

app = Bottle()

@app.route('/captcha')
@app.get('/captcha')
def serve_captcha():
protected = shared_state.get_db("protected").retrieve_all_titles()
if not protected:
return render_centered_html('<h1>Quasarr</h1><p>No protected packages found! CAPTCHA not needed.</p>')
try:
device = shared_state.values["device"]
except KeyError:
device = None
if not device:
return render_centered_html('<h1>Quasarr</h1><p>JDownloader connection not established.</p>')
return render_centered_html(f'''<h1>Quasarr</h1>
<p>JDownloader connection not established.</p>
{render_button("Back", "primary", {"onclick": "location.href='/'"})}''')

protected = shared_state.get_db("protected").retrieve_all_titles()
if not protected:
return render_centered_html(f'''<h1>Quasarr</h1>
<p>No protected packages found! CAPTCHA not needed.</p>
{render_button("Back", "primary", {"onclick": "location.href='/'"})}''')
else:
package = protected[0]
package_id = package[0]
data = json.loads(package[1])
title = data["title"]
links = data["links"]
password = data["password"]

link_options = ""
if len(links) > 1:
for link in links:
if "filecrypt." in link[0]:
link_options += f'<option value="{link[0]}">{link[1]}</option>'
link_select = f'''<div id="mirrors-select">
<label for="link-select">Mirror:</label>
<select id="link-select">
{link_options}
</select>
</div>
<script>
document.getElementById("link-select").addEventListener("change", function() {{
var selectedLink = this.value;
document.getElementById("link-hidden").value = selectedLink;
}});
</script>
'''
else:
link_select = f'<div id="mirrors-select">Mirror: <b>{links[0][1]}</b></div>'

content = render_centered_html(r'''
<script type="text/javascript">
var api_key = "''' + captcha_values()["api_key"] + r'''";
var endpoint = '/' + window.location.pathname.split('/')[1] + '/' + api_key + '.html';
function handleToken(token) {
document.getElementById("puzzle-captcha").remove();
document.getElementById("mirrors-select").remove();
document.getElementById("captcha-key").innerText = 'Using result "' + token + '" to decrypt links...';
var link = document.getElementById("link-hidden").value;
fetch('/decrypt-filecrypt', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: token })
body: JSON.stringify({
token: token,
''' + f'''package_id: '{package_id}',
title: '{title}',
link: link,
password: '{password}'
''' + '''})
})
.then(response => response.json())
.then(data => {
Expand All @@ -65,16 +108,19 @@ def serve_captcha():
''' + captcha_js() + f'''</script>
<div>
<h1>Quasarr</h1>
{link_select}<br><br>
<input type="hidden" id="link-hidden" value="{links[0][0]}" />
<div id="puzzle-captcha" aria-style="mobile">
<strong>Your adblocker prevents the captcha from loading. Disable it!</strong>
</div>
<div id="captcha-key"></div>
<div id="reload-button" style="display: none;">
{render_button("Solve another CAPTCHA", "secondary", {
"onclick": "location.reload()",
})}</div>
<br>{render_button("Back", "primary", {"onclick": "location.href='/'"})}
</div>
</html>''')

Expand All @@ -85,34 +131,31 @@ def submit_token():
protected = shared_state.get_db("protected").retrieve_all_titles()
if not protected:
return {"success": False, "title": "No protected packages found! CAPTCHA not needed."}
else:
first_protected = protected[0]
package_id = first_protected[0]
details = first_protected[1].split("|")
title = details[0]
link = details[1]
password = details[3]

links = []
download_links = []

try:
data = request.json
token = data.get('token')
package_id = data.get('package_id')
title = data.get('title')
link = data.get('link')
password = data.get('password')
if token:
print(f"Received token: {token}")
print(f"Decrypting links for {title}")
links = get_filecrypt_links(shared_state, token, title, link, password)
download_links = get_filecrypt_links(shared_state, token, title, link, password)

print(f"Decrypted {len(links)} download links for {title}")
print(f"Decrypted {len(download_links)} download links for {title}")

shared_state.download_package(links, title, password, package_id)
shared_state.download_package(download_links, title, password, package_id)

shared_state.get_db("protected").delete(package_id)

except Exception as e:
print(f"Error decrypting: {e}")

return {"success": bool(links), "title": title}
return {"success": bool(download_links), "title": title}

@app.post('/captcha/<captcha_id>.html')
def proxy(captcha_id):
Expand Down
11 changes: 6 additions & 5 deletions quasarr/captcha_solver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,21 @@ def get_filecrypt_links(shared_state, token, title, url, password=None):
print("Attempting to decrypt Filecrypt link: " + url)
session = requests.Session()

password_field = None
if password:
password_id = "password"
try:
output = requests.get(url, headers={'User-Agent': shared_state.values["user_agent"]})
soup = BeautifulSoup(output.text, 'html.parser')
input_element = soup.find('input', placeholder=lambda value: value and 'password' in value.lower())
password_id = input_element['name']
print("Password field name identified: " + password_id)
password_field = input_element['name']
print("Password field name identified: " + password_field)
url = output.url
except:
print("Could not get password field name! Filecrypt may have changed their layout.")
print("No password field found. Skipping password entry!")

if password and password_field:
print("Using Password: " + password)
output = session.post(url, data=password_id + "=" + password,
output = session.post(url, data=password_field + "=" + password,
headers={'User-Agent': shared_state.values["user_agent"],
'Content-Type': 'application/x-www-form-urlencoded'})
else:
Expand Down
83 changes: 48 additions & 35 deletions quasarr/downloads/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
# Quasarr
# Project by https://github.com/rix1337

import json

from quasarr.downloads.sources.dw import get_dw_download_links
from quasarr.downloads.sources.nx import get_nx_download_links
from quasarr.providers.myjd_api import TokenExpiredException, RequestTimeoutException, MYJDException
from quasarr.providers.notifications import send_discord_captcha_alert
Expand Down Expand Up @@ -40,13 +43,13 @@ def get_packages(shared_state):
if protected_packages:
for package in protected_packages:
package_id = package[0]
package_details = package[1].split("|")

data = json.loads(package[1])
details = {
"title": package_details[0],
"url": package_details[1],
"size_mb": package_details[2],
"password": package_details[3]
"title": data["title"],
"urls": data["links"],
"size_mb": data["size_mb"],
"password": data["password"]
}

packages.append({
Expand Down Expand Up @@ -195,36 +198,6 @@ def get_packages(shared_state):
return downloads


def download_package(shared_state, request_from, title, url, size_mb, password):
if "radarr".lower() in request_from.lower():
category = "movies"
else:
category = "tv"

package_id = ""

nx = shared_state.values["config"]("Hostnames").get("nx")
if nx.lower() in url.lower():
links = get_nx_download_links(shared_state, url, title)
print(f"Decrypted {len(links)} download links for {title}")
package_id = f"Quasarr_{category}_{str(hash(title + url)).replace('-', '')}"

added = shared_state.download_package(links, title, password, package_id)

if not added:
print(f"Failed to add {title} to linkgrabber")
package_id = None

elif "filecrypt".lower() in url.lower():
print(f"CAPTCHA-Solution required for {title}{shared_state.values['external_address']}/captcha")
send_discord_captcha_alert(shared_state, title)
package_id = f"Quasarr_{category}_{str(hash(title + url)).replace('-', '')}"
blob = f"{title}|{url}|{size_mb}|{password}"
shared_state.values["database"]("protected").update_store(package_id, blob)

return package_id


def delete_package(shared_state, package_id):
deleted = ""

Expand Down Expand Up @@ -255,3 +228,43 @@ def delete_package(shared_state, package_id):
else:
print(f"Failed to delete package {package_id}")
return deleted


def download_package(shared_state, request_from, title, url, size_mb, password):
if "radarr".lower() in request_from.lower():
category = "movies"
else:
category = "tv"

package_id = ""

dw = shared_state.values["config"]("Hostnames").get("dw")
nx = shared_state.values["config"]("Hostnames").get("nx")

if nx.lower() in url.lower():
links = get_nx_download_links(shared_state, url, title)
print(f"Decrypted {len(links)} download links for {title}")
package_id = f"Quasarr_{category}_{str(hash(title + url)).replace('-', '')}"

added = shared_state.download_package(links, title, password, package_id)

if not added:
print(f"Failed to add {title} to linkgrabber")
package_id = None

elif dw.lower() in url.lower():
links = get_dw_download_links(shared_state, url, title)
print(f"CAPTCHA-Solution required for {title} - {shared_state.values['external_address']}/captcha")
send_discord_captcha_alert(shared_state, title)
package_id = f"Quasarr_{category}_{str(hash(title + str(links))).replace('-', '')}"
blob = json.dumps({"title": title, "links": links, "size_mb": size_mb, "password": password})
shared_state.values["database"]("protected").update_store(package_id, blob)

elif "filecrypt".lower() in url.lower():
print(f"CAPTCHA-Solution required for {title} - {shared_state.values['external_address']}/captcha")
send_discord_captcha_alert(shared_state, title)
package_id = f"Quasarr_{category}_{str(hash(title + url)).replace('-', '')}"
blob = json.dumps({"title": title, "links": [[url, "filecrypt"]], "size_mb": size_mb, "password": password})
shared_state.values["database"]("protected").update_store(package_id, blob)

return package_id
58 changes: 58 additions & 0 deletions quasarr/downloads/sources/dw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Quasarr
# Project by https://github.com/rix1337

import re

import requests
from bs4 import BeautifulSoup


def get_dw_download_links(shared_state, url, title):
dw = shared_state.values["config"]("Hostnames").get("dw")
ajax_url = "https://" + dw + "/wp-admin/admin-ajax.php"

headers = {
'User-Agent': shared_state.values["user_agent"],
}

session = requests.Session()

try:
request = session.get(url, headers=headers)
content = BeautifulSoup(request.text, "html.parser")
download_buttons = content.findAll("button", {"class": "show_link"})
except:
print(f"DW site has been updated. Grabbing download links for {title} not possible!")
return False

download_links = []
try:
for button in download_buttons:
payload = f"action=show_link&link_id={button['value']}"
headers = {
'User-Agent': shared_state.values["user_agent"],
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}

response = session.post(ajax_url, payload, headers=headers)
if response.status_code != 200:
print(f"DW site has been updated. Grabbing download links for {title} not possible!")
continue
else:
response = response.json()
link = response["data"].split(",")[0]

if dw in link:
match = re.search(r'https://' + dw + r'/azn/af\.php\?v=([A-Z0-9]+)(#.*)?', link)
if match:
link = (f'https://filecrypt.cc/Container/{match.group(1)}'
f'.html{match.group(2) if match.group(2) else ""}')

hoster = button.nextSibling.img["src"].split("/")[-1].replace(".png", "")
download_links.append([link, hoster])
except:
print(f"DW site has been updated. Parsing download links for {title} not possible!")
pass

return download_links
Loading

0 comments on commit 4c49334

Please sign in to comment.