├── __pycache__
├── newznab.cpython-310.pyc
├── plugin_download_interface.cpython-310.pyc
├── plugin_loader.cpython-310.pyc
├── plugin_search_interface.cpython-310.pyc
└── sabnzbd.cpython-310.pyc
├── app.py
├── config
├── config.json
├── plugins
│ ├── download
│ │ ├── __pycache__
│ │ │ ├── libgendl.cpython-310.pyc
│ │ │ └── ytmusicdl.cpython-310.pyc
│ │ ├── libgendl.py
│ │ └── ytmusicdl.py
│ └── search
│ │ ├── __pycache__
│ │ ├── libgen.cpython-310.pyc
│ │ └── ytmusic.cpython-310.pyc
│ │ ├── libgen.py
│ │ └── ytmusic.py
└── sabqueue.conf
├── docker-compose.yaml
├── dockerfile
├── entrypoint.sh
├── images
├── newznab1.png
├── newznab2.png
├── sabnzbd1.png
└── sabnzbd2.png
├── newznab.py
├── plugin_download_interface.py
├── plugin_search_interface.py
├── readme.md
├── requirements.txt
├── sabnzbd.py
└── test.bash
/__pycache__/newznab.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/__pycache__/newznab.cpython-310.pyc
--------------------------------------------------------------------------------
/__pycache__/plugin_download_interface.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/__pycache__/plugin_download_interface.cpython-310.pyc
--------------------------------------------------------------------------------
/__pycache__/plugin_loader.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/__pycache__/plugin_loader.cpython-310.pyc
--------------------------------------------------------------------------------
/__pycache__/plugin_search_interface.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/__pycache__/plugin_search_interface.cpython-310.pyc
--------------------------------------------------------------------------------
/__pycache__/sabnzbd.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/__pycache__/sabnzbd.cpython-310.pyc
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import importlib
3 | import sys
4 | from flask import Flask, request, Response, jsonify
5 | import requests
6 | import xml.etree.ElementTree as ET
7 | import random
8 | import string
9 | import hashlib
10 | import threading
11 | import time
12 |
13 | from plugin_search_interface import PluginSearchBase
14 | from plugin_download_interface import PluginDownloadBase
15 | from newznab import searchresults_to_response
16 | from sabnzbd import *
17 |
18 | # directory variables
19 | BASE_DIR = os.path.dirname(os.path.abspath(__file__))
20 | CONFIG_DIR = os.environ.get('CONFIG')
21 | FLASK_PORT = os.environ.get("FLASK_RUN_PORT", "10000")
22 | FLASK_HOST = os.environ.get("FLASK_RUN_HOST", "0.0.0.0")
23 | PLUGIN_SEARCH_DIR = os.path.join(CONFIG_DIR, "plugins","search")
24 | PLUGIN_DOWNLOAD_DIR = os.path.join(CONFIG_DIR, "plugins","download")
25 | DOWNLOAD_DIR = "/data/downloads/downloadarr"
26 | SAB_API = "abcde"
27 | SAB_CATEGORIES = ["lidarr"]
28 |
29 | # array holding plugins
30 | search_plugins = []
31 | download_plugins = []
32 | sabqueue = []
33 |
34 | # falsk app
35 | app = Flask(__name__)
36 |
37 | # load all the search plugins
38 | def load_search_plugins(search_plugin_directory):
39 | search_plugins = []
40 | sys.path.insert(0, search_plugin_directory)
41 | print("Loading search plugins from:" + search_plugin_directory)
42 |
43 | for filename in os.listdir(search_plugin_directory):
44 | if filename.endswith(".py") and filename != "__init__.py":
45 | module_name = filename[:-3]
46 | try:
47 | module = importlib.import_module(module_name)
48 | for attr in dir(module):
49 | obj = getattr(module, attr)
50 | if isinstance(obj, type) and issubclass(obj, PluginSearchBase) and obj is not PluginSearchBase:
51 | search_plugins.append(obj())
52 | except Exception as e:
53 | print(f"Failed to load plugin {module_name}: {e}")
54 | sys.path.pop(0)
55 | print("Loaded search plugins: " + str(len(search_plugins)))
56 | return search_plugins
57 |
58 | def load_download_plugins(download_plugin_directory):
59 | download_plugins = []
60 | sys.path.insert(0, download_plugin_directory)
61 | print("Loading download plugins from:" + download_plugin_directory)
62 |
63 | for filename in os.listdir(download_plugin_directory):
64 | if filename.endswith(".py") and filename != "__init__.py":
65 | module_name = filename[:-3]
66 | try:
67 | module = importlib.import_module(module_name)
68 | for attr in dir(module):
69 | obj = getattr(module, attr)
70 | if isinstance(obj, type) and issubclass(obj, PluginDownloadBase) and obj is not PluginDownloadBase:
71 | download_plugins.append(obj())
72 | except Exception as e:
73 | print(f"Failed to load plugin {module_name}: {e}")
74 | sys.path.pop(0)
75 | print("Loaded download plugins: " + str(len(download_plugins)))
76 | return download_plugins
77 |
78 | def run_download_queue():
79 | print("Download queue started")
80 | global sabqueue
81 | global CONFIG_DIR
82 | while True:
83 | print("Items in queue: " + str(len(sabqueue)))
84 | for dl in sabqueue:
85 | if dl['status'] == "Queued":
86 | dl["status"] = "Downloading"
87 | sabsavequeue(CONFIG_DIR,sabqueue)
88 | for dlplugin in download_plugins:
89 | if dl["prefix"] in dlplugin.getprefix():
90 | result = dlplugin.download(dl["url"],dl["title"],DOWNLOAD_DIR,dl["cat"])
91 | if result == "404":
92 | dl["status"] = "Failed"
93 | sabsavequeue(CONFIG_DIR,sabqueue)
94 | else:
95 | dl["status"] = "Complete"
96 | dl["storage"] = result
97 | sabsavequeue(CONFIG_DIR,sabqueue)
98 | time.sleep(1)
99 |
100 | def read_config(config_file):
101 | try:
102 | with open(config_file, 'r') as file:
103 | config = json.load(file) # Load the JSON data from the file
104 | return config
105 | except FileNotFoundError:
106 | print(f"Error: The file '{config_file}' was not found.")
107 | return None
108 | except json.JSONDecodeError:
109 | print(f"Error: The file '{config_file}' contains invalid JSON.")
110 | return None
111 |
112 | def start():
113 | config = read_config(os.path.join(CONFIG_DIR, "config.json"))
114 | global DOWNLOAD_DIR
115 | global SAB_API
116 | global SAB_CATEGORIES
117 | if config:
118 | DOWNLOAD_DIR = config.get("download_directory", DOWNLOAD_DIR)
119 | SAB_API = config.get("sab_api", SAB_API)
120 | SAB_CATEGORIES = config.get("sab_categories", SAB_CATEGORIES)
121 | #load search plugins
122 | global search_plugins
123 | global download_plugins
124 | global sabqueue
125 | print("Going to load search plugins")
126 | print("Going to load search plugins")
127 | search_plugins = load_search_plugins(PLUGIN_SEARCH_DIR)
128 | download_plugins = load_download_plugins(PLUGIN_DOWNLOAD_DIR)
129 | sabqueue = sabloadqueue(CONFIG_DIR)
130 | for dl in sabqueue:
131 | if dl["status"] == "Downloading":
132 | dl["status"] = "Queued"
133 | sabsavequeue(CONFIG_DIR,sabqueue)
134 |
135 | # when api with t=caps, collect all supported cats from all search plugins
136 | # and report them correctly
137 | def newznab_caps_response():
138 | root = ET.Element("caps")
139 |
140 | server = ET.SubElement(root, "server")
141 | server.set("version", "0.1")
142 |
143 | for p in search_plugins:
144 | cat=p.getcat()
145 | for c in cat:
146 | categories = ET.SubElement(root, "categories")
147 | category = ET.SubElement(categories, "category")
148 | category.set("id", c)
149 |
150 | return ET.tostring(root, encoding="utf-8", method="xml")
151 |
152 | # flask routes and code
153 |
154 | @app.route("/")
155 | def home():
156 | return """
157 |
158 |
159 | Newznabarr
160 |
161 |
162 | Newznabarr is running!
163 | View SAB Queue
164 | Configure in *arr apps:
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 | """
174 |
175 | @app.route("/queue")
176 | def queue():
177 | queue_html = """
178 |
179 |
180 | SAB Queue
181 |
182 |
183 | SAB Queue
184 |
185 |
186 | Title |
187 | Category |
188 | Plugin |
189 | Status |
190 |
191 | """
192 | # Loop through sabqueue and append rows
193 | for item in sabqueue:
194 | queue_html += f"""
195 |
196 | {item['title']} |
197 | {item['cat']} |
198 | {item['prefix']} |
199 | {item['status']} |
200 |
201 | """
202 | queue_html += """
203 |
204 |
205 |
206 | """
207 | return queue_html
208 |
209 | @app.route("/api", methods=["GET", "POST"])
210 | def api():
211 | global sabqueue
212 | # return all cats supported by all our search plugins
213 | if request.args.get("t") == "caps":
214 | xml_response = newznab_caps_response()
215 | return Response(xml_response, mimetype="application/xml")
216 |
217 | # readarr uses t=book to check if the indexer works
218 | # pretty sure this is for rss, so will have to implement rss later, fine for now
219 | elif request.args.get("t") == "book" :
220 | request_cats=(request.args.get("cat").split(","))
221 | for plugin in search_plugins:
222 | for cat in plugin.getcat():
223 | if cat in request_cats:
224 | query = plugin.gettestquery()
225 | results = plugin.search(query, cat)
226 | xml_response = searchresults_to_response(request.host_url, results)
227 | return Response(xml_response, mimetype="application/xml")
228 | return "No search provider found"
229 |
230 | # lidarr uses t=music
231 | # if there is artist and album provided, it's a search
232 | # else it's probably rss feed
233 | elif request.args.get("t") == "music" :
234 | if "artist" in request.args:
235 | request_cats=(request.args.get("cat").split(","))
236 | for plugin in search_plugins:
237 | for cat in plugin.getcat():
238 | if cat in request_cats:
239 | query = request.args.get("artist") + " - " + request.args.get("album")
240 | results = plugin.search(query, cat)
241 | xml_response = searchresults_to_response(request.host_url, results)
242 | return Response(xml_response, mimetype="application/xml")
243 | return "No search provider found"
244 | else:
245 | request_cats=(request.args.get("cat").split(","))
246 | for plugin in search_plugins:
247 | for cat in plugin.getcat():
248 | if cat in request_cats:
249 | query = plugin.gettestquery()
250 | results = plugin.search(query, cat)
251 | xml_response = searchresults_to_response(request.host_url, results)
252 | return Response(xml_response, mimetype="application/xml")
253 | return "No search provider found"
254 |
255 | # t=search is the normal search function
256 | elif request.args.get("t") == "search" :
257 | request_cats=(request.args.get("cat").split(","))
258 | for plugin in search_plugins:
259 | for cat in plugin.getcat():
260 | if cat in request_cats:
261 | query = request.args.get("q")
262 | results = plugin.search(query, cat)
263 | xml_response = searchresults_to_response(request.host_url, results)
264 | return Response(xml_response, mimetype="application/xml")
265 | return "No search provider found"
266 |
267 |
268 | # starr app downloads the nzb
269 | elif request.args.get("download") == "nzb":
270 |
271 |
272 | # Create the root element with the required namespace
273 | nzb = ET.Element('nzb', xmlns="http://www.newzbin.com/DTD/2003/nzb")
274 |
275 | # Add a section to store the hidden URL in plain text
276 | meta = ET.SubElement(nzb, 'meta')
277 | url_element = ET.SubElement(meta, 'prefix')
278 | url_element.text = request.args.get("prefix")
279 | url_element = ET.SubElement(meta, 'url')
280 | url_element.text = request.args.get("url")
281 | url_element = ET.SubElement(meta, 'size')
282 | url_element.text = request.args.get("size")
283 | url_element = ET.SubElement(meta, 'title')
284 | url_element.text = request.args.get("title")
285 |
286 | # Create a element with required attributes
287 | file_elem = ET.SubElement(nzb, 'file', poster="none", subject="none")
288 |
289 | # Add a section within the element
290 | groups = ET.SubElement(file_elem, 'groups')
291 | group = ET.SubElement(groups, 'group')
292 |
293 | # Add a section with provided segments information
294 | segments = ET.SubElement(file_elem, 'segments')
295 | segment = ET.SubElement(segments, 'segment', bytes=request.args.get("size"), number="1")
296 |
297 | # Convert the XML tree to a string
298 | return ET.tostring(nzb, encoding='utf-8', xml_declaration=True).decode()
299 |
300 | # sabnzbd functions
301 | elif request.args.get("mode") == "version":
302 | return sabversion()
303 |
304 | elif request.args.get("mode") == 'get_config':
305 | if SAB_API == request.args.get("apikey"):
306 | sabconfig = sabgetconfig(SAB_CATEGORIES)
307 | sabconfig["config"]["misc"]["complete_dir"] = DOWNLOAD_DIR
308 | sabconfig["config"]["misc"]["api_key"] = SAB_API
309 | return sabconfig
310 | return jsonify({"error": "Access Denied"}), 403
311 |
312 | elif request.args.get("mode") == "addfile":
313 | if SAB_API == request.args.get("apikey"):
314 | uploaded_file=request.files["name"]
315 | file_text = uploaded_file.read()
316 | root = ET.fromstring(file_text)
317 | namespace = {'nzb': 'http://www.newzbin.com/DTD/2003/nzb'}
318 | url_element = root.find('.//nzb:meta/nzb:url', namespace)
319 | url = url_element.text if url_element is not None else None
320 | prefix_element = root.find('.//nzb:meta/nzb:prefix', namespace)
321 | prefix = prefix_element.text if prefix_element is not None else None
322 | size_element = root.find('.//nzb:meta/nzb:size', namespace)
323 | size = size_element.text if size_element is not None else None
324 | title_element = root.find('.//nzb:meta/nzb:title', namespace)
325 | title = title_element.text if title_element is not None else None
326 |
327 | nzo=hashlib.md5(url.encode()).hexdigest()
328 | print(nzo)
329 | sabqueue.append({
330 | "prefix": prefix,
331 | "size": size,
332 | "url": url,
333 | "nzo": nzo,
334 | "title": title,
335 | "status": "Queued",
336 | "cat": request.args.get("cat")
337 | })
338 | result=json.loads("""{"status":true,"nzo_ids":["SABnzbd_nzo_cqz8nwn8"]}""")
339 | result["nzo_ids"]=[f"SABnzbd_nzo_{nzo}"]
340 | sabsavequeue(CONFIG_DIR,sabqueue)
341 | print(sabqueue)
342 | return(result), 200
343 | return jsonify({"error": "Access Denied"}), 403
344 |
345 | elif request.args.get("mode") == 'queue':
346 | if SAB_API == request.args.get("apikey"):
347 | if "name" in request.args:
348 | if request.args.get("name") == "delete":
349 | sabqueue = sabdeletefromqueue(CONFIG_DIR,sabqueue,request.args.get("value"))
350 | return "ok"
351 | else:
352 | return sabgetqueue(sabqueue)
353 | return jsonify({"error": "Access Denied"}), 403
354 |
355 | elif request.args.get("mode") == 'history':
356 | if SAB_API == request.args.get("apikey"):
357 | if "name" in request.args:
358 | if request.args.get("name") == "delete":
359 | sabqueue = sabdeletefromqueue(CONFIG_DIR,sabqueue,request.args.get("value"))
360 | return "ok"
361 | else:
362 | return sabgethistory(sabqueue)
363 | return jsonify({"error": "Access Denied"}), 403
364 |
365 | download_thread = threading.Thread(target=run_download_queue)
366 | download_thread.daemon = True # Ensures the thread stops when the program ends
367 | download_thread.start()
368 |
369 | if __name__ == "__main__":
370 | # load configs and plugins
371 | start()
372 | # start flask
373 | app.run(host=FLASK_HOST, port=FLASK_PORT)
--------------------------------------------------------------------------------
/config/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "download_directory": "/data/downloads/downloadarr",
3 | "sab_api": "abcde",
4 | "sab_categories": ["readarr", "lidarr", "sonarr", "lidarr_audiobook"]
5 | }
--------------------------------------------------------------------------------
/config/plugins/download/__pycache__/libgendl.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/config/plugins/download/__pycache__/libgendl.cpython-310.pyc
--------------------------------------------------------------------------------
/config/plugins/download/__pycache__/ytmusicdl.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/config/plugins/download/__pycache__/ytmusicdl.cpython-310.pyc
--------------------------------------------------------------------------------
/config/plugins/download/libgendl.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from bs4 import BeautifulSoup
3 | import os
4 | import re
5 |
6 | from plugin_download_interface import PluginDownloadBase
7 |
8 | class LibGenDownload(PluginDownloadBase):
9 | def getprefix(self):
10 | return ["libgen"]
11 |
12 | def download(self, url, title, download_dir, cat):
13 | full_download_dir = os.path.join(download_dir, cat, title)
14 | os.makedirs(full_download_dir, exist_ok=True)
15 | print(full_download_dir)
16 | full_filename = os.path.join(full_download_dir, title)
17 |
18 | response = requests.get(url, timeout=120)
19 |
20 | if response.status_code == 200:
21 | soup = BeautifulSoup(response.text, "html.parser")
22 | download_div = soup.find("div", id="download")
23 |
24 | if download_div:
25 | download_link = download_div.find("a")
26 | if download_link:
27 | link_url = download_link.get("href")
28 | else:
29 | return "404"
30 | else:
31 | elements_with_get = soup.find_all(string=lambda text: "GET" in text)
32 |
33 | for element_text in elements_with_get:
34 | parent_element = element_text.parent
35 | download_link = parent_element.find("a") if parent_element else None
36 | if download_link:
37 | link_url = download_link.get("href")
38 | break
39 | else:
40 | return "404"
41 |
42 | dl_resp = requests.get(link_url, stream=True)
43 |
44 | if dl_resp.status_code == 200:
45 | file_type = os.path.splitext(link_url)[1]
46 | valid_book_extensions = [".pdf", ".epub", ".mobi", ".azw", ".djvu", ".azw3"]
47 | if file_type not in valid_book_extensions:
48 | return "404"
49 |
50 | # Download file
51 | full_filename += file_type
52 | with open(full_filename, "wb") as f:
53 | for chunk in dl_resp.iter_content(chunk_size=1024):
54 | f.write(chunk)
55 | return full_filename
56 | else:
57 | return "404"
58 | else:
59 | return "404"
--------------------------------------------------------------------------------
/config/plugins/download/ytmusicdl.py:
--------------------------------------------------------------------------------
1 | from ytmusicapi import YTMusic
2 | import os
3 | import yt_dlp
4 | import eyed3
5 |
6 | from plugin_download_interface import PluginDownloadBase
7 |
8 | def get_youtube_track_links(browse_id):
9 | ytmusic = YTMusic()
10 | trackcounter=0
11 |
12 | # Get the album tracks
13 | album_info = ytmusic.get_album(browse_id)
14 |
15 | if album_info and 'tracks' in album_info:
16 | tracks = album_info['tracks']
17 | youtube_links = []
18 | for track in tracks:
19 | trackcounter = trackcounter + 1
20 | title = track['title']
21 | video_id = track['videoId']
22 | youtube_link = f"https://www.youtube.com/watch?v={video_id}"
23 | youtube_links.append({'title': title, 'youtube_link': youtube_link, 'track': trackcounter})
24 |
25 | return youtube_links
26 | else:
27 | print("Album tracks not found.")
28 | return None
29 |
30 |
31 | class YTMusicDownload(PluginDownloadBase):
32 | def getprefix(self):
33 | return ["ytmusic"]
34 |
35 | def download(self, url, title, download_dir, cat):
36 | full_download_dir = os.path.join(download_dir, cat, title)
37 | os.makedirs(full_download_dir, exist_ok=True)
38 | full_filename = full_download_dir
39 |
40 | ytmusic = YTMusic()
41 | trackcounter=0
42 | album_info = ytmusic.get_album(url)
43 | if album_info and 'tracks' in album_info:
44 | tracks = album_info['tracks']
45 | youtube_links = []
46 | for track in tracks:
47 | trackcounter = track['trackNumber']
48 | title = track['title']
49 | artist = track['artists'][0]["name"]
50 | album = track["album"]
51 | video_id = track['videoId']
52 | youtube_link = f"https://www.youtube.com/watch?v={video_id}"
53 | youtube_links.append({'title': title, 'youtube_link': youtube_link, 'track': trackcounter, "artist": artist, "album":album})
54 | for item in youtube_links:
55 | full_filename = os.path.join(full_download_dir , str(item["track"]) + " - " + item["title"])
56 | try:
57 | ydl_opts = {
58 | 'quiet': True,
59 | 'noprogress': True,
60 | 'format': 'bestaudio/best',
61 | 'outtmpl': full_filename,
62 | 'postprocessors': [{
63 | 'key': 'FFmpegExtractAudio', # Extract audio using FFmpeg
64 | 'preferredcodec': 'mp3', # Save as MP3
65 | 'preferredquality': '128', # Set quality
66 | }],
67 | }
68 |
69 | with yt_dlp.YoutubeDL(ydl_opts) as ydl:
70 | ydl.download(item["youtube_link"])
71 | except Exception as e:
72 | print(f"Error: {str(e)}")
73 | return 404
74 | audio = eyed3.load(full_filename + ".mp3")
75 | audio.tag.artist=item["artist"]
76 | audio.tag.title=item["title"]
77 | audio.tag.album=item["album"]
78 | audio.tag.track_num=item["track"]
79 |
80 | audio.tag.save()
81 | return full_download_dir
82 |
--------------------------------------------------------------------------------
/config/plugins/search/__pycache__/libgen.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/config/plugins/search/__pycache__/libgen.cpython-310.pyc
--------------------------------------------------------------------------------
/config/plugins/search/__pycache__/ytmusic.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/config/plugins/search/__pycache__/ytmusic.cpython-310.pyc
--------------------------------------------------------------------------------
/config/plugins/search/libgen.py:
--------------------------------------------------------------------------------
1 | import re
2 | from bs4 import BeautifulSoup
3 | import requests
4 |
5 | from plugin_search_interface import PluginSearchBase
6 |
7 | def getmyprefix():
8 | return "libgen"
9 |
10 | def convert_size_to_bytes(size_str):
11 | size_str = size_str.lower()
12 | if 'kb' in size_str:
13 | size_in_bytes = float(size_str.replace('kb', '').strip()) * 1024
14 | elif 'mb' in size_str:
15 | size_in_bytes = float(size_str.replace('mb', '').strip()) * 1024 * 1024
16 | elif 'gb' in size_str:
17 | size_in_bytes = float(size_str.replace('gb', '').strip()) * 1024 * 1024 * 1024
18 | else:
19 | size_in_bytes = float(size_str) # Assume bytes if no unit is specified
20 | return str(int(size_in_bytes))
21 |
22 | def convert_results(books, cat):
23 | results = []
24 | for book in books:
25 | for link in book["links"]:
26 | results.append({
27 | "link": link,
28 | "title": book["author"] + " - " + book["title"] + " (retail) (" + book["format_type"].lower() + ")",
29 | "description": f"{book['author']} - {book['title']} (retail) ({book['format_type'].lower()})",
30 | "guid": link,
31 | "comments": link,
32 | "files": "1",
33 | "size": book["size"],
34 | "category": cat,
35 | "grabs": "100",
36 | "prefix": getmyprefix()
37 | })
38 | return results
39 |
40 | def search_libgen(book):
41 | results = []
42 | try:
43 | print("Searching for " + book)
44 | item = book
45 | found_links = []
46 | non_standard_chars_pattern = r"[^a-zA-Z0-9\s.]"
47 | item = item.replace("ø", "o")
48 | cleaned_string = re.sub(non_standard_chars_pattern, "", item)
49 | search_item = cleaned_string.replace(" ", "+")
50 | url = "http://libgen.is/fiction/?q=" + search_item
51 | response = requests.get(url, timeout=120)
52 | if response.status_code == 200:
53 | soup = BeautifulSoup(response.text, "html.parser")
54 | rows = soup.find_all('tr')
55 | for row in rows:
56 | try:
57 | # Extract author
58 | author = row.select_one('ul.catalog_authors li a').text.strip()
59 |
60 | # Extract title and link
61 | title_tag = row.select_one('td p a')
62 | title = title_tag.text.strip()
63 | title_link = title_tag['href']
64 |
65 | # Extract ASIN
66 | asin = row.select_one('p.catalog_identifier').text.replace("ASIN: ", "").strip()
67 |
68 | # Extract language
69 | language = row.find_all('td')[3].text.strip()
70 |
71 | format_size = row.find_all('td')[4].text.strip()
72 | format_type = format_size.split(" / ")[0].strip() # e.g., "EPUB"
73 | size = convert_size_to_bytes(format_size.split(" / ")[1].strip()) # e.g., "374 Kb"
74 |
75 | # Extract mirror links
76 | links = [a['href'] for a in row.select('ul.record_mirrors_compact li a')]
77 | links.append("https://ligben.is" + title_link)
78 |
79 | # Store result in a dictionary
80 | results.append({
81 | 'author': reverse_author_name(author),
82 | 'title': title,
83 | 'asin': asin,
84 | 'language': language,
85 | 'links': links,
86 | 'format_type': format_type,
87 | 'size': size
88 | })
89 |
90 | except AttributeError:
91 | # Skip rows that don't match the expected structure
92 | continue
93 |
94 | else:
95 | ret = {"Status": "Error", "Code": "Libgen Connection Error"}
96 | print("Libgen Connection Error: " + str(response.status_code) + "Data: " + response.text)
97 |
98 | except Exception as e:
99 | print(str(e))
100 | raise Exception("Error Searching libgen: " + str(e))
101 |
102 | finally:
103 | return results
104 |
105 |
106 | def reverse_author_name(name):
107 | # Split the name by the comma (if it's in the "Last, First" format)
108 | if ',' in name:
109 | last_name, first_names = name.split(',', 1)
110 | last_name = last_name.strip().capitalize() # Capitalize the last name
111 | first_names = first_names.strip()
112 | formatted_first_names = " ".join([first.capitalize() for first in first_names.split()])
113 | return f"{formatted_first_names} {last_name}"
114 | else:
115 | # If no comma, it's just a single word name (e.g., "King")
116 | return name.capitalize()
117 |
118 | class LibGenSearch(PluginSearchBase):
119 | def getcat(self):
120 | return ["7020"]
121 |
122 | def gettestquery(self):
123 | return "sample"
124 |
125 | def getprefix(self):
126 | return getmyprefix()
127 |
128 | def search(self, query, cat):
129 | books = search_libgen(query)
130 | results = convert_results(books, cat)
131 | return results
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/config/plugins/search/ytmusic.py:
--------------------------------------------------------------------------------
1 | from ytmusicapi import YTMusic
2 |
3 | from plugin_search_interface import PluginSearchBase
4 |
5 | class YTMusicSearch(PluginSearchBase):
6 | def getcat(self):
7 | return ["3010"]
8 |
9 | def gettestquery(self):
10 | return "joe sample - sample this"
11 |
12 | def getprefix(self):
13 | return "ytmusic"
14 |
15 | def search(self, query, cat):
16 | result = []
17 | ytmusic = YTMusic()
18 | try:
19 | # Search for the album
20 | search_results = ytmusic.search(query, filter='albums')
21 | for item in search_results:
22 | artists = ""
23 | for artist in item['artists']:
24 | artists = artists + artist['name'] + " "
25 | albumtitle = item['title']
26 | year = item['year']
27 | link = item['browseId']
28 | title = artists + "- " + albumtitle + " (" + year +") (mp3) (128kbps)"
29 | GoodResult = True
30 | for word in query.split(" "):
31 | if not (word.lower() in title.lower()):
32 | GoodResult = False
33 | if GoodResult:
34 | result.append({
35 | "link": link,
36 | "title": title,
37 | "description": title,
38 | "guid": link,
39 | "comments": link,
40 | "files": "1",
41 | "size": "10000",
42 | "category": cat,
43 | "grabs": "100",
44 | "prefix": self.getprefix()
45 | })
46 |
47 | except Exception as e:
48 | print(f"An error occurred: {e}")
49 | result = []
50 | if len(result) == 0:
51 | result.append({
52 | "link": "",
53 | "title": "",
54 | "description": "",
55 | "guid": "",
56 | "comments": "",
57 | "files": "0",
58 | "size": "0",
59 | "category": cat,
60 | "grabs": "100"
61 | })
62 | return result
--------------------------------------------------------------------------------
/config/sabqueue.conf:
--------------------------------------------------------------------------------
1 | []
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | newznabarr:
5 | image: riffsphereha/newznabarr:latest
6 | container_name: newznabarr
7 | environment:
8 | - FLASK_RUN_PORT=10000
9 | - PUID=1000
10 | - PGID=1000
11 | - CONFIG=/config
12 | ports:
13 | - "10000:10000"
14 | volumes:
15 | - /path/to/config:/config
16 | - /path/to/download:/data/downloads/downloadarr
17 | restart: unless-stopped
--------------------------------------------------------------------------------
/dockerfile:
--------------------------------------------------------------------------------
1 | # Use an official Python runtime as a parent image
2 | FROM python:3.9-slim
3 |
4 | # Set the working directory in the container
5 | WORKDIR /app
6 |
7 | # Set environment variables
8 | ENV CONFIG=/config
9 | ENV FLASK_RUN_PORT=10000
10 | ENV FLASK_RUN_HOST=0.0.0.0
11 | ENV FLASK_APP=app.py
12 | ENV PUID=1000
13 | ENV PGID=1000
14 |
15 | # Install gosu to run commands as the specified user
16 | RUN apt-get update && \
17 | apt-get install -y gosu ffmpeg && \
18 | rm -rf /var/lib/apt/lists/*
19 |
20 | # Copy the entire app directory into the container
21 | COPY . /app
22 |
23 | # Copy the default configuration files to a temporary location
24 | COPY config/ /default_config/
25 |
26 | # Install dependencies from requirements.txt
27 | RUN pip install --no-cache-dir -r requirements.txt
28 |
29 | # Expose the Flask app's port
30 | EXPOSE 10000
31 |
32 | # Add the entrypoint script to check and copy config files
33 | COPY entrypoint.sh /entrypoint.sh
34 | RUN chmod +x /entrypoint.sh
35 |
36 | # Set the entrypoint to run the startup script
37 | ENTRYPOINT ["/entrypoint.sh"]
38 |
39 | # The command to run the Flask app when the container starts
40 | CMD ["python", "app.py"]
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Ensure the $CONFIG directory exists
4 | mkdir -p "$CONFIG"
5 |
6 | # Set file ownership using PUID and PGID environment variables
7 | echo "Setting file ownership to PUID=${PUID} and PGID=${PGID}"
8 |
9 | # Set the ownership of the directories and files to the specified PUID and PGID
10 | chown -R ${PUID}:${PGID} /app /default_config "$CONFIG"
11 |
12 | # If the $CONFIG directory is empty, copy the default configuration files
13 | if [ -z "$(ls -A "$CONFIG")" ]; then
14 | echo "Copying default configuration files to $CONFIG..."
15 | cp -r /default_config/* "$CONFIG"
16 | chown -R ${PUID}:${PGID} "$CONFIG"
17 | fi
18 |
19 | # Copy default plugins to the config directory, overwriting existing plugins
20 | cp -r /default_config/plugins/* "$CONFIG/plugins/"
21 |
22 | # Execute the command passed to the container as a non-root user (no user creation, just chown)
23 | exec gosu ${PUID}:${PGID} "$@"
--------------------------------------------------------------------------------
/images/newznab1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/images/newznab1.png
--------------------------------------------------------------------------------
/images/newznab2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/images/newznab2.png
--------------------------------------------------------------------------------
/images/sabnzbd1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/images/sabnzbd1.png
--------------------------------------------------------------------------------
/images/sabnzbd2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riffsphereha/newznabarr/6701b9ca82e1fb8b769550c8c59b3b4cdb2fb8de/images/sabnzbd2.png
--------------------------------------------------------------------------------
/newznab.py:
--------------------------------------------------------------------------------
1 | import email.utils
2 | import time
3 | import xml.etree.ElementTree as ET
4 |
5 | def searchresults_to_response(server, results):
6 | root = ET.Element("rss", version="2.0", attrib={
7 | "xmlns:atom": "http://www.w3.org/2005/Atom",
8 | "xmlns:newznab": "http://www.newznab.com/DTD/2010/feeds/attributes/"
9 | })
10 | channel = ET.SubElement(root, "channel")
11 |
12 | ET.SubElement(channel, "title").text = "NewzNabArr"
13 | ET.SubElement(channel, "description").text = "Multiple newznab proxies for starr apps"
14 | ET.SubElement(channel, "link").text = server
15 |
16 | pub_date = email.utils.formatdate(time.time())
17 | ET.SubElement(channel, "pubDate").text = pub_date
18 |
19 | for result in results:
20 | prefix = result["prefix"]
21 | link2 = f"{server}api?download=nzb&prefix={prefix}&url={result['link']}&size={result['size']}&title={result['title']}"
22 | item = ET.SubElement(channel, "item")
23 | ET.SubElement(item, "title").text = result["title"]
24 | ET.SubElement(item, "description").text = result["description"]
25 | ET.SubElement(item, "guid").text = result["guid"]
26 | ET.SubElement(item, "comments").text = result["comments"]
27 | item_pub_date = email.utils.formatdate(time.time())
28 | ET.SubElement(item, "pubDate").text = item_pub_date
29 | ET.SubElement(item, "size").text = result["size"]
30 | ET.SubElement(item, "link").text = link2
31 | ET.SubElement(item, "category").text = result["category"]
32 | enclosure = ET.SubElement(item, "enclosure")
33 | enclosure.set("url", link2)
34 | enclosure.set("length", result["size"]) # Replace with actual file size if available
35 | enclosure.set("type", "application/x-nzb") # or "application/epub" based on book format
36 |
37 | newznab_attrs = ET.SubElement(item, "newznab:attr")
38 | newznab_attrs.set("name", "category")
39 | newznab_attrs.set("value", result["category"])
40 | newznab_attrs = ET.SubElement(item, "newznab:attr")
41 | newznab_attrs.set("name", "files")
42 | newznab_attrs.set("value", "1")
43 | newznab_attrs = ET.SubElement(item, "newznab:attr")
44 | newznab_attrs.set("name", "grabs")
45 | newznab_attrs.set("value", "100")
46 | xml_str = ET.tostring(root, encoding="utf-8", method="xml")
47 | return xml_str
48 |
--------------------------------------------------------------------------------
/plugin_download_interface.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | class PluginDownloadBase(ABC):
4 |
5 | @abstractmethod
6 | def getprefix(self):
7 | """The prefix for the the downloader supports"""
8 | pass
9 |
10 | @abstractmethod
11 | def download(self, url, title, download_dir, cat):
12 | """The download function"""
13 | pass
--------------------------------------------------------------------------------
/plugin_search_interface.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | class PluginSearchBase(ABC):
4 | @abstractmethod
5 | def getcat(self):
6 | """Return all cats supported by plugin."""
7 | pass
8 |
9 | @abstractmethod
10 | def gettestquery(self):
11 | """Return a test query supported by the plugin to do a test search"""
12 | pass
13 |
14 | @abstractmethod
15 | def getprefix(self):
16 | """The prefix for the search link, to identify a compatible downloader"""
17 | pass
18 |
19 | @abstractmethod
20 | def search(self, query, cat):
21 | """Perform a search"""
22 | # Must include:
23 | # link: some info on where to download from
24 | # title: used by starr apps to compare the result
25 | # description: can be anything
26 | # guid: unique identifier, not really used but there in case
27 | # comments: anything
28 | # size: file size
29 | # files: number of files
30 | # category: the exact category
31 | # grabs: how many downloads there are
32 | pass
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Newznabarr - Usenet Plugin Framework for *Arr Apps
2 |
3 | After using the *arr apps for years, I realized that while Usenet and torrents are great, they aren't always the best or only sources for content. Unfortunately, the *arr apps currently only support these two options. That’s why I created **Newznabarr** — a Usenet plugin framework for the *arr ecosystem designed to fill that gap.
4 |
5 | ## What is Newznabarr?
6 |
7 | **Newznabarr** presents itself as a Newznab indexer and SABnzbd client, making it compatible with the *arr apps you’re already using. However, the magic lies under the hood: all searches are handled by plugins, allowing for maximum flexibility and expandability. This means you can use Newznabarr to tap into other content sources beyond traditional Usenet and torrents.
8 |
9 | ### Current Features:
10 | - Plugin-based search and download functionality for easy expandability.
11 | - A plugin to integrate **Readarr** with a popular book site.
12 | - A plugin to integrate **Lidarr** with **YouTube Music** (128kbps mp3) 🎶
13 | - Designed to fit seamlessly into your existing *arr workflow.
14 |
15 | ### Roadmap:
16 | - RSS feed integration for the book site in **Readarr**
17 | - **Music Streaming Sites Integration** 🎧
18 | - **Video Streaming Sites Integration** 📺
19 |
20 | ### Contribute and Extend:
21 | - **Make Your Own Plugins**: One of the core ideas behind Newznabarr is expandability. You can create and add your own plugins to enhance functionality or integrate with other content sources. If you have an idea for a plugin, feel free to fork the repo and start building!
22 | - **Name Suggestion**: If you think there’s a better name for this project, feel free to suggest one! We’re open to ideas. **Expandarr** is a good option for now! (thanks u/waterloonies)
23 | - **Icon Design**: If you're a designer or just have a creative idea, help us out with a unique icon for Newznabarr!
24 | - I'm a bad programmer. I got this working, but if you think you can make it better in any way, please do!
25 |
26 | ### How to Get Started:
27 | - **Docker Hub**: `riffsphereha/newznabarr`
28 | - **GitHub**: `riffsphereha/newznabarr`
29 |
30 | ⚠️ **Note**: Newznabarr is in a very early alpha stage, so expect some bugs and rough edges. Feedback, suggestions, and contributions are welcome!
31 |
32 | Let me know what you think, and if you have any ideas for additional plugins, a new name, or an icon, I’d love to hear them! 🌟
33 |
34 | ---
35 |
36 | ## Installation
37 |
38 | ### Install using Docker
39 |
40 | - **Default Port**: `10000`. You can change this using the `FLASK_RUN_PORT` environment variable.
41 | - **Configuration Directory**: Mount `/config` to your appdata folder. This will contain:
42 | - `config.json` files
43 | - SAB queue
44 | - Plugins
45 | - **User and Group IDs**:
46 | - Use `PUID` and `PGID` to set the user and group IDs for permissions.
47 | - For Unraid, set `PUID` to `99` and `PGID` to `100`.
48 | - **Download Folder**:
49 | - Add a download folder that matches your setup and update the `config.json` file to reflect this folder.
50 | - **Note**: The default path is still using the old name `/data/downloads/downloadarr` (this will be updated in future versions).
51 |
52 | ---
53 |
54 | ## How to Use
55 |
56 | ### 1. Add a SABnzbd Client to Your *Arr App
57 |
58 | - Configure as you normally would with the following settings:
59 | - **Name**
60 | - **Enable**
61 | - **Host**
62 | - **Port**
63 | - **API Key**
64 |
65 | - **Notes**:
66 | - Currently, plugins are available only for **Readarr** and **Lidarr**, so other *Arr apps are untested.
67 | - The API key can be set in the `config.json` file.
68 | - Enable advanced settings and set the **Client Priority** to `50`. Since this doesn't support real NZBs, it should only be used for supported plugins.
69 | - **Important**: Lower the SABnzbd client priority (set to a lower value than other clients) to ensure it doesn't interfere with real NZB downloads.
70 |
71 | ### 2. Add a Newznab Indexer
72 |
73 | - Configure as you normally would with the following settings:
74 | - **Name**
75 | - **Enable Search** (RSS support is not yet available)
76 | - **URL** (use the `http://:` format).
77 |
78 | - **Notes**:
79 | - No API key is required.
80 | - Enable advanced settings and set the **Download Client** to the SABnzbd client you just added.
81 |
82 | ---
83 |
84 | ## Additional Information
85 |
86 | - For more advanced configurations, or if you need to modify the default behavior, check the `config.json` file.
87 | - Stay tuned for upcoming features, including more plugin support and enhancements!
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | beautifulsoup4==4.12.3
2 | eyed3==0.9.7
3 | Flask==3.1.0
4 | Requests==2.32.3
5 | yt_dlp==2024.11.18
6 | ytmusicapi==1.8.2
7 |
--------------------------------------------------------------------------------
/sabnzbd.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | def sabversion():
5 | return json.loads("""{"version":"4.3.1"}""")
6 |
7 | def sabgetconfig(sab_categories):
8 | config= """
9 | {
10 | "config": {
11 | "misc": {
12 | "helpful_warnings": 1,
13 | "queue_complete": "",
14 | "queue_complete_pers": 0,
15 | "bandwidth_perc": 100,
16 | "refresh_rate": 1,
17 | "interface_settings": "",
18 | "queue_limit": 20,
19 | "config_lock": 0,
20 | "notified_new_skin": 0,
21 | "check_new_rel": 1,
22 | "auto_browser": 1,
23 | "language": "en",
24 | "enable_https_verification": 1,
25 | "host": "::",
26 | "port": 10000,
27 | "https_port": "",
28 | "username": "",
29 | "password": "",
30 | "bandwidth_max": "",
31 | "cache_limit": "1G",
32 | "web_dir": "Glitter",
33 | "web_color": "Auto",
34 | "https_cert": "server.cert",
35 | "https_key": "server.key",
36 | "https_chain": "",
37 | "enable_https": 0,
38 | "inet_exposure": 0,
39 | "api_key": "(removed)",
40 | "nzb_key": "(removed)",
41 | "socks5_proxy_url": "",
42 | "permissions": "",
43 | "download_dir": "Downloads/incomplete",
44 | "download_free": "",
45 | "complete_dir": "Downloads/complete",
46 | "complete_free": "",
47 | "fulldisk_autoresume": 0,
48 | "script_dir": "",
49 | "nzb_backup_dir": "",
50 | "admin_dir": "admin",
51 | "backup_dir": "",
52 | "dirscan_dir": "",
53 | "dirscan_speed": 5,
54 | "password_file": "",
55 | "log_dir": "logs",
56 | "max_art_tries": 3,
57 | "top_only": 0,
58 | "sfv_check": 1,
59 | "script_can_fail": 0,
60 | "enable_recursive": 1,
61 | "flat_unpack": 0,
62 | "par_option": "",
63 | "pre_check": 0,
64 | "nice": "",
65 | "win_process_prio": 3,
66 | "ionice": "",
67 | "fail_hopeless_jobs": 1,
68 | "fast_fail": 1,
69 | "auto_disconnect": 1,
70 | "pre_script": "None",
71 | "end_queue_script": "None",
72 | "no_dupes": 0,
73 | "no_series_dupes": 0,
74 | "no_smart_dupes": 0,
75 | "dupes_propercheck": 1,
76 | "pause_on_pwrar": 1,
77 | "ignore_samples": 0,
78 | "deobfuscate_final_filenames": 1,
79 | "auto_sort": "",
80 | "direct_unpack": 0,
81 | "propagation_delay": 0,
82 | "folder_rename": 1,
83 | "replace_spaces": 0,
84 | "replace_underscores": 0,
85 | "replace_dots": 0,
86 | "safe_postproc": 1,
87 | "pause_on_post_processing": 0,
88 | "enable_all_par": 0,
89 | "sanitize_safe": 0,
90 | "cleanup_list": [],
91 | "unwanted_extensions": [],
92 | "action_on_unwanted_extensions": 0,
93 | "unwanted_extensions_mode": 0,
94 | "new_nzb_on_failure": 0,
95 | "history_retention": "",
96 | "history_retention_option": "all",
97 | "history_retention_number": 0,
98 | "quota_size": "",
99 | "quota_day": "",
100 | "quota_resume": 0,
101 | "quota_period": "m",
102 | "schedlines": [],
103 | "rss_rate": 60,
104 | "ampm": 0,
105 | "start_paused": 0,
106 | "preserve_paused_state": 0,
107 | "enable_par_cleanup": 1,
108 | "process_unpacked_par2": 1,
109 | "enable_multipar": 1,
110 | "enable_unrar": 1,
111 | "enable_7zip": 1,
112 | "enable_filejoin": 1,
113 | "enable_tsjoin": 1,
114 | "overwrite_files": 0,
115 | "ignore_unrar_dates": 0,
116 | "backup_for_duplicates": 0,
117 | "empty_postproc": 0,
118 | "wait_for_dfolder": 0,
119 | "rss_filenames": 0,
120 | "api_logging": 1,
121 | "html_login": 1,
122 | "warn_dupl_jobs": 0,
123 | "keep_awake": 1,
124 | "tray_icon": 1,
125 | "allow_incomplete_nzb": 0,
126 | "enable_broadcast": 1,
127 | "ipv6_hosting": 0,
128 | "ipv6_staging": 0,
129 | "api_warnings": 1,
130 | "no_penalties": 0,
131 | "x_frame_options": 1,
132 | "allow_old_ssl_tls": 0,
133 | "enable_season_sorting": 1,
134 | "verify_xff_header": 0,
135 | "rss_odd_titles": ["nzbindex.nl/", "nzbindex.com/", "nzbclub.com/"],
136 | "quick_check_ext_ignore": ["nfo", "sfv", "srr"],
137 | "req_completion_rate": 100.2,
138 | "selftest_host": "self-test.sabnzbd.org",
139 | "movie_rename_limit": "100M",
140 | "episode_rename_limit": "20M",
141 | "size_limit": 0,
142 | "direct_unpack_threads": 3,
143 | "history_limit": 10,
144 | "wait_ext_drive": 5,
145 | "max_foldername_length": 246,
146 | "nomedia_marker": "",
147 | "ipv6_servers": 1,
148 | "url_base": "/sabnzbd",
149 | "host_whitelist": ["fab0220616e4"],
150 | "local_ranges": [],
151 | "max_url_retries": 10,
152 | "downloader_sleep_time": 10,
153 | "receive_threads": 2,
154 | "switchinterval": 0.005,
155 | "ssdp_broadcast_interval": 15,
156 | "ext_rename_ignore": [],
157 | "email_server": "",
158 | "email_to": [],
159 | "email_from": "",
160 | "email_account": "",
161 | "email_pwd": "",
162 | "email_endjob": 0,
163 | "email_full": 0,
164 | "email_dir": "",
165 | "email_rss": 0,
166 | "email_cats": ["*"]
167 | },
168 | "logging": {
169 | "log_level": 1,
170 | "max_log_size": 5242880,
171 | "log_backups": 5
172 | },
173 | "categories": [
174 | {"name": "readarr", "order": 1, "pp": "", "script": "Default", "dir": "", "newzbin": "", "priority": -100}
175 | ]
176 | }
177 | }"""
178 | result = json.loads(config)
179 | order=0
180 | for category in sab_categories:
181 | order = order + 1
182 | result["config"]["categories"].append({
183 | "name": category,
184 | "order": order,
185 | "pp": "",
186 | "script": "Default",
187 | "dir": "",
188 | "newzbin": "",
189 | "priority": -100
190 | })
191 | return result
192 |
193 | def sabgetqueue(downloads):
194 | slots = []
195 | index=0
196 | for download in downloads:
197 | if download["status"] == "Queued":
198 | slots.append({
199 | "index": index,
200 | "nzo_id": f"SABnzbd_nzo_{download['nzo']}",
201 | "status": "Queued",
202 | "filename": download['title'],
203 | "cat": download['cat']
204 | })
205 | index = index + 1
206 | if download["status"] == "Downloading":
207 | slots.append({
208 | "index": index,
209 | "nzo_id": f"SABnzbd_nzo_{download['nzo']}",
210 | "status": "Downloading",
211 | "filename": download['title'],
212 | "cat": download['cat']
213 | })
214 | index = index + 1
215 | if download["status"] == "Failed":
216 | slots.append({
217 | "index": index,
218 | "nzo_id": f"SABnzbd_nzo_{download['nzo']}",
219 | "status": "Failed",
220 | "filename": download['title'],
221 | "cat": download['cat']
222 | })
223 | index = index + 1
224 | queue="""{"queue":{"version":"4.3.1","paused":false,"pause_int":"0","paused_all":false,"slots":"""
225 | queue=queue+str(slots)+"}}"
226 | return queue
227 |
228 | def sabgethistory(downloads):
229 | slots = []
230 | index=0
231 | for download in downloads:
232 | if download["status"] == "Complete":
233 | slots.append({
234 | "completed": download['size'],
235 | "name": download["title"],
236 | "category": download["cat"],
237 | "status": "Completed",
238 | "nzo_id": f"SABnzbd_nzo_{download['nzo']}",
239 | "storage": download["storage"],
240 | "path": download["storage"]
241 | })
242 | index = index + 1
243 |
244 | history="""{"history":{"slots":""" + str(slots) + ""","version":"4.3.1"}}"""
245 | return history
246 |
247 | def sabsavequeue(config_dir, downloadqueue):
248 | print("ping")
249 | print("saved queue: " + str(len(downloadqueue)))
250 | with open(os.path.join(config_dir, "sabqueue.conf"),"w") as file:
251 | json.dump(downloadqueue, file, indent=4)
252 |
253 | def sabloadqueue(config_dir):
254 | if os.path.exists(os.path.join(config_dir, "sabqueue.conf")):
255 | with open(os.path.join(config_dir, "sabqueue.conf"),"r") as file:
256 | downloadqueue = json.load(file)
257 | else:
258 | downloadqueue = []
259 | return downloadqueue
260 |
261 | def sabdeletefromqueue(config_dir, downloadqueue, item):
262 | for download in downloadqueue:
263 | if item == f"SABnzbd_nzo_{download['nzo']}":
264 | downloadqueue.remove(download)
265 |
266 | with open(os.path.join(config_dir, "sabqueue.conf"),"w") as file:
267 | json.dump(downloadqueue, file, indent=4)
268 | return downloadqueue
269 |
--------------------------------------------------------------------------------
/test.bash:
--------------------------------------------------------------------------------
1 | export CONFIG=/code/newznabarr/config
2 |
3 | git status
4 | git add .
5 | git commit -m "Comment"
6 | git push
7 |
8 |
9 | pip install pipreqs
10 | pipreqs . --force
--------------------------------------------------------------------------------