mirror of
https://github.com/sabnzbd/sabnzbd.git
synced 2026-08-07 04:44:18 -04:00
Compare commits
168
Commits
4.4.0Alpha2
...
3.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc831e16d8 | ||
|
|
b8dc46ad01 | ||
|
|
d8ab19087d | ||
|
|
ec8a79eedd | ||
|
|
f1e2a8e9d8 | ||
|
|
4042a5fe5d | ||
|
|
a4752751ed | ||
|
|
e23ecf46d1 | ||
|
|
70a8c597a6 | ||
|
|
fa639bdb53 | ||
|
|
233bdd5b1d | ||
|
|
a0ab6d35c7 | ||
|
|
bd29680ce7 | ||
|
|
7139e92554 | ||
|
|
897df53466 | ||
|
|
58281711f6 | ||
|
|
b524383aa3 | ||
|
|
75a16e3588 | ||
|
|
1453032ad6 | ||
|
|
824ab4afad | ||
|
|
73dd41c67f | ||
|
|
59ee77355d | ||
|
|
5c758773ad | ||
|
|
46de49df06 | ||
|
|
d1c54a9a74 | ||
|
|
e7527c45cd | ||
|
|
7d5207aa67 | ||
|
|
654302e691 | ||
|
|
ee673b57fd | ||
|
|
2be374b841 | ||
|
|
906e1eda89 | ||
|
|
ece02cc4fa | ||
|
|
876ad60ddf | ||
|
|
862da354ac | ||
|
|
8fd477b979 | ||
|
|
2d7005655c | ||
|
|
7322f8348a | ||
|
|
e3e3a12e73 | ||
|
|
77cdd057a4 | ||
|
|
e8206fbdd9 | ||
|
|
589f15a77b | ||
|
|
7bb443678a | ||
|
|
6390415101 | ||
|
|
4abf192e11 | ||
|
|
1fed37f9da | ||
|
|
a9d86a7447 | ||
|
|
2abe4c3cef | ||
|
|
0542c25003 | ||
|
|
1b8ee4e290 | ||
|
|
51128cba55 | ||
|
|
3612432581 | ||
|
|
deca000a1b | ||
|
|
39cccb5653 | ||
|
|
f6838dc985 | ||
|
|
8cd4d92395 | ||
|
|
3bf9906f45 | ||
|
|
9f7daf96ef | ||
|
|
67de4df155 | ||
|
|
bc51a4bd1c | ||
|
|
bb54616018 | ||
|
|
6bcff5e014 | ||
|
|
8970a03a9a | ||
|
|
3ad717ca35 | ||
|
|
b14f72c67a | ||
|
|
45d036804f | ||
|
|
8f606db233 | ||
|
|
3766ba5402 | ||
|
|
e851813cef | ||
|
|
4d49ad9141 | ||
|
|
16618b3af2 | ||
|
|
0e5c0f664f | ||
|
|
7be9281431 | ||
|
|
ee0327fac1 | ||
|
|
9930de3e7f | ||
|
|
e8503e89c6 | ||
|
|
1d9ed419eb | ||
|
|
0207652e3e | ||
|
|
0f1e99c5cb | ||
|
|
f134bc7efb | ||
|
|
dcd7c7180e | ||
|
|
fbbfcd075b | ||
|
|
f42d2e4140 | ||
|
|
88882cebbc | ||
|
|
17a979675c | ||
|
|
4642850c79 | ||
|
|
e8d6eebb04 | ||
|
|
864c5160c0 | ||
|
|
99b5a00c12 | ||
|
|
85ee1f07d7 | ||
|
|
e58b4394e0 | ||
|
|
1e91a57bf1 | ||
|
|
39cee52a7e | ||
|
|
72068f939d | ||
|
|
096d0d3cad | ||
|
|
2472ab0121 | ||
|
|
00421717b8 | ||
|
|
ae96d93f94 | ||
|
|
8522c40c8f | ||
|
|
23f86e95f1 | ||
|
|
eed2045189 | ||
|
|
217785bf0f | ||
|
|
6aef50dc5d | ||
|
|
16b6e3caa7 | ||
|
|
3de4c99a8a | ||
|
|
980aa19a75 | ||
|
|
fb4b57e056 | ||
|
|
03638365ea | ||
|
|
157cb1c83d | ||
|
|
e51f11c2b1 | ||
|
|
1ad0961dd8 | ||
|
|
46ff7dd4e2 | ||
|
|
8b067df914 | ||
|
|
ef43b13272 | ||
|
|
e8e9974224 | ||
|
|
feebbb9f04 | ||
|
|
bc4f06dd1d | ||
|
|
971e4fc909 | ||
|
|
51cc765949 | ||
|
|
19c6a4fffa | ||
|
|
105ac32d2f | ||
|
|
57550675d2 | ||
|
|
e674abc5c0 | ||
|
|
f965c96f51 | ||
|
|
c76b8ed9e0 | ||
|
|
4fbd0d8a7b | ||
|
|
2186c0fff6 | ||
|
|
1adca9a9c1 | ||
|
|
9408353f2b | ||
|
|
84f4d453d2 | ||
|
|
d10209f2a1 | ||
|
|
3ae149c72f | ||
|
|
47385acc3b | ||
|
|
814eeaa900 | ||
|
|
5f2ea13aad | ||
|
|
41ca217931 | ||
|
|
b57d36e8dd | ||
|
|
9a4be70734 | ||
|
|
a8443595a6 | ||
|
|
fd0a70ac58 | ||
|
|
8a8685c968 | ||
|
|
9e6cb8da8e | ||
|
|
054ec54d51 | ||
|
|
272ce773cb | ||
|
|
050b925f7b | ||
|
|
0087940898 | ||
|
|
e323c014f9 | ||
|
|
cc465c7554 | ||
|
|
14cb37564f | ||
|
|
094db56c3b | ||
|
|
aabb709b8b | ||
|
|
0833dd2db9 | ||
|
|
cd3f912be4 | ||
|
|
665c516db6 | ||
|
|
b670da9fa0 | ||
|
|
80bee9bffe | ||
|
|
d85a70e8ad | ||
|
|
8f21533e76 | ||
|
|
89996482a1 | ||
|
|
03c10dce91 | ||
|
|
bd5331be05 | ||
|
|
46e1645289 | ||
|
|
4ce3965747 | ||
|
|
9d4af19db3 | ||
|
|
48e034f4be | ||
|
|
f8959baa2f | ||
|
|
8ed5997eae | ||
|
|
daf9f50ac8 | ||
|
|
6b11013c1a |
No files matched your search
@@ -8,15 +8,16 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
|
||||||
os: [ubuntu-20.04]
|
os: [ubuntu-20.04]
|
||||||
include:
|
include:
|
||||||
|
# TODO: Update to 3.10 when all packages are available, currently lxml is missing
|
||||||
- name: macOS
|
- name: macOS
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
python-version: 3.9
|
python-version: "3.9"
|
||||||
- name: Windows
|
- name: Windows
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
python-version: 3.9
|
python-version: "3.9"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -30,7 +31,7 @@ jobs:
|
|||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: |
|
run: |
|
||||||
python --version
|
python --version
|
||||||
pip install --upgrade pip
|
pip install --upgrade pip wheel
|
||||||
pip install --upgrade -r requirements.txt
|
pip install --upgrade -r requirements.txt
|
||||||
pip install --upgrade -r tests/requirements.txt
|
pip install --upgrade -r tests/requirements.txt
|
||||||
- name: Test SABnzbd
|
- name: Test SABnzbd
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Metadata-Version: 1.0
|
Metadata-Version: 1.0
|
||||||
Name: SABnzbd
|
Name: SABnzbd
|
||||||
Version: 3.4.1
|
Version: 3.4.2
|
||||||
Summary: SABnzbd-3.4.1
|
Summary: SABnzbd-3.4.2
|
||||||
Home-page: https://sabnzbd.org
|
Home-page: https://sabnzbd.org
|
||||||
Author: The SABnzbd Team
|
Author: The SABnzbd Team
|
||||||
Author-email: team@sabnzbd.org
|
Author-email: team@sabnzbd.org
|
||||||
|
|||||||
+15
-2
@@ -1,6 +1,19 @@
|
|||||||
Release Notes - SABnzbd 3.4.1
|
Release Notes - SABnzbd 3.4.2
|
||||||
=========================================================
|
=========================================================
|
||||||
|
|
||||||
|
## Bugfixes since 3.4.1
|
||||||
|
- Sorting requires at least 1 category to be selected since 3.4.0.
|
||||||
|
Warning will be shown if no category is selected.
|
||||||
|
- Sorting would fail if `%ext` or `%fn` was not used.
|
||||||
|
- Job failure due to Sorting-problems was not shown in the History.
|
||||||
|
- `Ignore Samples` did not remove all sample files.
|
||||||
|
- Crash when `.par2` files were missing during download.
|
||||||
|
- Prevent scanning the whole file to identify the correct extension.
|
||||||
|
- `.rXX`, `.cbz` and `.cbr` extensions were wrongly renamed.
|
||||||
|
- Processing unpacked `.par2` files would also process source
|
||||||
|
`.par2` files and could result in duplicate (`.1`) filenames.
|
||||||
|
- Always show the number of MB missing during download.
|
||||||
|
|
||||||
## Bugfixes since 3.4.0
|
## Bugfixes since 3.4.0
|
||||||
- macOS: Failed to run on M1 systems or older macOS versions.
|
- macOS: Failed to run on M1 systems or older macOS versions.
|
||||||
|
|
||||||
@@ -26,7 +39,7 @@ Release Notes - SABnzbd 3.4.1
|
|||||||
|
|
||||||
## Upgrade notices
|
## Upgrade notices
|
||||||
- The download statistics file `totals10.sab` is updated in 3.2.x
|
- The download statistics file `totals10.sab` is updated in 3.2.x
|
||||||
version. If you downgrade to 3.1.x or lower, detailed download
|
version. If you downgrade to 3.1.x or lower, all detailed download
|
||||||
statistics will be lost.
|
statistics will be lost.
|
||||||
|
|
||||||
## Known problems and solutions
|
## Known problems and solutions
|
||||||
|
|||||||
+4
-3
@@ -46,6 +46,8 @@ try:
|
|||||||
import portend
|
import portend
|
||||||
import cryptography
|
import cryptography
|
||||||
import chardet
|
import chardet
|
||||||
|
import guessit
|
||||||
|
import puremagic
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print("Not all required Python modules are available, please check requirements.txt")
|
print("Not all required Python modules are available, please check requirements.txt")
|
||||||
print("Missing module:", e.name)
|
print("Missing module:", e.name)
|
||||||
@@ -1439,12 +1441,11 @@ def main():
|
|||||||
try:
|
try:
|
||||||
cherrypy.engine.start()
|
cherrypy.engine.start()
|
||||||
except:
|
except:
|
||||||
|
# Since the webserver is started by cherrypy in a separate thread, we can't really catch any
|
||||||
|
# start-up errors. This try/except only catches very few errors, the rest is only shown in the console.
|
||||||
logging.error(T("Failed to start web-interface: "), exc_info=True)
|
logging.error(T("Failed to start web-interface: "), exc_info=True)
|
||||||
abort_and_show_error(browserhost, cherryport)
|
abort_and_show_error(browserhost, cherryport)
|
||||||
|
|
||||||
# Wait for server to become ready
|
|
||||||
cherrypy.engine.wait(cherrypy.process.wspbus.states.STARTED)
|
|
||||||
|
|
||||||
if sabnzbd.WIN32:
|
if sabnzbd.WIN32:
|
||||||
if enable_https:
|
if enable_https:
|
||||||
mode = "s"
|
mode = "s"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<option value="$ct" <!--#if $ct in $getVar($section_label + '_cats') then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
|
<option value="$ct" <!--#if $ct in $getVar($section_label + '_cats') then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
|
||||||
<!--#end for#-->
|
<!--#end for#-->
|
||||||
</select>
|
</select>
|
||||||
|
<p>$T('defaultNotifiesAll')</p>
|
||||||
</div>
|
</div>
|
||||||
<!--#end def#-->
|
<!--#end def#-->
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
<option value="$ct" <!--#if $ct in $email_cats then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
|
<option value="$ct" <!--#if $ct in $email_cats then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
|
||||||
<!--#end for#-->
|
<!--#end for#-->
|
||||||
</select>
|
</select>
|
||||||
|
<p>$T('defaultNotifiesAll')</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col1">
|
<div class="col1">
|
||||||
|
|||||||
@@ -11,12 +11,13 @@
|
|||||||
<h3>$T('seriesSorting') <a href="$helpuri$help_uri#toc0" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
|
<h3>$T('seriesSorting') <a href="$helpuri$help_uri#toc0" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
|
||||||
<p>
|
<p>
|
||||||
<b>$T('affectedCat')</b><br/>
|
<b>$T('affectedCat')</b><br/>
|
||||||
<select name="tv_categories" multiple="multiple" class="multiple_cats">
|
<select name="tv_categories" multiple="multiple" class="multiple_cats" required="required">
|
||||||
<!--#for $ct in $categories#-->
|
<!--#for $ct in $categories#-->
|
||||||
<option value="$ct" <!--#if $ct in $tv_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
|
<option value="$ct" <!--#if $ct in $tv_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
|
||||||
<!--#end for#-->
|
<!--#end for#-->
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
|
<p>$T('selectOneCat')</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- /col2 -->
|
<!-- /col2 -->
|
||||||
<div class="col1">
|
<div class="col1">
|
||||||
@@ -223,12 +224,13 @@
|
|||||||
<h3>$T('movieSort') <a href="$helpuri$help_uri#toc6" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
|
<h3>$T('movieSort') <a href="$helpuri$help_uri#toc6" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
|
||||||
<p>
|
<p>
|
||||||
<b>$T('affectedCat')</b><br/>
|
<b>$T('affectedCat')</b><br/>
|
||||||
<select name="movie_categories" multiple="multiple" class="multiple_cats">
|
<select name="movie_categories" multiple="multiple" class="multiple_cats" required="required">
|
||||||
<!--#for $ct in $categories#-->
|
<!--#for $ct in $categories#-->
|
||||||
<option value="$ct" <!--#if $ct in $movie_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
|
<option value="$ct" <!--#if $ct in $movie_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
|
||||||
<!--#end for#-->
|
<!--#end for#-->
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
|
<p>$T('selectOneCat')</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- /col2 -->
|
<!-- /col2 -->
|
||||||
<div class="col1">
|
<div class="col1">
|
||||||
@@ -419,12 +421,13 @@
|
|||||||
<h3>$T('dateSorting') <a href="$helpuri$help_uri#toc9" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
|
<h3>$T('dateSorting') <a href="$helpuri$help_uri#toc9" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
|
||||||
<p>
|
<p>
|
||||||
<b>$T('affectedCat')</b><br/>
|
<b>$T('affectedCat')</b><br/>
|
||||||
<select name="date_categories" multiple="multiple" class="multiple_cats">
|
<select name="date_categories" multiple="multiple" class="multiple_cats" required="required">
|
||||||
<!--#for $ct in $categories#-->
|
<!--#for $ct in $categories#-->
|
||||||
<option value="$ct" <!--#if $ct in $date_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
|
<option value="$ct" <!--#if $ct in $date_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
|
||||||
<!--#end for#-->
|
<!--#end for#-->
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
|
<p>$T('selectOneCat')</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- /col2 -->
|
<!-- /col2 -->
|
||||||
<div class="col1">
|
<div class="col1">
|
||||||
|
|||||||
@@ -531,11 +531,10 @@ function QueueModel(parent, data) {
|
|||||||
return self.name()
|
return self.name()
|
||||||
})
|
})
|
||||||
self.missingText = ko.pureComputed(function() {
|
self.missingText = ko.pureComputed(function() {
|
||||||
// Check for missing data, the value is arbitrary! (1%)
|
// Check for missing data, can show 0 if article-size is smaller than 500K, but we accept that
|
||||||
if(self.missingMB()/self.totalMB() > 0.01) {
|
if(self.missingMB()) {
|
||||||
return self.missingMB().toFixed(0) + ' MB ' + glitterTranslate.misingArt
|
return self.missingMB().toFixed(0) + ' MB ' + glitterTranslate.misingArt
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
})
|
})
|
||||||
self.statusText = ko.computed(function() {
|
self.statusText = ko.computed(function() {
|
||||||
// Checking
|
// Checking
|
||||||
|
|||||||
+1
-1
@@ -233,7 +233,7 @@ rating_filter_pause_keywords = OptionStr("misc", "rating_filter_pause_keywords")
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
enable_tv_sorting = OptionBool("misc", "enable_tv_sorting", False)
|
enable_tv_sorting = OptionBool("misc", "enable_tv_sorting", False)
|
||||||
tv_sort_string = OptionStr("misc", "tv_sort_string")
|
tv_sort_string = OptionStr("misc", "tv_sort_string")
|
||||||
tv_categories = OptionList("misc", "tv_categories", "")
|
tv_categories = OptionList("misc", "tv_categories", ["tv"])
|
||||||
|
|
||||||
enable_movie_sorting = OptionBool("misc", "enable_movie_sorting", False)
|
enable_movie_sorting = OptionBool("misc", "enable_movie_sorting", False)
|
||||||
movie_sort_string = OptionStr("misc", "movie_sort_string")
|
movie_sort_string = OptionStr("misc", "movie_sort_string")
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ VALID_NZB_FILES = (".nzb", ".gz", ".bz2")
|
|||||||
CHEETAH_DIRECTIVES = {"directiveStartToken": "<!--#", "directiveEndToken": "#-->", "prioritizeSearchListOverSelf": True}
|
CHEETAH_DIRECTIVES = {"directiveStartToken": "<!--#", "directiveEndToken": "#-->", "prioritizeSearchListOverSelf": True}
|
||||||
|
|
||||||
IGNORED_FOLDERS = ("@eaDir", ".appleDouble")
|
IGNORED_FOLDERS = ("@eaDir", ".appleDouble")
|
||||||
|
IGNORED_MOVIE_FOLDERS = ("video_ts", "audio_ts", "bdmv")
|
||||||
|
|
||||||
EXCLUDED_GUESSIT_PROPERTIES = [
|
EXCLUDED_GUESSIT_PROPERTIES = [
|
||||||
"part",
|
"part",
|
||||||
|
|||||||
Regular → Executable
+5
-4
@@ -64,9 +64,9 @@ def decode_par2(parfile: str) -> List[str]:
|
|||||||
with open(filepath, "rb") as fileToMatch:
|
with open(filepath, "rb") as fileToMatch:
|
||||||
first16k_data = fileToMatch.read(16384)
|
first16k_data = fileToMatch.read(16384)
|
||||||
|
|
||||||
# Check if we have this hash
|
# Check if we have this hash and the filename is different
|
||||||
file_md5of16k = hashlib.md5(first16k_data).digest()
|
file_md5of16k = hashlib.md5(first16k_data).digest()
|
||||||
if file_md5of16k in md5of16k:
|
if file_md5of16k in md5of16k and fn != md5of16k[file_md5of16k]:
|
||||||
new_path = os.path.join(dirname, md5of16k[file_md5of16k])
|
new_path = os.path.join(dirname, md5of16k[file_md5of16k])
|
||||||
# Make sure it's a unique name
|
# Make sure it's a unique name
|
||||||
unique_filename = get_unique_filename(new_path)
|
unique_filename = get_unique_filename(new_path)
|
||||||
@@ -166,7 +166,7 @@ def deobfuscate_list(filelist: List[str], usefulname: str):
|
|||||||
# 2. if no meaningful extension, add it
|
# 2. if no meaningful extension, add it
|
||||||
# 3. based on detecting obfuscated filenames
|
# 3. based on detecting obfuscated filenames
|
||||||
|
|
||||||
# to be sure, only keep really exsiting files:
|
# to be sure, only keep really existing files:
|
||||||
filelist = [f for f in filelist if os.path.isfile(f)]
|
filelist = [f for f in filelist if os.path.isfile(f)]
|
||||||
|
|
||||||
# let's see if there are files with uncommon/unpopular (so: obfuscated) extensions
|
# let's see if there are files with uncommon/unpopular (so: obfuscated) extensions
|
||||||
@@ -176,7 +176,7 @@ def deobfuscate_list(filelist: List[str], usefulname: str):
|
|||||||
for file in filelist:
|
for file in filelist:
|
||||||
if file_extension.has_popular_extension(file):
|
if file_extension.has_popular_extension(file):
|
||||||
# common extension, like .doc or .iso, so assume OK and change nothing
|
# common extension, like .doc or .iso, so assume OK and change nothing
|
||||||
logging.debug("extension of %s looks common", file)
|
logging.debug("Extension of %s looks common", file)
|
||||||
newlist.append(file)
|
newlist.append(file)
|
||||||
else:
|
else:
|
||||||
# uncommon (so: obfuscated) extension
|
# uncommon (so: obfuscated) extension
|
||||||
@@ -220,6 +220,7 @@ def deobfuscate_list(filelist: List[str], usefulname: str):
|
|||||||
# check that file is still there (and not renamed by the secondary renaming process below)
|
# check that file is still there (and not renamed by the secondary renaming process below)
|
||||||
if not os.path.isfile(filename):
|
if not os.path.isfile(filename):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logging.debug("Deobfuscate inspecting %s", filename)
|
logging.debug("Deobfuscate inspecting %s", filename)
|
||||||
# Do we need to rename this file?
|
# Do we need to rename this file?
|
||||||
# Criteria: big, not-excluded extension, obfuscated (in that order)
|
# Criteria: big, not-excluded extension, obfuscated (in that order)
|
||||||
|
|||||||
@@ -480,6 +480,61 @@ def check_mount(path: str) -> bool:
|
|||||||
return not m
|
return not m
|
||||||
|
|
||||||
|
|
||||||
|
RAR_RE = re.compile(r"\.(?P<ext>part\d*\.rar|rar|r\d\d|s\d\d|t\d\d|u\d\d|v\d\d|\d\d\d?\d)$", re.I)
|
||||||
|
SPLITFILE_RE = re.compile(r"\.(\d\d\d?\d$)", re.I)
|
||||||
|
ZIP_RE = re.compile(r"\.(zip$)", re.I)
|
||||||
|
SEVENZIP_RE = re.compile(r"\.7z$", re.I)
|
||||||
|
SEVENMULTI_RE = re.compile(r"\.7z\.\d+$", re.I)
|
||||||
|
TS_RE = re.compile(r"\.(\d+)\.(ts$)", re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def build_filelists(
|
||||||
|
workdir: Optional[str], workdir_complete: Optional[str] = None, check_both: bool = False, check_rar: bool = True
|
||||||
|
) -> Tuple[List[str], List[str], List[str], List[str], List[str]]:
|
||||||
|
"""Build filelists, if workdir_complete has files, ignore workdir.
|
||||||
|
Optionally scan both directories.
|
||||||
|
Optionally test content to establish RAR-ness
|
||||||
|
"""
|
||||||
|
sevens, joinables, zips, rars, ts, filelist = ([], [], [], [], [], [])
|
||||||
|
|
||||||
|
if workdir_complete:
|
||||||
|
filelist.extend(listdir_full(workdir_complete))
|
||||||
|
|
||||||
|
if workdir and (not filelist or check_both):
|
||||||
|
filelist.extend(listdir_full(workdir, recursive=False))
|
||||||
|
|
||||||
|
for file in filelist:
|
||||||
|
# Extra check for rar (takes CPU/disk)
|
||||||
|
file_is_rar = False
|
||||||
|
if check_rar:
|
||||||
|
file_is_rar = rarfile.is_rarfile(file)
|
||||||
|
|
||||||
|
# Run through all the checks
|
||||||
|
if SEVENZIP_RE.search(file) or SEVENMULTI_RE.search(file):
|
||||||
|
# 7zip
|
||||||
|
sevens.append(file)
|
||||||
|
elif SPLITFILE_RE.search(file) and not file_is_rar:
|
||||||
|
# Joinables, optional with RAR check
|
||||||
|
joinables.append(file)
|
||||||
|
elif ZIP_RE.search(file):
|
||||||
|
# ZIP files
|
||||||
|
zips.append(file)
|
||||||
|
elif RAR_RE.search(file):
|
||||||
|
# RAR files
|
||||||
|
rars.append(file)
|
||||||
|
elif TS_RE.search(file):
|
||||||
|
# TS split files
|
||||||
|
ts.append(file)
|
||||||
|
|
||||||
|
logging.debug("build_filelists(): joinables: %s", joinables)
|
||||||
|
logging.debug("build_filelists(): zips: %s", zips)
|
||||||
|
logging.debug("build_filelists(): rars: %s", rars)
|
||||||
|
logging.debug("build_filelists(): 7zips: %s", sevens)
|
||||||
|
logging.debug("build_filelists(): ts: %s", ts)
|
||||||
|
|
||||||
|
return joinables, zips, rars, sevens, ts
|
||||||
|
|
||||||
|
|
||||||
def safe_fnmatch(f: str, pattern: str) -> bool:
|
def safe_fnmatch(f: str, pattern: str) -> bool:
|
||||||
"""fnmatch will fail if the pattern contains any of it's
|
"""fnmatch will fail if the pattern contains any of it's
|
||||||
key characters, like [, ] or !.
|
key characters, like [, ] or !.
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ from sabnzbd.filesystem import userxbit
|
|||||||
TAB_UNITS = ("", "K", "M", "G", "T", "P")
|
TAB_UNITS = ("", "K", "M", "G", "T", "P")
|
||||||
RE_UNITS = re.compile(r"(\d+\.*\d*)\s*([KMGTP]?)", re.I)
|
RE_UNITS = re.compile(r"(\d+\.*\d*)\s*([KMGTP]?)", re.I)
|
||||||
RE_VERSION = re.compile(r"(\d+)\.(\d+)\.(\d+)([a-zA-Z]*)(\d*)")
|
RE_VERSION = re.compile(r"(\d+)\.(\d+)\.(\d+)([a-zA-Z]*)(\d*)")
|
||||||
|
RE_SAMPLE = re.compile(r"((^|[\W_])(sample|proof))", re.I) # something-sample or something-proof
|
||||||
RE_IP4 = re.compile(r"inet\s+(addr:\s*)?(\d+\.\d+\.\d+\.\d+)")
|
RE_IP4 = re.compile(r"inet\s+(addr:\s*)?(\d+\.\d+\.\d+\.\d+)")
|
||||||
RE_IP6 = re.compile(r"inet6\s+(addr:\s*)?([0-9a-f:]+)", re.I)
|
RE_IP6 = re.compile(r"inet6\s+(addr:\s*)?([0-9a-f:]+)", re.I)
|
||||||
|
|
||||||
@@ -808,6 +809,11 @@ def get_all_passwords(nzo) -> List[str]:
|
|||||||
return unique_passwords
|
return unique_passwords
|
||||||
|
|
||||||
|
|
||||||
|
def is_sample(filename: str) -> bool:
|
||||||
|
"""Try to determine if filename is (most likely) a sample"""
|
||||||
|
return bool(re.search(RE_SAMPLE, filename))
|
||||||
|
|
||||||
|
|
||||||
def find_on_path(targets):
|
def find_on_path(targets):
|
||||||
"""Search the PATH for a program and return full path"""
|
"""Search the PATH for a program and return full path"""
|
||||||
if sabnzbd.WIN32:
|
if sabnzbd.WIN32:
|
||||||
|
|||||||
+4
-54
@@ -56,6 +56,8 @@ from sabnzbd.filesystem import (
|
|||||||
setname_from_path,
|
setname_from_path,
|
||||||
get_ext,
|
get_ext,
|
||||||
get_filename,
|
get_filename,
|
||||||
|
TS_RE,
|
||||||
|
build_filelists,
|
||||||
)
|
)
|
||||||
from sabnzbd.nzbstuff import NzbObject, NzbFile
|
from sabnzbd.nzbstuff import NzbObject, NzbFile
|
||||||
from sabnzbd.sorting import SeriesSorter
|
from sabnzbd.sorting import SeriesSorter
|
||||||
@@ -63,18 +65,12 @@ import sabnzbd.cfg as cfg
|
|||||||
from sabnzbd.constants import Status
|
from sabnzbd.constants import Status
|
||||||
|
|
||||||
# Regex globals
|
# Regex globals
|
||||||
RAR_RE = re.compile(r"\.(?P<ext>part\d*\.rar|rar|r\d\d|s\d\d|t\d\d|u\d\d|v\d\d|\d\d\d?\d)$", re.I)
|
|
||||||
RAR_RE_V3 = re.compile(r"\.(?P<ext>part\d*)$", re.I)
|
RAR_RE_V3 = re.compile(r"\.(?P<ext>part\d*)$", re.I)
|
||||||
|
|
||||||
TARGET_RE = re.compile(r'^(?:File|Target): "(.+)" -')
|
TARGET_RE = re.compile(r'^(?:File|Target): "(.+)" -')
|
||||||
EXTRACTFROM_RE = re.compile(r"^Extracting\sfrom\s(.+)")
|
EXTRACTFROM_RE = re.compile(r"^Extracting\sfrom\s(.+)")
|
||||||
EXTRACTED_RE = re.compile(r"^(Extracting|Creating|...)\s+(.*?)\s+OK\s*$")
|
EXTRACTED_RE = re.compile(r"^(Extracting|Creating|...)\s+(.*?)\s+OK\s*$")
|
||||||
SPLITFILE_RE = re.compile(r"\.(\d\d\d?\d$)", re.I)
|
|
||||||
ZIP_RE = re.compile(r"\.(zip$)", re.I)
|
|
||||||
SEVENZIP_RE = re.compile(r"\.7z$", re.I)
|
|
||||||
SEVENMULTI_RE = re.compile(r"\.7z\.\d+$", re.I)
|
|
||||||
TS_RE = re.compile(r"\.(\d+)\.(ts$)", re.I)
|
|
||||||
|
|
||||||
|
# Constants
|
||||||
PAR2_COMMAND = None
|
PAR2_COMMAND = None
|
||||||
MULTIPAR_COMMAND = None
|
MULTIPAR_COMMAND = None
|
||||||
RAR_COMMAND = None
|
RAR_COMMAND = None
|
||||||
@@ -1118,8 +1114,7 @@ def par2_repair(parfile_nzf: NzbFile, nzo: NzbObject, workdir, setname, single):
|
|||||||
readd = False
|
readd = False
|
||||||
for extrapar in nzo.extrapars[setname][:]:
|
for extrapar in nzo.extrapars[setname][:]:
|
||||||
# Make sure we only get new par2 files
|
# Make sure we only get new par2 files
|
||||||
if extrapar not in nzo.finished_files and extrapar not in nzo.files:
|
if nzo.add_parfile(extrapar):
|
||||||
nzo.add_parfile(extrapar)
|
|
||||||
readd = True
|
readd = True
|
||||||
if readd:
|
if readd:
|
||||||
return readd, result
|
return readd, result
|
||||||
@@ -1995,51 +1990,6 @@ def rar_sort(a, b):
|
|||||||
return cmp(a, b)
|
return cmp(a, b)
|
||||||
|
|
||||||
|
|
||||||
def build_filelists(workdir, workdir_complete=None, check_both=False, check_rar=True):
|
|
||||||
"""Build filelists, if workdir_complete has files, ignore workdir.
|
|
||||||
Optionally scan both directories.
|
|
||||||
Optionally test content to establish RAR-ness
|
|
||||||
"""
|
|
||||||
sevens, joinables, zips, rars, ts, filelist = ([], [], [], [], [], [])
|
|
||||||
|
|
||||||
if workdir_complete:
|
|
||||||
filelist.extend(listdir_full(workdir_complete))
|
|
||||||
|
|
||||||
if workdir and (not filelist or check_both):
|
|
||||||
filelist.extend(listdir_full(workdir, recursive=False))
|
|
||||||
|
|
||||||
for file in filelist:
|
|
||||||
# Extra check for rar (takes CPU/disk)
|
|
||||||
file_is_rar = False
|
|
||||||
if check_rar:
|
|
||||||
file_is_rar = rarfile.is_rarfile(file)
|
|
||||||
|
|
||||||
# Run through all the checks
|
|
||||||
if SEVENZIP_RE.search(file) or SEVENMULTI_RE.search(file):
|
|
||||||
# 7zip
|
|
||||||
sevens.append(file)
|
|
||||||
elif SPLITFILE_RE.search(file) and not file_is_rar:
|
|
||||||
# Joinables, optional with RAR check
|
|
||||||
joinables.append(file)
|
|
||||||
elif ZIP_RE.search(file):
|
|
||||||
# ZIP files
|
|
||||||
zips.append(file)
|
|
||||||
elif RAR_RE.search(file):
|
|
||||||
# RAR files
|
|
||||||
rars.append(file)
|
|
||||||
elif TS_RE.search(file):
|
|
||||||
# TS split files
|
|
||||||
ts.append(file)
|
|
||||||
|
|
||||||
logging.debug("build_filelists(): joinables: %s", joinables)
|
|
||||||
logging.debug("build_filelists(): zips: %s", zips)
|
|
||||||
logging.debug("build_filelists(): rars: %s", rars)
|
|
||||||
logging.debug("build_filelists(): 7zips: %s", sevens)
|
|
||||||
logging.debug("build_filelists(): ts: %s", ts)
|
|
||||||
|
|
||||||
return joinables, zips, rars, sevens, ts
|
|
||||||
|
|
||||||
|
|
||||||
def quick_check_set(set, nzo):
|
def quick_check_set(set, nzo):
|
||||||
"""Check all on-the-fly md5sums of a set"""
|
"""Check all on-the-fly md5sums of a set"""
|
||||||
md5pack = nzo.md5packs.get(set)
|
md5pack = nzo.md5packs.get(set)
|
||||||
|
|||||||
+13
-11
@@ -1105,8 +1105,7 @@ class NzbObject(TryList):
|
|||||||
self.postpone_pars(nzf, setname)
|
self.postpone_pars(nzf, setname)
|
||||||
# Get the next one
|
# Get the next one
|
||||||
for new_nzf in self.extrapars[setname]:
|
for new_nzf in self.extrapars[setname]:
|
||||||
if not new_nzf.completed:
|
if self.add_parfile(new_nzf):
|
||||||
self.add_parfile(new_nzf)
|
|
||||||
# Add it to the top
|
# Add it to the top
|
||||||
self.files.remove(new_nzf)
|
self.files.remove(new_nzf)
|
||||||
self.files.insert(0, new_nzf)
|
self.files.insert(0, new_nzf)
|
||||||
@@ -1143,8 +1142,8 @@ class NzbObject(TryList):
|
|||||||
added_blocks = 0
|
added_blocks = 0
|
||||||
while added_blocks < needed_blocks:
|
while added_blocks < needed_blocks:
|
||||||
new_nzf = block_list.pop()
|
new_nzf = block_list.pop()
|
||||||
self.add_parfile(new_nzf)
|
if self.add_parfile(new_nzf):
|
||||||
added_blocks += new_nzf.blocks
|
added_blocks += new_nzf.blocks
|
||||||
|
|
||||||
logging.info("Added %s blocks to %s", added_blocks, self.final_name)
|
logging.info("Added %s blocks to %s", added_blocks, self.final_name)
|
||||||
return added_blocks
|
return added_blocks
|
||||||
@@ -1433,15 +1432,18 @@ class NzbObject(TryList):
|
|||||||
self.unwanted_ext = 2
|
self.unwanted_ext = 2
|
||||||
|
|
||||||
@synchronized(NZO_LOCK)
|
@synchronized(NZO_LOCK)
|
||||||
def add_parfile(self, parfile: NzbFile):
|
def add_parfile(self, parfile: NzbFile) -> bool:
|
||||||
"""Add parfile to the files to be downloaded
|
"""Add parfile to the files to be downloaded
|
||||||
Resets trylist just to be sure
|
Resets trylist just to be sure
|
||||||
Adjust download-size accordingly
|
Adjust download-size accordingly
|
||||||
|
Returns False when the file couldn't be added
|
||||||
"""
|
"""
|
||||||
if not parfile.completed and parfile not in self.files and parfile not in self.finished_files:
|
if not parfile.completed and parfile not in self.files and parfile not in self.finished_files:
|
||||||
parfile.reset_try_list()
|
parfile.reset_try_list()
|
||||||
self.files.append(parfile)
|
self.files.append(parfile)
|
||||||
self.bytes_tried -= parfile.bytes_left
|
self.bytes_tried -= parfile.bytes_left
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@synchronized(NZO_LOCK)
|
@synchronized(NZO_LOCK)
|
||||||
def remove_parset(self, setname: str):
|
def remove_parset(self, setname: str):
|
||||||
@@ -1468,12 +1470,12 @@ class NzbObject(TryList):
|
|||||||
# from all the sets. This probably means we get too much par2, but it's worth it.
|
# from all the sets. This probably means we get too much par2, but it's worth it.
|
||||||
blocks_new = 0
|
blocks_new = 0
|
||||||
for new_nzf in self.extrapars[parset]:
|
for new_nzf in self.extrapars[parset]:
|
||||||
self.add_parfile(new_nzf)
|
if self.add_parfile(new_nzf):
|
||||||
blocks_new += new_nzf.blocks
|
blocks_new += new_nzf.blocks
|
||||||
# Enough now?
|
# Enough now?
|
||||||
if blocks_new >= self.bad_articles:
|
if blocks_new >= self.bad_articles:
|
||||||
logging.info("Prospectively added %s repair blocks to %s", blocks_new, self.final_name)
|
logging.info("Prospectively added %s repair blocks to %s", blocks_new, self.final_name)
|
||||||
break
|
break
|
||||||
# Reset NZO TryList
|
# Reset NZO TryList
|
||||||
self.reset_try_list()
|
self.reset_try_list()
|
||||||
|
|
||||||
|
|||||||
+10
-7
@@ -39,7 +39,7 @@ from sabnzbd.newsunpack import (
|
|||||||
is_sfv_file,
|
is_sfv_file,
|
||||||
)
|
)
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from sabnzbd.misc import on_cleanup_list
|
from sabnzbd.misc import on_cleanup_list, is_sample
|
||||||
from sabnzbd.filesystem import (
|
from sabnzbd.filesystem import (
|
||||||
real_path,
|
real_path,
|
||||||
get_unique_path,
|
get_unique_path,
|
||||||
@@ -65,7 +65,7 @@ from sabnzbd.filesystem import (
|
|||||||
get_filename,
|
get_filename,
|
||||||
)
|
)
|
||||||
from sabnzbd.nzbstuff import NzbObject
|
from sabnzbd.nzbstuff import NzbObject
|
||||||
from sabnzbd.sorting import Sorter, is_sample
|
from sabnzbd.sorting import Sorter
|
||||||
from sabnzbd.constants import (
|
from sabnzbd.constants import (
|
||||||
REPAIR_PRIORITY,
|
REPAIR_PRIORITY,
|
||||||
FORCE_PRIORITY,
|
FORCE_PRIORITY,
|
||||||
@@ -74,6 +74,7 @@ from sabnzbd.constants import (
|
|||||||
JOB_ADMIN,
|
JOB_ADMIN,
|
||||||
Status,
|
Status,
|
||||||
VERIFIED_FILE,
|
VERIFIED_FILE,
|
||||||
|
IGNORED_MOVIE_FOLDERS,
|
||||||
)
|
)
|
||||||
from sabnzbd.nzbparser import process_single_nzb
|
from sabnzbd.nzbparser import process_single_nzb
|
||||||
import sabnzbd.emailer as emailer
|
import sabnzbd.emailer as emailer
|
||||||
@@ -499,7 +500,7 @@ def process_job(nzo: NzbObject):
|
|||||||
)
|
)
|
||||||
logging.info("Traceback: ", exc_info=True)
|
logging.info("Traceback: ", exc_info=True)
|
||||||
# Better disable sorting because filenames are all off now
|
# Better disable sorting because filenames are all off now
|
||||||
file_sorter.sort_file = None
|
file_sorter.sorter_active = None
|
||||||
|
|
||||||
if empty:
|
if empty:
|
||||||
job_result = -1
|
job_result = -1
|
||||||
@@ -510,17 +511,19 @@ def process_job(nzo: NzbObject):
|
|||||||
remove_samples(workdir_complete)
|
remove_samples(workdir_complete)
|
||||||
|
|
||||||
# TV/Movie/Date Renaming code part 2 - rename and move files to parent folder
|
# TV/Movie/Date Renaming code part 2 - rename and move files to parent folder
|
||||||
if all_ok and file_sorter.sort_file:
|
if all_ok and file_sorter.sorter_active:
|
||||||
if newfiles:
|
if newfiles:
|
||||||
workdir_complete, ok = file_sorter.sorter.rename(newfiles, workdir_complete)
|
workdir_complete, ok = file_sorter.sorter.rename(newfiles, workdir_complete)
|
||||||
if not ok:
|
if not ok:
|
||||||
nzo.set_unpack_info("Unpack", T("Failed to move files"))
|
nzo.set_unpack_info("Unpack", T("Failed to move files"))
|
||||||
|
nzo.fail_msg = T("Failed to move files")
|
||||||
all_ok = False
|
all_ok = False
|
||||||
|
|
||||||
# Run further post-processing
|
# Run further post-processing
|
||||||
if (all_ok or not cfg.safe_postproc()) and not nzb_list:
|
if (all_ok or not cfg.safe_postproc()) and not nzb_list:
|
||||||
# Use par2 files to deobfuscate unpacked file names
|
# Use par2 files to deobfuscate unpacked file names
|
||||||
if cfg.process_unpacked_par2():
|
# Only if we also run cleanup, so not to process the "regular" par2 files
|
||||||
|
if flag_delete and cfg.process_unpacked_par2():
|
||||||
newfiles = deobfuscate.recover_par2_names(newfiles)
|
newfiles = deobfuscate.recover_par2_names(newfiles)
|
||||||
|
|
||||||
if cfg.deobfuscate_final_filenames():
|
if cfg.deobfuscate_final_filenames():
|
||||||
@@ -691,7 +694,7 @@ def prepare_extraction_path(nzo: NzbObject) -> Tuple[str, str, Sorter, bool, Opt
|
|||||||
else:
|
else:
|
||||||
file_sorter = Sorter(None, nzo.cat)
|
file_sorter = Sorter(None, nzo.cat)
|
||||||
complete_dir = file_sorter.detect(nzo.final_name, complete_dir)
|
complete_dir = file_sorter.detect(nzo.final_name, complete_dir)
|
||||||
if file_sorter.sort_file:
|
if file_sorter.sorter_active:
|
||||||
one_folder = False
|
one_folder = False
|
||||||
|
|
||||||
complete_dir = sanitize_and_trim_path(complete_dir)
|
complete_dir = sanitize_and_trim_path(complete_dir)
|
||||||
@@ -1175,7 +1178,7 @@ def rename_and_collapse_folder(oldpath, newpath, files):
|
|||||||
if len(items) == 1:
|
if len(items) == 1:
|
||||||
folder = items[0]
|
folder = items[0]
|
||||||
folder_path = os.path.join(oldpath, folder)
|
folder_path = os.path.join(oldpath, folder)
|
||||||
if os.path.isdir(folder_path) and folder not in ("VIDEO_TS", "AUDIO_TS"):
|
if os.path.isdir(folder_path) and folder.lower() not in IGNORED_MOVIE_FOLDERS:
|
||||||
logging.info("Collapsing %s", os.path.join(newpath, folder))
|
logging.info("Collapsing %s", os.path.join(newpath, folder))
|
||||||
oldpath = folder_path
|
oldpath = folder_path
|
||||||
|
|
||||||
|
|||||||
@@ -701,6 +701,9 @@ SKIN_TEXT = {
|
|||||||
"link-download": TT("Download"), #: Config->RSS button "download item"
|
"link-download": TT("Download"), #: Config->RSS button "download item"
|
||||||
"button-rssNow": TT("Read All Feeds Now"), #: Config->RSS button
|
"button-rssNow": TT("Read All Feeds Now"), #: Config->RSS button
|
||||||
# Config->Notifications
|
# Config->Notifications
|
||||||
|
"defaultNotifiesAll": TT(
|
||||||
|
"If only the <em>Default</em> category is selected, notifications are enabled for jobs in all categories."
|
||||||
|
),
|
||||||
"opt-email_endjob": TT("Email Notification On Job Completion"),
|
"opt-email_endjob": TT("Email Notification On Job Completion"),
|
||||||
"email-never": TT("Never"), #: When to send email
|
"email-never": TT("Never"), #: When to send email
|
||||||
"email-always": TT("Always"), #: When to send email
|
"email-always": TT("Always"), #: When to send email
|
||||||
@@ -773,6 +776,7 @@ SKIN_TEXT = {
|
|||||||
"catTags": TT("Indexer Categories / Groups"),
|
"catTags": TT("Indexer Categories / Groups"),
|
||||||
"button-delCat": TT("X"), #: Small delete button
|
"button-delCat": TT("X"), #: Small delete button
|
||||||
# Config->Sorting
|
# Config->Sorting
|
||||||
|
"selectOneCat": TT("Select at least 1 category."),
|
||||||
"seriesSorting": TT("Series Sorting"),
|
"seriesSorting": TT("Series Sorting"),
|
||||||
"opt-tvsort": TT("Enable TV Sorting"),
|
"opt-tvsort": TT("Enable TV Sorting"),
|
||||||
"sort-legenda": TT("Pattern Key"),
|
"sort-legenda": TT("Pattern Key"),
|
||||||
|
|||||||
+39
-51
@@ -38,7 +38,8 @@ from sabnzbd.filesystem import (
|
|||||||
clip_path,
|
clip_path,
|
||||||
)
|
)
|
||||||
import sabnzbd.cfg as cfg
|
import sabnzbd.cfg as cfg
|
||||||
from sabnzbd.constants import EXCLUDED_GUESSIT_PROPERTIES
|
from sabnzbd.constants import EXCLUDED_GUESSIT_PROPERTIES, IGNORED_MOVIE_FOLDERS
|
||||||
|
from sabnzbd.misc import is_sample
|
||||||
from sabnzbd.nzbstuff import NzbObject, scan_password
|
from sabnzbd.nzbstuff import NzbObject, scan_password
|
||||||
|
|
||||||
# Do not rename .vob files as they are usually DVD's
|
# Do not rename .vob files as they are usually DVD's
|
||||||
@@ -76,7 +77,7 @@ class BaseSorter:
|
|||||||
self.cat = cat
|
self.cat = cat
|
||||||
self.filename_set = ""
|
self.filename_set = ""
|
||||||
self.fname = "" # Value for %fn substitution in folders
|
self.fname = "" # Value for %fn substitution in folders
|
||||||
self.do_rename = False
|
self.rename_files = False
|
||||||
self.info = {}
|
self.info = {}
|
||||||
self.type = None
|
self.type = None
|
||||||
self.guess = guess
|
self.guess = guess
|
||||||
@@ -259,7 +260,7 @@ class BaseSorter:
|
|||||||
# Split the last part of the path up for the renamer
|
# Split the last part of the path up for the renamer
|
||||||
if extension:
|
if extension:
|
||||||
path, self.filename_set = os.path.split(path)
|
path, self.filename_set = os.path.split(path)
|
||||||
self.do_rename = True
|
self.rename_files = True
|
||||||
|
|
||||||
# The normpath function translates "" to "." which results in an incorrect path
|
# The normpath function translates "" to "." which results in an incorrect path
|
||||||
return os.path.normpath(path) if path else path
|
return os.path.normpath(path) if path else path
|
||||||
@@ -305,7 +306,7 @@ class BaseSorter:
|
|||||||
except:
|
except:
|
||||||
logging.error(T("Failed to rename: %s to %s"), clip_path(current_path), clip_path(newpath))
|
logging.error(T("Failed to rename: %s to %s"), clip_path(current_path), clip_path(newpath))
|
||||||
logging.info("Traceback: ", exc_info=True)
|
logging.info("Traceback: ", exc_info=True)
|
||||||
rename_similar(current_path, ext, self.filename_set, ())
|
rename_similar(current_path, ext, self.filename_set)
|
||||||
else:
|
else:
|
||||||
logging.debug("Nothing to rename, %s", files)
|
logging.debug("Nothing to rename, %s", files)
|
||||||
|
|
||||||
@@ -317,7 +318,7 @@ class Sorter:
|
|||||||
|
|
||||||
def __init__(self, nzo: Optional[NzbObject], cat: str):
|
def __init__(self, nzo: Optional[NzbObject], cat: str):
|
||||||
self.sorter: Optional[BaseSorter] = None
|
self.sorter: Optional[BaseSorter] = None
|
||||||
self.sort_file = False
|
self.sorter_active = False
|
||||||
self.nzo = nzo
|
self.nzo = nzo
|
||||||
self.cat = cat
|
self.cat = cat
|
||||||
|
|
||||||
@@ -334,9 +335,9 @@ class Sorter:
|
|||||||
self.sorter = MovieSorter(self.nzo, job_name, complete_dir, self.cat, guess)
|
self.sorter = MovieSorter(self.nzo, job_name, complete_dir, self.cat, guess)
|
||||||
|
|
||||||
if self.sorter and self.sorter.matched:
|
if self.sorter and self.sorter.matched:
|
||||||
self.sort_file = True
|
self.sorter_active = True
|
||||||
|
|
||||||
return self.sorter.get_final_path() if self.sort_file else complete_dir
|
return self.sorter.get_final_path() if self.sorter_active else complete_dir
|
||||||
|
|
||||||
|
|
||||||
class SeriesSorter(BaseSorter):
|
class SeriesSorter(BaseSorter):
|
||||||
@@ -357,12 +358,17 @@ class SeriesSorter(BaseSorter):
|
|||||||
def match(self):
|
def match(self):
|
||||||
"""Try to guess series info if config and category sort out or force is set"""
|
"""Try to guess series info if config and category sort out or force is set"""
|
||||||
if self.force or (cfg.enable_tv_sorting() and cfg.tv_sort_string() and self.cat.lower() in self.cats):
|
if self.force or (cfg.enable_tv_sorting() and cfg.tv_sort_string() and self.cat.lower() in self.cats):
|
||||||
self.guess = guess_what(self.original_job_name, sort_type="episode")
|
if not self.guess:
|
||||||
|
self.guess = guess_what(self.original_job_name, sort_type="episode")
|
||||||
if self.guess.get("type") == "episode" and "date" not in self.guess:
|
if self.guess.get("type") == "episode" and "date" not in self.guess:
|
||||||
logging.debug("Using tv sorter for %s", self.original_job_name)
|
logging.debug("Using tv sorter for %s", self.original_job_name)
|
||||||
self.matched = True
|
self.matched = True
|
||||||
self.type = "tv"
|
self.type = "tv"
|
||||||
|
|
||||||
|
# Require at least 1 category, this was not enforced before 3.4.0
|
||||||
|
if cfg.enable_tv_sorting() and not self.cats:
|
||||||
|
logging.warning("%s: %s", T("Series Sorting"), T("Select at least 1 category."))
|
||||||
|
|
||||||
def get_values(self):
|
def get_values(self):
|
||||||
"""Collect all values needed for path replacement"""
|
"""Collect all values needed for path replacement"""
|
||||||
self.get_year()
|
self.get_year()
|
||||||
@@ -394,8 +400,8 @@ class SeriesSorter(BaseSorter):
|
|||||||
"""Rename for Series"""
|
"""Rename for Series"""
|
||||||
if min_size < 0:
|
if min_size < 0:
|
||||||
min_size = cfg.episode_rename_limit.get_int()
|
min_size = cfg.episode_rename_limit.get_int()
|
||||||
if not self.do_rename:
|
if not self.rename_files:
|
||||||
return current_path, False
|
return move_to_parent_directory(current_path)
|
||||||
else:
|
else:
|
||||||
logging.debug("Renaming series file(s)")
|
logging.debug("Renaming series file(s)")
|
||||||
return super().rename(files, current_path, min_size)
|
return super().rename(files, current_path, min_size)
|
||||||
@@ -420,12 +426,17 @@ class MovieSorter(BaseSorter):
|
|||||||
def match(self):
|
def match(self):
|
||||||
"""Try to guess movie info if config and category sort out or force is set"""
|
"""Try to guess movie info if config and category sort out or force is set"""
|
||||||
if self.force or (cfg.enable_movie_sorting() and self.sort_string and self.cat.lower() in self.cats):
|
if self.force or (cfg.enable_movie_sorting() and self.sort_string and self.cat.lower() in self.cats):
|
||||||
self.guess = guess_what(self.original_job_name, sort_type="movie")
|
if not self.guess:
|
||||||
|
self.guess = guess_what(self.original_job_name, sort_type="movie")
|
||||||
if self.guess.get("type") == "movie":
|
if self.guess.get("type") == "movie":
|
||||||
logging.debug("Using movie sorter for %s", self.original_job_name)
|
logging.debug("Using movie sorter for %s", self.original_job_name)
|
||||||
self.matched = True
|
self.matched = True
|
||||||
self.type = "movie"
|
self.type = "movie"
|
||||||
|
|
||||||
|
# Require at least 1 category, this was not enforced before 3.4.0
|
||||||
|
if cfg.enable_movie_sorting() and not self.cats:
|
||||||
|
logging.warning("%s: %s", T("Movie Sorting"), T("Select at least 1 category."))
|
||||||
|
|
||||||
def get_values(self):
|
def get_values(self):
|
||||||
"""Collect all values needed for path replacement"""
|
"""Collect all values needed for path replacement"""
|
||||||
self.get_year()
|
self.get_year()
|
||||||
@@ -437,8 +448,9 @@ class MovieSorter(BaseSorter):
|
|||||||
if min_size < 0:
|
if min_size < 0:
|
||||||
min_size = cfg.movie_rename_limit.get_int()
|
min_size = cfg.movie_rename_limit.get_int()
|
||||||
|
|
||||||
if not self.do_rename:
|
if not self.rename_files:
|
||||||
return current_path, False
|
return move_to_parent_directory(current_path)
|
||||||
|
|
||||||
logging.debug("Renaming movie file(s)")
|
logging.debug("Renaming movie file(s)")
|
||||||
|
|
||||||
def filter_files(f, current_path):
|
def filter_files(f, current_path):
|
||||||
@@ -500,12 +512,17 @@ class DateSorter(BaseSorter):
|
|||||||
def match(self):
|
def match(self):
|
||||||
"""Checks the category for a match, if so set self.matched to true"""
|
"""Checks the category for a match, if so set self.matched to true"""
|
||||||
if self.force or (cfg.enable_date_sorting() and self.sort_string and self.cat.lower() in self.cats):
|
if self.force or (cfg.enable_date_sorting() and self.sort_string and self.cat.lower() in self.cats):
|
||||||
self.guess = guess_what(self.original_job_name, sort_type="episode")
|
if not self.guess:
|
||||||
|
self.guess = guess_what(self.original_job_name, sort_type="episode")
|
||||||
if self.guess.get("type") == "episode" and "date" in self.guess:
|
if self.guess.get("type") == "episode" and "date" in self.guess:
|
||||||
logging.debug("Using date sorter for %s", self.original_job_name)
|
logging.debug("Using date sorter for %s", self.original_job_name)
|
||||||
self.matched = True
|
self.matched = True
|
||||||
self.type = "date"
|
self.type = "date"
|
||||||
|
|
||||||
|
# Require at least 1 category, this was not enforced before 3.4.0
|
||||||
|
if cfg.enable_date_sorting() and not self.cats:
|
||||||
|
logging.warning("%s: %s", T("Date Sorting"), T("Select at least 1 category."))
|
||||||
|
|
||||||
def get_date(self):
|
def get_date(self):
|
||||||
"""Get month and day"""
|
"""Get month and day"""
|
||||||
self.info["month"] = str(self.guess.get("date").month)
|
self.info["month"] = str(self.guess.get("date").month)
|
||||||
@@ -526,8 +543,8 @@ class DateSorter(BaseSorter):
|
|||||||
"""Renaming Date file"""
|
"""Renaming Date file"""
|
||||||
if min_size < 0:
|
if min_size < 0:
|
||||||
min_size = cfg.episode_rename_limit.get_int()
|
min_size = cfg.episode_rename_limit.get_int()
|
||||||
if not self.do_rename:
|
if not self.rename_files:
|
||||||
return current_path, False
|
return move_to_parent_directory(current_path)
|
||||||
else:
|
else:
|
||||||
logging.debug("Renaming date file(s)")
|
logging.debug("Renaming date file(s)")
|
||||||
return super().rename(files, current_path, min_size)
|
return super().rename(files, current_path, min_size)
|
||||||
@@ -546,9 +563,11 @@ def move_to_parent_directory(workdir: str) -> Tuple[str, bool]:
|
|||||||
workdir = os.path.abspath(os.path.normpath(workdir))
|
workdir = os.path.abspath(os.path.normpath(workdir))
|
||||||
dest = os.path.abspath(os.path.normpath(os.path.join(workdir, "..")))
|
dest = os.path.abspath(os.path.normpath(os.path.join(workdir, "..")))
|
||||||
|
|
||||||
|
logging.debug("Moving all files from %s to %s", workdir, dest)
|
||||||
|
|
||||||
# Check for DVD folders and bail out if found
|
# Check for DVD folders and bail out if found
|
||||||
for item in os.listdir(workdir):
|
for item in os.listdir(workdir):
|
||||||
if item.lower() in ("video_ts", "audio_ts", "bdmv"):
|
if item.lower() in IGNORED_MOVIE_FOLDERS:
|
||||||
return workdir, True
|
return workdir, True
|
||||||
|
|
||||||
for root, dirs, files in os.walk(workdir):
|
for root, dirs, files in os.walk(workdir):
|
||||||
@@ -612,40 +631,9 @@ def guess_what(name: str, sort_type: Optional[str] = None) -> MatchesDict:
|
|||||||
):
|
):
|
||||||
guess["type"] = "unknown"
|
guess["type"] = "unknown"
|
||||||
|
|
||||||
# Remove sample indicators from groupnames, e.g. 'sample-groupname' or 'groupname-proof'
|
|
||||||
group = guess.get("release_group", "")
|
|
||||||
if group.lower().startswith(("sample-", "proof-")) or group.lower().endswith(("-sample", "-proof")):
|
|
||||||
# Set clean groupname
|
|
||||||
guess["release_group"] = re.sub("^(sample|proof)-|-(sample|proof)$", "", group, re.I)
|
|
||||||
# Add 'Sample' property to the guess
|
|
||||||
other = guess.get("other")
|
|
||||||
if not other:
|
|
||||||
guess.setdefault("other", "Sample")
|
|
||||||
else:
|
|
||||||
if "Sample" not in guess["other"]:
|
|
||||||
# Pre-existing 'other' may be a string or a list
|
|
||||||
try:
|
|
||||||
guess["other"].append("Sample")
|
|
||||||
except AttributeError:
|
|
||||||
guess["other"] = [other, "Sample"]
|
|
||||||
|
|
||||||
return guess
|
return guess
|
||||||
|
|
||||||
|
|
||||||
def is_sample(filename: str) -> bool:
|
|
||||||
"""Try to determine if filename belongs to a sample"""
|
|
||||||
if os.path.splitext(filename)[0].lower().strip() in ("sample", "proof"):
|
|
||||||
# The entire filename is just 'sample.ext' or similar
|
|
||||||
return True
|
|
||||||
|
|
||||||
# If that didn't work, start guessing
|
|
||||||
guess = guess_what(filename).get("other", "")
|
|
||||||
if isinstance(guess, list):
|
|
||||||
return any(item in ("Sample", "Proof") for item in guess)
|
|
||||||
else:
|
|
||||||
return guess in ("Sample", "Proof")
|
|
||||||
|
|
||||||
|
|
||||||
def path_subst(path: str, mapping: List[Tuple[str, str]]) -> str:
|
def path_subst(path: str, mapping: List[Tuple[str, str]]) -> str:
|
||||||
"""Replace the sort string elements in the path with the real values provided by the mapping;
|
"""Replace the sort string elements in the path with the real values provided by the mapping;
|
||||||
non-elements are copied verbatim."""
|
non-elements are copied verbatim."""
|
||||||
@@ -794,7 +782,7 @@ def strip_path_elements(path: str) -> str:
|
|||||||
return "\\\\" + path if is_unc else path
|
return "\\\\" + path if is_unc else path
|
||||||
|
|
||||||
|
|
||||||
def rename_similar(folder: str, skip_ext: str, name: str, skipped_files: List[str]):
|
def rename_similar(folder: str, skip_ext: str, name: str, skipped_files: Optional[List[str]] = None):
|
||||||
"""Rename all other files in the 'folder' hierarchy after 'name'
|
"""Rename all other files in the 'folder' hierarchy after 'name'
|
||||||
and move them to the root of 'folder'.
|
and move them to the root of 'folder'.
|
||||||
Files having extension 'skip_ext' will be moved, but not renamed.
|
Files having extension 'skip_ext' will be moved, but not renamed.
|
||||||
@@ -807,7 +795,7 @@ def rename_similar(folder: str, skip_ext: str, name: str, skipped_files: List[st
|
|||||||
for root, dirs, files in os.walk(folder):
|
for root, dirs, files in os.walk(folder):
|
||||||
for f in files:
|
for f in files:
|
||||||
path = os.path.join(root, f)
|
path = os.path.join(root, f)
|
||||||
if path in skipped_files:
|
if skipped_files and path in skipped_files:
|
||||||
continue
|
continue
|
||||||
org, ext = os.path.splitext(f)
|
org, ext = os.path.splitext(f)
|
||||||
if ext.lower() == skip_ext:
|
if ext.lower() == skip_ext:
|
||||||
@@ -861,7 +849,7 @@ def eval_sort(sort_type: str, expression: str, name: str = None, multipart: str
|
|||||||
if "%fn" in path:
|
if "%fn" in path:
|
||||||
path = path.replace("%fn", fname + ".ext")
|
path = path.replace("%fn", fname + ".ext")
|
||||||
else:
|
else:
|
||||||
if sorter.do_rename:
|
if sorter.rename_files:
|
||||||
path = fpath + ".ext"
|
path = fpath + ".ext"
|
||||||
else:
|
else:
|
||||||
path += "\\" if sabnzbd.WIN32 else "/"
|
path += "\\" if sabnzbd.WIN32 else "/"
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import puremagic
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import List
|
from typing import List
|
||||||
from pathlib import Path
|
from sabnzbd.filesystem import get_ext, RAR_RE
|
||||||
from sabnzbd.filesystem import get_ext
|
|
||||||
|
|
||||||
# common extension from https://www.computerhope.com/issues/ch001789.htm
|
# common extension from https://www.computerhope.com/issues/ch001789.htm
|
||||||
POPULAR_EXT = (
|
POPULAR_EXT = (
|
||||||
@@ -168,6 +167,8 @@ DOWNLOAD_EXT = (
|
|||||||
"bdmv",
|
"bdmv",
|
||||||
"bin",
|
"bin",
|
||||||
"bup",
|
"bup",
|
||||||
|
"cbr",
|
||||||
|
"cbz",
|
||||||
"clpi",
|
"clpi",
|
||||||
"crx",
|
"crx",
|
||||||
"db",
|
"db",
|
||||||
@@ -234,16 +235,16 @@ DOWNLOAD_EXT = (
|
|||||||
"xpi",
|
"xpi",
|
||||||
)
|
)
|
||||||
|
|
||||||
# combine to one tuple, with unique entries:
|
# Combine to one tuple, with unique entries:
|
||||||
ALL_EXT = tuple(set(POPULAR_EXT + DOWNLOAD_EXT))
|
ALL_EXT = tuple(set(POPULAR_EXT + DOWNLOAD_EXT))
|
||||||
# prepend a dot to each extension, because we work with a leading dot in extensions
|
# Prepend a dot to each extension, because we work with a leading dot in extensions
|
||||||
ALL_EXT = tuple(["." + i for i in ALL_EXT])
|
ALL_EXT = tuple(["." + i for i in ALL_EXT])
|
||||||
|
|
||||||
|
|
||||||
def has_popular_extension(file_path: str) -> bool:
|
def has_popular_extension(file_path: str) -> bool:
|
||||||
"""returns boolean if the extension of file_path is a popular, well-known extension"""
|
"""returns boolean if the extension of file_path is a popular, well-known extension"""
|
||||||
file_extension = get_ext(file_path)
|
file_extension = get_ext(file_path)
|
||||||
return file_extension in ALL_EXT
|
return file_extension in ALL_EXT or RAR_RE.match(file_extension)
|
||||||
|
|
||||||
|
|
||||||
def all_possible_extensions(file_path: str) -> List[str]:
|
def all_possible_extensions(file_path: str) -> List[str]:
|
||||||
@@ -264,9 +265,12 @@ def what_is_most_likely_extension(file_path: str) -> str:
|
|||||||
|
|
||||||
# Check if text or NZB, as puremagic is not good at that.
|
# Check if text or NZB, as puremagic is not good at that.
|
||||||
try:
|
try:
|
||||||
txt = Path(file_path).read_text()
|
# Only read the start, don't need the whole file
|
||||||
|
with open(file_path, "r") as inp_file:
|
||||||
|
txt = inp_file.read(200).lower()
|
||||||
|
|
||||||
# Yes, a text file ... so let's check if it's even an NZB:
|
# Yes, a text file ... so let's check if it's even an NZB:
|
||||||
if txt.lower().find("<nzb xmlns=") >= 0 or txt.lower().find("!doctype nzb public") >= 0:
|
if "!doctype nzb public" in txt or "<nzb xmlns=" in txt:
|
||||||
# yes, contains NZB signals:
|
# yes, contains NZB signals:
|
||||||
return ".nzb"
|
return ".nzb"
|
||||||
else:
|
else:
|
||||||
|
|||||||
+2
-2
@@ -5,5 +5,5 @@
|
|||||||
|
|
||||||
# You MUST use double quotes (so " and not ')
|
# You MUST use double quotes (so " and not ')
|
||||||
|
|
||||||
__version__ = "develop"
|
__version__ = "3.4.2"
|
||||||
__baseline__ = "unknown"
|
__baseline__ = "d8ab19087d541cb3c6bfe123d8f8e2619946995a"
|
||||||
@@ -7,6 +7,7 @@ pytest-httpbin
|
|||||||
pytest-httpserver
|
pytest-httpserver
|
||||||
flaky
|
flaky
|
||||||
xmltodict
|
xmltodict
|
||||||
tavern
|
tavern<1.16.2; python_version == '3.6'
|
||||||
|
tavern; python_version > '3.6'
|
||||||
tavalidate
|
tavalidate
|
||||||
lxml>=4.5.0 # needed by tavalidate
|
lxml>=4.5.0 # needed by tavalidate
|
||||||
@@ -29,6 +29,10 @@ class Test_File_Extension:
|
|||||||
assert file_extension.has_popular_extension("blabla/blabla.mkv")
|
assert file_extension.has_popular_extension("blabla/blabla.mkv")
|
||||||
assert file_extension.has_popular_extension("blabla/blabla.srt")
|
assert file_extension.has_popular_extension("blabla/blabla.srt")
|
||||||
assert file_extension.has_popular_extension("djjddj/aaaaa.epub")
|
assert file_extension.has_popular_extension("djjddj/aaaaa.epub")
|
||||||
|
assert file_extension.has_popular_extension("test/testing.r01")
|
||||||
|
assert file_extension.has_popular_extension("test/testing.s91")
|
||||||
|
assert not file_extension.has_popular_extension("test/testing")
|
||||||
|
assert not file_extension.has_popular_extension("test/testing.rar01")
|
||||||
assert not file_extension.has_popular_extension("98ads098f098fa.a0ds98f098asdf")
|
assert not file_extension.has_popular_extension("98ads098f098fa.a0ds98f098asdf")
|
||||||
|
|
||||||
def test_what_is_most_likely_extension(self):
|
def test_what_is_most_likely_extension(self):
|
||||||
|
|||||||
@@ -214,6 +214,44 @@ class TestMisc:
|
|||||||
os.unlink("test.cert")
|
os.unlink("test.cert")
|
||||||
os.unlink("test.key")
|
os.unlink("test.key")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"name, result",
|
||||||
|
[
|
||||||
|
("Free.Open.Source.Movie.2001.1080p.WEB-DL.DD5.1.H264-FOSS", False), # Not samples
|
||||||
|
("Setup.exe", False),
|
||||||
|
("23.123.hdtv-rofl", False),
|
||||||
|
("Something.1080p.WEB-DL.DD5.1.H264-EMRG-sample", True), # Samples
|
||||||
|
("Something.1080p.WEB-DL.DD5.1.H264-EMRG-sample.ogg", True),
|
||||||
|
("Sumtin_Else_1080p_WEB-DL_DD5.1_H264_proof-EMRG", True),
|
||||||
|
("Wot.Eva.540i.WEB-DL.aac.H264-Groupie sample.mp4", True),
|
||||||
|
("file-sample.mkv", True),
|
||||||
|
("PROOF.JPG", True),
|
||||||
|
("Bla.s01e02.title.1080p.aac-sample proof.mkv", True),
|
||||||
|
("Bla.s01e02.title.1080p.aac-proof.mkv", True),
|
||||||
|
("Bla.s01e02.title.1080p.aac sample proof.mkv", True),
|
||||||
|
("Bla.s01e02.title.1080p.aac proof.mkv", True),
|
||||||
|
("Lwtn.s08e26.1080p.web.h264-glhf-sample.par2", True),
|
||||||
|
("Lwtn.s08e26.1080p.web.h264-glhf-sample.vol001-002.par2", True),
|
||||||
|
("Look at That 2011 540i WEB-DL.H265-NoSample", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_is_sample(self, name, result):
|
||||||
|
assert misc.is_sample(name) == result
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"name, result",
|
||||||
|
[
|
||||||
|
("Not Death Proof (2022) 1080p x264 (DD5.1) BE Subs", False), # Try to trigger some false positives
|
||||||
|
("Proof.of.Everything.(2042).4320p.x266-4U", False),
|
||||||
|
("Crime_Scene_S01E13_Free_Sample_For_Sale_480p-OhDear", False),
|
||||||
|
("Sample That 2011 480p WEB-DL.H265-aMiGo", False),
|
||||||
|
("NOT A SAMPLE.JPG", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_is_sample_known_false_positives(self, name, result):
|
||||||
|
"""We know these fail, but don't have a better solution for them at the moment."""
|
||||||
|
assert misc.is_sample(name) != result
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_input, expected_output",
|
"test_input, expected_output",
|
||||||
[
|
[
|
||||||
|
|||||||
+15
-36
@@ -25,6 +25,7 @@ import sys
|
|||||||
from random import choice
|
from random import choice
|
||||||
|
|
||||||
from sabnzbd import sorting
|
from sabnzbd import sorting
|
||||||
|
from sabnzbd.constants import IGNORED_MOVIE_FOLDERS
|
||||||
from tests.testhelper import *
|
from tests.testhelper import *
|
||||||
|
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class TestSortingFunctions:
|
|||||||
"country": "US",
|
"country": "US",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
("Test Movie 720p HDTV AAC x265 sample-MYgroup", {"release_group": "MYgroup", "other": "Sample"}),
|
("Test Movie 720p HDTV AAC x265 MYgroup-Sample", {"release_group": "MYgroup", "other": "Sample"}),
|
||||||
(None, None), # Jobname missing
|
(None, None), # Jobname missing
|
||||||
("", None),
|
("", None),
|
||||||
],
|
],
|
||||||
@@ -85,33 +86,6 @@ class TestSortingFunctions:
|
|||||||
else:
|
else:
|
||||||
assert guess[key] == value
|
assert guess[key] == value
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"name, result",
|
|
||||||
[
|
|
||||||
("Free.Open.Source.Movie.2001.1080p.WEB-DL.DD5.1.H264-FOSS", False), # Not samples
|
|
||||||
("Setup.exe", False),
|
|
||||||
("23.123.hdtv-rofl", False),
|
|
||||||
("Something.1080p.WEB-DL.DD5.1.H264-EMRG-sample", True), # Samples
|
|
||||||
("Something.1080p.WEB-DL.DD5.1.H264-EMRG-sample.ogg", True),
|
|
||||||
("Sumtin_Else_1080p_WEB-DL_DD5.1_H264_proof-EMRG", True),
|
|
||||||
("Wot.Eva.540i.WEB-DL.aac.H264-Groupie sample.mp4", True),
|
|
||||||
("file-sample.mkv", True),
|
|
||||||
("PROOF.JPG", True),
|
|
||||||
("Bla.s01e02.title.1080p.aac-sample proof.mkv", True),
|
|
||||||
("Bla.s01e02.title.1080p.aac-proof.mkv", True),
|
|
||||||
("Bla.s01e02.title.1080p.aac sample proof.mkv", True),
|
|
||||||
("Bla.s01e02.title.1080p.aac proof.mkv", True),
|
|
||||||
("Not Death Proof (2022) 1080p x264 (DD5.1) BE Subs", False), # Try to trigger some false positives
|
|
||||||
("Proof.of.Everything.(2042).4320p.x266-4U", False),
|
|
||||||
("Crime_Scene_S01E13_Free_Sample_For_Sale_480p-OhDear", False),
|
|
||||||
("Sample That 2011 480p WEB-DL.H265-aMiGo", False),
|
|
||||||
("Look at That 2011 540i WEB-DL.H265-NoSample", False),
|
|
||||||
("NOT A SAMPLE.JPG", False),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_is_sample(self, name, result):
|
|
||||||
assert sorting.is_sample(name) == result
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("platform", ["linux", "darwin", "win32"])
|
@pytest.mark.parametrize("platform", ["linux", "darwin", "win32"])
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"path, result_unix, result_win",
|
"path, result_unix, result_win",
|
||||||
@@ -315,7 +289,7 @@ class TestSortingFunctions:
|
|||||||
pyfakefs.fake_filesystem_unittest.set_uid(0)
|
pyfakefs.fake_filesystem_unittest.set_uid(0)
|
||||||
# Create a fake filesystem in a random base directory, and included a typical DVD directory
|
# Create a fake filesystem in a random base directory, and included a typical DVD directory
|
||||||
base_dir = "/" + os.urandom(4).hex() + "/" + os.urandom(2).hex()
|
base_dir = "/" + os.urandom(4).hex() + "/" + os.urandom(2).hex()
|
||||||
dvd = choice(("video_ts", "audio_ts", "bdmv"))
|
dvd = choice(IGNORED_MOVIE_FOLDERS)
|
||||||
for test_dir in ["dir/2", "TEST/DIR2"]:
|
for test_dir in ["dir/2", "TEST/DIR2"]:
|
||||||
ffs.fs.create_dir(base_dir + "/" + test_dir, perm_bits=755)
|
ffs.fs.create_dir(base_dir + "/" + test_dir, perm_bits=755)
|
||||||
assert os.path.exists(base_dir + "/" + test_dir) is True
|
assert os.path.exists(base_dir + "/" + test_dir) is True
|
||||||
@@ -373,7 +347,7 @@ class TestSortingFunctions:
|
|||||||
pyfakefs.fake_filesystem_unittest.set_uid(0)
|
pyfakefs.fake_filesystem_unittest.set_uid(0)
|
||||||
# Create a fake filesystem in a random base directory, and included a typical DVD directory
|
# Create a fake filesystem in a random base directory, and included a typical DVD directory
|
||||||
base_dir = "D:\\" + os.urandom(4).hex() + "\\" + os.urandom(2).hex()
|
base_dir = "D:\\" + os.urandom(4).hex() + "\\" + os.urandom(2).hex()
|
||||||
dvd = choice(("video_ts", "audio_ts", "bdmv"))
|
dvd = choice(IGNORED_MOVIE_FOLDERS)
|
||||||
for test_dir in ["dir\\2", "TEST\\DIR2"]:
|
for test_dir in ["dir\\2", "TEST\\DIR2"]:
|
||||||
ffs.fs.create_dir(base_dir + "\\" + test_dir, perm_bits=755)
|
ffs.fs.create_dir(base_dir + "\\" + test_dir, perm_bits=755)
|
||||||
assert os.path.exists(base_dir + "\\" + test_dir) is True
|
assert os.path.exists(base_dir + "\\" + test_dir) is True
|
||||||
@@ -553,11 +527,14 @@ class TestSortingSorters:
|
|||||||
_func()
|
_func()
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"s_class, job_tag, sort_string, sort_result", # sort_result without extension
|
"s_class, job_tag, sort_string, sort_filename_result", # Supply sort_filename_result without extension
|
||||||
[
|
[
|
||||||
(sorting.SeriesSorter, "S01E02", "%r/%sn s%0se%0e.%ext", "Simulated Job s01e02"),
|
(sorting.SeriesSorter, "S01E02", "%r/%sn s%0se%0e.%ext", "Simulated Job s01e02"),
|
||||||
|
(sorting.SeriesSorter, "S01E02", "%r/%sn s%0se%0e", ""),
|
||||||
(sorting.MovieSorter, "2021", "%y_%.title.%r.%ext", "2021_Simulated.Job.2160p"),
|
(sorting.MovieSorter, "2021", "%y_%.title.%r.%ext", "2021_Simulated.Job.2160p"),
|
||||||
(sorting.DateSorter, "2020-02-29", "%y/%0m/%0d/%.t-%GI<release_group>", "Simulated.Job-SAB"),
|
(sorting.MovieSorter, "2021", "%y_%.title.%r", ""),
|
||||||
|
(sorting.DateSorter, "2020-02-29", "%y/%0m/%0d/%.t-%GI<release_group>.%ext", "Simulated.Job-SAB"),
|
||||||
|
(sorting.DateSorter, "2020-02-29", "%y/%0m/%0d/%.t-%GI<release_group>", ""),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("size_limit, file_size", [(512, 1024), (1024, 512)])
|
@pytest.mark.parametrize("size_limit, file_size", [(512, 1024), (1024, 512)])
|
||||||
@@ -569,7 +546,7 @@ class TestSortingSorters:
|
|||||||
s_class,
|
s_class,
|
||||||
job_tag,
|
job_tag,
|
||||||
sort_string,
|
sort_string,
|
||||||
sort_result,
|
sort_filename_result,
|
||||||
size_limit,
|
size_limit,
|
||||||
file_size,
|
file_size,
|
||||||
extension,
|
extension,
|
||||||
@@ -631,8 +608,10 @@ class TestSortingSorters:
|
|||||||
|
|
||||||
# Check the result
|
# Check the result
|
||||||
try:
|
try:
|
||||||
|
# If there's no "%ext" in the sort_string, no filenames should be changed
|
||||||
if (
|
if (
|
||||||
is_ok
|
is_ok
|
||||||
|
and sort_filename_result
|
||||||
and file_size > size_limit
|
and file_size > size_limit
|
||||||
and extension not in sorting.EXCLUDED_FILE_EXTS
|
and extension not in sorting.EXCLUDED_FILE_EXTS
|
||||||
and not (sorter.type == "movie" and number_of_files > 1 and not generate_sequential_filenames)
|
and not (sorter.type == "movie" and number_of_files > 1 and not generate_sequential_filenames)
|
||||||
@@ -642,10 +621,10 @@ class TestSortingSorters:
|
|||||||
if number_of_files > 1 and generate_sequential_filenames and sorter.type == "movie":
|
if number_of_files > 1 and generate_sequential_filenames and sorter.type == "movie":
|
||||||
# Movie sequential file handling
|
# Movie sequential file handling
|
||||||
for n in range(1, number_of_files + 1):
|
for n in range(1, number_of_files + 1):
|
||||||
expected = os.path.join(sort_dest, sort_result + " CD" + str(n) + extension)
|
expected = os.path.join(sort_dest, sort_filename_result + " CD" + str(n) + extension)
|
||||||
assert os.path.exists(expected)
|
assert os.path.exists(expected)
|
||||||
else:
|
else:
|
||||||
expected = os.path.join(sort_dest, sort_result + extension)
|
expected = os.path.join(sort_dest, sort_filename_result + extension)
|
||||||
assert os.path.exists(expected)
|
assert os.path.exists(expected)
|
||||||
else:
|
else:
|
||||||
# No renaming should happen
|
# No renaming should happen
|
||||||
@@ -699,7 +678,7 @@ class TestSortingSorters:
|
|||||||
generic = sorting.Sorter(None, "test_cat")
|
generic = sorting.Sorter(None, "test_cat")
|
||||||
generic.detect(job_name, SAB_CACHE_DIR)
|
generic.detect(job_name, SAB_CACHE_DIR)
|
||||||
|
|
||||||
assert generic.sort_file is result_sort_file
|
assert generic.sorter_active is result_sort_file
|
||||||
if result_sort_file:
|
if result_sort_file:
|
||||||
assert generic.sorter
|
assert generic.sorter
|
||||||
assert generic.sorter.__class__ is result_class
|
assert generic.sorter.__class__ is result_class
|
||||||
|
|||||||
Reference in new issue
Block a user