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.
- quasarr-0.0.1/LICENSE +21 -0
- quasarr-0.0.1/PKG-INFO +60 -0
- quasarr-0.0.1/README.md +43 -0
- quasarr-0.0.1/quasarr/__init__.py +177 -0
- quasarr-0.0.1/quasarr/arr/__init__.py +206 -0
- quasarr-0.0.1/quasarr/downloads/__init__.py +165 -0
- quasarr-0.0.1/quasarr/downloads/sources/__init__.py +0 -0
- quasarr-0.0.1/quasarr/downloads/sources/nx.py +146 -0
- quasarr-0.0.1/quasarr/persistence/__init__.py +0 -0
- quasarr-0.0.1/quasarr/persistence/config.py +140 -0
- quasarr-0.0.1/quasarr/persistence/sqlite_database.py +95 -0
- quasarr-0.0.1/quasarr/providers/__init__.py +0 -0
- quasarr-0.0.1/quasarr/providers/html_templates.py +128 -0
- quasarr-0.0.1/quasarr/providers/imdb_metadata.py +37 -0
- quasarr-0.0.1/quasarr/providers/myjd_api.py +678 -0
- quasarr-0.0.1/quasarr/providers/setup.py +294 -0
- quasarr-0.0.1/quasarr/providers/shared_state.py +181 -0
- quasarr-0.0.1/quasarr/providers/version.py +59 -0
- quasarr-0.0.1/quasarr/providers/web_server.py +49 -0
- quasarr-0.0.1/quasarr/search/__init__.py +15 -0
- quasarr-0.0.1/quasarr/search/sources/__init__.py +0 -0
- quasarr-0.0.1/quasarr/search/sources/nx.py +158 -0
- quasarr-0.0.1/quasarr.egg-info/PKG-INFO +60 -0
- quasarr-0.0.1/quasarr.egg-info/SOURCES.txt +29 -0
- quasarr-0.0.1/quasarr.egg-info/dependency_links.txt +1 -0
- quasarr-0.0.1/quasarr.egg-info/entry_points.txt +2 -0
- quasarr-0.0.1/quasarr.egg-info/not-zip-safe +1 -0
- quasarr-0.0.1/quasarr.egg-info/requires.txt +4 -0
- quasarr-0.0.1/quasarr.egg-info/top_level.txt +1 -0
- quasarr-0.0.1/setup.cfg +4 -0
- 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
|
+
[](https://badge.fury.io/py/quasarr)
|
|
21
|
+
[](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
|
quasarr-0.0.1/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Quasarr
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/quasarr)
|
|
4
|
+
[](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
|