quasarr 1.20.6__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 +460 -0
- quasarr/api/__init__.py +187 -0
- quasarr/api/arr/__init__.py +373 -0
- quasarr/api/captcha/__init__.py +1075 -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 +267 -0
- 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 +467 -0
- quasarr/downloads/sources/__init__.py +0 -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/dt.py +66 -0
- quasarr/downloads/sources/dw.py +65 -0
- quasarr/downloads/sources/he.py +112 -0
- quasarr/downloads/sources/mb.py +47 -0
- quasarr/downloads/sources/nk.py +51 -0
- quasarr/downloads/sources/nx.py +105 -0
- 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/providers/__init__.py +0 -0
- quasarr/providers/cloudflare.py +204 -0
- quasarr/providers/html_images.py +20 -0
- quasarr/providers/html_templates.py +241 -0
- quasarr/providers/imdb_metadata.py +142 -0
- quasarr/providers/log.py +19 -0
- quasarr/providers/myjd_api.py +917 -0
- quasarr/providers/notifications.py +124 -0
- quasarr/providers/obfuscated.py +51 -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/nx.py +76 -0
- quasarr/providers/shared_state.py +826 -0
- quasarr/providers/statistics.py +154 -0
- quasarr/providers/version.py +118 -0
- quasarr/providers/web_server.py +49 -0
- quasarr/search/__init__.py +153 -0
- quasarr/search/sources/__init__.py +0 -0
- quasarr/search/sources/al.py +448 -0
- quasarr/search/sources/by.py +203 -0
- quasarr/search/sources/dd.py +135 -0
- quasarr/search/sources/dj.py +213 -0
- quasarr/search/sources/dt.py +265 -0
- quasarr/search/sources/dw.py +214 -0
- quasarr/search/sources/fx.py +223 -0
- 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 +197 -0
- 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/storage/__init__.py +0 -0
- quasarr/storage/config.py +163 -0
- quasarr/storage/setup.py +458 -0
- quasarr/storage/sqlite_database.py +80 -0
- quasarr-1.20.6.dist-info/METADATA +304 -0
- quasarr-1.20.6.dist-info/RECORD +72 -0
- quasarr-1.20.6.dist-info/WHEEL +5 -0
- quasarr-1.20.6.dist-info/entry_points.txt +2 -0
- quasarr-1.20.6.dist-info/licenses/LICENSE +21 -0
- quasarr-1.20.6.dist-info/top_level.txt +1 -0
quasarr/storage/setup.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from bottle import Bottle, request
|
|
10
|
+
|
|
11
|
+
import quasarr
|
|
12
|
+
import quasarr.providers.html_images as images
|
|
13
|
+
import quasarr.providers.sessions.al
|
|
14
|
+
import quasarr.providers.sessions.dd
|
|
15
|
+
import quasarr.providers.sessions.nx
|
|
16
|
+
from quasarr.providers.html_templates import render_button, render_form, render_success, render_fail
|
|
17
|
+
from quasarr.providers.log import info
|
|
18
|
+
from quasarr.providers.shared_state import extract_valid_hostname
|
|
19
|
+
from quasarr.providers.web_server import Server
|
|
20
|
+
from quasarr.storage.config import Config
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def path_config(shared_state):
|
|
24
|
+
app = Bottle()
|
|
25
|
+
|
|
26
|
+
current_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
27
|
+
|
|
28
|
+
@app.get('/')
|
|
29
|
+
def config_form():
|
|
30
|
+
config_form_html = f'''
|
|
31
|
+
<form action="/api/config" method="post">
|
|
32
|
+
<label for="config_path">Path</label>
|
|
33
|
+
<input type="text" id="config_path" name="config_path" placeholder="{current_path}"><br>
|
|
34
|
+
{render_button("Save", "primary", {"type": "submit"})}
|
|
35
|
+
</form>
|
|
36
|
+
'''
|
|
37
|
+
return render_form("Press 'Save' to set desired path for configuration",
|
|
38
|
+
config_form_html)
|
|
39
|
+
|
|
40
|
+
def set_config_path(config_path):
|
|
41
|
+
config_path_file = "Quasarr.conf"
|
|
42
|
+
|
|
43
|
+
if not config_path:
|
|
44
|
+
config_path = current_path
|
|
45
|
+
|
|
46
|
+
config_path = config_path.replace("\\", "/")
|
|
47
|
+
config_path = config_path[:-1] if config_path.endswith('/') else config_path
|
|
48
|
+
|
|
49
|
+
if not os.path.exists(config_path):
|
|
50
|
+
os.makedirs(config_path)
|
|
51
|
+
|
|
52
|
+
with open(config_path_file, "w") as f:
|
|
53
|
+
f.write(config_path)
|
|
54
|
+
|
|
55
|
+
return config_path
|
|
56
|
+
|
|
57
|
+
@app.post("/api/config")
|
|
58
|
+
def set_config():
|
|
59
|
+
config_path = request.forms.get("config_path")
|
|
60
|
+
config_path = set_config_path(config_path)
|
|
61
|
+
quasarr.providers.web_server.temp_server_success = True
|
|
62
|
+
return render_success(f'Config path set to: "{config_path}"',
|
|
63
|
+
5)
|
|
64
|
+
|
|
65
|
+
info(f'Starting web server for config at: "{shared_state.values['internal_address']}".')
|
|
66
|
+
info("Please set desired config path there!")
|
|
67
|
+
return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def hostname_form_html(shared_state, message):
|
|
71
|
+
hostname_fields = '''
|
|
72
|
+
<label for="{id}" style="display:inline-flex; align-items:center; gap:4px;">{label}{img_html}</label>
|
|
73
|
+
<input type="text" id="{id}" name="{id}" placeholder="example.com" autocorrect="off" autocomplete="off" value="{value}"><br>
|
|
74
|
+
'''
|
|
75
|
+
|
|
76
|
+
field_html = []
|
|
77
|
+
hostnames = Config('Hostnames') # Load once outside the loop
|
|
78
|
+
for label in shared_state.values["sites"]:
|
|
79
|
+
field_id = label.lower()
|
|
80
|
+
img_html = ''
|
|
81
|
+
try:
|
|
82
|
+
img_data = getattr(images, field_id)
|
|
83
|
+
if img_data:
|
|
84
|
+
img_html = f' <img src="{img_data}" width="16" height="16" style="filter: blur(2px);" alt="{label} icon">'
|
|
85
|
+
except AttributeError:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# Get the current value (if any and non-empty)
|
|
89
|
+
current_value = hostnames.get(field_id)
|
|
90
|
+
if not current_value:
|
|
91
|
+
current_value = '' # Ensure it's empty if None or ""
|
|
92
|
+
|
|
93
|
+
field_html.append(hostname_fields.format(
|
|
94
|
+
id=field_id,
|
|
95
|
+
label=label,
|
|
96
|
+
img_html=img_html,
|
|
97
|
+
value=current_value
|
|
98
|
+
))
|
|
99
|
+
|
|
100
|
+
hostname_form_content = "".join(field_html)
|
|
101
|
+
button_html = render_button("Save", "primary", {"type": "submit"})
|
|
102
|
+
|
|
103
|
+
template = """
|
|
104
|
+
<div id="message" style="margin-bottom:0.5em;">{message}</div>
|
|
105
|
+
<div id="error-msg" style="color:red; margin-bottom:1em;"></div>
|
|
106
|
+
|
|
107
|
+
<form action="/api/hostnames" method="post" onsubmit="return validateHostnames(this)">
|
|
108
|
+
{hostname_form_content}
|
|
109
|
+
{button}
|
|
110
|
+
</form>
|
|
111
|
+
|
|
112
|
+
<script>
|
|
113
|
+
function validateHostnames(form) {{
|
|
114
|
+
var errorDiv = document.getElementById('error-msg');
|
|
115
|
+
errorDiv.textContent = '';
|
|
116
|
+
|
|
117
|
+
var inputs = form.querySelectorAll('input[type="text"]');
|
|
118
|
+
for (var i = 0; i < inputs.length; i++) {{
|
|
119
|
+
if (inputs[i].value.trim() !== '') {{
|
|
120
|
+
return true;
|
|
121
|
+
}}
|
|
122
|
+
}}
|
|
123
|
+
|
|
124
|
+
errorDiv.textContent = 'Please fill in at least one hostname!';
|
|
125
|
+
inputs[0].focus();
|
|
126
|
+
return false;
|
|
127
|
+
}}
|
|
128
|
+
</script>
|
|
129
|
+
"""
|
|
130
|
+
return template.format(
|
|
131
|
+
message=message,
|
|
132
|
+
hostname_form_content=hostname_form_content,
|
|
133
|
+
button=button_html
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def save_hostnames(shared_state, timeout=5, first_run=True):
|
|
138
|
+
hostnames = Config('Hostnames')
|
|
139
|
+
|
|
140
|
+
# Collect submitted hostnames, validate, and track errors
|
|
141
|
+
valid_domains = {}
|
|
142
|
+
errors = {}
|
|
143
|
+
|
|
144
|
+
for site_key in shared_state.values['sites']:
|
|
145
|
+
shorthand = site_key.lower()
|
|
146
|
+
raw_value = request.forms.get(shorthand)
|
|
147
|
+
# treat missing or empty string as intentional clear, no validation
|
|
148
|
+
if raw_value is None or raw_value.strip() == '':
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
# non-empty submission: must validate
|
|
152
|
+
result = extract_valid_hostname(raw_value, shorthand)
|
|
153
|
+
domain = result.get('domain')
|
|
154
|
+
message = result.get('message', 'Error checking the hostname you provided!')
|
|
155
|
+
if domain:
|
|
156
|
+
valid_domains[site_key] = domain
|
|
157
|
+
else:
|
|
158
|
+
errors[site_key] = message
|
|
159
|
+
|
|
160
|
+
# Filter out any accidental empty domains and require at least one valid hostname overall
|
|
161
|
+
valid_domains = {k: d for k, d in valid_domains.items() if d}
|
|
162
|
+
if not valid_domains:
|
|
163
|
+
# report last or generic message
|
|
164
|
+
fail_msg = next(iter(errors.values()), 'No valid hostname provided!')
|
|
165
|
+
return render_fail(fail_msg)
|
|
166
|
+
|
|
167
|
+
# Save: valid ones, explicit empty for those omitted cleanly, leave untouched if error
|
|
168
|
+
changed_sites = []
|
|
169
|
+
for site_key in shared_state.values['sites']:
|
|
170
|
+
shorthand = site_key.lower()
|
|
171
|
+
raw_value = request.forms.get(shorthand)
|
|
172
|
+
# determine if change applies
|
|
173
|
+
if site_key in valid_domains:
|
|
174
|
+
new_val = valid_domains[site_key]
|
|
175
|
+
old_val = hostnames.get(shorthand) or ''
|
|
176
|
+
if old_val != new_val:
|
|
177
|
+
hostnames.save(shorthand, new_val)
|
|
178
|
+
changed_sites.append(shorthand)
|
|
179
|
+
elif raw_value is None:
|
|
180
|
+
# no submission: leave untouched
|
|
181
|
+
continue
|
|
182
|
+
elif raw_value.strip() == '':
|
|
183
|
+
old_val = hostnames.get(shorthand) or ''
|
|
184
|
+
if old_val != '':
|
|
185
|
+
hostnames.save(shorthand, '')
|
|
186
|
+
|
|
187
|
+
quasarr.providers.web_server.temp_server_success = True
|
|
188
|
+
|
|
189
|
+
# Build success message, include any per-site errors
|
|
190
|
+
success_msg = 'At least one valid hostname set!'
|
|
191
|
+
if errors:
|
|
192
|
+
optional_text = "<br>".join(f"{site}: {msg}" for site, msg in errors.items()) + "<br>"
|
|
193
|
+
else:
|
|
194
|
+
optional_text = "All provided hostnames are valid.<br>"
|
|
195
|
+
|
|
196
|
+
if not first_run:
|
|
197
|
+
# Append restart notice for specific sites that actually changed
|
|
198
|
+
for site in changed_sites:
|
|
199
|
+
if site.lower() in {'al', 'dd', 'nx'}:
|
|
200
|
+
optional_text += f"{site.upper()}: You must restart Quasarr and follow additional steps to start using this site.<br>"
|
|
201
|
+
|
|
202
|
+
return render_success(success_msg, timeout, optional_text=optional_text)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def hostnames_config(shared_state):
|
|
206
|
+
app = Bottle()
|
|
207
|
+
|
|
208
|
+
@app.get('/')
|
|
209
|
+
def hostname_form():
|
|
210
|
+
message = """<p>
|
|
211
|
+
If you're having trouble setting this up, take a closer look at
|
|
212
|
+
<a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions" target="_blank" rel="noopener noreferrer">
|
|
213
|
+
step one of these instructions.
|
|
214
|
+
</a>
|
|
215
|
+
</p>"""
|
|
216
|
+
return render_form("Set at least one valid hostname", hostname_form_html(shared_state, message))
|
|
217
|
+
|
|
218
|
+
@app.post("/api/hostnames")
|
|
219
|
+
def set_hostnames():
|
|
220
|
+
return save_hostnames(shared_state)
|
|
221
|
+
|
|
222
|
+
info(f'Hostnames not set. Starting web server for config at: "{shared_state.values['internal_address']}".')
|
|
223
|
+
info("Please set at least one valid hostname there!")
|
|
224
|
+
return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def hostname_credentials_config(shared_state, shorthand, domain):
|
|
228
|
+
app = Bottle()
|
|
229
|
+
|
|
230
|
+
shorthand = shorthand.upper()
|
|
231
|
+
|
|
232
|
+
@app.get('/')
|
|
233
|
+
def credentials_form():
|
|
234
|
+
form_content = f'''
|
|
235
|
+
<span>If required register account at: <a href="https://{domain}">{domain}</a>!</span><br><br>
|
|
236
|
+
<label for="user">Username</label>
|
|
237
|
+
<input type="text" id="user" name="user" placeholder="User" autocorrect="off"><br>
|
|
238
|
+
|
|
239
|
+
<label for="password">Password</label>
|
|
240
|
+
<input type="password" id="password" name="password" placeholder="Password"><br>
|
|
241
|
+
'''
|
|
242
|
+
|
|
243
|
+
form_html = f'''
|
|
244
|
+
<form action="/api/credentials/{shorthand}" method="post">
|
|
245
|
+
{form_content}
|
|
246
|
+
{render_button("Save", "primary", {"type": "submit"})}
|
|
247
|
+
</form>
|
|
248
|
+
'''
|
|
249
|
+
|
|
250
|
+
return render_form(f"Set User and Password for {shorthand}", form_html)
|
|
251
|
+
|
|
252
|
+
@app.post("/api/credentials/<sh>")
|
|
253
|
+
def set_credentials(sh):
|
|
254
|
+
user = request.forms.get('user')
|
|
255
|
+
password = request.forms.get('password')
|
|
256
|
+
config = Config(shorthand)
|
|
257
|
+
|
|
258
|
+
if user and password:
|
|
259
|
+
config.save("user", user)
|
|
260
|
+
config.save("password", password)
|
|
261
|
+
|
|
262
|
+
if sh.lower() == "al":
|
|
263
|
+
if quasarr.providers.sessions.al.create_and_persist_session(shared_state):
|
|
264
|
+
quasarr.providers.web_server.temp_server_success = True
|
|
265
|
+
return render_success(f"{sh} credentials set successfully", 5)
|
|
266
|
+
if sh.lower() == "dd":
|
|
267
|
+
if quasarr.providers.sessions.dd.create_and_persist_session(shared_state):
|
|
268
|
+
quasarr.providers.web_server.temp_server_success = True
|
|
269
|
+
return render_success(f"{sh} credentials set successfully", 5)
|
|
270
|
+
if sh.lower() == "nx":
|
|
271
|
+
if quasarr.providers.sessions.nx.create_and_persist_session(shared_state):
|
|
272
|
+
quasarr.providers.web_server.temp_server_success = True
|
|
273
|
+
return render_success(f"{sh} credentials set successfully", 5)
|
|
274
|
+
|
|
275
|
+
config.save("user", "")
|
|
276
|
+
config.save("password", "")
|
|
277
|
+
return render_fail("User and Password wrong or empty!")
|
|
278
|
+
|
|
279
|
+
info(
|
|
280
|
+
f'"{shorthand.lower()}" credentials required to access download links. '
|
|
281
|
+
f'Starting web server for config at: "{shared_state.values['internal_address']}".')
|
|
282
|
+
info(f"If needed register here: 'https://{domain}'")
|
|
283
|
+
info("Please set your credentials now, to allow Quasarr to launch!")
|
|
284
|
+
return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def flaresolverr_config(shared_state):
|
|
288
|
+
app = Bottle()
|
|
289
|
+
|
|
290
|
+
@app.get('/')
|
|
291
|
+
def url_form():
|
|
292
|
+
form_content = '''
|
|
293
|
+
<span><a href="https://github.com/FlareSolverr/FlareSolverr?tab=readme-ov-file#installation">A local instance</a>
|
|
294
|
+
must be running and reachable to Quasarr!</span><br><br>
|
|
295
|
+
<label for="url">FlareSolverr URL</label>
|
|
296
|
+
<input type="text" id="url" name="url" placeholder="http://192.168.0.1:8191/v1"><br>
|
|
297
|
+
'''
|
|
298
|
+
form_html = f'''
|
|
299
|
+
<form action="/api/flaresolverr" method="post">
|
|
300
|
+
{form_content}
|
|
301
|
+
{render_button("Save", "primary", {"type": "submit"})}
|
|
302
|
+
</form>
|
|
303
|
+
'''
|
|
304
|
+
return render_form("Set FlareSolverr URL", form_html)
|
|
305
|
+
|
|
306
|
+
@app.post('/api/flaresolverr')
|
|
307
|
+
def set_flaresolverr_url():
|
|
308
|
+
url = request.forms.get('url').strip()
|
|
309
|
+
config = Config("FlareSolverr")
|
|
310
|
+
|
|
311
|
+
if url:
|
|
312
|
+
try:
|
|
313
|
+
headers = {"Content-Type": "application/json"}
|
|
314
|
+
data = {
|
|
315
|
+
"cmd": "request.get",
|
|
316
|
+
"url": "http://www.google.com/",
|
|
317
|
+
"maxTimeout": 30000
|
|
318
|
+
}
|
|
319
|
+
response = requests.post(url, headers=headers, json=data, timeout=30)
|
|
320
|
+
if response.status_code == 200:
|
|
321
|
+
config.save("url", url)
|
|
322
|
+
print(f'Using Flaresolverr URL: "{url}"')
|
|
323
|
+
quasarr.providers.web_server.temp_server_success = True
|
|
324
|
+
return render_success("FlareSolverr URL saved successfully!", 5)
|
|
325
|
+
except requests.RequestException:
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
# on failure, clear any existing value and notify user
|
|
329
|
+
config.save("url", "")
|
|
330
|
+
return render_fail("Could not reach FlareSolverr at that URL (expected HTTP 200).")
|
|
331
|
+
|
|
332
|
+
info(
|
|
333
|
+
'"flaresolverr" URL is required for proper operation. '
|
|
334
|
+
f'Starting web server for config at: "{shared_state.values["internal_address"]}".'
|
|
335
|
+
)
|
|
336
|
+
info("Please enter your FlareSolverr URL now.")
|
|
337
|
+
return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def jdownloader_config(shared_state):
|
|
341
|
+
app = Bottle()
|
|
342
|
+
|
|
343
|
+
@app.get('/')
|
|
344
|
+
def jd_form():
|
|
345
|
+
verify_form_html = f'''
|
|
346
|
+
<span>If required register account at: <a href="https://my.jdownloader.org/login.html#register">
|
|
347
|
+
my.jdownloader.org</a>!</span><br>
|
|
348
|
+
|
|
349
|
+
<p><strong>JDownloader must be running and connected to My JDownloader!</strong></p><br>
|
|
350
|
+
|
|
351
|
+
<form id="verifyForm" action="/api/verify_jdownloader" method="post">
|
|
352
|
+
<label for="user">E-Mail</label>
|
|
353
|
+
<input type="text" id="user" name="user" placeholder="user@example.org" autocorrect="off"><br>
|
|
354
|
+
<label for="pass">Password</label>
|
|
355
|
+
<input type="password" id="pass" name="pass" placeholder="Password"><br>
|
|
356
|
+
{render_button("Verify Credentials",
|
|
357
|
+
"secondary",
|
|
358
|
+
{"id": "verifyButton", "type": "button", "onclick": "verifyCredentials()"})}
|
|
359
|
+
</form>
|
|
360
|
+
|
|
361
|
+
<p>Some JDownloader settings will be enforced by Quasarr on startup.</p>
|
|
362
|
+
|
|
363
|
+
<form action="/api/store_jdownloader" method="post" id="deviceForm" style="display: none;">
|
|
364
|
+
<input type="hidden" id="hiddenUser" name="user">
|
|
365
|
+
<input type="hidden" id="hiddenPass" name="pass">
|
|
366
|
+
<label for="device">JDownloader</label>
|
|
367
|
+
<select id="device" name="device"></select><br>
|
|
368
|
+
{render_button("Save", "primary", {"type": "submit"})}
|
|
369
|
+
</form>
|
|
370
|
+
<p><strong>Saving may take a while!</strong></p><br>
|
|
371
|
+
'''
|
|
372
|
+
|
|
373
|
+
verify_script = '''
|
|
374
|
+
<script>
|
|
375
|
+
function verifyCredentials() {
|
|
376
|
+
var user = document.getElementById('user').value;
|
|
377
|
+
var pass = document.getElementById('pass').value;
|
|
378
|
+
fetch('/api/verify_jdownloader', {
|
|
379
|
+
method: 'POST',
|
|
380
|
+
headers: {
|
|
381
|
+
'Content-Type': 'application/json',
|
|
382
|
+
},
|
|
383
|
+
body: JSON.stringify({user: user, pass: pass}),
|
|
384
|
+
})
|
|
385
|
+
.then(response => response.json())
|
|
386
|
+
.then(data => {
|
|
387
|
+
if (data.success) {
|
|
388
|
+
var select = document.getElementById('device');
|
|
389
|
+
data.devices.forEach(device => {
|
|
390
|
+
var opt = document.createElement('option');
|
|
391
|
+
opt.value = device;
|
|
392
|
+
opt.innerHTML = device;
|
|
393
|
+
select.appendChild(opt);
|
|
394
|
+
});
|
|
395
|
+
document.getElementById('hiddenUser').value = document.getElementById('user').value;
|
|
396
|
+
document.getElementById('hiddenPass').value = document.getElementById('pass').value;
|
|
397
|
+
document.getElementById("verifyButton").style.display = "none";
|
|
398
|
+
document.getElementById('deviceForm').style.display = 'block';
|
|
399
|
+
} else {
|
|
400
|
+
alert('Fehler! Bitte die Zugangsdaten überprüfen.');
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
.catch((error) => {
|
|
404
|
+
console.error('Error:', error);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
</script>
|
|
408
|
+
'''
|
|
409
|
+
return render_form("Set your credentials for My JDownloader", verify_form_html, verify_script)
|
|
410
|
+
|
|
411
|
+
@app.post("/api/verify_jdownloader")
|
|
412
|
+
def verify_jdownloader():
|
|
413
|
+
data = request.json
|
|
414
|
+
username = data['user']
|
|
415
|
+
password = data['pass']
|
|
416
|
+
|
|
417
|
+
devices = shared_state.get_devices(username, password)
|
|
418
|
+
device_names = []
|
|
419
|
+
|
|
420
|
+
if devices:
|
|
421
|
+
for device in devices:
|
|
422
|
+
device_names.append(device['name'])
|
|
423
|
+
|
|
424
|
+
if device_names:
|
|
425
|
+
return {"success": True, "devices": device_names}
|
|
426
|
+
else:
|
|
427
|
+
return {"success": False}
|
|
428
|
+
|
|
429
|
+
@app.post("/api/store_jdownloader")
|
|
430
|
+
def store_jdownloader():
|
|
431
|
+
username = request.forms.get('user')
|
|
432
|
+
password = request.forms.get('pass')
|
|
433
|
+
device = request.forms.get('device')
|
|
434
|
+
|
|
435
|
+
config = Config('JDownloader')
|
|
436
|
+
|
|
437
|
+
if username and password and device:
|
|
438
|
+
config.save('user', username)
|
|
439
|
+
config.save('password', password)
|
|
440
|
+
config.save('device', device)
|
|
441
|
+
|
|
442
|
+
if not shared_state.set_device_from_config():
|
|
443
|
+
config.save('user', "")
|
|
444
|
+
config.save('password', "")
|
|
445
|
+
config.save('device', "")
|
|
446
|
+
else:
|
|
447
|
+
quasarr.providers.web_server.temp_server_success = True
|
|
448
|
+
return render_success("Credentials set",
|
|
449
|
+
15)
|
|
450
|
+
|
|
451
|
+
return render_fail("Could not set credentials!")
|
|
452
|
+
|
|
453
|
+
info(
|
|
454
|
+
f'My-JDownloader-Credentials not set. '
|
|
455
|
+
f'Starting web server for config at: "{shared_state.values['internal_address']}".')
|
|
456
|
+
info("If needed register here: 'https://my.jdownloader.org/login.html#register'")
|
|
457
|
+
info("Please set your credentials now, to allow Quasarr to launch!")
|
|
458
|
+
return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from quasarr.providers import shared_state
|
|
9
|
+
from quasarr.providers.log import info
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataBase(object):
|
|
13
|
+
def __init__(self, table):
|
|
14
|
+
try:
|
|
15
|
+
self._conn = sqlite3.connect(shared_state.values["dbfile"], check_same_thread=False, timeout=5)
|
|
16
|
+
self._table = table
|
|
17
|
+
if not self._conn.execute(
|
|
18
|
+
f"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = '{self._table}';").fetchall():
|
|
19
|
+
self._conn.execute(f"CREATE TABLE {self._table} (key, value)")
|
|
20
|
+
self._conn.commit()
|
|
21
|
+
except sqlite3.OperationalError as e:
|
|
22
|
+
try:
|
|
23
|
+
time.sleep(5)
|
|
24
|
+
self._conn = sqlite3.connect(shared_state.values["dbfile"], check_same_thread=False, timeout=10)
|
|
25
|
+
self._table = table
|
|
26
|
+
if not self._conn.execute(
|
|
27
|
+
f"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = '{self._table}';").fetchall():
|
|
28
|
+
self._conn.execute(f"CREATE TABLE {self._table} (key, value)")
|
|
29
|
+
self._conn.commit()
|
|
30
|
+
except sqlite3.OperationalError as e:
|
|
31
|
+
info(f"Error accessing Quasarr.db: {e}")
|
|
32
|
+
|
|
33
|
+
def retrieve(self, key):
|
|
34
|
+
query = f"SELECT value FROM {self._table} WHERE key=?"
|
|
35
|
+
# using this parameterized query to prevent SQL injection, which requires a tuple as second argument
|
|
36
|
+
res = self._conn.execute(query, (key,)).fetchone()
|
|
37
|
+
return res[0] if res else None
|
|
38
|
+
|
|
39
|
+
def retrieve_all(self, key):
|
|
40
|
+
query = f"SELECT distinct value FROM {self._table} WHERE key=? ORDER BY value"
|
|
41
|
+
# using this parameterized query to prevent SQL injection, which requires a tuple as second argument
|
|
42
|
+
res = self._conn.execute(query, (key,))
|
|
43
|
+
items = [str(r[0]) for r in res]
|
|
44
|
+
return items
|
|
45
|
+
|
|
46
|
+
def retrieve_all_titles(self):
|
|
47
|
+
query = f"SELECT distinct key, value FROM {self._table} ORDER BY key"
|
|
48
|
+
# using this parameterized query to prevent SQL injection, which requires a tuple as second argument
|
|
49
|
+
res = self._conn.execute(query)
|
|
50
|
+
items = [[str(r[0]), str(r[1])] for r in res]
|
|
51
|
+
return items if items else None
|
|
52
|
+
|
|
53
|
+
def store(self, key, value):
|
|
54
|
+
query = f"INSERT INTO {self._table} VALUES (?, ?)"
|
|
55
|
+
# using this parameterized query to prevent SQL injection, which requires a tuple as second argument
|
|
56
|
+
self._conn.execute(query, (key, value))
|
|
57
|
+
self._conn.commit()
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
def update_store(self, key, value):
|
|
61
|
+
delete_query = f"DELETE FROM {self._table} WHERE key=?"
|
|
62
|
+
# using this parameterized query to prevent SQL injection, which requires a tuple as second argument
|
|
63
|
+
self._conn.execute(delete_query, (key,))
|
|
64
|
+
insert_query = f"INSERT INTO {self._table} VALUES (?, ?)"
|
|
65
|
+
# using this parameterized query to prevent SQL injection, which requires a tuple as second argument
|
|
66
|
+
self._conn.execute(insert_query, (key, value))
|
|
67
|
+
self._conn.commit()
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
def delete(self, key):
|
|
71
|
+
query = f"DELETE FROM {self._table} WHERE key=?"
|
|
72
|
+
# using this parameterized query to prevent SQL injection, which requires a tuple as second argument
|
|
73
|
+
self._conn.execute(query, (key,))
|
|
74
|
+
self._conn.commit()
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
def reset(self):
|
|
78
|
+
self._conn.execute(f"DROP TABLE IF EXISTS {self._table}")
|
|
79
|
+
self._conn.commit()
|
|
80
|
+
return True
|