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.

Files changed (72) hide show
  1. quasarr/__init__.py +460 -0
  2. quasarr/api/__init__.py +187 -0
  3. quasarr/api/arr/__init__.py +373 -0
  4. quasarr/api/captcha/__init__.py +1075 -0
  5. quasarr/api/config/__init__.py +23 -0
  6. quasarr/api/sponsors_helper/__init__.py +166 -0
  7. quasarr/api/statistics/__init__.py +196 -0
  8. quasarr/downloads/__init__.py +267 -0
  9. quasarr/downloads/linkcrypters/__init__.py +0 -0
  10. quasarr/downloads/linkcrypters/al.py +237 -0
  11. quasarr/downloads/linkcrypters/filecrypt.py +444 -0
  12. quasarr/downloads/linkcrypters/hide.py +123 -0
  13. quasarr/downloads/packages/__init__.py +467 -0
  14. quasarr/downloads/sources/__init__.py +0 -0
  15. quasarr/downloads/sources/al.py +697 -0
  16. quasarr/downloads/sources/by.py +106 -0
  17. quasarr/downloads/sources/dd.py +76 -0
  18. quasarr/downloads/sources/dj.py +7 -0
  19. quasarr/downloads/sources/dt.py +66 -0
  20. quasarr/downloads/sources/dw.py +65 -0
  21. quasarr/downloads/sources/he.py +112 -0
  22. quasarr/downloads/sources/mb.py +47 -0
  23. quasarr/downloads/sources/nk.py +51 -0
  24. quasarr/downloads/sources/nx.py +105 -0
  25. quasarr/downloads/sources/sf.py +159 -0
  26. quasarr/downloads/sources/sj.py +7 -0
  27. quasarr/downloads/sources/sl.py +90 -0
  28. quasarr/downloads/sources/wd.py +110 -0
  29. quasarr/providers/__init__.py +0 -0
  30. quasarr/providers/cloudflare.py +204 -0
  31. quasarr/providers/html_images.py +20 -0
  32. quasarr/providers/html_templates.py +241 -0
  33. quasarr/providers/imdb_metadata.py +142 -0
  34. quasarr/providers/log.py +19 -0
  35. quasarr/providers/myjd_api.py +917 -0
  36. quasarr/providers/notifications.py +124 -0
  37. quasarr/providers/obfuscated.py +51 -0
  38. quasarr/providers/sessions/__init__.py +0 -0
  39. quasarr/providers/sessions/al.py +286 -0
  40. quasarr/providers/sessions/dd.py +78 -0
  41. quasarr/providers/sessions/nx.py +76 -0
  42. quasarr/providers/shared_state.py +826 -0
  43. quasarr/providers/statistics.py +154 -0
  44. quasarr/providers/version.py +118 -0
  45. quasarr/providers/web_server.py +49 -0
  46. quasarr/search/__init__.py +153 -0
  47. quasarr/search/sources/__init__.py +0 -0
  48. quasarr/search/sources/al.py +448 -0
  49. quasarr/search/sources/by.py +203 -0
  50. quasarr/search/sources/dd.py +135 -0
  51. quasarr/search/sources/dj.py +213 -0
  52. quasarr/search/sources/dt.py +265 -0
  53. quasarr/search/sources/dw.py +214 -0
  54. quasarr/search/sources/fx.py +223 -0
  55. quasarr/search/sources/he.py +196 -0
  56. quasarr/search/sources/mb.py +195 -0
  57. quasarr/search/sources/nk.py +188 -0
  58. quasarr/search/sources/nx.py +197 -0
  59. quasarr/search/sources/sf.py +374 -0
  60. quasarr/search/sources/sj.py +213 -0
  61. quasarr/search/sources/sl.py +246 -0
  62. quasarr/search/sources/wd.py +208 -0
  63. quasarr/storage/__init__.py +0 -0
  64. quasarr/storage/config.py +163 -0
  65. quasarr/storage/setup.py +458 -0
  66. quasarr/storage/sqlite_database.py +80 -0
  67. quasarr-1.20.6.dist-info/METADATA +304 -0
  68. quasarr-1.20.6.dist-info/RECORD +72 -0
  69. quasarr-1.20.6.dist-info/WHEEL +5 -0
  70. quasarr-1.20.6.dist-info/entry_points.txt +2 -0
  71. quasarr-1.20.6.dist-info/licenses/LICENSE +21 -0
  72. quasarr-1.20.6.dist-info/top_level.txt +1 -0
@@ -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