quasarr 0.0.1__tar.gz

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.
Files changed (31) hide show
  1. quasarr-0.0.1/LICENSE +21 -0
  2. quasarr-0.0.1/PKG-INFO +60 -0
  3. quasarr-0.0.1/README.md +43 -0
  4. quasarr-0.0.1/quasarr/__init__.py +177 -0
  5. quasarr-0.0.1/quasarr/arr/__init__.py +206 -0
  6. quasarr-0.0.1/quasarr/downloads/__init__.py +165 -0
  7. quasarr-0.0.1/quasarr/downloads/sources/__init__.py +0 -0
  8. quasarr-0.0.1/quasarr/downloads/sources/nx.py +146 -0
  9. quasarr-0.0.1/quasarr/persistence/__init__.py +0 -0
  10. quasarr-0.0.1/quasarr/persistence/config.py +140 -0
  11. quasarr-0.0.1/quasarr/persistence/sqlite_database.py +95 -0
  12. quasarr-0.0.1/quasarr/providers/__init__.py +0 -0
  13. quasarr-0.0.1/quasarr/providers/html_templates.py +128 -0
  14. quasarr-0.0.1/quasarr/providers/imdb_metadata.py +37 -0
  15. quasarr-0.0.1/quasarr/providers/myjd_api.py +678 -0
  16. quasarr-0.0.1/quasarr/providers/setup.py +294 -0
  17. quasarr-0.0.1/quasarr/providers/shared_state.py +181 -0
  18. quasarr-0.0.1/quasarr/providers/version.py +59 -0
  19. quasarr-0.0.1/quasarr/providers/web_server.py +49 -0
  20. quasarr-0.0.1/quasarr/search/__init__.py +15 -0
  21. quasarr-0.0.1/quasarr/search/sources/__init__.py +0 -0
  22. quasarr-0.0.1/quasarr/search/sources/nx.py +158 -0
  23. quasarr-0.0.1/quasarr.egg-info/PKG-INFO +60 -0
  24. quasarr-0.0.1/quasarr.egg-info/SOURCES.txt +29 -0
  25. quasarr-0.0.1/quasarr.egg-info/dependency_links.txt +1 -0
  26. quasarr-0.0.1/quasarr.egg-info/entry_points.txt +2 -0
  27. quasarr-0.0.1/quasarr.egg-info/not-zip-safe +1 -0
  28. quasarr-0.0.1/quasarr.egg-info/requires.txt +4 -0
  29. quasarr-0.0.1/quasarr.egg-info/top_level.txt +1 -0
  30. quasarr-0.0.1/setup.cfg +4 -0
  31. quasarr-0.0.1/setup.py +43 -0
quasarr-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 RiX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
quasarr-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.1
2
+ Name: quasarr
3
+ Version: 0.0.1
4
+ Summary: Full template for python web projects with Docker, GitHub Actions, PyPI, and more.
5
+ Home-page: https://github.com/rix1337/Quasarr
6
+ Author: rix1337
7
+ Author-email:
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: beautifulsoup4==4.12.3
14
+ Requires-Dist: bottle==0.12.25
15
+ Requires-Dist: pycryptodomex==3.20.0
16
+ Requires-Dist: requests
17
+
18
+ # Quasarr
19
+
20
+ [![PyPI version](https://badge.fury.io/py/quasarr.svg)](https://badge.fury.io/py/quasarr)
21
+ [![GitHub Sponsorship](https://img.shields.io/badge/support-me-red.svg)](https://github.com/users/rix1337/sponsorship)
22
+
23
+ JDownloader Bridge for Radarr and Sonarr.
24
+
25
+ * Follow instructions to set up at least one hostname
26
+ * Provide your [My JDownloader credentials](https://my.jdownloader.org)
27
+ * Then use Quasarr's URL as 'Newznab Indexer' and 'SABnzbd Download Client' in Sonarr/Radarr.
28
+ * Leave settings at default
29
+ * Use this API key: `quasarr`
30
+
31
+ **Warning: this is a very early dev version. Only tested with Radarr. Only one hostname supported.**
32
+
33
+ # Setup
34
+
35
+ `pip install quasarr`
36
+
37
+ # Run
38
+
39
+ ```
40
+ quasarr
41
+ --port=8080
42
+ --external_address=https://quasarr.example.org:9443
43
+ ```
44
+
45
+ * External Address: required, if you want to fully use Quasarr from outside your local network
46
+
47
+ # Docker
48
+
49
+ ```
50
+ docker run -d \
51
+ --name="Quasarr" \
52
+ -p port:8080 \
53
+ -v /path/to/config/:/config:rw \
54
+ -e 'INTERNAL_ADDRESS'='http://quasarr:8080'
55
+ -e 'EXTERNAL_ADDRESS'='https://quasarr.example.org:9443'
56
+ rix1337/docker-quasarr:latest
57
+ ```
58
+
59
+ * Internal Address: required so Radarr/Sonarr can reach Quasarr
60
+ * External Address: required, if you want to fully use Quasarr from outside your local network
@@ -0,0 +1,43 @@
1
+ # Quasarr
2
+
3
+ [![PyPI version](https://badge.fury.io/py/quasarr.svg)](https://badge.fury.io/py/quasarr)
4
+ [![GitHub Sponsorship](https://img.shields.io/badge/support-me-red.svg)](https://github.com/users/rix1337/sponsorship)
5
+
6
+ JDownloader Bridge for Radarr and Sonarr.
7
+
8
+ * Follow instructions to set up at least one hostname
9
+ * Provide your [My JDownloader credentials](https://my.jdownloader.org)
10
+ * Then use Quasarr's URL as 'Newznab Indexer' and 'SABnzbd Download Client' in Sonarr/Radarr.
11
+ * Leave settings at default
12
+ * Use this API key: `quasarr`
13
+
14
+ **Warning: this is a very early dev version. Only tested with Radarr. Only one hostname supported.**
15
+
16
+ # Setup
17
+
18
+ `pip install quasarr`
19
+
20
+ # Run
21
+
22
+ ```
23
+ quasarr
24
+ --port=8080
25
+ --external_address=https://quasarr.example.org:9443
26
+ ```
27
+
28
+ * External Address: required, if you want to fully use Quasarr from outside your local network
29
+
30
+ # Docker
31
+
32
+ ```
33
+ docker run -d \
34
+ --name="Quasarr" \
35
+ -p port:8080 \
36
+ -v /path/to/config/:/config:rw \
37
+ -e 'INTERNAL_ADDRESS'='http://quasarr:8080'
38
+ -e 'EXTERNAL_ADDRESS'='https://quasarr.example.org:9443'
39
+ rix1337/docker-quasarr:latest
40
+ ```
41
+
42
+ * Internal Address: required so Radarr/Sonarr can reach Quasarr
43
+ * External Address: required, if you want to fully use Quasarr from outside your local network
@@ -0,0 +1,177 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ import argparse
6
+ import multiprocessing
7
+ import os
8
+ import socket
9
+ import sys
10
+ import tempfile
11
+ import time
12
+
13
+ from quasarr.arr import api
14
+ from quasarr.persistence.config import Config, get_clean_hostnames
15
+ from quasarr.persistence.sqlite_database import DataBase
16
+ from quasarr.providers import shared_state, version
17
+ from quasarr.providers.setup import path_config, hostnames_config, nx_credentials_config, jdownloader_config
18
+ from quasarr.providers.shared_state import sanitize_external_address
19
+
20
+
21
+ def run():
22
+ with multiprocessing.Manager() as manager:
23
+ shared_state_dict = manager.dict()
24
+ shared_state_lock = manager.Lock()
25
+ shared_state.set_state(shared_state_dict, shared_state_lock)
26
+
27
+ parser = argparse.ArgumentParser()
28
+ parser.add_argument("--port", help="Desired Port, defaults to 8080")
29
+ parser.add_argument("--internal_address", help="Must be provided when running in Docker")
30
+ parser.add_argument("--external_address",
31
+ help="Address/URL of Quasarr available outside your LAN, must include port and protocol")
32
+ arguments = parser.parse_args()
33
+
34
+ sys.stdout = Unbuffered(sys.stdout)
35
+
36
+ print(f"""┌────────────────────────────────────┐
37
+ Quasarr {version.get_version()} by RiX
38
+ https://github.com/rix1337/Quasarr
39
+ └────────────────────────────────────┘""")
40
+
41
+ port = int('8080')
42
+
43
+ config_path = ""
44
+ if os.environ.get('DOCKER'):
45
+ config_path = "/config"
46
+ if arguments.internal_address:
47
+ internal_address = arguments.internal_address
48
+ else:
49
+ print(
50
+ "You must set the INTERNAL_ADDRESS variable to a locally reachable URL, e.g. http://localhost:8080")
51
+ print("The local URL will be used by Radarr/Sonarr to connect to Quasarr")
52
+ print("Stopping Quasarr...")
53
+ sys.exit(1)
54
+ else:
55
+ if arguments.port:
56
+ port = int(arguments.port)
57
+ internal_address = f'http://{check_ip()}'
58
+
59
+ external_address = ""
60
+ if arguments.external_address:
61
+ sanitized_url = sanitize_external_address(arguments.external_address)
62
+ if sanitized_url:
63
+ external_address = sanitized_url
64
+
65
+ shared_state.set_connection_info(internal_address, port, external_address)
66
+
67
+ if not config_path:
68
+ config_path_file = "Quasarr.conf"
69
+ if not os.path.exists(config_path_file):
70
+ path_config(shared_state)
71
+ with open(config_path_file, "r") as f:
72
+ config_path = f.readline().strip()
73
+
74
+ os.makedirs(config_path, exist_ok=True)
75
+
76
+ try:
77
+ temp_file = tempfile.TemporaryFile(dir=config_path)
78
+ temp_file.close()
79
+ except Exception as e:
80
+ print(f'Could not access "{config_path}": {e}"'
81
+ f'Stopping Quasarr...')
82
+ sys.exit(1)
83
+
84
+ shared_state.set_files(config_path)
85
+ shared_state.update("config", Config)
86
+ shared_state.update("database", DataBase)
87
+ shared_state.update("user_agent",
88
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
89
+
90
+ print(f'Config path: "{config_path}"')
91
+
92
+ shared_state.set_sites()
93
+
94
+ if not get_clean_hostnames(shared_state):
95
+ hostnames_config(shared_state)
96
+ get_clean_hostnames(shared_state)
97
+
98
+ if Config('Hostnames').get('nx'):
99
+ user = Config('NX').get('user')
100
+ password = Config('NX').get('password')
101
+ if not user or not password:
102
+ nx_credentials_config(shared_state)
103
+
104
+ config = Config('JDownloader')
105
+ user = config.get('user')
106
+ password = config.get('password')
107
+ device = config.get('device')
108
+
109
+ if not user or not password or not device:
110
+ jdownloader_config(shared_state)
111
+
112
+ jdownloader = multiprocessing.Process(target=jdownloader_connection,
113
+ args=(shared_state_dict, shared_state_lock))
114
+ jdownloader.start()
115
+
116
+ print(f'\nQuasarr API now running at "{shared_state.values["internal_address"]}"')
117
+ print('Use this exact URL as "Newznab Indexer" and "SABnzbd Download Client" in Sonarr/Radarr')
118
+ print("Leave settings at default and use this API key: 'quasarr'")
119
+ if shared_state.values["external_address"] != shared_state.values["internal_address"]:
120
+ print(f'External address: "{shared_state.values["external_address"]}"')
121
+
122
+ try:
123
+ api(shared_state_dict, shared_state_lock)
124
+ except KeyboardInterrupt:
125
+ sys.exit(0)
126
+
127
+
128
+ def jdownloader_connection(shared_state_dict, shared_state_lock):
129
+ shared_state.set_state(shared_state_dict, shared_state_lock)
130
+
131
+ shared_state.set_device_from_config()
132
+
133
+ connection_established = shared_state.get_device() and shared_state.get_device().name
134
+ if not connection_established:
135
+ i = 0
136
+ while i < 10:
137
+ i += 1
138
+ print(f'Connection {i} to JDownloader failed. Device name: "{shared_state.values["device"]}"')
139
+ time.sleep(60)
140
+ shared_state.set_device_from_config()
141
+ connection_established = shared_state.get_device() and shared_state.get_device().name
142
+ if connection_established:
143
+ break
144
+
145
+ if connection_established:
146
+ print(f'Connection to JDownloader successful. Device name: "{shared_state.get_device().name}"')
147
+ else:
148
+ print('Error connecting to JDownloader! Stopping Quasarr!')
149
+ sys.exit(1)
150
+
151
+
152
+ class Unbuffered(object):
153
+ def __init__(self, stream):
154
+ self.stream = stream
155
+
156
+ def write(self, data):
157
+ self.stream.write(data)
158
+ self.stream.flush()
159
+
160
+ def writelines(self, datas):
161
+ self.stream.writelines(datas)
162
+ self.stream.flush()
163
+
164
+ def __getattr__(self, attr):
165
+ return getattr(self.stream, attr)
166
+
167
+
168
+ def check_ip():
169
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
170
+ try:
171
+ s.connect(('10.255.255.255', 0))
172
+ ip = s.getsockname()[0]
173
+ except:
174
+ ip = '127.0.0.1'
175
+ finally:
176
+ s.close()
177
+ return ip
@@ -0,0 +1,206 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ from base64 import urlsafe_b64decode
6
+ from xml.etree import ElementTree as ET
7
+
8
+ from bottle import Bottle, request, redirect
9
+
10
+ from quasarr.downloads import download_package, delete_package, get_packages
11
+ from quasarr.providers import shared_state
12
+ from quasarr.providers.html_templates import render_centered_html
13
+ from quasarr.providers.web_server import Server
14
+ from quasarr.search import get_search_results
15
+
16
+
17
+ def api(shared_state_dict, shared_state_lock):
18
+ shared_state.set_state(shared_state_dict, shared_state_lock)
19
+
20
+ app = Bottle()
21
+
22
+ @app.get('/')
23
+ def index():
24
+ info = f"""
25
+ <h1>Quasarr</h1>
26
+ <p>
27
+ <code id="current-url" style="background-color: #f0f0f0; padding: 5px; border-radius: 3px;">
28
+ {shared_state.values["external_address"]}
29
+ </code>
30
+ </p>
31
+ <p>Use this exact URL as 'Newznab Indexer' and 'SABnzbd Download Client' in Sonarr/Radarr.
32
+ Leave settings at default and use this API key: 'quasarr'</p>
33
+ """
34
+ return render_centered_html(info)
35
+
36
+ @app.get('/download/')
37
+ def fake_download_container():
38
+ payload = request.query.payload
39
+ decoded_payload = urlsafe_b64decode(payload).decode("utf-8").split("|")
40
+ title = decoded_payload[0]
41
+ url = decoded_payload[1]
42
+
43
+ request_from = request.headers.get('User-Agent')
44
+ if request_from:
45
+ if not any(arr_client in request_from for arr_client in ["Radarr", "Sonarr"]):
46
+ redirect(url, 302)
47
+
48
+ return f'<nzb><file title="{title}" url="{url}"/></nzb>'
49
+
50
+ @app.post('/api')
51
+ def download():
52
+ downloads = request.files.getall('name')
53
+ nzo_ids = []
54
+ for upload in downloads:
55
+ file_content = upload.file.read()
56
+ root = ET.fromstring(file_content)
57
+ title = root.find(".//file").attrib["title"]
58
+ url = root.find(".//file").attrib["url"]
59
+ print(f"Attempting download for {title}")
60
+
61
+ nzo_id = download_package(shared_state, title, url)
62
+ if nzo_id:
63
+ print(f"Download started for {title}")
64
+ nzo_ids.append(nzo_id)
65
+ else:
66
+ print(f"Download failed for {title}")
67
+
68
+ return {
69
+ "status": True,
70
+ "nzo_ids": nzo_ids
71
+ }
72
+
73
+ @app.get('/api')
74
+ def fake_api():
75
+ api_type = 'sabnzbd' if request.query.mode and request.query.apikey else 'newznab' if request.query.t else None
76
+
77
+ if api_type == 'sabnzbd':
78
+ try:
79
+ mode = request.query.mode
80
+ if mode == "version":
81
+ return {
82
+ "version": "4.3.2"
83
+ }
84
+ elif mode == "get_config":
85
+ return {
86
+ "config": {
87
+ "misc": {
88
+ "quasarr": True,
89
+ "complete_dir": "/tmp/"
90
+ },
91
+ "categories": [
92
+ {
93
+ "name": "*",
94
+ "order": 0,
95
+ "dir": "",
96
+ },
97
+ {
98
+ "name": "movies",
99
+ "order": 1,
100
+ "dir": "",
101
+ },
102
+ {
103
+ "name": "tv",
104
+ "order": 2,
105
+ "dir": "",
106
+ }
107
+ ]
108
+ }
109
+ }
110
+ elif mode == "fullstatus":
111
+ return {
112
+ "status": {
113
+ "quasarr": True
114
+ }
115
+ }
116
+ elif mode == "queue" or mode == "history":
117
+ if request.query.name and request.query.name == "delete":
118
+ package_id = request.query.value
119
+ deleted = delete_package(shared_state, package_id)
120
+ print(f"Package {package_id} deleted {'successfully' if deleted else 'unsuccessfully'}")
121
+ return {
122
+ "status": deleted,
123
+ "nzo_ids": [package_id]
124
+ }
125
+
126
+ packages = get_packages(shared_state)
127
+ if mode == "queue":
128
+ return {
129
+ "queue": {
130
+ "paused": False,
131
+ "slots": packages["queue"]
132
+ }
133
+ }
134
+ elif mode == "history":
135
+ return {
136
+ "history": {
137
+ "paused": False,
138
+ "slots": packages["history"]
139
+ }
140
+ }
141
+ except Exception as e:
142
+ print(f"Error: {e}")
143
+ return {
144
+ "status": False
145
+ }
146
+
147
+ elif api_type == 'newznab':
148
+ try:
149
+ mode = request.query.t
150
+ if mode == 'movie':
151
+ if request.query.imdbid:
152
+ imdb_id = f"tt{request.query.imdbid}"
153
+ else:
154
+ imdb_id = None
155
+
156
+ request_from = request.headers.get('User-Agent')
157
+
158
+ releases = get_search_results(shared_state, request_from, imdb_id=imdb_id)
159
+
160
+ items = ""
161
+
162
+ for release in releases:
163
+ release = release["details"]
164
+
165
+ items += f'''
166
+ <item>
167
+ <title>{release["title"]}</title>
168
+ <guid isPermaLink="True">{release["link"]}</guid>
169
+ <link>{release["link"]}</link>
170
+ <comments>{release["link"]}</comments>
171
+ <pubDate>{release["date"]}</pubDate>
172
+ <enclosure url="{release["link"]}" length="{release["size"]}" type="application/x-nzb" />
173
+ </item>'''
174
+
175
+ return f'''<?xml version="1.0" encoding="UTF-8"?>
176
+ <rss version="2.0">
177
+ <channel>
178
+ {items}
179
+ </channel>
180
+ </rss>'''
181
+ elif mode == 'caps':
182
+ return '''<?xml version="1.0" encoding="UTF-8"?>
183
+ <caps>
184
+ <categories>
185
+ <category id="2000" name="Movies">
186
+ <subcat id="2010" name="Foreign"/>
187
+ <subcat id="2020" name="Other"/>
188
+ <subcat id="2030" name="SD"/>
189
+ <subcat id="2040" name="HD"/>
190
+ <subcat id="2050" name="BluRay"/>
191
+ <subcat id="2060" name="3D"/>
192
+ </category>
193
+ <category id="5000" name="TV">
194
+ <subcat id="5020" name="Foreign"/>
195
+ <subcat id="5030" name="SD"/>
196
+ <subcat id="5040" name="HD"/>
197
+ <subcat id="5050" name="Other"/>
198
+ <subcat id="5060" name="Sport"/>
199
+ </category>
200
+ </categories>
201
+ </caps>'''
202
+ except Exception as e:
203
+ print(f"Error: {e}")
204
+ return {"error": True}
205
+
206
+ Server(app, listen='0.0.0.0', port=shared_state.values["port"]).serve_forever()
@@ -0,0 +1,165 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ from quasarr.downloads.sources.nx import get_nx_download_links
6
+ from quasarr.providers.myjd_api import TokenExpiredException, RequestTimeoutException, MYJDException
7
+
8
+
9
+ def get_packages(shared_state):
10
+ packages = []
11
+
12
+ protected_packages = shared_state.get_db("to_decrypt").retrieve_all_titles() # todo not implemented yet
13
+ if protected_packages:
14
+ for package in protected_packages:
15
+ packages.append({
16
+ "details": package,
17
+ "location": "queue",
18
+ "type": "protected"
19
+ })
20
+ try:
21
+ linkgrabber_packages = shared_state.get_device().linkgrabber.query_packages()
22
+ except (TokenExpiredException, RequestTimeoutException, MYJDException):
23
+ linkgrabber_packages = []
24
+
25
+ if linkgrabber_packages:
26
+ for package in linkgrabber_packages:
27
+ packages.append({
28
+ "details": package,
29
+ "location": "queue",
30
+ "type": "linkgrabber"
31
+ })
32
+ try:
33
+ downloader_packages = shared_state.get_device().downloads.query_packages()
34
+ except (TokenExpiredException, RequestTimeoutException, MYJDException):
35
+ downloader_packages = []
36
+
37
+ if downloader_packages:
38
+ for package in downloader_packages:
39
+ finished = False
40
+ try:
41
+ finished = package["finished"]
42
+ except KeyError:
43
+ pass
44
+ packages.append({
45
+ "details": package,
46
+ "location": "history" if finished else "queue",
47
+ "type": "downloader"
48
+ })
49
+
50
+ downloads = {
51
+ "queue": [],
52
+ "history": []
53
+ }
54
+ for package in packages:
55
+ queue_index = 0
56
+ history_index = 0
57
+
58
+ def format_eta(seconds):
59
+ hours = seconds // 3600
60
+ minutes = (seconds % 3600) // 60
61
+ seconds = seconds % 60
62
+ return f"{hours:02}:{minutes:02}:{seconds:02}"
63
+
64
+ if package["location"] == "queue":
65
+ time_left = "2385:09:09" # to signify that its not running
66
+ if package["type"] == "protected": # todo load from db
67
+ details = package["details"]
68
+ name = "Protected package"
69
+ mb = 1000
70
+ mb_left = mb
71
+ nzo_id = "Quasarr_protected_1"
72
+ elif package["type"] == "linkgrabber":
73
+ details = package["details"]
74
+ name = details["name"]
75
+ mb = int(details["bytesTotal"]) / (1024 * 1024)
76
+ mb_left = mb
77
+ nzo_id = "Quasarr_protected_234"
78
+ elif package["type"] == "downloader":
79
+ details = package["details"]
80
+ name = details["name"]
81
+ try:
82
+ if details["eta"]:
83
+ time_left = format_eta(int(details["eta"]))
84
+ except KeyError:
85
+ pass
86
+ mb = int(details["bytesTotal"]) / (1024 * 1024)
87
+ mb_left = (int(details["bytesTotal"]) - int(details["bytesLoaded"])) / (1024 * 1024)
88
+ nzo_id = "Quasarr_protected_2"
89
+
90
+ try:
91
+ downloads["queue"].append({
92
+ "index": queue_index,
93
+ "nzo_id": nzo_id,
94
+ "priority": "Normal",
95
+ "filename": name,
96
+ "cat": "movies",
97
+ "mbleft": int(mb_left),
98
+ "mb": int(mb),
99
+ "status": "Downloading",
100
+ "timeleft": time_left,
101
+ })
102
+ except:
103
+ print(f"Parameters missing for {package}")
104
+ queue_index += 1
105
+ elif package["location"] == "history":
106
+ package = package["details"]
107
+ name = package["name"]
108
+ bytes = int(package["bytesLoaded"])
109
+ storage = package["saveTo"]
110
+ downloads["history"].append({
111
+ "fail_message": "",
112
+ "category": "movies",
113
+ "storage": storage,
114
+ "status": "Completed",
115
+ "nzo_id": "Quasarr_nzo_3",
116
+ "name": name,
117
+ "bytes": int(bytes),
118
+ })
119
+ history_index += 1
120
+ else:
121
+ print(f"Invalid package location {package['location']}")
122
+
123
+ return downloads
124
+
125
+
126
+ def download_package(shared_state, title, url):
127
+ package_id = ""
128
+
129
+ nx = shared_state.values["config"]("Hostnames").get("nx")
130
+ if nx.lower() in url.lower():
131
+ links = get_nx_download_links(shared_state, url, title)
132
+ print(f"Decrypted {len(links)} download links for {title}")
133
+
134
+ download_links = str(links).replace(" ", "")
135
+ download_path = "Quasarr/<jd:packagename>"
136
+ package_id = f"Quasarr_decrypted_{str(hash(title + url)).replace('-', '')}"
137
+
138
+ added = shared_state.get_device().linkgrabber.add_links(params=[
139
+ {
140
+ "autostart": True,
141
+ "links": download_links,
142
+ "packageName": title,
143
+ "extractPassword": nx,
144
+ "priority": "DEFAULT",
145
+ "downloadPassword": nx,
146
+ "destinationFolder": download_path,
147
+ "comment": package_id,
148
+ "overwritePackagizerRules": True
149
+ }
150
+ ])
151
+ if not added:
152
+ print(f"Failed to add {title} to linkgrabber")
153
+ package_id = ""
154
+
155
+ # Todo links are protected -> add them to the database for decryption in the web ui
156
+
157
+ return package_id
158
+
159
+
160
+ def delete_package(shared_state, package_id):
161
+ deleted = False
162
+ # todo implement (detect package by id from jdownloader or table)
163
+ # delete it at the correct location
164
+ print(f"Deleting package {package_id} - not implemented yet")
165
+ return deleted
File without changes