quasarr 1.3.5__py3-none-any.whl → 1.20.4__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 (67) hide show
  1. quasarr/__init__.py +157 -56
  2. quasarr/api/__init__.py +141 -36
  3. quasarr/api/arr/__init__.py +197 -78
  4. quasarr/api/captcha/__init__.py +897 -42
  5. quasarr/api/config/__init__.py +23 -0
  6. quasarr/api/sponsors_helper/__init__.py +84 -22
  7. quasarr/api/statistics/__init__.py +196 -0
  8. quasarr/downloads/__init__.py +237 -434
  9. quasarr/downloads/linkcrypters/al.py +237 -0
  10. quasarr/downloads/linkcrypters/filecrypt.py +178 -31
  11. quasarr/downloads/linkcrypters/hide.py +123 -0
  12. quasarr/downloads/packages/__init__.py +461 -0
  13. quasarr/downloads/sources/al.py +697 -0
  14. quasarr/downloads/sources/by.py +106 -0
  15. quasarr/downloads/sources/dd.py +6 -78
  16. quasarr/downloads/sources/dj.py +7 -0
  17. quasarr/downloads/sources/dt.py +1 -1
  18. quasarr/downloads/sources/dw.py +2 -2
  19. quasarr/downloads/sources/he.py +112 -0
  20. quasarr/downloads/sources/mb.py +47 -0
  21. quasarr/downloads/sources/nk.py +51 -0
  22. quasarr/downloads/sources/nx.py +36 -81
  23. quasarr/downloads/sources/sf.py +27 -4
  24. quasarr/downloads/sources/sj.py +7 -0
  25. quasarr/downloads/sources/sl.py +90 -0
  26. quasarr/downloads/sources/wd.py +110 -0
  27. quasarr/providers/cloudflare.py +204 -0
  28. quasarr/providers/html_images.py +20 -0
  29. quasarr/providers/html_templates.py +210 -108
  30. quasarr/providers/imdb_metadata.py +15 -2
  31. quasarr/providers/myjd_api.py +36 -5
  32. quasarr/providers/notifications.py +30 -5
  33. quasarr/providers/obfuscated.py +35 -0
  34. quasarr/providers/sessions/__init__.py +0 -0
  35. quasarr/providers/sessions/al.py +286 -0
  36. quasarr/providers/sessions/dd.py +78 -0
  37. quasarr/providers/sessions/nx.py +76 -0
  38. quasarr/providers/shared_state.py +368 -23
  39. quasarr/providers/statistics.py +154 -0
  40. quasarr/providers/version.py +60 -1
  41. quasarr/search/__init__.py +112 -36
  42. quasarr/search/sources/al.py +448 -0
  43. quasarr/search/sources/by.py +203 -0
  44. quasarr/search/sources/dd.py +17 -6
  45. quasarr/search/sources/dj.py +213 -0
  46. quasarr/search/sources/dt.py +37 -7
  47. quasarr/search/sources/dw.py +27 -47
  48. quasarr/search/sources/fx.py +27 -29
  49. quasarr/search/sources/he.py +196 -0
  50. quasarr/search/sources/mb.py +195 -0
  51. quasarr/search/sources/nk.py +188 -0
  52. quasarr/search/sources/nx.py +22 -6
  53. quasarr/search/sources/sf.py +143 -151
  54. quasarr/search/sources/sj.py +213 -0
  55. quasarr/search/sources/sl.py +246 -0
  56. quasarr/search/sources/wd.py +208 -0
  57. quasarr/storage/config.py +20 -4
  58. quasarr/storage/setup.py +224 -56
  59. quasarr-1.20.4.dist-info/METADATA +304 -0
  60. quasarr-1.20.4.dist-info/RECORD +72 -0
  61. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/WHEEL +1 -1
  62. quasarr/providers/tvmaze_metadata.py +0 -23
  63. quasarr-1.3.5.dist-info/METADATA +0 -174
  64. quasarr-1.3.5.dist-info/RECORD +0 -43
  65. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/entry_points.txt +0 -0
  66. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/licenses/LICENSE +0 -0
  67. {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,23 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ from quasarr.providers.html_templates import render_form
6
+ from quasarr.providers.html_templates import render_button
7
+ from quasarr.storage.setup import hostname_form_html, save_hostnames
8
+
9
+
10
+ def setup_config(app, shared_state):
11
+ @app.get('/hostnames')
12
+ def hostnames_ui():
13
+ message = """<p>
14
+ At least one hostname must be kept.
15
+ </p>"""
16
+ back_button = f'''<p>
17
+ {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
18
+ </p>'''
19
+ return render_form("Hostnames", hostname_form_html(shared_state, message) + back_button)
20
+
21
+ @app.post("/api/hostnames")
22
+ def hostnames_api():
23
+ return save_hostnames(shared_state, timeout=1, first_run=False)
@@ -6,41 +6,58 @@ import json
6
6
 
7
7
  from bottle import request, abort
8
8
 
9
+ from quasarr.downloads import fail
9
10
  from quasarr.providers import shared_state
10
11
  from quasarr.providers.log import info
11
12
  from quasarr.providers.notifications import send_discord_message
13
+ from quasarr.providers.statistics import StatsHelper
12
14
 
13
15
 
14
16
  def setup_sponsors_helper_routes(app):
15
17
  @app.get("/sponsors_helper/api/to_decrypt/")
16
18
  def to_decrypt_api():
17
19
  try:
20
+ if not shared_state.values["helper_active"]:
21
+ shared_state.update("helper_active", True)
22
+ info(f"Sponsor status activated successfully")
23
+
18
24
  protected = shared_state.get_db("protected").retrieve_all_titles()
19
25
  if not protected:
20
26
  return abort(404, "No encrypted packages found")
21
- else:
22
- package = protected[0]
23
- package_id = package[0]
27
+
28
+ # Find the first package without a "session" key
29
+ selected_package = None
30
+ for package in protected:
24
31
  data = json.loads(package[1])
25
- title = data["title"]
26
- links = data["links"]
27
- mirror = None if (mirror := data.get('mirror')) == "None" else mirror
28
- password = data["password"]
29
- mirror = None if (mirror := data.get('mirror')) == "None" else mirror
32
+ if "session" not in data:
33
+ selected_package = (package[0], data)
34
+ break
35
+
36
+ if not selected_package:
37
+ return abort(404, "No valid packages without session found")
38
+
39
+ package_id, data = selected_package
40
+ title = data["title"]
41
+ links = data["links"]
42
+ mirror = None if (mirror := data.get('mirror')) == "None" else mirror
43
+ password = data["password"]
44
+
45
+ rapid = [ln for ln in links if "rapidgator" in ln[1].lower()]
46
+ others = [ln for ln in links if "rapidgator" not in ln[1].lower()]
47
+ prioritized_links = rapid + others
30
48
 
31
49
  return {
32
50
  "to_decrypt": {
33
51
  "name": title,
34
52
  "id": package_id,
35
- "url": links,
53
+ "url": prioritized_links,
36
54
  "mirror": mirror,
37
55
  "password": password,
38
- "mirror": mirror,
39
56
  "max_attempts": 3
40
57
  }
41
58
  }
42
- except:
43
- return abort(500, "Failed")
59
+ except Exception as e:
60
+ return abort(500, str(e))
44
61
 
45
62
  @app.post("/sponsors_helper/api/to_download/")
46
63
  def to_download_api():
@@ -56,6 +73,8 @@ def setup_sponsors_helper_routes(app):
56
73
  if download_links:
57
74
  downloaded = shared_state.download_package(download_links, title, password, package_id)
58
75
  if downloaded:
76
+ StatsHelper(shared_state).increment_package_with_links(download_links)
77
+ StatsHelper(shared_state).increment_captcha_decryptions_automatic()
59
78
  shared_state.get_db("protected").delete(package_id)
60
79
  send_discord_message(shared_state, title=title, case="solved")
61
80
  info(f"Download successfully started for {title}")
@@ -66,31 +85,74 @@ def setup_sponsors_helper_routes(app):
66
85
  except Exception as e:
67
86
  info(f"Error decrypting: {e}")
68
87
 
69
- return abort(500, "Failed")
88
+ StatsHelper(shared_state).increment_failed_decryptions_automatic()
89
+ return abort(500, "Failed") #
90
+
91
+ @app.post("/sponsors_helper/api/to_replace/")
92
+ def to_replace_api():
93
+ try:
94
+ data = request.json
95
+ name = data.get('name')
96
+ package_id = data.get('package_id')
97
+ password = data.get('password')
98
+ replace_url = data.get('replace_url')
99
+ mirror = data.get('mirror')
100
+ session = data.get('session')
101
+
102
+ if not all([name, package_id, replace_url, mirror, session]):
103
+ info("Missing required replacement data")
104
+ return {"error": "Missing required replacement data"}, 400
105
+
106
+ if password is None:
107
+ password = ""
108
+
109
+ blob = json.dumps(
110
+ {
111
+ "title": name,
112
+ "links": [replace_url, mirror],
113
+ "size_mb": 0,
114
+ "password": password,
115
+ "mirror": mirror,
116
+ "session": session
117
+ })
118
+
119
+ shared_state.get_db("protected").update_store(package_id, blob)
120
+
121
+ info(f"Another CAPTCHA solution is required for {mirror} link: {replace_url}")
122
+
123
+ StatsHelper(shared_state).increment_captcha_decryptions_automatic()
124
+
125
+ return f"Replacement link stored for {name}"
126
+
127
+ except Exception as e:
128
+ StatsHelper(shared_state).increment_failed_decryptions_automatic()
129
+ info(f"Error handling replacement: {e}")
130
+ return {"error": str(e)}, 500
70
131
 
71
132
  @app.delete("/sponsors_helper/api/to_failed/")
72
- @app.delete("/sponsors_helper/api/to_delete/")
73
133
  def move_to_failed_api():
74
134
  try:
135
+ StatsHelper(shared_state).increment_failed_decryptions_automatic()
136
+
75
137
  data = request.json
76
138
  package_id = data.get('package_id')
77
139
 
78
140
  data = json.loads(shared_state.get_db("protected").retrieve(package_id))
79
141
  title = data.get('title')
80
- moved = shared_state.get_db("failed").store(package_id, json.dumps(data))
81
- if moved and title:
82
- deleted = shared_state.get_db("protected").delete(package_id)
83
- if deleted:
142
+
143
+ if package_id:
144
+ info(f'Marking package "{title}" with ID "{package_id}" as failed')
145
+ failed = fail(title, package_id, shared_state, reason="Too many failed attempts by SponsorsHelper")
146
+ if failed:
147
+ shared_state.get_db("protected").delete(package_id)
84
148
  send_discord_message(shared_state, title=title, case="failed")
85
- info(f"Package {title} with ID {package_id} marked as failed!")
86
149
  return f'Package "{title}" with ID "{package_id} marked as failed!"'
87
-
88
150
  except Exception as e:
89
- info(f"Error marking package as failed: {e}")
151
+ info(f"Error moving to failed: {e}")
90
152
 
91
153
  return abort(500, "Failed")
92
154
 
93
- @app.put("/sponsors_helper/api/activate_sponsor_status/")
155
+ @app.put("/sponsors_helper/api/set_sponsor_status/")
94
156
  def activate_sponsor_status():
95
157
  try:
96
158
  data = request.body.read().decode("utf-8")
@@ -0,0 +1,196 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ import quasarr.providers.html_images as images
6
+ from quasarr.providers.html_templates import render_button, render_centered_html
7
+ from quasarr.providers.statistics import StatsHelper
8
+
9
+
10
+ def setup_statistics(app, shared_state):
11
+ @app.get('/statistics')
12
+ def statistics():
13
+ stats_helper = StatsHelper(shared_state)
14
+ stats = stats_helper.get_stats()
15
+
16
+ stats_html = f"""
17
+ <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
18
+ <h2>Statistics</h2>
19
+ <div class="stats-container">
20
+ <h3>📊 Overview</h3>
21
+ <div class="stats-grid compact">
22
+ <div class="stat-card highlight">
23
+ <h3>📦 Total Download Attempts</h3>
24
+ <div class="stat-value">{stats['total_download_attempts']}</div>
25
+ <div class="stat-subtitle">Success Rate: {stats['download_success_rate']:.1f}%</div>
26
+ </div>
27
+ <div class="stat-card highlight">
28
+ <h3>🔐 Total CAPTCHA Decryptions</h3>
29
+ <div class="stat-value">{stats['total_captcha_decryptions']}</div>
30
+ <div class="stat-subtitle">Success Rate: {stats['decryption_success_rate']:.1f}%</div>
31
+ </div>
32
+ </div>
33
+
34
+ <h3>📥 Downloads</h3>
35
+ <div class="stats-grid compact">
36
+ <div class="stat-card">
37
+ <h3>✅ Packages Downloaded</h3>
38
+ <div class="stat-value">{stats['packages_downloaded']}</div>
39
+ </div>
40
+ <div class="stat-card">
41
+ <h3>⚙️ Links Processed</h3>
42
+ <div class="stat-value">{stats['links_processed']}</div>
43
+ </div>
44
+ <div class="stat-card">
45
+ <h3>❌ Failed Downloads</h3>
46
+ <div class="stat-value">{stats['failed_downloads']}</div>
47
+ </div>
48
+ <div class="stat-card">
49
+ <h3>🔗 Average Links per Package</h3>
50
+ <div class="stat-value">{stats['average_links_per_package']:.1f}</div>
51
+ </div>
52
+ </div>
53
+
54
+ <h3>🧩 CAPTCHAs</h3>
55
+ <div class="stats-grid compact">
56
+ <div class="stat-card">
57
+ <h3>🤖 Automatic Decryptions</h3>
58
+ <div class="stat-value">{stats['captcha_decryptions_automatic']}</div>
59
+ <div class="stat-subtitle">Success Rate: {stats['automatic_decryption_success_rate']:.1f}%</div>
60
+ </div>
61
+ <div class="stat-card">
62
+ <h3>👤 Manual Decryptions</h3>
63
+ <div class="stat-value">{stats['captcha_decryptions_manual']}</div>
64
+ <div class="stat-subtitle">Success Rate: {stats['manual_decryption_success_rate']:.1f}%</div>
65
+ </div>
66
+ <div class="stat-card">
67
+ <h3>⛔ Failed Auto Decryptions</h3>
68
+ <div class="stat-value">{stats['failed_decryptions_automatic']}</div>
69
+ </div>
70
+ <div class="stat-card">
71
+ <h3>🚫 Failed Manual Decryptions</h3>
72
+ <div class="stat-value">{stats['failed_decryptions_manual']}</div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <p>
78
+ {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
79
+ </p>
80
+
81
+ <style>
82
+ .stats-container {{
83
+ max-width: 1000px;
84
+ margin: 0 auto;
85
+ }}
86
+
87
+ .stats-grid {{
88
+ display: grid;
89
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
90
+ gap: 15px;
91
+ margin: 15px 0;
92
+ }}
93
+
94
+ .stats-grid.compact {{
95
+ gap: 12px;
96
+ margin: 12px 0;
97
+ }}
98
+
99
+ .stat-card {{
100
+ background: var(--card-bg, #f8f9fa);
101
+ border: 1px solid var(--card-border, #dee2e6);
102
+ border-radius: 8px;
103
+ padding: 15px;
104
+ text-align: center;
105
+ transition: transform 0.2s, box-shadow 0.2s;
106
+ }}
107
+
108
+ .stat-card:hover {{
109
+ transform: translateY(-2px);
110
+ box-shadow: 0 4px 12px var(--card-shadow, rgba(0,0,0,0.1));
111
+ }}
112
+
113
+ .stat-card.highlight {{
114
+ background: var(--highlight-bg, #e3f2fd);
115
+ border-color: var(--highlight-border, #2196f3);
116
+ }}
117
+
118
+ .stat-card h3 {{
119
+ margin: 0 0 8px 0;
120
+ font-size: 13px;
121
+ color: var(--text-muted, #666);
122
+ text-transform: uppercase;
123
+ letter-spacing: 0.5px;
124
+ }}
125
+
126
+ .stat-value {{
127
+ font-size: 24px;
128
+ font-weight: bold;
129
+ color: var(--text-primary, #333);
130
+ margin: 8px 0;
131
+ }}
132
+
133
+ .stat-subtitle {{
134
+ font-size: 11px;
135
+ color: var(--text-secondary, #888);
136
+ margin-top: 4px;
137
+ }}
138
+
139
+ h3 {{
140
+ color: var(--heading-color, #444);
141
+ padding-bottom: 8px;
142
+ margin-top: 25px;
143
+ margin-bottom: 15px;
144
+ }}
145
+
146
+ /* Dark mode styles */
147
+ @media (prefers-color-scheme: dark) {{
148
+ :root {{
149
+ --card-border: #4a5568;
150
+ --card-shadow: rgba(0,0,0,0.3);
151
+ --highlight-bg: #1a365d;
152
+ --highlight-border: #3182ce;
153
+ --text-muted: #a0aec0;
154
+ --text-primary: #f7fafc;
155
+ --text-secondary: #cbd5e0;
156
+ --heading-color: #e2e8f0;
157
+ --border-color: #4a5568;
158
+ }}
159
+ }}
160
+
161
+ /* Force dark mode styles for applications that don't support prefers-color-scheme */
162
+ body.dark-mode .stat-card {{
163
+ background: #2d3748;
164
+ border-color: #4a5568;
165
+ color: #f7fafc;
166
+ }}
167
+
168
+ body.dark-mode .stat-card:hover {{
169
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
170
+ }}
171
+
172
+ body.dark-mode .stat-card.highlight {{
173
+ background: #1a365d;
174
+ border-color: #3182ce;
175
+ }}
176
+
177
+ body.dark-mode .stat-card h3 {{
178
+ color: #a0aec0;
179
+ }}
180
+
181
+ body.dark-mode .stat-value {{
182
+ color: #f7fafc;
183
+ }}
184
+
185
+ body.dark-mode .stat-subtitle {{
186
+ color: #cbd5e0;
187
+ }}
188
+
189
+ body.dark-mode h2 {{
190
+ color: #e2e8f0;
191
+ border-bottom-color: #4a5568;
192
+ }}
193
+ </style>
194
+ """
195
+
196
+ return render_centered_html(stats_html)