mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2025-12-25 08:38:05 -05:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39cccb5653 | ||
|
|
f6838dc985 | ||
|
|
8cd4d92395 | ||
|
|
3bf9906f45 | ||
|
|
9f7daf96ef | ||
|
|
67de4df155 | ||
|
|
bc51a4bd1c | ||
|
|
bb54616018 | ||
|
|
6bcff5e014 | ||
|
|
8970a03a9a | ||
|
|
3ad717ca35 | ||
|
|
b14f72c67a | ||
|
|
45d036804f | ||
|
|
8f606db233 | ||
|
|
3766ba5402 | ||
|
|
e851813cef | ||
|
|
4d49ad9141 | ||
|
|
7be9281431 | ||
|
|
ee0327fac1 | ||
|
|
9930de3e7f | ||
|
|
e8503e89c6 | ||
|
|
1d9ed419eb | ||
|
|
0207652e3e | ||
|
|
0f1e99c5cb | ||
|
|
f134bc7efb | ||
|
|
dcd7c7180e | ||
|
|
fbbfcd075b | ||
|
|
f42d2e4140 | ||
|
|
88882cebbc | ||
|
|
17a979675c | ||
|
|
4642850c79 |
8
.github/workflows/build_release.yml
vendored
8
.github/workflows/build_release.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
path: "*-win32-bin.zip"
|
||||
name: Windows Windows standalone binary (32bit and legacy)
|
||||
- name: Prepare official release
|
||||
if: env.AUTOMATION_GITHUB_TOKEN
|
||||
if: env.AUTOMATION_GITHUB_TOKEN && !startsWith(github.ref, 'refs/tags/')
|
||||
run: python builder/package.py release
|
||||
|
||||
build_macos:
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
# We need the official Python, because the GA ones only support newer macOS versions
|
||||
# The deployment target is picked up by the Python build tools automatically
|
||||
# If updated, make sure to also set LSMinimumSystemVersion in SABnzbd.spec
|
||||
PYTHON_VERSION: 3.9.1
|
||||
PYTHON_VERSION: 3.9.2
|
||||
MACOSX_DEPLOYMENT_TARGET: 10.9
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
if: steps.cache-python-download.outputs.cache-hit != 'true'
|
||||
run: curl https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-macosx10.9.pkg -o ~/python.pkg
|
||||
- name: Install Python
|
||||
run: sudo installer -pkg ~/python.pkg -target /Applications
|
||||
run: sudo installer -pkg ~/python.pkg -target /
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python3 --version
|
||||
@@ -110,5 +110,5 @@ jobs:
|
||||
path: "*-osx.dmg"
|
||||
name: macOS binary (not notarized)
|
||||
- name: Prepare official release
|
||||
if: env.AUTOMATION_GITHUB_TOKEN
|
||||
if: env.AUTOMATION_GITHUB_TOKEN && !startsWith(github.ref, 'refs/tags/')
|
||||
run: python3 builder/package.py release
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,6 +31,9 @@ SABnzbd-*/
|
||||
*.wp[ru]
|
||||
.idea
|
||||
|
||||
# VScode
|
||||
.vscode/
|
||||
|
||||
# Testing folders
|
||||
.cache
|
||||
.xprocess
|
||||
|
||||
4
PKG-INFO
4
PKG-INFO
@@ -1,7 +1,7 @@
|
||||
Metadata-Version: 1.0
|
||||
Name: SABnzbd
|
||||
Version: 3.2.0RC1
|
||||
Summary: SABnzbd-3.2.0RC1
|
||||
Version: 3.2.1RC2
|
||||
Summary: SABnzbd-3.2.1RC2
|
||||
Home-page: https://sabnzbd.org
|
||||
Author: The SABnzbd Team
|
||||
Author-email: team@sabnzbd.org
|
||||
|
||||
41
README.mkd
41
README.mkd
@@ -1,10 +1,28 @@
|
||||
Release Notes - SABnzbd 3.2.0 Release Candidate 1
|
||||
Release Notes - SABnzbd 3.2.1 Release Candidate 2
|
||||
=========================================================
|
||||
|
||||
## Changes and bugfixes since 3.2.1 RC 1
|
||||
- Improvements to the encrypted RAR-detection.
|
||||
|
||||
## Changes and bugfixes since 3.2.0
|
||||
- Single `Indexer Categories` in Categories were broken.
|
||||
- Program would fail to start if Quota was previously exceeded.
|
||||
- Setting `Automatically sort queue` by `Age` was inverted.
|
||||
- Show the name of the item to be deleted from the Queue/History
|
||||
in the confirmation dialog.
|
||||
- Handle directories in `.par2`-files during Quick-check.
|
||||
- Improvements to `Deobfuscate final filenames`:
|
||||
Rename accompanying (smaller) files with the same basename.
|
||||
Do not rename collections of the same extension.
|
||||
- Sanitize names possibly derived from `X-DNZB-EpisodeName`.
|
||||
- Widened the RSS feeds table.
|
||||
- Add traceback-logging when failing to read the password file.
|
||||
- Windows: Use binary mode to make the write test more accurate.
|
||||
|
||||
## Changes since 3.1.1
|
||||
- Python 3.6 is the minimum required version.
|
||||
- The Windows installer can only be used on 64bit Windows 8.1 and
|
||||
above. For 32bit systems or older Windows versions the
|
||||
above. For 32bit systems or older Windows versions, the
|
||||
standalone 32bit legacy version can be used.
|
||||
- Post-processing can be aborted at any stage, including scripts.
|
||||
- Improvements in the downloader to reduce CPU-load.
|
||||
@@ -12,19 +30,22 @@ Release Notes - SABnzbd 3.2.0 Release Candidate 1
|
||||
- Custom date ranges for server graphs can be selected.
|
||||
- Keep track of article fetching success-rate of each server.
|
||||
- Added option to add download quota warning for each server.
|
||||
- Added option to add expiration waring for each server.
|
||||
- Added option to add expiration warning for each server.
|
||||
- Added `Minimum Free Space for Completed Download Folder` option.
|
||||
- Added option to `Auto resume` for both `Minimum Free Space` settings.
|
||||
- Added `Auto` option for Glitter that enables `Night` style
|
||||
based on system settings. Default for new installations.
|
||||
- Multiple additional Queue and History columns can be added.
|
||||
- Added option to always use full screen width.
|
||||
- Added option to always use full-screen width.
|
||||
- Additional interface settings can be stored server-side.
|
||||
- Right-to-Left support (Hebrew) for Glitter and Config.
|
||||
- Using SSDP, SABnzbd instances are now listed in `Network` on Windows.
|
||||
- Improvements to parsing of job name and filenames listed in the NZB.
|
||||
- RSS titles can be edited.
|
||||
- Prospective par2 will add blocks from all sets in a job.
|
||||
- Sanitize all filenames to a maximum of 245 characters.
|
||||
- Show commit hash when running from `git` sources.
|
||||
- Notify through Notifications if new version is available.
|
||||
- Notify through Notifications if a new version is available.
|
||||
- Program shutdown time reduced to almost instant.
|
||||
- Added `10 GB` test download.
|
||||
- IPv6 is no longer preferred in HappyEyeballs address selection.
|
||||
@@ -38,14 +59,14 @@ Release Notes - SABnzbd 3.2.0 Release Candidate 1
|
||||
- Repairing or Retrying jobs could result in a crash.
|
||||
- API-call `reset_quota` returned nothing.
|
||||
- New categories were not always forced to lowercase.
|
||||
- Broken downloads could result in crash during RAR-renaming
|
||||
- Broken downloads could result in a crash during RAR-renaming
|
||||
- Improved obfuscation detection for `Deobfuscate final filenames`.
|
||||
- Keep original priority of duplicate jobs.
|
||||
- Increase Maximum number of connections per server to `1000`.
|
||||
- Increase the maximum number of connections per server to `1000`.
|
||||
- Update encryption check to handle partially assembled files.
|
||||
- Don't activate Windows notifications when running as service.
|
||||
- Command line option `--console` did not work.
|
||||
- Crash in API-call to delete history items for non-existing `nzo_id`.
|
||||
- Don't activate Windows notifications when running as a service.
|
||||
- Command-line option `--console` did not work.
|
||||
- Crash in API-call to delete history items for nonexistent `nzo_id`.
|
||||
- Prevent repetition of unwanted extension warnings.
|
||||
- Correct notification category for failed URL fetches.
|
||||
- Improvements to the `Add NZB` modal window.
|
||||
|
||||
68
SABnzbd.py
68
SABnzbd.py
@@ -19,7 +19,7 @@ import sys
|
||||
|
||||
if sys.hexversion < 0x03060000:
|
||||
print("Sorry, requires Python 3.6 or above")
|
||||
print("You can read more at: https://sabnzbd.org/python3")
|
||||
print("You can read more at: https://sabnzbd.org/wiki/installation/install-off-modules")
|
||||
sys.exit(1)
|
||||
|
||||
import logging
|
||||
@@ -48,7 +48,7 @@ try:
|
||||
except ImportError as e:
|
||||
print("Not all required Python modules are available, please check requirements.txt")
|
||||
print("Missing module:", e.name)
|
||||
print("You can read more at: https://sabnzbd.org/python3")
|
||||
print("You can read more at: https://sabnzbd.org/wiki/installation/install-off-modules")
|
||||
print("If you still experience problems, remove all .pyc files in this folder and subfolders")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -68,7 +68,8 @@ from sabnzbd.misc import (
|
||||
get_serv_parms,
|
||||
get_from_url,
|
||||
upload_file_to_sabnzbd,
|
||||
probablyipv4,
|
||||
is_localhost,
|
||||
is_lan_addr,
|
||||
)
|
||||
from sabnzbd.filesystem import get_ext, real_path, long_path, globber_full, remove_file
|
||||
from sabnzbd.panic import panic_tmpl, panic_port, panic_host, panic, launch_a_browser
|
||||
@@ -533,7 +534,7 @@ def get_webhost(cherryhost, cherryport, https_port):
|
||||
# Valid user defined name?
|
||||
info = socket.getaddrinfo(cherryhost, None)
|
||||
except socket.error:
|
||||
if cherryhost not in LOCALHOSTS:
|
||||
if not is_localhost(cherryhost):
|
||||
cherryhost = "0.0.0.0"
|
||||
try:
|
||||
info = socket.getaddrinfo(localhost, None)
|
||||
@@ -600,7 +601,7 @@ def get_webhost(cherryhost, cherryport, https_port):
|
||||
except socket.error:
|
||||
cherryhost = cherryhost.strip("[]")
|
||||
|
||||
if ipv6 and ipv4 and browserhost not in LOCALHOSTS:
|
||||
if ipv6 and ipv4 and not is_localhost(browserhost):
|
||||
sabnzbd.AMBI_LOCALHOST = True
|
||||
logging.info("IPV6 has priority on this system, potential Firefox issue")
|
||||
|
||||
@@ -1489,34 +1490,37 @@ def main():
|
||||
check_latest_version()
|
||||
autorestarted = False
|
||||
|
||||
# bonjour/zeroconf needs an ip. Lets try to find it.
|
||||
external_host = localipv4() # IPv4 address of the LAN interface. This is the normal use case
|
||||
if not external_host:
|
||||
# None, so no network / default route, so let's set to ...
|
||||
external_host = "127.0.0.1"
|
||||
elif probablyipv4(cherryhost) and cherryhost not in LOCALHOSTS + ("0.0.0.0", "::"):
|
||||
# a hard-configured cherryhost other than the usual, so let's take that (good or wrong)
|
||||
external_host = cherryhost
|
||||
logging.debug("bonjour/zeroconf/SSDP using host: %s", external_host)
|
||||
sabnzbd.zconfig.set_bonjour(external_host, cherryport)
|
||||
|
||||
# Start SSDP if SABnzbd is running exposed
|
||||
if cherryhost not in LOCALHOSTS:
|
||||
# Set URL for browser for external hosts
|
||||
if enable_https:
|
||||
ssdp_url = "https://%s:%s%s" % (external_host, cherryport, sabnzbd.cfg.url_base())
|
||||
# Start SSDP and Bonjour if SABnzbd isn't listening on localhost only
|
||||
if sabnzbd.cfg.enable_broadcast() and not is_localhost(cherryhost):
|
||||
# Try to find a LAN IP address for SSDP/Bonjour
|
||||
if is_lan_addr(cherryhost):
|
||||
# A specific listening address was configured, use that
|
||||
external_host = cherryhost
|
||||
else:
|
||||
ssdp_url = "http://%s:%s%s" % (external_host, cherryport, sabnzbd.cfg.url_base())
|
||||
ssdp.start_ssdp(
|
||||
external_host,
|
||||
"SABnzbd",
|
||||
ssdp_url,
|
||||
"SABnzbd %s" % sabnzbd.__version__,
|
||||
"SABnzbd Team",
|
||||
"https://sabnzbd.org/",
|
||||
"SABnzbd %s" % sabnzbd.__version__,
|
||||
ssdp_broadcast_interval=sabnzbd.cfg.ssdp_broadcast_interval(),
|
||||
)
|
||||
# Fall back to the IPv4 address of the LAN interface
|
||||
external_host = localipv4()
|
||||
logging.debug("Using %s as host address for Bonjour and SSDP", external_host)
|
||||
|
||||
if is_lan_addr(external_host):
|
||||
sabnzbd.zconfig.set_bonjour(external_host, cherryport)
|
||||
|
||||
# Set URL for browser for external hosts
|
||||
ssdp_url = "%s://%s:%s%s" % (
|
||||
("https" if enable_https else "http"),
|
||||
external_host,
|
||||
cherryport,
|
||||
sabnzbd.cfg.url_base(),
|
||||
)
|
||||
ssdp.start_ssdp(
|
||||
external_host,
|
||||
"SABnzbd",
|
||||
ssdp_url,
|
||||
"SABnzbd %s" % sabnzbd.__version__,
|
||||
"SABnzbd Team",
|
||||
"https://sabnzbd.org/",
|
||||
"SABnzbd %s" % sabnzbd.__version__,
|
||||
ssdp_broadcast_interval=sabnzbd.cfg.ssdp_broadcast_interval(),
|
||||
)
|
||||
|
||||
# Have to keep this running, otherwise logging will terminate
|
||||
timer = 0
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#set global $root = '../../'#
|
||||
#end if#
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="$active_lang">
|
||||
<html lang="$active_lang" #if $rtl#dir="rtl"#end if#>
|
||||
<head>
|
||||
<title>
|
||||
SABnzbd $T('menu-config')
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<p>$T('explain-RSS')</p>
|
||||
<form action="add_rss_feed" method="post" autocomplete="off">
|
||||
<input type="hidden" name="apikey" value="$apikey" />
|
||||
<table class="catTable">
|
||||
<table class="catTable addRssTable">
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>$T('name')</th>
|
||||
@@ -21,10 +21,10 @@
|
||||
<td>
|
||||
<input type="checkbox" name="enable" value="1" checked />
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="feed" class="smaller_input" value="$feed" />
|
||||
<td class="new-feed-title">
|
||||
<input type="text" name="feed" value="$feed" />
|
||||
</td>
|
||||
<td>
|
||||
<td class="new-feed-url">
|
||||
<input type="text" name="uri" placeholder="$T('addMultipleFeeds')" />
|
||||
</td>
|
||||
<td class="nowrap">
|
||||
@@ -59,7 +59,7 @@
|
||||
<td class="controls">
|
||||
<button type="button" class="btn btn-default testFeed" rel="$feed_item_html"><span class="glyphicon glyphicon-sort"></span> $T('button-preFeed')</button>
|
||||
<input type="hidden" name="uri" value="$rss[$feed_item]['uris']" />
|
||||
<button type="button" class="btn btn-default editFeed" rel="$feed_item_html"><span class="glyphicon glyphicon-pencil"></span> $T('Edit')</button>
|
||||
<button type="button" class="btn btn-default editFeed" rel="$feed_item_html"><span class="glyphicon glyphicon-pencil"></span> $T('rss-edit')</button>
|
||||
<button type="button" class="btn btn-default delFeed" rel="$feed_item_html"><span class="glyphicon glyphicon-trash"></span></button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -92,7 +92,7 @@
|
||||
<label class="config narrow" for="rss_rate">$T('opt-rss_rate')</label>
|
||||
<input type="number" name="rss_rate" id="rss_rate" value="$rss_rate" min="15" max="1440" />
|
||||
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-ok"></span> $T('button-save')</button>
|
||||
<span class="config narrow"> $T('Next scan at:') $rss_next</span>
|
||||
<span class="config narrow"> $T('rss-nextscan'): $rss_next</span>
|
||||
<span class="desc narrow">$T('explain-rss_rate')</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -136,8 +136,8 @@
|
||||
<label class="config" for="auto_sort">$T('opt-auto_sort')</label>
|
||||
<select name="auto_sort" id="auto_sort">
|
||||
<option value="">$T('default')</option>
|
||||
<option value="avg_age asc" <!--#if $auto_sort == "avg_age asc" then 'selected="selected"' else ""#--> >$T('Glitter-sortAgeAsc')</option>
|
||||
<option value="avg_age desc" <!--#if $auto_sort == "avg_age desc" then 'selected="selected"' else ""#--> >$T('Glitter-sortAgeDesc')</option>
|
||||
<option value="avg_age desc" <!--#if $auto_sort == "avg_age desc" then 'selected="selected"' else ""#--> >$T('Glitter-sortAgeAsc')</option>
|
||||
<option value="avg_age asc" <!--#if $auto_sort == "avg_age asc" then 'selected="selected"' else ""#--> >$T('Glitter-sortAgeDesc')</option>
|
||||
<option value="name asc" <!--#if $auto_sort == "name asc" then 'selected="selected"' else ""#--> >$T('Glitter-sortNameAsc')</option>
|
||||
<option value="name desc" <!--#if $auto_sort == "name desc" then 'selected="selected"' else ""#--> >$T('Glitter-sortNameDesc')</option>
|
||||
<option value="size asc" <!--#if $auto_sort == "size asc" then 'selected="selected"' else ""#--> >$T('Glitter-sortSizeAsc')</option>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -162,6 +162,7 @@ input[type="checkbox"]+.desc {
|
||||
float: none;
|
||||
overflow: hidden;
|
||||
min-width: 555px;
|
||||
position: relative;
|
||||
}
|
||||
.Key tr:nth-child(odd),
|
||||
.tab-pane tr:nth-child(odd),
|
||||
@@ -550,6 +551,16 @@ tr.separator {
|
||||
padding-right: 13px;
|
||||
}
|
||||
/* -- */
|
||||
.RSS .addRssTable,
|
||||
.RSS .addRssTable input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
.RSS .addRssTable .new-feed-title {
|
||||
max-width: 250px;
|
||||
}
|
||||
.RSS .addRssTable .new-feed-url {
|
||||
width: 70%;
|
||||
}
|
||||
h2.activeRSS {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -559,12 +570,12 @@ h2.activeRSS {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
.favicon {
|
||||
background-position: center center!important;
|
||||
background-size: 16px 16px;
|
||||
background-position: center center !important;
|
||||
background-size: 22px 22px;
|
||||
opacity: 1;
|
||||
top: -1px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
float: left;
|
||||
margin: 0 6px 0 2px;
|
||||
text-align: center;
|
||||
@@ -584,6 +595,7 @@ h2.activeRSS {
|
||||
}
|
||||
#subscriptions {
|
||||
border: 1px solid #E5E5E5;
|
||||
width: 100%;
|
||||
}
|
||||
.data-row {
|
||||
border-top: 1px solid #E5E5E5;
|
||||
@@ -595,6 +607,7 @@ h2.activeRSS {
|
||||
#subscriptions .chk {
|
||||
padding: 8px 5px 5px;
|
||||
vertical-align: middle;
|
||||
width: 40px;
|
||||
}
|
||||
#subscriptions .title {
|
||||
font-weight: bold;
|
||||
@@ -602,10 +615,11 @@ h2.activeRSS {
|
||||
width: auto;
|
||||
}
|
||||
#subscriptions .favicon {
|
||||
margin-left: 8px;
|
||||
margin-left: 7px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.ie6 .subscription-title {
|
||||
width: 20em;
|
||||
#subscriptions .glyphicon {
|
||||
margin-top: 3px;
|
||||
}
|
||||
.subscription-title,
|
||||
.subscription-title:hover {
|
||||
@@ -1168,6 +1182,27 @@ input[type="checkbox"] {
|
||||
100% { transform: rotate(359deg); }
|
||||
}
|
||||
|
||||
/***
|
||||
RTL Fixes
|
||||
***/
|
||||
html[dir="rtl"] .col1 input[type='checkbox'],
|
||||
html[dir="rtl"] .col2 h3 a {
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .modal-header .close {
|
||||
float: left;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .Sorting .presets.float-left,
|
||||
html[dir="rtl"] .checkbox-days {
|
||||
float: none;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .Scheduling form[action="addSchedule"] input[type="checkbox"] {
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
.Categories input[name="dir"] {
|
||||
max-width: 240px !important;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<!--#set $active_lang=$active_lang.replace('_', '-').lower()#-->
|
||||
<html lang="$active_lang" id="sabnzbd" data-bind="filedrop: { overlaySelector: '.main-filedrop', onFileDrop: addNZBFromFile }">
|
||||
<html lang="$active_lang" <!--#if $rtl#-->dir="rtl"<!--#end if#--> id="sabnzbd" data-bind="filedrop: { overlaySelector: '.main-filedrop', onFileDrop: addNZBFromFile }">
|
||||
<head>
|
||||
<!--
|
||||
Glitter V2
|
||||
@@ -36,7 +36,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="./static/bootstrap/css/bootstrap.min.css?v=$version" />
|
||||
<link rel="stylesheet" type="text/css" href="./static/stylesheets/glitter.css?v=$version" />
|
||||
<link rel="stylesheet" type="text/css" href="./static/stylesheets/glitter.mobile.css?v=$version" media="all and (max-width: 768px)" />
|
||||
<!--#if $color_scheme not in ('Default', '') #-->
|
||||
<!--#if $color_scheme not in ('Light', '') #-->
|
||||
<link rel="stylesheet" type="text/css" href="./static/stylesheets/colorschemes/${color_scheme}.css?v=$version"/>
|
||||
<!--#end if#-->
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
glitterTranslate.shutdown = "$T('shutdownOK?')";
|
||||
glitterTranslate.restart = "$T('explain-Restart') $T('explain-needNewLogin')".replace(/\<br(\s*\/|)\>/g, '\n');
|
||||
glitterTranslate.repair = "$T('explain-Repair')".replace(/<br \/>/g, "\n").replace(/"/g,'"');
|
||||
glitterTranslate.deleteMsg = "$T('nzo-delete')";
|
||||
glitterTranslate.removeDown = "$T('Glitter-confirmClearDownloads')";
|
||||
glitterTranslate.removeDow1 = "$T('Glitter-confirmClear1Download')";
|
||||
glitterTranslate.retryAll = "$T('link-retryAll')?";
|
||||
|
||||
@@ -421,7 +421,7 @@ function HistoryModel(parent, data) {
|
||||
// Delete button
|
||||
self.deleteSlot = function(item, event) {
|
||||
// Confirm?
|
||||
if(!self.parent.parent.confirmDeleteHistory() || confirm(glitterTranslate.removeDow1)) {
|
||||
if(!self.parent.parent.confirmDeleteHistory() || confirm(glitterTranslate.deleteMsg + ":\n" + item.historyStatus.name() + "\n\n" + glitterTranslate.removeDow1)) {
|
||||
// Are we still processing and it can be stopped?
|
||||
if(item.processingDownload() == 2) {
|
||||
callAPI({
|
||||
|
||||
@@ -724,7 +724,7 @@ function QueueModel(parent, data) {
|
||||
// Remove 1 download from queue
|
||||
self.removeDownload = function(item, event) {
|
||||
// Confirm and remove
|
||||
if(!self.parent.parent.confirmDeleteQueue() || confirm(glitterTranslate.removeDow1)) {
|
||||
if(!self.parent.parent.confirmDeleteQueue() || confirm(glitterTranslate.deleteMsg + ":\n" + item.name() + "\n\n" + glitterTranslate.removeDow1)) {
|
||||
var itemToDelete = this;
|
||||
|
||||
// Show notification
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
@import url('Night.css') screen and (prefers-color-scheme: dark);
|
||||
@@ -55,6 +55,10 @@ legend,
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.form-control[disabled] {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: #DADADA;
|
||||
}
|
||||
@@ -126,6 +130,10 @@ select.form-control,
|
||||
.main-content .btn-default,
|
||||
.modal-body .btn-default,
|
||||
.modal-footer .btn-default,
|
||||
.btn-default.disabled:hover,
|
||||
.btn-default.disabled:active,
|
||||
.btn-default.disabled:focus,
|
||||
.form-control[disabled],
|
||||
#modal-options .options-function-box .input-group-addon {
|
||||
background-color: #555555;
|
||||
color: #EBEBEB;
|
||||
@@ -157,6 +165,8 @@ tbody>tr:last-child td,
|
||||
input,
|
||||
input.form-control,
|
||||
.input-group-addon,
|
||||
.search-box input:focus,
|
||||
.search-box input:valid,
|
||||
select.form-control,
|
||||
#modal-options .table-server-connections th,
|
||||
.main-content .btn-default,
|
||||
|
||||
@@ -1979,6 +1979,45 @@ input[name="nzbURL"] {
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
RTL Fixes
|
||||
***/
|
||||
|
||||
html[dir="rtl"] .navbar-nav {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .queue h2,
|
||||
html[dir="rtl"] .history h2 {
|
||||
float: right;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .dropdown-menu {
|
||||
text-align: right;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .speedlimit-dropdown,
|
||||
html[dir="rtl"] .progress-indicator,
|
||||
html[dir="rtl"] #modal-item-filelist,
|
||||
html[dir="rtl"] #modal-item-files .modal-title,
|
||||
html[dir="rtl"] .info-container-box,
|
||||
html[dir="rtl"] .queue-table,
|
||||
html[dir="rtl"] .history-table {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .search-box a {
|
||||
right: initial;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .navbar-logo,
|
||||
html[dir="rtl"] .info-container,
|
||||
html[dir="rtl"] .modal-header .close,
|
||||
html[dir="rtl"] #modal-options .modal-header a {
|
||||
float: left;
|
||||
}
|
||||
|
||||
/***
|
||||
Bootstrap overwrites
|
||||
|
||||
@@ -3771,6 +3771,16 @@ msgstr ""
|
||||
msgid "Force Download"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -3946,6 +3946,16 @@ msgstr ""
|
||||
msgid "Force Download"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4054,6 +4054,16 @@ msgstr "Læs Feed"
|
||||
msgid "Force Download"
|
||||
msgstr "Gennemtving download"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4174,6 +4174,16 @@ msgstr "Feed lesen"
|
||||
msgid "Force Download"
|
||||
msgstr "Download erzwingen"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4166,6 +4166,16 @@ msgstr "Leer Fuente"
|
||||
msgid "Force Download"
|
||||
msgstr "Forzar Descarga"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4053,6 +4053,16 @@ msgstr "Lue syöte"
|
||||
msgid "Force Download"
|
||||
msgstr "Pakota lataus"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4182,6 +4182,16 @@ msgstr "Lire le flux RSS"
|
||||
msgid "Force Download"
|
||||
msgstr "Forcer le téléchargement"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
1033
po/main/he.po
1033
po/main/he.po
File diff suppressed because it is too large
Load Diff
@@ -4029,6 +4029,16 @@ msgstr "Les kilde"
|
||||
msgid "Force Download"
|
||||
msgstr "Tving nedlasting"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4135,6 +4135,16 @@ msgstr "Uitlezen"
|
||||
msgid "Force Download"
|
||||
msgstr "Forceer download"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4039,6 +4039,16 @@ msgstr "Pobierz kanał"
|
||||
msgid "Force Download"
|
||||
msgstr "Wymuś pobranie"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4042,6 +4042,16 @@ msgstr "Ler Feed"
|
||||
msgid "Force Download"
|
||||
msgstr "Forçar Download"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4068,6 +4068,16 @@ msgstr "Citeşte Flux"
|
||||
msgid "Force Download"
|
||||
msgstr "Descărcare Forţată"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4027,6 +4027,16 @@ msgstr "Прочитать ленту"
|
||||
msgid "Force Download"
|
||||
msgstr "Загрузить принудительно"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4015,6 +4015,16 @@ msgstr "Читај фид"
|
||||
msgid "Force Download"
|
||||
msgstr "Натерај преузимање"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -4028,6 +4028,16 @@ msgstr "Läs flöde"
|
||||
msgid "Force Download"
|
||||
msgstr "Tvinga nedladdning"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -3960,6 +3960,16 @@ msgstr "读取 Feed"
|
||||
msgid "Force Download"
|
||||
msgstr "强制下载"
|
||||
|
||||
#. Config->RSS edit button
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS when will be the next RSS scan
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Next scan at"
|
||||
msgstr ""
|
||||
|
||||
#. Config->RSS table column header
|
||||
#: sabnzbd/skintext.py
|
||||
msgid "Filter"
|
||||
|
||||
@@ -36,9 +36,9 @@ msgid ""
|
||||
"reinstall the SABnzbd service. \\n\\nClick `OK` to remove the existing "
|
||||
"services or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"שירות SABnzbd Windows השתנה בגרסה SABnzbd 3.0.0. \\nתצטרך להתקין מחדש את "
|
||||
"השירות SABnzbd. \\n\\nלחץ `אשר` כדי להסיר את השירותים הקיימים או `בטל` כדי "
|
||||
"לבטל שדרוג זה."
|
||||
"שירות Windows של SABnzbd השתנה ב־SABnzbd 3.0.0. \\nתצטרך להתקין מחדש את "
|
||||
"השירות של SABnzbd. \\n\\nלחץ על `אישור` כדי להסיר את השירותים הקיימים או על "
|
||||
"`ביטול` כדי לבטל שדרוג זה."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid ""
|
||||
@@ -58,19 +58,19 @@ msgstr ""
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "This will uninstall SABnzbd from your system"
|
||||
msgstr "זה יסיר את SABnzbd ממערכתך"
|
||||
msgstr "זה יסיר את SABnzbd מהמערכת שלך"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Run at startup"
|
||||
msgstr "הפעלה בהזנק"
|
||||
msgstr "הרץ בהזנק"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Desktop Icon"
|
||||
msgstr "צלמית שולחן עבודה"
|
||||
msgstr "צור קיצור דרך בשולחן העבודה"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "NZB File association"
|
||||
msgstr "NZB שיוך קבצי"
|
||||
msgstr "NZB שייך קבצי"
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Delete Program"
|
||||
@@ -85,9 +85,9 @@ msgid ""
|
||||
"You cannot overwrite an existing installation. \\n\\nClick `OK` to remove "
|
||||
"the previous version or `Cancel` to cancel this upgrade."
|
||||
msgstr ""
|
||||
"אינך יכול לדרוס התקנה קיימת.\\n\\nלחץ על `אישור` כדי להסיר את הגרסה הקודמת "
|
||||
"אינך יכול לדרוס התקנה קיימת. \\n\\nלחץ על `אישור` כדי להסיר את הגרסה הקודמת "
|
||||
"או על `ביטול` כדי לבטל שדרוג זה."
|
||||
|
||||
#: builder/win/NSIS_Installer.nsi
|
||||
msgid "Your settings and data will be preserved."
|
||||
msgstr "ההגדרות והנתונים שלך יישמרו."
|
||||
msgstr "ההגדרות והנתונים שלך ישתמרו."
|
||||
|
||||
@@ -59,7 +59,10 @@ elif os.name == "posix":
|
||||
# See if we have Linux memory functions
|
||||
try:
|
||||
LIBC = ctypes.CDLL("libc.so.6")
|
||||
LIBC.malloc_trim(0)
|
||||
except:
|
||||
# No malloc_trim(), probably because no libc
|
||||
LIBC = None
|
||||
pass
|
||||
|
||||
# Parse macOS version numbers
|
||||
|
||||
@@ -63,6 +63,7 @@ from sabnzbd.encoding import xml_name
|
||||
from sabnzbd.utils.servertests import test_nntp_server_dict
|
||||
from sabnzbd.getipaddress import localipv4, publicipv4, ipv6, addresslookup
|
||||
from sabnzbd.database import build_history_info, unpack_history_info, HistoryDB
|
||||
from sabnzbd.lang import is_rtl
|
||||
import sabnzbd.notifier
|
||||
import sabnzbd.rss
|
||||
import sabnzbd.emailer
|
||||
@@ -1600,6 +1601,7 @@ def build_header(webdir="", output=None, trans_functions=True):
|
||||
header["restart_req"] = sabnzbd.RESTART_REQ
|
||||
header["pid"] = os.getpid()
|
||||
header["active_lang"] = cfg.language()
|
||||
header["rtl"] = is_rtl(header["active_lang"])
|
||||
|
||||
header["my_lcldata"] = clip_path(sabnzbd.DIR_LCLDATA)
|
||||
header["my_home"] = clip_path(sabnzbd.DIR_HOME)
|
||||
|
||||
@@ -334,29 +334,29 @@ def check_encrypted_and_unwanted_files(nzo: NzbObject, filepath: str) -> Tuple[b
|
||||
zf.setpassword(password)
|
||||
except rarfile.Error:
|
||||
# On weird passwords the setpassword() will fail
|
||||
# but the actual rartest() will work
|
||||
# but the actual testrar() will work
|
||||
pass
|
||||
try:
|
||||
zf.testrar()
|
||||
password_hit = password
|
||||
break
|
||||
except rarfile.RarWrongPassword:
|
||||
# This one really didn't work
|
||||
pass
|
||||
except rarfile.RarCRCError as e:
|
||||
# CRC errors can be thrown for wrong password or
|
||||
# missing the next volume (with correct password)
|
||||
if "cannot find volume" in str(e).lower():
|
||||
# CRC errors can be thrown for wrong password and actual CRC errors
|
||||
if "wrong password" not in str(e).lower():
|
||||
# We assume this one worked!
|
||||
password_hit = password
|
||||
break
|
||||
# This one didn't work
|
||||
pass
|
||||
except Exception as e:
|
||||
# Did we start from the right volume? Skip the checks for now.
|
||||
if match_str(
|
||||
str(e).lower(),
|
||||
("need to start extraction from a previous volume", "non-fatal error"),
|
||||
):
|
||||
return encrypted, unwanted
|
||||
# This one failed
|
||||
pass
|
||||
except:
|
||||
# All the other errors we skip, they might be fixable in post-proc.
|
||||
# For example starting from the wrong volume, or damaged files
|
||||
# This will cause the check to be performed again for the next rar, might
|
||||
# be disk-intensive! Could be removed later and just accept the password.
|
||||
return encrypted, unwanted
|
||||
|
||||
# Did any work?
|
||||
if password_hit:
|
||||
|
||||
@@ -195,13 +195,6 @@ class BPSMeter:
|
||||
res = self.reset_quota()
|
||||
except:
|
||||
self.defaults()
|
||||
# Force update of counters and validate data
|
||||
try:
|
||||
for server in self.grand_total.keys():
|
||||
self.update(server)
|
||||
except TypeError:
|
||||
self.defaults()
|
||||
self.update()
|
||||
return res
|
||||
|
||||
def update(self, server: Optional[str] = None, amount: int = 0):
|
||||
|
||||
@@ -282,7 +282,7 @@ helpfull_warnings = OptionBool("misc", "helpfull_warnings", True)
|
||||
keep_awake = OptionBool("misc", "keep_awake", True)
|
||||
win_menu = OptionBool("misc", "win_menu", True)
|
||||
allow_incomplete_nzb = OptionBool("misc", "allow_incomplete_nzb", False)
|
||||
enable_bonjour = OptionBool("misc", "enable_bonjour", True)
|
||||
enable_broadcast = OptionBool("misc", "enable_broadcast", True)
|
||||
max_art_opt = OptionBool("misc", "max_art_opt", False)
|
||||
ipv6_hosting = OptionBool("misc", "ipv6_hosting", False)
|
||||
fixed_ports = OptionBool("misc", "fixed_ports", False)
|
||||
|
||||
@@ -1137,7 +1137,7 @@ def validate_single_tag(value):
|
||||
"""
|
||||
if len(value) == 3:
|
||||
if value[1] == ">":
|
||||
return None, " ".join(value)
|
||||
return None, [" ".join(value)]
|
||||
return None, value
|
||||
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ DEF_INTERFACES = "interfaces"
|
||||
DEF_EMAIL_TMPL = "email"
|
||||
DEF_STDCONFIG = "Config"
|
||||
DEF_STDINTF = "Glitter"
|
||||
DEF_SKIN_COLORS = {"Glitter": "Default", "plush": "gold"}
|
||||
DEF_SKIN_COLORS = {"Glitter": "Auto", "plush": "gold"}
|
||||
DEF_MAIN_TMPL = os.path.normpath("templates/main.tmpl")
|
||||
DEF_INI_FILE = "sabnzbd.ini"
|
||||
DEF_HOST = "127.0.0.1"
|
||||
@@ -124,8 +124,6 @@ CHEETAH_DIRECTIVES = {"directiveStartToken": "<!--#", "directiveEndToken": "#-->
|
||||
|
||||
IGNORED_FOLDERS = ("@eaDir", ".appleDouble")
|
||||
|
||||
LOCALHOSTS = ("localhost", "127.0.0.1", "[::1]", "::1")
|
||||
|
||||
# (MATCHER, [EXTRA, MATCHERS])
|
||||
series_match = [
|
||||
(compile(r"( [sS]|[\d]+)x(\d+)"), [compile(r"^[-\.]+([sS]|[\d])+x(\d+)"), compile(r"^[-\.](\d+)")]), # 1x01
|
||||
|
||||
@@ -127,14 +127,13 @@ def is_probably_obfuscated(myinputfilename):
|
||||
|
||||
|
||||
def deobfuscate_list(filelist, usefulname):
|
||||
""" Check all files in filelist, and if wanted, deobfuscate """
|
||||
""" Check all files in filelist, and if wanted, deobfuscate: rename to filename based on usefulname"""
|
||||
|
||||
# to be sure, only keep really exsiting files:
|
||||
filelist = [f for f in filelist if os.path.exists(f)]
|
||||
|
||||
# Search for par2 files in the filelist
|
||||
par2_files = [f for f in filelist if f.endswith(".par2")]
|
||||
|
||||
# Found any par2 files we can use?
|
||||
run_renamer = True
|
||||
if not par2_files:
|
||||
@@ -152,22 +151,57 @@ def deobfuscate_list(filelist, usefulname):
|
||||
|
||||
# No par2 files? Then we try to rename qualifying (big, not-excluded, obfuscated) files to the job-name
|
||||
if run_renamer:
|
||||
excluded_file_exts = EXCLUDED_FILE_EXTS
|
||||
# If there is a collection with bigger files with the same extension, we don't want to rename it
|
||||
extcounter = {}
|
||||
for file in filelist:
|
||||
if os.path.getsize(file) < MIN_FILE_SIZE:
|
||||
# too small to care
|
||||
continue
|
||||
_, ext = os.path.splitext(file)
|
||||
if ext in extcounter:
|
||||
extcounter[ext] += 1
|
||||
else:
|
||||
extcounter[ext] = 1
|
||||
if extcounter[ext] >= 3 and ext not in excluded_file_exts:
|
||||
# collection, and extension not yet in excluded_file_exts, so add it
|
||||
excluded_file_exts = (*excluded_file_exts, ext)
|
||||
logging.debug(
|
||||
"Found a collection of at least %s files with extension %s, so not renaming those files",
|
||||
extcounter[ext],
|
||||
ext,
|
||||
)
|
||||
|
||||
logging.debug("Trying to see if there are qualifying files to be deobfuscated")
|
||||
# We start with he biggest file ... probably the most important file
|
||||
filelist = sorted(filelist, key=os.path.getsize, reverse=True)
|
||||
for filename in filelist:
|
||||
# check that file is still there (and not renamed by the secondary renaming process below)
|
||||
if not os.path.isfile(filename):
|
||||
continue
|
||||
logging.debug("Deobfuscate inspecting %s", filename)
|
||||
file_size = os.path.getsize(filename)
|
||||
# Do we need to rename this file?
|
||||
# Criteria: big, not-excluded extension, obfuscated (in that order)
|
||||
if (
|
||||
file_size > MIN_FILE_SIZE
|
||||
and get_ext(filename) not in EXCLUDED_FILE_EXTS
|
||||
os.path.getsize(filename) > MIN_FILE_SIZE
|
||||
and get_ext(filename) not in excluded_file_exts
|
||||
and is_probably_obfuscated(filename) # this as last test to avoid unnecessary analysis
|
||||
):
|
||||
# OK, rename
|
||||
# Rename and make sure the new filename is unique
|
||||
path, file = os.path.split(filename)
|
||||
# construct new_name: <path><usefulname><extension>
|
||||
new_name = get_unique_filename("%s%s" % (os.path.join(path, usefulname), get_ext(filename)))
|
||||
logging.info("Deobfuscate renaming %s to %s", filename, new_name)
|
||||
# Rename and make sure the new filename is unique
|
||||
renamer(filename, new_name)
|
||||
# find other files with the same basename in filelist, and rename them in the same way:
|
||||
basedirfile, _ = os.path.splitext(filename) # something like "/home/this/myiso"
|
||||
for otherfile in filelist:
|
||||
if otherfile.startswith(basedirfile + ".") and os.path.isfile(otherfile):
|
||||
# yes, same basedirfile, only different extension
|
||||
remainingextension = otherfile.replace(basedirfile, "") # might be long ext, like ".dut.srt"
|
||||
new_name = get_unique_filename("%s%s" % (os.path.join(path, usefulname), remainingextension))
|
||||
logging.info("Deobfuscate renaming %s to %s", otherfile, new_name)
|
||||
# Rename and make sure the new filename is unique
|
||||
renamer(otherfile, new_name)
|
||||
else:
|
||||
logging.info("No qualifying files found to deobfuscate")
|
||||
|
||||
@@ -95,7 +95,7 @@ class Server:
|
||||
|
||||
self.busy_threads: List[NewsWrapper] = []
|
||||
self.idle_threads: List[NewsWrapper] = []
|
||||
self.next_article_search: int = 0
|
||||
self.next_article_search: float = 0
|
||||
self.active: bool = True
|
||||
self.bad_cons: int = 0
|
||||
self.errormsg: str = ""
|
||||
@@ -480,7 +480,7 @@ class Downloader(Thread):
|
||||
|
||||
for server in self.servers:
|
||||
# Skip this server if there's no point searching for new stuff to do
|
||||
if server.next_article_search > now:
|
||||
if not server.busy_threads and server.next_article_search > now:
|
||||
continue
|
||||
|
||||
for nw in server.busy_threads[:]:
|
||||
@@ -534,8 +534,8 @@ class Downloader(Thread):
|
||||
article = sabnzbd.NzbQueue.get_article(server, self.servers)
|
||||
|
||||
if not article:
|
||||
# Skip this server for 1 second
|
||||
server.next_article_search = now + 1
|
||||
# Skip this server for 0.5 second
|
||||
server.next_article_search = now + 0.5
|
||||
break
|
||||
|
||||
if server.retention and article.nzf.nzo.avg_stamp < now - server.retention:
|
||||
|
||||
@@ -806,8 +806,9 @@ def get_filepath(path: str, nzo, filename: str):
|
||||
|
||||
|
||||
@synchronized(DIR_LOCK)
|
||||
def renamer(old: str, new: str):
|
||||
""" Rename file/folder with retries for Win32 """
|
||||
def renamer(old: str, new: str, create_local_directories: bool = False):
|
||||
"""Rename file/folder with retries for Win32
|
||||
Optionally alows the creation of local directories if they don't exist yet"""
|
||||
# Sanitize last part of new name
|
||||
path, name = os.path.split(new)
|
||||
new = os.path.join(path, sanitize_filename(name))
|
||||
@@ -816,6 +817,19 @@ def renamer(old: str, new: str):
|
||||
if old == new:
|
||||
return
|
||||
|
||||
# In case we want nonexistent directories to be created, check for directory escape (forbidden)
|
||||
if create_local_directories:
|
||||
oldpath, _ = os.path.split(old)
|
||||
# Check not outside directory
|
||||
# In case of "same_file() == 1": same directory, so nothing to do
|
||||
if same_file(oldpath, path) == 0:
|
||||
# Outside current directory, this is most likely malicious
|
||||
logging.error(T("Blocked attempt to create directory %s"), path)
|
||||
raise OSError("Refusing to go outside directory")
|
||||
elif same_file(oldpath, path) == 2:
|
||||
# Sub-directory, so create if does not yet exist:
|
||||
create_all_dirs(path)
|
||||
|
||||
logging.debug('Renaming "%s" to "%s"', old, new)
|
||||
if sabnzbd.WIN32:
|
||||
retries = 10
|
||||
|
||||
@@ -44,10 +44,11 @@ from sabnzbd.misc import (
|
||||
calc_age,
|
||||
int_conv,
|
||||
get_base_url,
|
||||
probablyipv4,
|
||||
probablyipv6,
|
||||
is_ipv4_addr,
|
||||
is_ipv6_addr,
|
||||
opts_to_pp,
|
||||
get_server_addrinfo,
|
||||
is_lan_addr,
|
||||
)
|
||||
from sabnzbd.filesystem import real_path, long_path, globber, globber_full, remove_all, clip_path, same_file
|
||||
from sabnzbd.encoding import xml_name, utob
|
||||
@@ -165,7 +166,7 @@ def check_hostname():
|
||||
host = re.sub(":[0123456789]+$", "", host).lower()
|
||||
|
||||
# Fine if localhost or IP
|
||||
if host == "localhost" or probablyipv4(host) or probablyipv6(host):
|
||||
if host == "localhost" or is_ipv4_addr(host) or is_ipv6_addr(host):
|
||||
return True
|
||||
|
||||
# Check on the whitelist
|
||||
@@ -477,8 +478,11 @@ class MainPage:
|
||||
cherrypy.request.remote.ip,
|
||||
cherrypy.request.headers.get("User-Agent", "??"),
|
||||
)
|
||||
cherrypy.response.headers["Content-Type"] = "application/xml"
|
||||
return utob(sabnzbd.utils.ssdp.server_ssdp_xml())
|
||||
if is_lan_addr(cherrypy.request.remote.ip):
|
||||
cherrypy.response.headers["Content-Type"] = "application/xml"
|
||||
return utob(sabnzbd.utils.ssdp.server_ssdp_xml())
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
##############################################################################
|
||||
@@ -509,7 +513,7 @@ class Wizard:
|
||||
cfg.language.set(kwargs.get("lang"))
|
||||
|
||||
# Always setup Glitter
|
||||
change_web_dir("Glitter - Default")
|
||||
change_web_dir("Glitter - Auto")
|
||||
|
||||
info = build_header(sabnzbd.WIZARD_DIR)
|
||||
info["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION
|
||||
@@ -1327,7 +1331,7 @@ SPECIAL_BOOL_LIST = (
|
||||
"html_login",
|
||||
"wait_for_dfolder",
|
||||
"max_art_opt",
|
||||
"enable_bonjour",
|
||||
"enable_broadcast",
|
||||
"warn_dupl_jobs",
|
||||
"replace_illegal",
|
||||
"backup_for_duplicates",
|
||||
|
||||
189
sabnzbd/lang.py
189
sabnzbd/lang.py
@@ -92,99 +92,104 @@ def list_languages():
|
||||
return lst
|
||||
|
||||
|
||||
def is_rtl(lang):
|
||||
return LanguageTable.get(lang, "en")[3]
|
||||
|
||||
|
||||
# English name, native name, code page, right-to-left
|
||||
LanguageTable = {
|
||||
"aa": ("Afar", "Afaraf", 0),
|
||||
"af": ("Afrikaans", "Afrikaans", 0),
|
||||
"ak": ("Akan", "Akan", 0),
|
||||
"sq": ("Albanian", "Shqip", 0),
|
||||
"an": ("Aragonese", "Aragonés", 0),
|
||||
"ae": ("Avestan", "Avesta", 0),
|
||||
"ay": ("Aymara", "Aymararu", 0),
|
||||
"bm": ("Bambara", "Bamanankan", 0),
|
||||
"eu": ("Basque", "Euskara", 0),
|
||||
"bi": ("Bislama", "Bislama", 0),
|
||||
"bs": ("Bosnian", "Bosanskijezik", 0),
|
||||
"br": ("Breton", "Brezhoneg", 0),
|
||||
"ca": ("Catalan", "Català", 0),
|
||||
"ch": ("Chamorro", "Chamoru", 0),
|
||||
"kw": ("Cornish", "Kernewek", 0),
|
||||
"co": ("Corsican", "Corsu", 0),
|
||||
"hr": ("Croatian", "Hrvatski", 0),
|
||||
"cs": ("Czech", "Cesky, ceština", 0),
|
||||
"da": ("Danish", "Dansk", 0),
|
||||
"nl": ("Dutch", "Nederlands", 0),
|
||||
"en": ("English", "English", 0),
|
||||
"eo": ("Esperanto", "Esperanto", 0),
|
||||
"et": ("Estonian", "Eesti", 0),
|
||||
"fo": ("Faroese", "Føroyskt", 0),
|
||||
"fj": ("Fijian", "Vosa Vakaviti", 0),
|
||||
"fi": ("Finnish", "Suomi", 0),
|
||||
"fr": ("French", "Français", 0),
|
||||
"gl": ("Galician", "Galego", 0),
|
||||
"de": ("German", "Deutsch", 0),
|
||||
"he": ("Hebrew", "עִבְרִית", 1255),
|
||||
"hz": ("Herero", "Otjiherero", 0),
|
||||
"ho": ("Hiri Motu", "Hiri Motu", 0),
|
||||
"hu": ("Hungarian", "Magyar", 0),
|
||||
"id": ("Indonesian", "Bahasa Indonesia", 0),
|
||||
"ga": ("Irish", "Gaeilge", 0),
|
||||
"io": ("Ido", "Ido", 0),
|
||||
"is": ("Icelandic", "Íslenska", 0),
|
||||
"it": ("Italian", "Italiano", 0),
|
||||
"jv": ("Javanese", "BasaJawa", 0),
|
||||
"rw": ("Kinyarwanda", "Ikinyarwanda", 0),
|
||||
"kg": ("Kongo", "KiKongo", 0),
|
||||
"kj": ("Kwanyama", "Kuanyama", 0),
|
||||
"la": ("Latin", "Lingua latina", 0),
|
||||
"lb": ("Luxembourgish", "Lëtzebuergesch", 0),
|
||||
"lg": ("Luganda", "Luganda", 0),
|
||||
"li": ("Limburgish", "Limburgs", 0),
|
||||
"ln": ("Lingala", "Lingála", 0),
|
||||
"lt": ("Lithuanian", "Lietuviukalba", 0),
|
||||
"lv": ("Latvian", "Latviešuvaloda", 0),
|
||||
"gv": ("Manx", "Gaelg", 0),
|
||||
"mg": ("Malagasy", "Malagasy fiteny", 0),
|
||||
"mt": ("Maltese", "Malti", 0),
|
||||
"nb": ("Norwegian Bokmål", "Norsk bokmål", 0),
|
||||
"nn": ("Norwegian Nynorsk", "Norsk nynorsk", 0),
|
||||
"no": ("Norwegian", "Norsk", 0),
|
||||
"oc": ("Occitan", "Occitan", 0),
|
||||
"om": ("Oromo", "Afaan Oromoo", 0),
|
||||
"pl": ("Polish", "Polski", 0),
|
||||
"pt": ("Portuguese", "Português", 0),
|
||||
"pt_BR": ("Portuguese Brazillian", "Português Brasileiro", 0),
|
||||
"rm": ("Romansh", "Rumantsch grischun", 0),
|
||||
"rn": ("Kirundi", "kiRundi", 0),
|
||||
"ro": ("Romanian", "Româna", 1250),
|
||||
"sc": ("Sardinian", "Sardu", 0),
|
||||
"se": ("Northern Sami", "Davvisámegiella", 0),
|
||||
"sm": ("Samoan", "Gagana fa'a Samoa", 0),
|
||||
"gd": ("Gaelic", "Gàidhlig", 0),
|
||||
"ru": ("Russian", "русский язык", 1251),
|
||||
"sr": ("Serbian", "српски", 1251),
|
||||
"sn": ("Shona", "Chi Shona", 0),
|
||||
"sk": ("Slovak", "Slovencina", 0),
|
||||
"sl": ("Slovene", "Slovenšcina", 0),
|
||||
"st": ("Southern Sotho", "Sesotho", 0),
|
||||
"es": ("Spanish Castilian", "Español, castellano", 0),
|
||||
"su": ("Sundanese", "Basa Sunda", 0),
|
||||
"sw": ("Swahili", "Kiswahili", 0),
|
||||
"ss": ("Swati", "SiSwati", 0),
|
||||
"sv": ("Swedish", "Svenska", 0),
|
||||
"tn": ("Tswana", "Setswana", 0),
|
||||
"to": ("Tonga (Tonga Islands)", "faka Tonga", 0),
|
||||
"tr": ("Turkish", "Türkçe", 0),
|
||||
"ts": ("Tsonga", "Xitsonga", 0),
|
||||
"tw": ("Twi", "Twi", 0),
|
||||
"ty": ("Tahitian", "Reo Tahiti", 0),
|
||||
"wa": ("Walloon", "Walon", 0),
|
||||
"cy": ("Welsh", "Cymraeg", 0),
|
||||
"wo": ("Wolof", "Wollof", 0),
|
||||
"fy": ("Western Frisian", "Frysk", 0),
|
||||
"xh": ("Xhosa", "isi Xhosa", 0),
|
||||
"yo": ("Yoruba", "Yorùbá", 0),
|
||||
"zu": ("Zulu", "isi Zulu", 0),
|
||||
"zh_CN": ("SimpChinese", "简体中文", 936),
|
||||
"aa": ("Afar", "Afaraf", 0, False),
|
||||
"af": ("Afrikaans", "Afrikaans", 0, False),
|
||||
"ak": ("Akan", "Akan", 0, False),
|
||||
"sq": ("Albanian", "Shqip", 0, False),
|
||||
"an": ("Aragonese", "Aragonés", 0, False),
|
||||
"ae": ("Avestan", "Avesta", 0, False),
|
||||
"ay": ("Aymara", "Aymararu", 0, False),
|
||||
"bm": ("Bambara", "Bamanankan", 0, False),
|
||||
"eu": ("Basque", "Euskara", 0, False),
|
||||
"bi": ("Bislama", "Bislama", 0, False),
|
||||
"bs": ("Bosnian", "Bosanskijezik", 0, False),
|
||||
"br": ("Breton", "Brezhoneg", 0, False),
|
||||
"ca": ("Catalan", "Català", 0, False),
|
||||
"ch": ("Chamorro", "Chamoru", 0, False),
|
||||
"kw": ("Cornish", "Kernewek", 0, False),
|
||||
"co": ("Corsican", "Corsu", 0, False),
|
||||
"hr": ("Croatian", "Hrvatski", 0, False),
|
||||
"cs": ("Czech", "Cesky, ceština", 0, False),
|
||||
"da": ("Danish", "Dansk", 0, False),
|
||||
"nl": ("Dutch", "Nederlands", 0, False),
|
||||
"en": ("English", "English", 0, False),
|
||||
"eo": ("Esperanto", "Esperanto", 0, False),
|
||||
"et": ("Estonian", "Eesti", 0, False),
|
||||
"fo": ("Faroese", "Føroyskt", 0, False),
|
||||
"fj": ("Fijian", "Vosa Vakaviti", 0, False),
|
||||
"fi": ("Finnish", "Suomi", 0, False),
|
||||
"fr": ("French", "Français", 0, False),
|
||||
"gl": ("Galician", "Galego", 0, False),
|
||||
"de": ("German", "Deutsch", 0, False),
|
||||
"he": ("Hebrew", "עִבְרִית", 1255, True),
|
||||
"hz": ("Herero", "Otjiherero", 0, False),
|
||||
"ho": ("Hiri Motu", "Hiri Motu", 0, False),
|
||||
"hu": ("Hungarian", "Magyar", 0, False),
|
||||
"id": ("Indonesian", "Bahasa Indonesia", 0, False),
|
||||
"ga": ("Irish", "Gaeilge", 0, False),
|
||||
"io": ("Ido", "Ido", 0, False),
|
||||
"is": ("Icelandic", "Íslenska", 0, False),
|
||||
"it": ("Italian", "Italiano", 0, False),
|
||||
"jv": ("Javanese", "BasaJawa", 0, False),
|
||||
"rw": ("Kinyarwanda", "Ikinyarwanda", 0, False),
|
||||
"kg": ("Kongo", "KiKongo", 0, False),
|
||||
"kj": ("Kwanyama", "Kuanyama", 0, False),
|
||||
"la": ("Latin", "Lingua latina", 0, False),
|
||||
"lb": ("Luxembourgish", "Lëtzebuergesch", 0, False),
|
||||
"lg": ("Luganda", "Luganda", 0, False),
|
||||
"li": ("Limburgish", "Limburgs", 0, False),
|
||||
"ln": ("Lingala", "Lingála", 0, False),
|
||||
"lt": ("Lithuanian", "Lietuviukalba", 0, False),
|
||||
"lv": ("Latvian", "Latviešuvaloda", 0, False),
|
||||
"gv": ("Manx", "Gaelg", 0, False),
|
||||
"mg": ("Malagasy", "Malagasy fiteny", 0, False),
|
||||
"mt": ("Maltese", "Malti", 0, False),
|
||||
"nb": ("Norwegian Bokmål", "Norsk bokmål", 0, False),
|
||||
"nn": ("Norwegian Nynorsk", "Norsk nynorsk", 0, False),
|
||||
"no": ("Norwegian", "Norsk", 0, False),
|
||||
"oc": ("Occitan", "Occitan", 0, False),
|
||||
"om": ("Oromo", "Afaan Oromoo", 0, False),
|
||||
"pl": ("Polish", "Polski", 0, False),
|
||||
"pt": ("Portuguese", "Português", 0, False),
|
||||
"pt_BR": ("Portuguese Brazillian", "Português Brasileiro", 0, False),
|
||||
"rm": ("Romansh", "Rumantsch grischun", 0, False),
|
||||
"rn": ("Kirundi", "kiRundi", 0, False),
|
||||
"ro": ("Romanian", "Româna", 1250, False),
|
||||
"sc": ("Sardinian", "Sardu", 0, False),
|
||||
"se": ("Northern Sami", "Davvisámegiella", 0, False),
|
||||
"sm": ("Samoan", "Gagana fa'a Samoa", 0, False),
|
||||
"gd": ("Gaelic", "Gàidhlig", 0, False),
|
||||
"ru": ("Russian", "русский язык", 1251, False),
|
||||
"sr": ("Serbian", "српски", 1251, False),
|
||||
"sn": ("Shona", "Chi Shona", 0, False),
|
||||
"sk": ("Slovak", "Slovencina", 0, False),
|
||||
"sl": ("Slovene", "Slovenšcina", 0, False),
|
||||
"st": ("Southern Sotho", "Sesotho", 0, False),
|
||||
"es": ("Spanish Castilian", "Español, castellano", 0, False),
|
||||
"su": ("Sundanese", "Basa Sunda", 0, False),
|
||||
"sw": ("Swahili", "Kiswahili", 0, False),
|
||||
"ss": ("Swati", "SiSwati", 0, False),
|
||||
"sv": ("Swedish", "Svenska", 0, False),
|
||||
"tn": ("Tswana", "Setswana", 0, False),
|
||||
"to": ("Tonga (Tonga Islands)", "faka Tonga", 0, False),
|
||||
"tr": ("Turkish", "Türkçe", 0, False),
|
||||
"ts": ("Tsonga", "Xitsonga", 0, False),
|
||||
"tw": ("Twi", "Twi", 0, False),
|
||||
"ty": ("Tahitian", "Reo Tahiti", 0, False),
|
||||
"wa": ("Walloon", "Walon", 0, False),
|
||||
"cy": ("Welsh", "Cymraeg", 0, False),
|
||||
"wo": ("Wolof", "Wollof", 0, False),
|
||||
"fy": ("Western Frisian", "Frysk", 0, False),
|
||||
"xh": ("Xhosa", "isi Xhosa", 0, False),
|
||||
"yo": ("Yoruba", "Yorùbá", 0, False),
|
||||
"zu": ("Zulu", "isi Zulu", 0, False),
|
||||
"zh_CN": ("SimpChinese", "简体中文", 936, False),
|
||||
}
|
||||
|
||||
# Setup a safe null-translation
|
||||
|
||||
@@ -781,11 +781,7 @@ def get_all_passwords(nzo):
|
||||
meta_passwords.append(pw)
|
||||
|
||||
if meta_passwords:
|
||||
if nzo.password == meta_passwords[0]:
|
||||
# this nzo.password came from meta, so don't use it twice
|
||||
passwords.extend(meta_passwords[1:])
|
||||
else:
|
||||
passwords.extend(meta_passwords)
|
||||
passwords.extend(meta_passwords)
|
||||
logging.info("Read %s passwords from meta data in NZB: %s", len(meta_passwords), meta_passwords)
|
||||
|
||||
pw_file = cfg.password_file.get_path()
|
||||
@@ -808,6 +804,7 @@ def get_all_passwords(nzo):
|
||||
)
|
||||
except:
|
||||
logging.warning(T("Failed to read the password file %s"), pw_file)
|
||||
logging.info("Traceback: ", exc_info=True)
|
||||
|
||||
if nzo.password:
|
||||
# If an explicit password was set, add a retry without password, just in case.
|
||||
@@ -816,7 +813,7 @@ def get_all_passwords(nzo):
|
||||
# If we're not sure about encryption, start with empty password
|
||||
# and make sure we have at least the empty password
|
||||
passwords.insert(0, "")
|
||||
return passwords
|
||||
return set(passwords)
|
||||
|
||||
|
||||
def find_on_path(targets):
|
||||
@@ -837,27 +834,47 @@ def find_on_path(targets):
|
||||
return None
|
||||
|
||||
|
||||
def probablyipv4(ip):
|
||||
def is_ipv4_addr(ip: str) -> bool:
|
||||
""" Determine if the ip is an IPv4 address """
|
||||
try:
|
||||
return ipaddress.ip_address(ip).version == 4
|
||||
except:
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def probablyipv6(ip):
|
||||
# Returns True if the given input is probably an IPv6 address
|
||||
# Square Brackets like '[2001::1]' are OK
|
||||
def is_ipv6_addr(ip: str) -> bool:
|
||||
""" Determine if the ip is an IPv6 address; square brackets ([2001::1]) are OK """
|
||||
try:
|
||||
# Check for plain IPv6 address
|
||||
return ipaddress.ip_address(ip).version == 6
|
||||
except:
|
||||
try:
|
||||
# Remove '[' and ']' and test again:
|
||||
ip = re.search(r"^\[(.*)\]$", ip).group(1)
|
||||
return ipaddress.ip_address(ip).version == 6
|
||||
except:
|
||||
# No, not an IPv6 address
|
||||
return False
|
||||
return ipaddress.ip_address(ip.strip("[]")).version == 6
|
||||
except (ValueError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def is_loopback_addr(ip: str) -> bool:
|
||||
""" Determine if the ip is an IPv4 or IPv6 local loopback address """
|
||||
try:
|
||||
if ip.find(".") < 0:
|
||||
ip = ip.strip("[]")
|
||||
return ipaddress.ip_address(ip).is_loopback
|
||||
except (ValueError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def is_localhost(value: str) -> bool:
|
||||
""" Determine if the input is some variety of 'localhost' """
|
||||
return (value == "localhost") or is_loopback_addr(value)
|
||||
|
||||
|
||||
def is_lan_addr(ip: str) -> bool:
|
||||
""" Determine if the ip is a local area network address """
|
||||
try:
|
||||
return (
|
||||
ip not in ("0.0.0.0", "255.255.255.255", "::")
|
||||
and ipaddress.ip_address(ip).is_private
|
||||
and not is_loopback_addr(ip)
|
||||
)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def ip_extract() -> List[str]:
|
||||
|
||||
@@ -55,6 +55,7 @@ from sabnzbd.filesystem import (
|
||||
setname_from_path,
|
||||
get_ext,
|
||||
get_filename,
|
||||
same_file,
|
||||
)
|
||||
from sabnzbd.nzbstuff import NzbObject, NzbFile
|
||||
from sabnzbd.sorting import SeriesSorter
|
||||
@@ -2077,7 +2078,14 @@ def quick_check_set(set, nzo):
|
||||
if nzf.md5sum == md5pack[file]:
|
||||
try:
|
||||
logging.debug("Quick-check will rename %s to %s", nzf.filename, file)
|
||||
renamer(os.path.join(nzo.download_path, nzf.filename), os.path.join(nzo.download_path, file))
|
||||
|
||||
# Note: file can and is allowed to be in a subdirectory.
|
||||
# Subdirectories in par2 always contain "/", not "\"
|
||||
renamer(
|
||||
os.path.join(nzo.download_path, nzf.filename),
|
||||
os.path.join(nzo.download_path, file),
|
||||
create_local_directories=True,
|
||||
)
|
||||
renames[file] = nzf.filename
|
||||
nzf.filename = file
|
||||
result &= True
|
||||
|
||||
@@ -32,7 +32,7 @@ import sabnzbd
|
||||
import sabnzbd.cfg
|
||||
from sabnzbd.constants import DEF_TIMEOUT
|
||||
from sabnzbd.encoding import utob
|
||||
from sabnzbd.misc import nntp_to_msg, probablyipv4, probablyipv6, get_server_addrinfo
|
||||
from sabnzbd.misc import nntp_to_msg, is_ipv4_addr, is_ipv6_addr, get_server_addrinfo
|
||||
|
||||
# Set pre-defined socket timeout
|
||||
socket.setdefaulttimeout(DEF_TIMEOUT)
|
||||
@@ -269,9 +269,9 @@ class NNTP:
|
||||
af, socktype, proto, canonname, sa = self.nw.server.info[0]
|
||||
|
||||
# there will be a connect to host (or self.host, so let's force set 'af' to the correct value
|
||||
if probablyipv4(self.host):
|
||||
if is_ipv4_addr(self.host):
|
||||
af = socket.AF_INET
|
||||
if probablyipv6(self.host):
|
||||
if is_ipv6_addr(self.host):
|
||||
af = socket.AF_INET6
|
||||
|
||||
# Secured or unsecured?
|
||||
|
||||
@@ -58,7 +58,7 @@ def nzbfile_parser(raw_data, nzo):
|
||||
if meta_type not in nzo.meta:
|
||||
nzo.meta[meta_type] = []
|
||||
nzo.meta[meta_type].append(meta.text)
|
||||
logging.debug("NZB Meta-data = %s", nzo.meta)
|
||||
logging.debug("NZB file meta-data = %s", nzo.meta)
|
||||
|
||||
# Parse the files
|
||||
for file in nzb_tree.iter("file"):
|
||||
|
||||
@@ -170,21 +170,18 @@ class Article(TryList):
|
||||
if not self.fetcher and not self.server_in_try_list(server):
|
||||
if log:
|
||||
logging.debug("Article %s | Server: %s | in second if", self.article, server.host)
|
||||
# Is the current selected server of the same priority as this article?
|
||||
if log:
|
||||
# Is the current selected server of the same priority as this article?
|
||||
logging.debug(
|
||||
"Article %s | Server: %s | Article priority: %s", self.article, server.host, self.fetcher_priority
|
||||
)
|
||||
if log:
|
||||
logging.debug(
|
||||
"Article %s | Server: %s | Server priority: %s", self.article, server.host, server.priority
|
||||
)
|
||||
if server.priority == self.fetcher_priority:
|
||||
if log:
|
||||
logging.debug("Article %s | Server: %s | same priority, use it", self.article, server.host)
|
||||
self.fetcher = server
|
||||
self.tries += 1
|
||||
if log:
|
||||
logging.debug("Article %s | Server: %s | same priority, use it", self.article, server.host)
|
||||
logging.debug("Article %s | Server: %s | Article-try: %s", self.article, server.host, self.tries)
|
||||
return self
|
||||
else:
|
||||
@@ -904,6 +901,7 @@ class NzbObject(TryList):
|
||||
for kw in self.meta:
|
||||
if not self.nzo_info.get(kw):
|
||||
self.nzo_info[kw] = self.meta[kw][0]
|
||||
logging.debug("NZB nzo-info = %s", self.nzo_info)
|
||||
|
||||
# Show first meta-password (if any), when there's no explicit password
|
||||
if not self.password and self.meta.get("password"):
|
||||
|
||||
@@ -678,6 +678,8 @@ SKIN_TEXT = {
|
||||
"addMultipleFeeds": TT("Seperate multiple URLs by a comma"), #: Config->RSS, placeholder (cannot be too long)
|
||||
"button-preFeed": TT("Read Feed"), #: Config->RSS button
|
||||
"button-forceFeed": TT("Force Download"), #: Config->RSS button
|
||||
"rss-edit": TT("Edit"), #: Config->RSS edit button
|
||||
"rss-nextscan": TT("Next scan at"), #: Config->RSS when will be the next RSS scan
|
||||
"rss-order": TT("Order"), #: Config->RSS table column header
|
||||
"rss-type": TT("Type"), #: Config->RSS table column header
|
||||
"rss-filter": TT("Filter"), #: Config->RSS table column header
|
||||
|
||||
@@ -35,6 +35,7 @@ from sabnzbd.filesystem import (
|
||||
get_unique_filename,
|
||||
get_ext,
|
||||
renamer,
|
||||
sanitize_and_trim_path,
|
||||
sanitize_foldername,
|
||||
clip_path,
|
||||
)
|
||||
@@ -492,6 +493,7 @@ class SeriesSorter:
|
||||
newpath = os.path.join(current_path, newname)
|
||||
# Replace %ext with extension
|
||||
newpath = newpath.replace("%ext", self.ext)
|
||||
newpath = sanitize_and_trim_path(newpath)
|
||||
try:
|
||||
logging.debug("Rename: %s to %s", filepath, newpath)
|
||||
renamer(filepath, newpath)
|
||||
|
||||
@@ -22,7 +22,10 @@ def diskspeedmeasure(my_dirname: str) -> float:
|
||||
|
||||
try:
|
||||
# Use low-level I/O
|
||||
fp_testfile = os.open(filename, os.O_CREAT | os.O_WRONLY, 0o777)
|
||||
try:
|
||||
fp_testfile = os.open(filename, os.O_CREAT | os.O_WRONLY | os.O_BINARY, 0o777)
|
||||
except AttributeError:
|
||||
fp_testfile = os.open(filename, os.O_CREAT | os.O_WRONLY, 0o777)
|
||||
|
||||
# Start looping
|
||||
total_time = 0.0
|
||||
|
||||
@@ -35,7 +35,7 @@ except:
|
||||
|
||||
import sabnzbd
|
||||
import sabnzbd.cfg as cfg
|
||||
from sabnzbd.constants import LOCALHOSTS
|
||||
from sabnzbd.misc import is_localhost
|
||||
|
||||
_BONJOUR_OBJECT = None
|
||||
|
||||
@@ -58,7 +58,7 @@ def set_bonjour(host=None, port=None):
|
||||
""" Publish host/port combo through Bonjour """
|
||||
global _HOST_PORT, _BONJOUR_OBJECT
|
||||
|
||||
if not _HAVE_BONJOUR or not cfg.enable_bonjour():
|
||||
if not _HAVE_BONJOUR or not cfg.enable_broadcast():
|
||||
logging.info("No bonjour/zeroconf support installed")
|
||||
return
|
||||
|
||||
@@ -71,8 +71,8 @@ def set_bonjour(host=None, port=None):
|
||||
zhost = None
|
||||
domain = None
|
||||
|
||||
if host in LOCALHOSTS:
|
||||
logging.info("bonjour/zeroconf cannot be one of %s", LOCALHOSTS)
|
||||
if is_localhost(host):
|
||||
logging.info("Cannot setup bonjour/zeroconf for localhost (%s)", host)
|
||||
# All implementations fail to implement "localhost" properly
|
||||
# A false address is published even when scope==kDNSServiceInterfaceIndexLocalOnly
|
||||
return
|
||||
|
||||
@@ -32,6 +32,11 @@ def create_big_file(filename):
|
||||
myfile.truncate(15 * 1024 * 1024)
|
||||
|
||||
|
||||
def create_small_file(filename):
|
||||
with open(filename, "wb") as myfile:
|
||||
myfile.truncate(1024)
|
||||
|
||||
|
||||
class TestDeobfuscateFinalResult:
|
||||
def test_is_probably_obfuscated(self):
|
||||
# Test the base function test_is_probably_obfuscated(), which gives a boolean as RC
|
||||
@@ -178,6 +183,107 @@ class TestDeobfuscateFinalResult:
|
||||
# Done. Remove (non-empty) directory
|
||||
shutil.rmtree(dirname)
|
||||
|
||||
def test_deobfuscate_big_file_small_accompanying_files(self):
|
||||
# input: myiso.iso, with accompanying files (.srt files in this case)
|
||||
# test that the small accompanying files (with same basename) are renamed accordingly to the big ISO
|
||||
|
||||
# Create directory (with a random directory name)
|
||||
dirname = os.path.join(SAB_DATA_DIR, "testdir" + str(random.randint(10000, 99999)))
|
||||
os.mkdir(dirname)
|
||||
|
||||
# Create a big enough file with a non-useful filename
|
||||
isofile = os.path.join(dirname, "myiso.iso")
|
||||
create_big_file(isofile)
|
||||
assert os.path.isfile(isofile)
|
||||
|
||||
# and a srt file
|
||||
srtfile = os.path.join(dirname, "myiso.srt")
|
||||
create_small_file(srtfile)
|
||||
assert os.path.isfile(srtfile)
|
||||
|
||||
# and a dut.srt file
|
||||
dutsrtfile = os.path.join(dirname, "myiso.dut.srt")
|
||||
create_small_file(dutsrtfile)
|
||||
assert os.path.isfile(dutsrtfile)
|
||||
|
||||
# and a non-related file
|
||||
txtfile = os.path.join(dirname, "something.txt")
|
||||
create_small_file(txtfile)
|
||||
assert os.path.isfile(txtfile)
|
||||
|
||||
# create the filelist, with just the above files
|
||||
myfilelist = [isofile, srtfile, dutsrtfile, txtfile]
|
||||
|
||||
# and now unleash the magic on that filelist, with a more useful jobname:
|
||||
jobname = "My Important Download 2020"
|
||||
deobfuscate_list(myfilelist, jobname)
|
||||
|
||||
# Check original files:
|
||||
assert not os.path.isfile(isofile) # original iso not be there anymore
|
||||
assert not os.path.isfile(srtfile) # ... and accompanying file neither
|
||||
assert not os.path.isfile(dutsrtfile) # ... and this one neither
|
||||
assert os.path.isfile(txtfile) # should still be there: not accompanying, and too small to rename
|
||||
|
||||
# Check the renaming
|
||||
assert os.path.isfile(os.path.join(dirname, jobname + ".iso")) # ... should be renamed to the jobname
|
||||
assert os.path.isfile(os.path.join(dirname, jobname + ".srt")) # ... should be renamed to the jobname
|
||||
assert os.path.isfile(os.path.join(dirname, jobname + ".dut.srt")) # ... should be renamed to the jobname
|
||||
|
||||
# Done. Remove (non-empty) directory
|
||||
shutil.rmtree(dirname)
|
||||
|
||||
def test_deobfuscate_collection_with_same_extension(self):
|
||||
# input: a collection of bigger files with the same extension
|
||||
# test that there is no renaming on the collection ... as that's useless on a collection
|
||||
|
||||
# Create directory (with a random directory name)
|
||||
dirname = os.path.join(SAB_DATA_DIR, "testdir" + str(random.randint(10000, 99999)))
|
||||
os.mkdir(dirname)
|
||||
|
||||
# Create big enough files with a non-useful filenames, all with same extension
|
||||
file1 = os.path.join(dirname, "file1.bla")
|
||||
create_big_file(file1)
|
||||
assert os.path.isfile(file1)
|
||||
|
||||
file2 = os.path.join(dirname, "file2.bla")
|
||||
create_big_file(file2)
|
||||
assert os.path.isfile(file2)
|
||||
|
||||
file3 = os.path.join(dirname, "file3.bla")
|
||||
create_big_file(file3)
|
||||
assert os.path.isfile(file3)
|
||||
|
||||
file4 = os.path.join(dirname, "file4.bla")
|
||||
create_big_file(file4)
|
||||
assert os.path.isfile(file4)
|
||||
|
||||
# other extension ... so this one should get renamed
|
||||
otherfile = os.path.join(dirname, "other.bin")
|
||||
create_big_file(otherfile)
|
||||
assert os.path.isfile(otherfile)
|
||||
|
||||
# create the filelist, with the above files
|
||||
myfilelist = [file1, file2, file3, file4, otherfile]
|
||||
|
||||
# and now unleash the magic on that filelist, with a more useful jobname:
|
||||
jobname = "My Important Download 2020"
|
||||
deobfuscate_list(myfilelist, jobname)
|
||||
|
||||
# Check original files:
|
||||
# the collection with same extension should still be there:
|
||||
assert os.path.isfile(file1) # still there
|
||||
assert os.path.isfile(file2) # still there
|
||||
assert os.path.isfile(file3) # still there
|
||||
assert os.path.isfile(file4) # still there
|
||||
# but the one separate file with obfuscated name should be renamed:
|
||||
assert not os.path.isfile(otherfile) # should be renamed
|
||||
|
||||
# Check the renaming
|
||||
assert os.path.isfile(os.path.join(dirname, jobname + ".bin")) # ... should be renamed to the jobname
|
||||
|
||||
# Done. Remove (non-empty) directory
|
||||
shutil.rmtree(dirname)
|
||||
|
||||
def test_deobfuscate_filelist_nasty_tests(self):
|
||||
# check no problems occur with nasty use cases
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ tests.test_filesystem - Testing functions in filesystem.py
|
||||
import stat
|
||||
import sys
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pyfakefs.fake_filesystem_unittest as ffs
|
||||
|
||||
@@ -983,3 +986,73 @@ class TestSetPermissions(ffs.TestCase, PermissionCheckerHelper):
|
||||
def test_dir1755_umask4755_setting(self):
|
||||
# Sticky bit on directory, umask with setuid
|
||||
self._runner("1755", "4755")
|
||||
|
||||
|
||||
class TestRenamer:
|
||||
# test filesystem.renamer() for different scenario's
|
||||
def test_renamer(self):
|
||||
# First of all, create a working directory (with a random name)
|
||||
dirname = os.path.join(SAB_DATA_DIR, "testdir" + str(random.randint(10000, 99999)))
|
||||
os.mkdir(dirname)
|
||||
|
||||
# base case: rename file within directory
|
||||
filename = os.path.join(dirname, "myfile.txt")
|
||||
Path(filename).touch() # create file
|
||||
newfilename = os.path.join(dirname, "newfile.txt")
|
||||
filesystem.renamer(filename, newfilename) # rename() does not return a value ...
|
||||
assert not os.path.isfile(filename)
|
||||
assert os.path.isfile(newfilename)
|
||||
|
||||
# standard behaviour: renaming (moving) into an exiting other directory *is* allowed
|
||||
filename = os.path.join(dirname, "myfile.txt")
|
||||
Path(filename).touch() # create file
|
||||
sameleveldirname = os.path.join(SAB_DATA_DIR, "othertestdir" + str(random.randint(10000, 99999)))
|
||||
os.mkdir(sameleveldirname)
|
||||
newfilename = os.path.join(sameleveldirname, "newfile.txt")
|
||||
filesystem.renamer(filename, newfilename)
|
||||
assert not os.path.isfile(filename)
|
||||
assert os.path.isfile(newfilename)
|
||||
shutil.rmtree(sameleveldirname)
|
||||
|
||||
# Default: renaming into a non-existing subdirectory not allowed
|
||||
Path(filename).touch() # create file
|
||||
newfilename = os.path.join(dirname, "nonexistingsubdir", "newfile.txt")
|
||||
try:
|
||||
filesystem.renamer(filename, newfilename) # rename() does not return a value ...
|
||||
except:
|
||||
pass
|
||||
assert os.path.isfile(filename)
|
||||
assert not os.path.isfile(newfilename)
|
||||
|
||||
# Creation of subdirectory is allowed if create_local_directories=True
|
||||
Path(filename).touch()
|
||||
newfilename = os.path.join(dirname, "newsubdir", "newfile.txt")
|
||||
try:
|
||||
filesystem.renamer(filename, newfilename, create_local_directories=True)
|
||||
except:
|
||||
pass
|
||||
assert not os.path.isfile(filename)
|
||||
assert os.path.isfile(newfilename)
|
||||
|
||||
# Creation of subdirectory plus deeper sudbdir is allowed if create_local_directories=True
|
||||
Path(filename).touch()
|
||||
newfilename = os.path.join(dirname, "newsubdir", "deepersubdir", "newfile.txt")
|
||||
try:
|
||||
filesystem.renamer(filename, newfilename, create_local_directories=True)
|
||||
except:
|
||||
pass
|
||||
assert not os.path.isfile(filename)
|
||||
assert os.path.isfile(newfilename)
|
||||
|
||||
# ... escaping the directory plus subdir creation is not allowed
|
||||
Path(filename).touch()
|
||||
newfilename = os.path.join(dirname, "..", "newsubdir", "newfile.txt")
|
||||
try:
|
||||
filesystem.renamer(filename, newfilename, create_local_directories=True)
|
||||
except:
|
||||
pass
|
||||
assert os.path.isfile(filename)
|
||||
assert not os.path.isfile(newfilename)
|
||||
|
||||
# Cleanup working directory
|
||||
shutil.rmtree(dirname)
|
||||
|
||||
@@ -21,7 +21,7 @@ tests.test_utils.test_check_dir - Testing SABnzbd checkdir util
|
||||
|
||||
from sabnzbd.cfg import selftest_host
|
||||
from sabnzbd.getipaddress import *
|
||||
from sabnzbd.misc import probablyipv4, probablyipv6
|
||||
from sabnzbd.misc import is_ipv4_addr, is_ipv6_addr
|
||||
|
||||
|
||||
class TestGetIpAddress:
|
||||
@@ -33,14 +33,14 @@ class TestGetIpAddress:
|
||||
|
||||
def test_publicipv4(self):
|
||||
public_ipv4 = publicipv4()
|
||||
assert probablyipv4(public_ipv4)
|
||||
assert is_ipv4_addr(public_ipv4)
|
||||
|
||||
def test_localipv4(self):
|
||||
local_ipv4 = localipv4()
|
||||
assert probablyipv4(local_ipv4)
|
||||
assert is_ipv4_addr(local_ipv4)
|
||||
|
||||
def test_ipv6(self):
|
||||
test_ipv6 = ipv6()
|
||||
# Not all systems have IPv6
|
||||
if test_ipv6:
|
||||
assert probablyipv6(test_ipv6)
|
||||
assert is_ipv6_addr(test_ipv6)
|
||||
|
||||
@@ -230,6 +230,154 @@ class TestMisc:
|
||||
# Make sure the output is cmd.exe-compatible
|
||||
assert res == expected_output
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, result",
|
||||
[
|
||||
("1.2.3.4", True),
|
||||
("255.255.255.255", True),
|
||||
("0.0.0.0", True),
|
||||
("10.11.12.13", True),
|
||||
("127.0.0.1", True),
|
||||
("400.500.600.700", False),
|
||||
("blabla", False),
|
||||
("2001::1", False),
|
||||
("::1", False),
|
||||
("::", False),
|
||||
("example.org", False),
|
||||
(None, False),
|
||||
("", False),
|
||||
("3.2.0", False),
|
||||
(-42, False),
|
||||
],
|
||||
)
|
||||
def test_is_ipv4_addr(self, value, result):
|
||||
assert misc.is_ipv4_addr(value) is result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, result",
|
||||
[
|
||||
("2001::1", True),
|
||||
("::1", True),
|
||||
("[2001::1]", True),
|
||||
("fdd6:5a2d:3f20:0:14b0:d8f4:ccb9:fab6", True),
|
||||
("::", True),
|
||||
("a::b", True),
|
||||
("1.2.3.4", False),
|
||||
("255.255.255.255", False),
|
||||
("0.0.0.0", False),
|
||||
("10.11.12.13", False),
|
||||
("127.0.0.1", False),
|
||||
("400.500.600.700", False),
|
||||
("blabla", False),
|
||||
(666, False),
|
||||
("example.org", False),
|
||||
(None, False),
|
||||
("", False),
|
||||
("[1.2.3.4]", False),
|
||||
("2001:1", False),
|
||||
("2001::[2001::1]", False),
|
||||
],
|
||||
)
|
||||
def test_is_ipv6_addr(self, value, result):
|
||||
assert misc.is_ipv6_addr(value) is result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, result",
|
||||
[
|
||||
("::1", True),
|
||||
("[::1]", True),
|
||||
("127.0.0.1", True),
|
||||
("127.255.0.0", True),
|
||||
("127.1.2.7", True),
|
||||
("fdd6:5a2d:3f20:0:14b0:d8f4:ccb9:fab6", False),
|
||||
("::", False),
|
||||
("a::b", False),
|
||||
("1.2.3.4", False),
|
||||
("255.255.255.255", False),
|
||||
("0.0.0.0", False),
|
||||
("10.11.12.13", False),
|
||||
("400.500.600.700", False),
|
||||
("localhost", False),
|
||||
(-666, False),
|
||||
("example.org", False),
|
||||
(None, False),
|
||||
("", False),
|
||||
("[127.6.6.6]", False),
|
||||
("2001:1", False),
|
||||
("2001::[2001::1]", False),
|
||||
],
|
||||
)
|
||||
def test_is_loopback_addr(self, value, result):
|
||||
assert misc.is_loopback_addr(value) is result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, result",
|
||||
[
|
||||
("localhost", True),
|
||||
("::1", True),
|
||||
("[::1]", True),
|
||||
("localhost", True),
|
||||
("127.0.0.1", True),
|
||||
("127.255.0.0", True),
|
||||
("127.1.2.7", True),
|
||||
(".local", False),
|
||||
("test.local", False),
|
||||
("fdd6:5a2d:3f20:0:14b0:d8f4:ccb9:fab6", False),
|
||||
("::", False),
|
||||
("a::b", False),
|
||||
("1.2.3.4", False),
|
||||
("255.255.255.255", False),
|
||||
("0.0.0.0", False),
|
||||
("10.11.12.13", False),
|
||||
("400.500.600.700", False),
|
||||
(-1984, False),
|
||||
("example.org", False),
|
||||
(None, False),
|
||||
("", False),
|
||||
("[127.6.6.6]", False),
|
||||
("2001:1", False),
|
||||
("2001::[2001::1]", False),
|
||||
],
|
||||
)
|
||||
def test_is_localhost(self, value, result):
|
||||
assert misc.is_localhost(value) is result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, result",
|
||||
[
|
||||
("10.11.12.13", True),
|
||||
("172.16.2.81", True),
|
||||
("192.168.255.255", True),
|
||||
("169.254.42.42", True), # Link-local
|
||||
("fd00::ffff", True), # Part of fc00::/7, IPv6 "Unique Local Addresses"
|
||||
("fe80::a1", True), # IPv6 Link-local
|
||||
("::1", False),
|
||||
("localhost", False),
|
||||
("127.0.0.1", False),
|
||||
("2001:1337:babe::", False),
|
||||
("172.32.32.32", False), # Near but not part of 172.16.0.0/12
|
||||
("100.64.0.1", False), # Test net
|
||||
("[2001::1]", False),
|
||||
("::", False),
|
||||
("::a:b:c", False),
|
||||
("1.2.3.4", False),
|
||||
("255.255.255.255", False),
|
||||
("0.0.0.0", False),
|
||||
("127.0.0.1", False),
|
||||
("400.500.600.700", False),
|
||||
("blabla", False),
|
||||
(-666, False),
|
||||
("example.org", False),
|
||||
(None, False),
|
||||
("", False),
|
||||
("[1.2.3.4]", False),
|
||||
("2001:1", False),
|
||||
("2001::[2001::1]", False),
|
||||
],
|
||||
)
|
||||
def test_is_lan_addr(self, value, result):
|
||||
assert misc.is_lan_addr(value) is result
|
||||
|
||||
|
||||
class TestBuildAndRunCommand:
|
||||
# Path should exist
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/python3 -OO
|
||||
# Copyright 2007-2021 The SABnzbd-Team <team@sabnzbd.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
"""
|
||||
tests.test_utils.test_probablyip - Testing SABnzbd's probablyipX() functions
|
||||
"""
|
||||
|
||||
from sabnzbd.misc import *
|
||||
|
||||
|
||||
class TestProbablyIP:
|
||||
def test_probablyipv4(self):
|
||||
# Positive testing
|
||||
assert probablyipv4("1.2.3.4")
|
||||
assert probablyipv4("255.255.255.255")
|
||||
assert probablyipv4("0.0.0.0")
|
||||
# Negative testing
|
||||
assert not probablyipv4("400.500.600.700")
|
||||
assert not probablyipv4("blabla")
|
||||
assert not probablyipv4("2001::1")
|
||||
|
||||
def test_probablyipv6(self):
|
||||
# Positive testing
|
||||
assert probablyipv6("2001::1")
|
||||
assert probablyipv6("[2001::1]")
|
||||
assert probablyipv6("fdd6:5a2d:3f20:0:14b0:d8f4:ccb9:fab6")
|
||||
# Negative testing
|
||||
assert not probablyipv6("blabla")
|
||||
assert not probablyipv6("1.2.3.4")
|
||||
assert not probablyipv6("[1.2.3.4]")
|
||||
assert not probablyipv6("2001:1")
|
||||
assert not probablyipv6("2001::[2001::1]")
|
||||
Reference in New Issue
Block a user