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.

Files changed (77) hide show
  1. quasarr/__init__.py +316 -42
  2. quasarr/api/__init__.py +187 -0
  3. quasarr/api/arr/__init__.py +387 -0
  4. quasarr/api/captcha/__init__.py +1189 -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 +319 -256
  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 +476 -0
  14. quasarr/downloads/sources/al.py +697 -0
  15. quasarr/downloads/sources/by.py +106 -0
  16. quasarr/downloads/sources/dd.py +76 -0
  17. quasarr/downloads/sources/dj.py +7 -0
  18. quasarr/downloads/sources/dl.py +199 -0
  19. quasarr/downloads/sources/dt.py +66 -0
  20. quasarr/downloads/sources/dw.py +14 -7
  21. quasarr/downloads/sources/he.py +112 -0
  22. quasarr/downloads/sources/mb.py +47 -0
  23. quasarr/downloads/sources/nk.py +54 -0
  24. quasarr/downloads/sources/nx.py +42 -83
  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/downloads/sources/wx.py +127 -0
  30. quasarr/providers/cloudflare.py +204 -0
  31. quasarr/providers/html_images.py +22 -0
  32. quasarr/providers/html_templates.py +211 -104
  33. quasarr/providers/imdb_metadata.py +108 -3
  34. quasarr/providers/log.py +19 -0
  35. quasarr/providers/myjd_api.py +201 -40
  36. quasarr/providers/notifications.py +99 -11
  37. quasarr/providers/obfuscated.py +65 -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/dl.py +175 -0
  42. quasarr/providers/sessions/nx.py +76 -0
  43. quasarr/providers/shared_state.py +656 -79
  44. quasarr/providers/statistics.py +154 -0
  45. quasarr/providers/version.py +60 -1
  46. quasarr/providers/web_server.py +1 -1
  47. quasarr/search/__init__.py +144 -15
  48. quasarr/search/sources/al.py +448 -0
  49. quasarr/search/sources/by.py +204 -0
  50. quasarr/search/sources/dd.py +135 -0
  51. quasarr/search/sources/dj.py +213 -0
  52. quasarr/search/sources/dl.py +354 -0
  53. quasarr/search/sources/dt.py +265 -0
  54. quasarr/search/sources/dw.py +94 -67
  55. quasarr/search/sources/fx.py +89 -33
  56. quasarr/search/sources/he.py +196 -0
  57. quasarr/search/sources/mb.py +195 -0
  58. quasarr/search/sources/nk.py +188 -0
  59. quasarr/search/sources/nx.py +75 -21
  60. quasarr/search/sources/sf.py +374 -0
  61. quasarr/search/sources/sj.py +213 -0
  62. quasarr/search/sources/sl.py +246 -0
  63. quasarr/search/sources/wd.py +208 -0
  64. quasarr/search/sources/wx.py +337 -0
  65. quasarr/storage/config.py +39 -10
  66. quasarr/storage/setup.py +269 -97
  67. quasarr/storage/sqlite_database.py +6 -1
  68. quasarr-1.23.0.dist-info/METADATA +306 -0
  69. quasarr-1.23.0.dist-info/RECORD +77 -0
  70. {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/WHEEL +1 -1
  71. quasarr/arr/__init__.py +0 -423
  72. quasarr/captcha_solver/__init__.py +0 -284
  73. quasarr-0.1.6.dist-info/METADATA +0 -81
  74. quasarr-0.1.6.dist-info/RECORD +0 -31
  75. {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/entry_points.txt +0 -0
  76. {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info/licenses}/LICENSE +0 -0
  77. {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/top_level.txt +0 -0
@@ -2,133 +2,240 @@
2
2
  # Quasarr
3
3
  # Project by https://github.com/rix1337
4
4
 
5
- def render_centered_html(inner_content):
6
- style_outer = """
7
- display: flex;
8
- justify-content: center;
9
- align-items: center;
10
- height: 100vh;
11
- max-height: 100vh;
12
- overflow-y: auto;
13
- background-color: #212529;
14
- color: #fff;
15
- font-family: system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',
16
- 'Noto Sans','Liberation Sans',Arial,sans-serif,'Apple Color Emoji',
17
- 'Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';
18
- """
19
- style_inner = """
20
- background-color: #fff;
21
- border-radius: 0.375rem;
22
- box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
23
- padding: 20px;
24
- text-align: center;
25
- color: #212529;
26
- font-size: 1rem;
27
- font-weight: 400;
28
- line-height: 1.5;
29
- width: -webkit-fit-content; width: -moz-fit-content; width: fit-content;
30
- margin: auto;
31
- """
5
+ import quasarr.providers.html_images as images
6
+ from quasarr.providers.version import get_version
7
+
32
8
 
33
- return f'''
34
- <html>
9
+ def render_centered_html(inner_content):
10
+ head = '''
35
11
  <head>
12
+ <meta charset="utf-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1">
36
14
  <title>Quasarr</title>
37
- </head>
15
+ <link rel="icon" href="''' + images.logo + '''" type="image/png">
16
+ <style>
17
+ /* Theme variables */
18
+ :root {
19
+ --bg-color: #ffffff;
20
+ --fg-color: #212529;
21
+ --card-bg: #ffffff;
22
+ --card-shadow: rgba(0, 0, 0, 0.1);
23
+ --primary: #0d6efd;
24
+ --secondary: #6c757d;
25
+ --code-bg: #f8f9fa;
26
+ --spacing: 1rem;
27
+ }
28
+ @media (prefers-color-scheme: dark) {
29
+ :root {
30
+ --bg-color: #181a1b;
31
+ --fg-color: #f1f1f1;
32
+ --card-bg: #242526;
33
+ --card-shadow: rgba(0, 0, 0, 0.5);
34
+ --code-bg: #2c2f33;
35
+ }
36
+ }
37
+ /* Logo and heading alignment */
38
+ h1 {
39
+ display: inline-flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ margin-bottom: 0.5rem;
43
+ font-size: 2rem;
44
+ }
45
+ .logo {
46
+ width: 48px;
47
+ height: 48px;
48
+ margin-right: 0.5rem;
49
+ }
50
+ /* Form labels and inputs */
51
+ label {
52
+ display: block;
53
+ font-weight: 600;
54
+ margin-bottom: 0.5rem;
55
+ }
56
+ input, select {
57
+ display: block;
58
+ width: 100%;
59
+ padding: 0.5rem;
60
+ font-size: 1rem;
61
+ border: 1px solid #ced4da;
62
+ border-radius: 0.5rem;
63
+ background-color: var(--card-bg);
64
+ color: var(--fg-color);
65
+ box-sizing: border-box;
66
+ }
67
+ *, *::before, *::after {
68
+ box-sizing: border-box;
69
+ }
70
+ /* make body a column flex so footer can stick to bottom */
71
+ html, body {
72
+ margin: 0;
73
+ padding: 0;
74
+ width: 100%;
75
+ height: 100%;
76
+ background-color: var(--bg-color);
77
+ color: var(--fg-color);
78
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue',
79
+ 'Noto Sans', Arial, sans-serif;
80
+ line-height: 1.6;
81
+ display: flex;
82
+ flex-direction: column;
83
+ min-height: 100vh;
84
+ }
85
+ .outer {
86
+ flex: 1;
87
+ display: flex;
88
+ justify-content: center;
89
+ align-items: center;
90
+ padding: var(--spacing);
91
+ }
92
+ .inner {
93
+ background-color: var(--card-bg);
94
+ border-radius: 1rem;
95
+ box-shadow: 0 0.5rem 1.5rem var(--card-shadow);
96
+ padding: calc(var(--spacing) * 2);
97
+ text-align: center;
98
+ width: 100%;
99
+ max-width: fit-content;
100
+ }
101
+ /* No padding on the sides for captcha view on small screens */
102
+ @media (max-width: 600px) {
103
+ body:has(iframe) .outer {
104
+ padding-left: 0;
105
+ padding-right: 0;
106
+ }
107
+ body:has(iframe) .inner {
108
+ padding-left: 0;
109
+ padding-right: 0;
110
+ }
111
+ }
112
+ h2 {
113
+ margin-top: var(--spacing);
114
+ margin-bottom: 0.75rem;
115
+ font-size: 1.5rem;
116
+ }
117
+ h3 {
118
+ margin-top: var(--spacing);
119
+ margin-bottom: 0.5rem;
120
+ font-size: 1.125rem;
121
+ font-weight: 500;
122
+ }
123
+ p {
124
+ margin: 0.5rem 0;
125
+ }
126
+ .copy-input {
127
+ background-color: var(--code-bg);
128
+ }
129
+ .url-wrapper .api-key-wrapper {
130
+ display: flex;
131
+ gap: 0.5rem;
132
+ flex-wrap: wrap;
133
+ justify-content: center;
134
+ margin-bottom: var(--spacing);
135
+ }
136
+ .captcha-container {
137
+ background-color: var(--secondary);
138
+ }
139
+ button {
140
+ padding: 0.5rem 1rem;
141
+ font-size: 1rem;
142
+ border-radius: 0.5rem;
143
+ font-weight: 500;
144
+ cursor: pointer;
145
+ transition: background-color 0.2s ease, border-color 0.2s ease;
146
+ border: none;
147
+ margin-top: 0.5rem;
148
+ }
149
+ .btn-primary {
150
+ background-color: var(--primary);
151
+ color: #fff;
152
+ }
153
+ .btn-secondary {
154
+ background-color: var(--secondary);
155
+ color: #fff;
156
+ }
157
+ a {
158
+ color: var(--primary);
159
+ text-decoration: none;
160
+ }
161
+ a:hover {
162
+
163
+ }
164
+ /* footer styling */
165
+ footer {
166
+ text-align: center;
167
+ font-size: 0.75rem;
168
+ color: var(--secondary);
169
+ padding: 0.5rem 0;
170
+ }
171
+ </style>
172
+ </head>'''
173
+
174
+ body = f'''
175
+ {head}
38
176
  <body>
39
- <div style="{style_outer.strip()}">
40
- <div style="{style_inner.strip()}">
41
- {inner_content}
177
+ <div class="outer">
178
+ <div class="inner">
179
+ {inner_content}
180
+ </div>
42
181
  </div>
43
- </div>
182
+ <footer>
183
+ Quasarr v.{get_version()}
184
+ </footer>
44
185
  </body>
45
186
  '''
187
+ return f'<html>{body}</html>'
46
188
 
47
189
 
48
190
  def render_button(text, button_type="primary", attributes=None):
49
- base_style = (
50
- "padding: 0.375rem 0.75rem; font-size: 1rem; line-height: 1.5; "
51
- "border-radius: 0.375rem; color: #fff; display: inline-block; "
52
- "font-weight: 400; text-align: center; vertical-align: middle; "
53
- "cursor: pointer; -webkit-user-select: none; -moz-user-select: none; "
54
- "user-select: none; transition: color 0.15s ease-in-out, "
55
- "background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, "
56
- "box-shadow 0.15s ease-in-out; "
57
- )
58
-
59
- if button_type == "primary":
60
- style = base_style + "background-color: #0d6efd; border: 1px solid #0d6efd; "
61
- else:
62
- style = base_style + "background-color: #6c757d; border: 1px solid #6c757d; "
63
-
64
- attr_str = ' '.join(f'{key}="{value}"' for key, value in attributes.items()) if attributes else ""
65
-
66
- return f'<button style="{style}" {attr_str}>{text}</button>'
191
+ cls = "btn-primary" if button_type == "primary" else "btn-secondary"
192
+ attr_str = ''
193
+ if attributes:
194
+ attr_str = ' '.join(f'{key}="{value}"' for key, value in attributes.items())
195
+ return f'<button class="{cls}" {attr_str}>{text}</button>'
67
196
 
68
197
 
69
198
  def render_form(header, form="", script=""):
70
- styles = """
71
- <style>
72
- input, select {
73
- display: block;
74
- padding: .375rem .75rem;
75
- width: 100%;
76
- font-size: 1rem;
77
- font-weight: 400;
78
- line-height: 1.5;
79
- color: #212529;
80
- background-color: #fff;
81
- border: 1px solid #dee2e6;
82
- border-radius: .375rem;
83
- transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
84
- text-align: center;
85
- margin: 10px auto;
86
- }
87
- </style>
88
- """
89
199
  content = f'''
90
- <h1>Quasarr</h1>
91
- <h3>{header}</h3>
92
- {styles}
200
+ <h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
201
+ <h2>{header}</h2>
93
202
  {form}
94
203
  {script}
95
204
  '''
96
205
  return render_centered_html(content)
97
206
 
98
207
 
99
- def render_success(message, timeout=10):
208
+ def render_success(message, timeout=10, optional_text=""):
100
209
  button_html = render_button(f"Wait time... {timeout}", "secondary", {"id": "nextButton", "disabled": "true"})
101
- script = f"""
102
- <script>
103
- var counter = {timeout};
104
- var interval = setInterval(function() {{
105
- counter--;
106
- document.getElementById('nextButton').innerText = 'Wait time... ' + counter;
107
- if (counter === 0) {{
108
- clearInterval(interval);
109
- document.getElementById('nextButton').innerText = 'Continue';
110
- document.getElementById('nextButton').disabled = false;
111
- document.getElementById('nextButton').onclick = function() {{
112
- window.location.href='/';
113
- }};
114
- // Change button style to primary
115
- document.getElementById('nextButton').style.backgroundColor = '#0d6efd';
116
- document.getElementById('nextButton').style.borderColor = '#0d6efd';
117
- }}
118
- }}, 1000);
119
- </script>
120
- """
121
- content = f"""<h1>Quasarr</h1>
122
- <h3>{message}</h3>
123
- {button_html}
124
- {script}
125
- """
210
+ script = f'''
211
+ <script>
212
+ let counter = {timeout};
213
+ const btn = document.getElementById('nextButton');
214
+ const interval = setInterval(() => {{
215
+ counter--;
216
+ btn.innerText = `Wait time... ${{counter}}`;
217
+ if (counter === 0) {{
218
+ clearInterval(interval);
219
+ btn.innerText = 'Continue';
220
+ btn.disabled = false;
221
+ btn.className = 'btn-primary';
222
+ btn.onclick = () => window.location.href = '/';
223
+ }}
224
+ }}, 1000);
225
+ </script>
226
+ '''
227
+ content = f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
228
+ <h2>{message}</h2>
229
+ {optional_text}
230
+ {button_html}
231
+ {script}
232
+ '''
126
233
  return render_centered_html(content)
127
234
 
128
235
 
129
236
  def render_fail(message):
130
237
  button_html = render_button("Back", "secondary", {"onclick": "window.location.href='/'"})
131
- return render_centered_html(f"""<h1>Quasarr</h1>
132
- <h3>{message}</h3>
133
- {button_html}
134
- """)
238
+ return render_centered_html(f"""<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
239
+ <h2>{message}</h2>
240
+ {button_html}
241
+ """)
@@ -2,9 +2,37 @@
2
2
  # Quasarr
3
3
  # Project by https://github.com/rix1337
4
4
 
5
+ import html
5
6
  import re
7
+ from datetime import datetime, timedelta
8
+ from json import loads
9
+ from urllib.parse import quote
6
10
 
7
11
  import requests
12
+ from bs4 import BeautifulSoup
13
+
14
+ from quasarr.providers.log import info, debug
15
+
16
+
17
+ def get_poster_link(shared_state, imdb_id):
18
+ poster_link = None
19
+ if imdb_id:
20
+ headers = {'User-Agent': shared_state.values["user_agent"]}
21
+ request = requests.get(f"https://www.imdb.com/title/{imdb_id}/", headers=headers, timeout=10).text
22
+ soup = BeautifulSoup(request, "html.parser")
23
+ try:
24
+ poster_set = soup.find('div', class_='ipc-poster').div.img[
25
+ "srcset"] # contains links to posters in ascending resolution
26
+ poster_links = [x for x in poster_set.split(" ") if
27
+ len(x) > 10] # extract all poster links ignoring resolution info
28
+ poster_link = poster_links[-1] # get the highest resolution poster
29
+ except:
30
+ pass
31
+
32
+ if not poster_link:
33
+ debug(f"Could not get poster title for {imdb_id} from IMDb")
34
+
35
+ return poster_link
8
36
 
9
37
 
10
38
  def get_localized_title(shared_state, imdb_id, language='de'):
@@ -16,9 +44,9 @@ def get_localized_title(shared_state, imdb_id, language='de'):
16
44
  }
17
45
 
18
46
  try:
19
- response = requests.get(f"https://www.imdb.com/title/{imdb_id}/", headers=headers)
47
+ response = requests.get(f"https://www.imdb.com/title/{imdb_id}/", headers=headers, timeout=10)
20
48
  except Exception as e:
21
- print(f"Error loading IMDb metadata for {imdb_id}: {e}")
49
+ info(f"Error loading IMDb metadata for {imdb_id}: {e}")
22
50
  return localized_title
23
51
 
24
52
  try:
@@ -32,6 +60,83 @@ def get_localized_title(shared_state, imdb_id, language='de'):
32
60
  pass
33
61
 
34
62
  if not localized_title:
35
- print(f"Could not get localized title for {imdb_id} in {language} from IMDb")
63
+ debug(f"Could not get localized title for {imdb_id} in {language} from IMDb")
64
+
65
+ localized_title = html.unescape(localized_title)
66
+ localized_title = re.sub(r"[^a-zA-Z0-9äöüÄÖÜß&-']", ' ', localized_title).strip()
67
+ localized_title = localized_title.replace(" - ", "-")
68
+ localized_title = re.sub(r'\s{2,}', ' ', localized_title)
36
69
 
37
70
  return localized_title
71
+
72
+
73
+ def get_clean_title(title):
74
+ try:
75
+ extracted_title = re.findall(r"(.*?)(?:.(?!19|20)\d{2}|\.German|.GERMAN|\.\d{3,4}p|\.S(?:\d{1,3}))", title)[0]
76
+ leftover_tags_removed = re.sub(
77
+ r'(|.UNRATED.*|.Unrated.*|.Uncut.*|.UNCUT.*)(|.Directors.Cut.*|.Final.Cut.*|.DC.*|.REMASTERED.*|.EXTENDED.*|.Extended.*|.Theatrical.*|.THEATRICAL.*)',
78
+ "", extracted_title)
79
+ clean_title = leftover_tags_removed.replace(".", " ").strip().replace(" ", "+")
80
+
81
+ except:
82
+ clean_title = title
83
+ return clean_title
84
+
85
+
86
+ def get_imdb_id_from_title(shared_state, title, language="de"):
87
+ imdb_id = None
88
+
89
+ if re.search(r"S\d{1,3}(E\d{1,3})?", title, re.IGNORECASE):
90
+ ttype = "tv"
91
+ else:
92
+ ttype = "ft"
93
+
94
+ title = get_clean_title(title)
95
+
96
+ threshold = 60 * 60 * 48 # 48 hours
97
+ context = "recents_imdb"
98
+ recently_searched = shared_state.get_recently_searched(shared_state, context, threshold)
99
+ if title in recently_searched:
100
+ title_item = recently_searched[title]
101
+ if title_item["timestamp"] > datetime.now() - timedelta(seconds=threshold):
102
+ return title_item["imdb_id"]
103
+
104
+ headers = {
105
+ 'Accept-Language': language,
106
+ 'User-Agent': shared_state.values["user_agent"]
107
+ }
108
+
109
+ results = requests.get(f"https://www.imdb.com/find/?q={quote(title)}&s=tt&ttype={ttype}&ref_=fn_{ttype}",
110
+ headers=headers, timeout=10)
111
+
112
+ if results.status_code == 200:
113
+ soup = BeautifulSoup(results.text, "html.parser")
114
+ props = soup.find("script", text=re.compile("props"))
115
+ details = loads(props.string)
116
+ search_results = details['props']['pageProps']['titleResults']['results']
117
+
118
+ if len(search_results) > 0:
119
+ for result in search_results:
120
+ try:
121
+ found_title = result["listItem"]["titleText"]
122
+ found_id = result["listItem"]["titleId"]
123
+ except KeyError:
124
+ found_title = result["titleNameText"]
125
+ found_id = result['id']
126
+
127
+ if shared_state.search_string_in_sanitized_title(title, found_title):
128
+ imdb_id = found_id
129
+ break
130
+ else:
131
+ debug(f"Request on IMDb failed: {results.status_code}")
132
+
133
+ recently_searched[title] = {
134
+ "imdb_id": imdb_id,
135
+ "timestamp": datetime.now()
136
+ }
137
+ shared_state.update(context, recently_searched)
138
+
139
+ if not imdb_id:
140
+ debug(f"No IMDb-ID found for {title}")
141
+
142
+ return imdb_id
@@ -0,0 +1,19 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ import datetime
6
+ import os
7
+
8
+
9
+ def timestamp():
10
+ return datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
11
+
12
+
13
+ def info(string):
14
+ print(f"{timestamp()} {string}")
15
+
16
+
17
+ def debug(string):
18
+ if os.getenv('DEBUG'):
19
+ info(string)