@ -22,35 +22,15 @@ sabnzbd.dirscanner - Scanner for Watched Folder
import os
import os
import time
import time
import logging
import logging
import zipfile
import gzip
import bz2
import threading
import threading
import sabnzbd
import sabnzbd
from sabnzbd . constants import SCAN_FILE_NAME , VALID_ARCHIVES , VALID_NZB_FILES
from sabnzbd . constants import SCAN_FILE_NAME , VALID_ARCHIVES , VALID_NZB_FILES
import sabnzbd . utils . rarfile as rarfile
from sabnzbd . decorators import NzbQueueLocker
from sabnzbd . encoding import correct_unknown_encoding
from sabnzbd . newsunpack import is_sevenfile , SevenZip
import sabnzbd . nzbstuff as nzbstuff
import sabnzbd . filesystem as filesystem
import sabnzbd . filesystem as filesystem
import sabnzbd . config as config
import sabnzbd . config as config
import sabnzbd . cfg as cfg
import sabnzbd . cfg as cfg
def name_to_cat ( fname , cat = None ) :
""" Retrieve category from file name, but only if " cat " is None. """
if cat is None and fname . startswith ( " {{ " ) :
n = fname . find ( " }} " )
if n > 2 :
cat = fname [ 2 : n ] . strip ( )
fname = fname [ n + 2 : ] . strip ( )
logging . debug ( " Job %s has category %s " , fname , cat )
return fname , cat
def compare_stat_tuple ( tup1 , tup2 ) :
def compare_stat_tuple ( tup1 , tup2 ) :
""" Test equality of two stat-tuples, content-related parts only """
""" Test equality of two stat-tuples, content-related parts only """
if tup1 . st_ino != tup2 . st_ino :
if tup1 . st_ino != tup2 . st_ino :
@ -64,40 +44,6 @@ def compare_stat_tuple(tup1, tup2):
return True
return True
def is_archive ( path ) :
""" Check if file in path is an ZIP, RAR or 7z file
: param path : path to file
: return : ( zf , status , expected_extension )
status : - 1 == Error / Retry , 0 == OK , 1 == Ignore
"""
if zipfile . is_zipfile ( path ) :
try :
zf = zipfile . ZipFile ( path )
return 0 , zf , " .zip "
except :
logging . info ( T ( " Cannot read %s " ) , path , exc_info = True )
return - 1 , None , " "
elif rarfile . is_rarfile ( path ) :
try :
# Set path to tool to open it
rarfile . UNRAR_TOOL = sabnzbd . newsunpack . RAR_COMMAND
zf = rarfile . RarFile ( path )
return 0 , zf , " .rar "
except :
logging . info ( T ( " Cannot read %s " ) , path , exc_info = True )
return - 1 , None , " "
elif is_sevenfile ( path ) :
try :
zf = SevenZip ( path )
return 0 , zf , " .7z "
except :
logging . info ( T ( " Cannot read %s " ) , path , exc_info = True )
return - 1 , None , " "
else :
logging . info ( " Archive %s is not a real archive! " , os . path . basename ( path ) )
return 1 , None , " "
def clean_file_list ( inp_list , folder , files ) :
def clean_file_list ( inp_list , folder , files ) :
""" Remove elements of " inp_list " not found in " files " """
""" Remove elements of " inp_list " not found in " files " """
for path in sorted ( inp_list . keys ( ) ) :
for path in sorted ( inp_list . keys ( ) ) :
@ -112,205 +58,6 @@ def clean_file_list(inp_list, folder, files):
del inp_list [ path ]
del inp_list [ path ]
@NzbQueueLocker
def process_nzb_archive_file (
filename ,
path ,
pp = None ,
script = None ,
cat = None ,
catdir = None ,
keep = False ,
priority = None ,
url = " " ,
nzbname = None ,
password = None ,
nzo_id = None ,
) :
""" Analyse ZIP file and create job(s).
Accepts ZIP files with ONLY nzb / nfo / folder files in it .
returns ( status , nzo_ids )
status : - 1 == Error / Retry , 0 == OK , 1 == Ignore
"""
nzo_ids = [ ]
if catdir is None :
catdir = cat
filename , cat = name_to_cat ( filename , catdir )
status , zf , extension = is_archive ( path )
if status != 0 :
return status , [ ]
status = 1
names = zf . namelist ( )
nzbcount = 0
for name in names :
name = name . lower ( )
if name . endswith ( " .nzb " ) :
status = 0
nzbcount + = 1
if status == 0 :
if nzbcount != 1 :
nzbname = None
for name in names :
if name . lower ( ) . endswith ( " .nzb " ) :
try :
data = correct_unknown_encoding ( zf . read ( name ) )
except OSError :
logging . error ( T ( " Cannot read %s " ) , name , exc_info = True )
zf . close ( )
return - 1 , [ ]
name = filesystem . setname_from_path ( name )
if data :
nzo = None
try :
nzo = nzbstuff . NzbObject (
name , pp , script , data , cat = cat , url = url , priority = priority , nzbname = nzbname
)
if not nzo . password :
nzo . password = password
except ( TypeError , ValueError ) :
# Duplicate or empty, ignore
pass
except :
# Something else is wrong, show error
logging . error ( T ( " Error while adding %s , removing " ) , name , exc_info = True )
if nzo :
if nzo_id :
# Re-use existing nzo_id, when a "future" job gets it payload
sabnzbd . nzbqueue . NzbQueue . do . remove ( nzo_id , add_to_history = False , delete_all_data = False )
nzo . nzo_id = nzo_id
nzo_id = None
nzo_ids . append ( sabnzbd . nzbqueue . NzbQueue . do . add ( nzo ) )
nzo . update_rating ( )
zf . close ( )
try :
if not keep :
filesystem . remove_file ( path )
except OSError :
logging . error ( T ( " Error removing %s " ) , filesystem . clip_path ( path ) )
logging . info ( " Traceback: " , exc_info = True )
status = 1
else :
zf . close ( )
status = 1
return status , nzo_ids
@NzbQueueLocker
def process_single_nzb (
filename ,
path ,
pp = None ,
script = None ,
cat = None ,
catdir = None ,
keep = False ,
priority = None ,
nzbname = None ,
reuse = False ,
nzo_info = None ,
dup_check = True ,
url = " " ,
password = None ,
nzo_id = None ,
) :
""" Analyze file and create a job from it
Supports NZB , NZB . BZ2 , NZB . GZ and GZ . NZB - in - disguise
returns ( status , nzo_ids )
status : - 2 == Error / retry , - 1 == Error , 0 == OK , 1 == OK - but - ignorecannot - delete
"""
nzo_ids = [ ]
if catdir is None :
catdir = cat
try :
with open ( path , " rb " ) as nzb_file :
check_bytes = nzb_file . read ( 2 )
if check_bytes == b " \x1f \x8b " :
# gzip file or gzip in disguise
name = filename . replace ( " .nzb.gz " , " .nzb " )
nzb_reader_handler = gzip . GzipFile
elif check_bytes == b " BZ " :
# bz2 file or bz2 in disguise
name = filename . replace ( " .nzb.bz2 " , " .nzb " )
nzb_reader_handler = bz2 . BZ2File
else :
name = filename
nzb_reader_handler = open
# Let's get some data and hope we can decode it
with nzb_reader_handler ( path , " rb " ) as nzb_file :
data = correct_unknown_encoding ( nzb_file . read ( ) )
except :
logging . warning ( T ( " Cannot read %s " ) , filesystem . clip_path ( path ) )
logging . info ( " Traceback: " , exc_info = True )
return - 2 , nzo_ids
if name :
name , cat = name_to_cat ( name , catdir )
# The name is used as the name of the folder, so sanitize it using folder specific santization
if not nzbname :
# Prevent embedded password from being damaged by sanitize and trimming
nzbname = os . path . split ( name ) [ 1 ]
try :
nzo = nzbstuff . NzbObject (
name ,
pp ,
script ,
data ,
cat = cat ,
priority = priority ,
nzbname = nzbname ,
nzo_info = nzo_info ,
url = url ,
reuse = reuse ,
dup_check = dup_check ,
)
if not nzo . password :
nzo . password = password
except TypeError :
# Duplicate, ignore
if nzo_id :
sabnzbd . nzbqueue . NzbQueue . do . remove ( nzo_id , add_to_history = False )
nzo = None
except ValueError :
# Empty, but correct file
return - 1 , nzo_ids
except :
if data . find ( " <nzb " ) > = 0 > data . find ( " </nzb " ) :
# Looks like an incomplete file, retry
return - 2 , nzo_ids
else :
# Something else is wrong, show error
logging . error ( T ( " Error while adding %s , removing " ) , name , exc_info = True )
return - 1 , nzo_ids
if nzo :
if nzo_id :
# Re-use existing nzo_id, when a "future" job gets it payload
sabnzbd . nzbqueue . NzbQueue . do . remove ( nzo_id , add_to_history = False , delete_all_data = False )
nzo . nzo_id = nzo_id
nzo_ids . append ( sabnzbd . nzbqueue . NzbQueue . do . add ( nzo , quiet = reuse ) )
nzo . update_rating ( )
try :
if not keep :
filesystem . remove_file ( path )
except OSError :
logging . error ( T ( " Error removing %s " ) , filesystem . clip_path ( path ) )
logging . info ( " Traceback: " , exc_info = True )
return 1 , nzo_ids
return 0 , nzo_ids
class DirScanner ( threading . Thread ) :
class DirScanner ( threading . Thread ) :
""" Thread that periodically scans a given directory and picks up any
""" Thread that periodically scans a given directory and picks up any
valid NZB , NZB . GZ ZIP - with - only - NZB and even NZB . GZ named as . NZB
valid NZB , NZB . GZ ZIP - with - only - NZB and even NZB . GZ named as . NZB
@ -398,15 +145,14 @@ class DirScanner(threading.Thread):
if os . path . isdir ( path ) or path in self . ignored or filename [ 0 ] == " . " :
if os . path . isdir ( path ) or path in self . ignored or filename [ 0 ] == " . " :
continue
continue
ext = os . path . splitext ( path ) [ 1 ] . lower ( )
if filesystem . get_ext ( path ) in VALID_NZB_FILES + VALID_ARCHIVES :
candidate = ext in VALID_NZB_FILES + VALID_ARCHIVES
if candidate :
try :
try :
stat_tuple = os . stat ( path )
stat_tuple = os . stat ( path )
except OSError :
except OSError :
continue
continue
else :
else :
self . ignored [ path ] = 1
self . ignored [ path ] = 1
continue
if path in self . suspected :
if path in self . suspected :
if compare_stat_tuple ( self . suspected [ path ] , stat_tuple ) :
if compare_stat_tuple ( self . suspected [ path ] , stat_tuple ) :
@ -415,12 +161,11 @@ class DirScanner(threading.Thread):
else :
else :
del self . suspected [ path ]
del self . suspected [ path ]
if candidate and stat_tuple . st_size > 0 :
if stat_tuple . st_size > 0 :
logging . info ( " Trying to import %s " , path )
logging . info ( " Trying to import %s " , path )
# Wait until the attributes are stable for 1 second
# Wait until the attributes are stable for 1 second, but give up after 3 sec
# but give up after 3 sec
# This indicates that the file is fully written to disk
stable = False
for n in range ( 3 ) :
for n in range ( 3 ) :
time . sleep ( 1.0 )
time . sleep ( 1.0 )
try :
try :
@ -428,37 +173,23 @@ class DirScanner(threading.Thread):
except OSError :
except OSError :
continue
continue
if compare_stat_tuple ( stat_tuple , stat_tuple_tmp ) :
if compare_stat_tuple ( stat_tuple , stat_tuple_tmp ) :
stable = True
break
break
else :
stat_tuple = stat_tuple_tmp
stat_tuple = stat_tuple_tmp
if not stable :
continue
# Handle archive files, but only when containing just NZB files
if ext in VALID_ARCHIVES :
res , nzo_ids = process_nzb_archive_file ( filename , path , catdir = catdir , url = path )
if res == - 1 :
self . suspected [ path ] = stat_tuple
elif res == 0 :
self . error_reported = False
else :
else :
self . ignored [ path ] = 1
# Not stable
continue
# Handle .nzb, .nzb.gz or gzip-disguised-as-nzb or .bz2
# Add the NZB's
elif ext == " .nzb " or filename . lower ( ) . endswith ( " .nzb.gz " ) or filename . lower ( ) . endswith ( " .nzb.bz2 " ) :
res , _ = sabnzbd . add_nzbfile ( path , catdir = catdir , keep = False )
res , nzo_id = process_single_nzb ( filename , path , catdir = catdir , url = path )
if res < 0 :
if res < 0 :
# Retry later, for example when we can't read the file
self . suspected [ path ] = stat_tuple
self . suspected [ path ] = stat_tuple
elif res == 0 :
elif res == 0 :
self . error_reported = False
self . error_reported = False
else :
else :
self . ignored [ path ] = 1
self . ignored [ path ] = 1
else :
# Remove files from the bookkeeping that are no longer on the disk
self . ignored [ path ] = 1
clean_file_list ( self . ignored , folder , files )
clean_file_list ( self . ignored , folder , files )
clean_file_list ( self . suspected , folder , files )
clean_file_list ( self . suspected , folder , files )