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,154 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ from typing import Dict, Any
6
+
7
+
8
+ class StatsHelper:
9
+ """
10
+ Multiprocessing-safe stats helper using separate rows.
11
+ Uses shared_state for database access across processes.
12
+ """
13
+
14
+ def __init__(self, shared_state):
15
+ self.shared_state = shared_state
16
+ self._ensure_stats_exist()
17
+
18
+ def _get_db(self):
19
+ """Get database interface through shared_state"""
20
+ return self.shared_state.values["database"]("statistics")
21
+
22
+ def _ensure_stats_exist(self):
23
+ """Initialize stats if they don't exist"""
24
+ default_stats = {
25
+ "packages_downloaded": 0,
26
+ "links_processed": 0,
27
+ "captcha_decryptions_automatic": 0,
28
+ "captcha_decryptions_manual": 0,
29
+ "failed_downloads": 0,
30
+ "failed_decryptions_automatic": 0,
31
+ "failed_decryptions_manual": 0
32
+ }
33
+
34
+ db = self._get_db()
35
+ for key, default_value in default_stats.items():
36
+ if db.retrieve(key) is None:
37
+ db.store(key, str(default_value))
38
+
39
+ def _get_stat(self, key: str, default: int = 0) -> int:
40
+ """Get a single stat value"""
41
+ try:
42
+ db = self._get_db()
43
+ value = db.retrieve(key)
44
+ return int(value) if value is not None else default
45
+ except (ValueError, TypeError):
46
+ return default
47
+
48
+ def _increment_stat(self, key: str, count: int = 1):
49
+ """Process-safe increment of a single stat"""
50
+ db = self._get_db()
51
+ current = self._get_stat(key, 0)
52
+ db.update_store(key, str(current + count))
53
+
54
+ def increment_package_with_links(self, links):
55
+ """Increment package downloaded and links processed for one package, or failed download if no links
56
+
57
+ Args:
58
+ links: Can be:
59
+ - list/array: counts the length
60
+ - int: uses the value directly
61
+ - None/False/empty: treats as failed download
62
+ """
63
+ # Handle different input types
64
+ if links is None or links is False:
65
+ link_count = 0
66
+ elif isinstance(links, (list, tuple)):
67
+ link_count = len(links)
68
+ elif isinstance(links, int):
69
+ link_count = links
70
+ else:
71
+ # Handle other falsy values or unexpected types
72
+ try:
73
+ link_count = int(links) if links else 0
74
+ except (ValueError, TypeError):
75
+ link_count = 0
76
+
77
+ # Now handle the actual increment logic
78
+ if link_count == 0:
79
+ self._increment_stat("failed_downloads", 1)
80
+ else:
81
+ self._increment_stat("packages_downloaded", 1)
82
+ self._increment_stat("links_processed", link_count)
83
+
84
+ def increment_captcha_decryptions_automatic(self):
85
+ """Increment automatic captcha decryptions counter"""
86
+ self._increment_stat("captcha_decryptions_automatic", 1)
87
+
88
+ def increment_captcha_decryptions_manual(self):
89
+ """Increment manual captcha decryptions counter"""
90
+ self._increment_stat("captcha_decryptions_manual", 1)
91
+
92
+ def increment_failed_downloads(self):
93
+ """Increment failed downloads counter"""
94
+ self._increment_stat("failed_downloads", 1)
95
+
96
+ def increment_failed_decryptions_automatic(self):
97
+ """Increment failed automatic decryptions counter"""
98
+ self._increment_stat("failed_decryptions_automatic", 1)
99
+
100
+ def increment_failed_decryptions_manual(self):
101
+ """Increment failed manual decryptions counter"""
102
+ self._increment_stat("failed_decryptions_manual", 1)
103
+
104
+ def get_stats(self) -> Dict[str, Any]:
105
+ """Get all current statistics"""
106
+ stats = {
107
+ "packages_downloaded": self._get_stat("packages_downloaded", 0),
108
+ "links_processed": self._get_stat("links_processed", 0),
109
+ "captcha_decryptions_automatic": self._get_stat("captcha_decryptions_automatic", 0),
110
+ "captcha_decryptions_manual": self._get_stat("captcha_decryptions_manual", 0),
111
+ "failed_downloads": self._get_stat("failed_downloads", 0),
112
+ "failed_decryptions_automatic": self._get_stat("failed_decryptions_automatic", 0),
113
+ "failed_decryptions_manual": self._get_stat("failed_decryptions_manual", 0)
114
+ }
115
+
116
+ # Calculate totals and rates
117
+ total_captcha_decryptions = stats["captcha_decryptions_automatic"] + stats["captcha_decryptions_manual"]
118
+ total_failed_decryptions = stats["failed_decryptions_automatic"] + stats["failed_decryptions_manual"]
119
+ total_download_attempts = stats["packages_downloaded"] + stats["failed_downloads"]
120
+ total_decryption_attempts = total_captcha_decryptions + total_failed_decryptions
121
+ total_automatic_attempts = stats["captcha_decryptions_automatic"] + stats["failed_decryptions_automatic"]
122
+ total_manual_attempts = stats["captcha_decryptions_manual"] + stats["failed_decryptions_manual"]
123
+
124
+ # Add calculated fields
125
+ stats.update({
126
+ "total_captcha_decryptions": total_captcha_decryptions,
127
+ "total_failed_decryptions": total_failed_decryptions,
128
+ "total_download_attempts": total_download_attempts,
129
+ "total_decryption_attempts": total_decryption_attempts,
130
+ "total_automatic_attempts": total_automatic_attempts,
131
+ "total_manual_attempts": total_manual_attempts,
132
+ "download_success_rate": (
133
+ (stats["packages_downloaded"] / total_download_attempts * 100)
134
+ if total_download_attempts > 0 else 0
135
+ ),
136
+ "decryption_success_rate": (
137
+ (total_captcha_decryptions / total_decryption_attempts * 100)
138
+ if total_decryption_attempts > 0 else 0
139
+ ),
140
+ "automatic_decryption_success_rate": (
141
+ (stats["captcha_decryptions_automatic"] / total_automatic_attempts * 100)
142
+ if total_automatic_attempts > 0 else 0
143
+ ),
144
+ "manual_decryption_success_rate": (
145
+ (stats["captcha_decryptions_manual"] / total_manual_attempts * 100)
146
+ if total_manual_attempts > 0 else 0
147
+ ),
148
+ "average_links_per_package": (
149
+ stats["links_processed"] / stats["packages_downloaded"]
150
+ if stats["packages_downloaded"] > 0 else 0
151
+ )
152
+ })
153
+
154
+ return stats
@@ -0,0 +1,118 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ import re
6
+
7
+ import requests
8
+
9
+
10
+ def get_version():
11
+ return "1.20.6"
12
+
13
+
14
+ def get_latest_version():
15
+ """
16
+ Query GitHub API for the latest release of the Quasarr repository.
17
+ Returns the tag name string (e.g. "1.5.0" or "1.4.2a1").
18
+ Raises RuntimeError on HTTP errors.
19
+ """
20
+ api_url = "https://api.github.com/repos/rix1337/Quasarr/releases/latest"
21
+ resp = requests.get(api_url, headers={"Accept": "application/vnd.github.v3+json"})
22
+ if resp.status_code != 200:
23
+ raise RuntimeError(f"GitHub API error: {resp.status_code} {resp.text}")
24
+ data = resp.json()
25
+ tag = data.get("tag_name") or data.get("name")
26
+ if not tag:
27
+ raise RuntimeError("Could not find tag_name in GitHub response")
28
+ return tag
29
+
30
+
31
+ def _version_key(v):
32
+ """
33
+ Normalize a version string into a tuple for comparisons.
34
+ E.g. "1.4.2a3" -> (1, 4, 2, 'a', 3), "1.4.2" -> (1, 4, 2, '', 0)
35
+ """
36
+ m = re.match(r"^([0-9]+(?:\.[0-9]+)*)([a-z]?)([0-9]*)$", v)
37
+ if not m:
38
+ clean = re.sub(r"[^\d.]", "", v)
39
+ parts = clean.split(".")
40
+ nums = tuple(int(x) for x in parts if x.isdigit())
41
+ return nums + ("", 0)
42
+ base, alpha, num = m.groups()
43
+ nums = tuple(int(x) for x in base.split("."))
44
+ suffix_num = int(num) if num.isdigit() else 0
45
+ return nums + (alpha or "", suffix_num)
46
+
47
+
48
+ def is_newer(latest, current):
49
+ """
50
+ Return True if latest > current using semantic+alpha comparison.
51
+ """
52
+ return _version_key(latest) > _version_key(current)
53
+
54
+
55
+ def newer_version_available():
56
+ """
57
+ Check local vs. GitHub latest version.
58
+ Returns the latest version string if a newer release is available,
59
+ otherwise returns None.
60
+ """
61
+ try:
62
+ current = get_version()
63
+ latest = get_latest_version()
64
+ except:
65
+ raise
66
+ if is_newer(latest, current):
67
+ return latest
68
+ return None
69
+
70
+
71
+ def create_version_file():
72
+ version = get_version()
73
+ version_clean = re.sub(r'[^\d.]', '', version)
74
+ if "a" in version:
75
+ suffix = version.split("a")[1]
76
+ else:
77
+ suffix = 0
78
+ version_split = version_clean.split(".")
79
+ version_info = [
80
+ "VSVersionInfo(",
81
+ " ffi=FixedFileInfo(",
82
+ " filevers=(" + str(int(version_split[0])) + ", " + str(int(version_split[1])) + ", " + str(
83
+ int(version_split[2])) + ", " + str(int(suffix)) + "),",
84
+ " prodvers=(" + str(int(version_split[0])) + ", " + str(int(version_split[1])) + ", " + str(
85
+ int(version_split[2])) + ", " + str(int(suffix)) + "),",
86
+ " mask=0x3f,",
87
+ " flags=0x0,",
88
+ " OS=0x4,",
89
+ " fileType=0x1,",
90
+ " subtype=0x0,",
91
+ " date=(0, 0)",
92
+ " ),",
93
+ " kids=[",
94
+ " StringFileInfo(",
95
+ " [",
96
+ " StringTable(",
97
+ " u'040704b0',",
98
+ " [StringStruct(u'CompanyName', u'RiX'),",
99
+ " StringStruct(u'FileDescription', u'Quasarr'),",
100
+ " StringStruct(u'FileVersion', u'" + str(int(version_split[0])) + "." + str(
101
+ int(version_split[1])) + "." + str(int(version_split[2])) + "." + str(int(suffix)) + "'),",
102
+ " StringStruct(u'InternalName', u'Quasarr'),",
103
+ " StringStruct(u'LegalCopyright', u'Copyright © RiX'),",
104
+ " StringStruct(u'OriginalFilename', u'Quasarr.exe'),",
105
+ " StringStruct(u'ProductName', u'Quasarr'),",
106
+ " StringStruct(u'ProductVersion', u'" + str(int(version_split[0])) + "." + str(
107
+ int(version_split[1])) + "." + str(int(version_split[2])) + "." + str(int(suffix)) + "')])",
108
+ " ]),",
109
+ " VarFileInfo([VarStruct(u'Translation', [1031, 1200])])",
110
+ " ]",
111
+ ")"
112
+ ]
113
+ print("\n".join(version_info), file=open('file_version_info.txt', 'w', encoding='utf-8'))
114
+
115
+
116
+ if __name__ == '__main__':
117
+ print(get_version())
118
+ create_version_file()
@@ -0,0 +1,49 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ import time
6
+ from socketserver import ThreadingMixIn
7
+ from wsgiref.simple_server import WSGIServer, WSGIRequestHandler, make_server
8
+
9
+ temp_server_success = False
10
+
11
+
12
+ class ThreadingWSGIServer(ThreadingMixIn, WSGIServer):
13
+ daemon_threads = True
14
+
15
+
16
+ class NoLoggingWSGIRequestHandler(WSGIRequestHandler):
17
+ def log_message(self, format, *args):
18
+ pass
19
+
20
+
21
+ class Server:
22
+ def __init__(self, wsgi_app, listen='127.0.0.1', port=8080):
23
+ self.wsgi_app = wsgi_app
24
+ self.listen = listen
25
+ self.port = port
26
+ self.server = make_server(self.listen, self.port, self.wsgi_app,
27
+ ThreadingWSGIServer, handler_class=NoLoggingWSGIRequestHandler)
28
+
29
+ def serve_temporarily(self):
30
+ global temp_server_success
31
+ self.server.timeout = 1
32
+ try:
33
+ while not temp_server_success:
34
+ self.server.handle_request()
35
+ self.server.handle_request() # handle the last request
36
+ except Exception:
37
+ self.server.server_close()
38
+ return False
39
+ time.sleep(1)
40
+ self.server.server_close()
41
+ temp_server_success = False
42
+ return True
43
+
44
+ def serve_forever(self):
45
+ try:
46
+ self.server.serve_forever()
47
+ except KeyboardInterrupt:
48
+ self.server.shutdown()
49
+ self.server.server_close()
@@ -0,0 +1,153 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ import time
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
+
8
+ from quasarr.providers.log import info, debug
9
+ from quasarr.search.sources.al import al_feed, al_search
10
+ from quasarr.search.sources.by import by_feed, by_search
11
+ from quasarr.search.sources.dd import dd_search, dd_feed
12
+ from quasarr.search.sources.dj import dj_search, dj_feed
13
+ from quasarr.search.sources.dt import dt_feed, dt_search
14
+ from quasarr.search.sources.dw import dw_feed, dw_search
15
+ from quasarr.search.sources.fx import fx_feed, fx_search
16
+ from quasarr.search.sources.he import he_feed, he_search
17
+ from quasarr.search.sources.mb import mb_feed, mb_search
18
+ from quasarr.search.sources.nk import nk_feed, nk_search
19
+ from quasarr.search.sources.nx import nx_feed, nx_search
20
+ from quasarr.search.sources.sf import sf_feed, sf_search
21
+ from quasarr.search.sources.sj import sj_search, sj_feed
22
+ from quasarr.search.sources.sl import sl_feed, sl_search
23
+ from quasarr.search.sources.wd import wd_feed, wd_search
24
+
25
+
26
+ def get_search_results(shared_state, request_from, imdb_id="", search_phrase="", mirror=None, season="", episode=""):
27
+ results = []
28
+
29
+ if imdb_id and not imdb_id.startswith('tt'):
30
+ imdb_id = f'tt{imdb_id}'
31
+
32
+ docs_search = "lazylibrarian" in request_from.lower()
33
+
34
+ al = shared_state.values["config"]("Hostnames").get("al")
35
+ by = shared_state.values["config"]("Hostnames").get("by")
36
+ dd = shared_state.values["config"]("Hostnames").get("dd")
37
+ dt = shared_state.values["config"]("Hostnames").get("dt")
38
+ dj = shared_state.values["config"]("Hostnames").get("dj")
39
+ dw = shared_state.values["config"]("Hostnames").get("dw")
40
+ fx = shared_state.values["config"]("Hostnames").get("fx")
41
+ he = shared_state.values["config"]("Hostnames").get("he")
42
+ mb = shared_state.values["config"]("Hostnames").get("mb")
43
+ nk = shared_state.values["config"]("Hostnames").get("nk")
44
+ nx = shared_state.values["config"]("Hostnames").get("nx")
45
+ sf = shared_state.values["config"]("Hostnames").get("sf")
46
+ sj = shared_state.values["config"]("Hostnames").get("sj")
47
+ sl = shared_state.values["config"]("Hostnames").get("sl")
48
+ wd = shared_state.values["config"]("Hostnames").get("wd")
49
+
50
+ start_time = time.time()
51
+
52
+ functions = []
53
+
54
+ # Radarr/Sonarr use imdb_id for searches
55
+ imdb_map = [
56
+ (al, al_search),
57
+ (by, by_search),
58
+ (dd, dd_search),
59
+ (dt, dt_search),
60
+ (dj, dj_search),
61
+ (dw, dw_search),
62
+ (fx, fx_search),
63
+ (he, he_search),
64
+ (mb, mb_search),
65
+ (nk, nk_search),
66
+ (nx, nx_search),
67
+ (sf, sf_search),
68
+ (sj, sj_search),
69
+ (sl, sl_search),
70
+ (wd, wd_search),
71
+ ]
72
+
73
+ # LazyLibrarian uses search_phrase for searches
74
+ phrase_map = [
75
+ (by, by_search),
76
+ (dt, dt_search),
77
+ (nx, nx_search),
78
+ (sl, sl_search),
79
+ (wd, wd_search),
80
+ ]
81
+
82
+ # Feed searches omit imdb_id and search_phrase
83
+ feed_map = [
84
+ (al, al_feed),
85
+ (by, by_feed),
86
+ (dd, dd_feed),
87
+ (dj, dj_feed),
88
+ (dt, dt_feed),
89
+ (dw, dw_feed),
90
+ (fx, fx_feed),
91
+ (he, he_feed),
92
+ (mb, mb_feed),
93
+ (nk, nk_feed),
94
+ (nx, nx_feed),
95
+ (sf, sf_feed),
96
+ (sj, sj_feed),
97
+ (sl, sl_feed),
98
+ (wd, wd_feed),
99
+ ]
100
+
101
+ if imdb_id: # only Radarr/Sonarr are using imdb_id
102
+ args, kwargs = (
103
+ (shared_state, start_time, request_from, imdb_id),
104
+ {'mirror': mirror, 'season': season, 'episode': episode}
105
+ )
106
+ for flag, func in imdb_map:
107
+ if flag:
108
+ functions.append(lambda f=func, a=args, kw=kwargs: f(*a, **kw))
109
+
110
+ elif search_phrase and docs_search: # only LazyLibrarian is allowed to use search_phrase
111
+ args, kwargs = (
112
+ (shared_state, start_time, request_from, search_phrase),
113
+ {'mirror': mirror, 'season': season, 'episode': episode}
114
+ )
115
+ for flag, func in phrase_map:
116
+ if flag:
117
+ functions.append(lambda f=func, a=args, kw=kwargs: f(*a, **kw))
118
+
119
+ elif search_phrase:
120
+ debug(
121
+ f"Search phrase '{search_phrase}' is not supported for {request_from}. Only LazyLibrarian can use search phrases.")
122
+
123
+ else:
124
+ args, kwargs = (
125
+ (shared_state, start_time, request_from),
126
+ {'mirror': mirror}
127
+ )
128
+ for flag, func in feed_map:
129
+ if flag:
130
+ functions.append(lambda f=func, a=args, kw=kwargs: f(*a, **kw))
131
+
132
+ if imdb_id:
133
+ stype = f'IMDb-ID "{imdb_id}"'
134
+ elif search_phrase:
135
+ stype = f'Search-Phrase "{search_phrase}"'
136
+ else:
137
+ stype = "feed search"
138
+
139
+ info(f'Starting {len(functions)} search functions for {stype}... This may take some time.')
140
+
141
+ with ThreadPoolExecutor() as executor:
142
+ futures = [executor.submit(func) for func in functions]
143
+ for future in as_completed(futures):
144
+ try:
145
+ result = future.result()
146
+ results.extend(result)
147
+ except Exception as e:
148
+ info(f"An error occurred: {e}")
149
+
150
+ elapsed_time = time.time() - start_time
151
+ info(f"Providing {len(results)} releases to {request_from} for {stype}. Time taken: {elapsed_time:.2f} seconds")
152
+
153
+ return results
File without changes