You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
211 lines
7.6 KiB
211 lines
7.6 KiB
#!/usr/bin/python3 -OO
|
|
# Copyright 2007-2020 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.
|
|
|
|
"""
|
|
sabnzbd.dirscanner - Scanner for Watched Folder
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
import logging
|
|
import threading
|
|
|
|
import sabnzbd
|
|
from sabnzbd.constants import SCAN_FILE_NAME, VALID_ARCHIVES, VALID_NZB_FILES
|
|
import sabnzbd.filesystem as filesystem
|
|
import sabnzbd.config as config
|
|
import sabnzbd.cfg as cfg
|
|
|
|
|
|
def compare_stat_tuple(tup1, tup2):
|
|
""" Test equality of two stat-tuples, content-related parts only """
|
|
if tup1.st_ino != tup2.st_ino:
|
|
return False
|
|
if tup1.st_size != tup2.st_size:
|
|
return False
|
|
if tup1.st_mtime != tup2.st_mtime:
|
|
return False
|
|
if tup1.st_ctime != tup2.st_ctime:
|
|
return False
|
|
return True
|
|
|
|
|
|
def clean_file_list(inp_list, folder, files):
|
|
""" Remove elements of "inp_list" not found in "files" """
|
|
for path in sorted(inp_list.keys()):
|
|
fld, name = os.path.split(path)
|
|
if fld == folder:
|
|
present = False
|
|
for name in files:
|
|
if os.path.join(folder, name) == path:
|
|
present = True
|
|
break
|
|
if not present:
|
|
del inp_list[path]
|
|
|
|
|
|
class DirScanner(threading.Thread):
|
|
"""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
|
|
Candidates which turned out wrong, will be remembered and skipped in
|
|
subsequent scans, unless changed.
|
|
"""
|
|
|
|
def __init__(self):
|
|
threading.Thread.__init__(self)
|
|
|
|
self.newdir()
|
|
try:
|
|
dirscan_dir, self.ignored, self.suspected = sabnzbd.load_admin(SCAN_FILE_NAME)
|
|
if dirscan_dir != self.dirscan_dir:
|
|
self.ignored = {}
|
|
self.suspected = {}
|
|
except:
|
|
self.ignored = {} # Will hold all unusable files and the
|
|
# successfully processed ones that cannot be deleted
|
|
self.suspected = {} # Will hold name/attributes of suspected candidates
|
|
|
|
self.shutdown = False
|
|
self.error_reported = False # Prevents mulitple reporting of missing watched folder
|
|
self.dirscan_dir = cfg.dirscan_dir.get_path()
|
|
self.dirscan_speed = cfg.dirscan_speed()
|
|
self.busy = False
|
|
self.trigger = False
|
|
cfg.dirscan_dir.callback(self.newdir)
|
|
cfg.dirscan_speed.callback(self.newspeed)
|
|
|
|
def newdir(self):
|
|
""" We're notified of a dir change """
|
|
self.ignored = {}
|
|
self.suspected = {}
|
|
self.dirscan_dir = cfg.dirscan_dir.get_path()
|
|
self.dirscan_speed = cfg.dirscan_speed()
|
|
|
|
def newspeed(self):
|
|
""" We're notified of a scan speed change """
|
|
self.dirscan_speed = cfg.dirscan_speed()
|
|
self.trigger = True
|
|
|
|
def stop(self):
|
|
""" Stop the dir scanner """
|
|
self.shutdown = True
|
|
|
|
def save(self):
|
|
""" Save dir scanner bookkeeping """
|
|
sabnzbd.save_admin((self.dirscan_dir, self.ignored, self.suspected), SCAN_FILE_NAME)
|
|
|
|
def run(self):
|
|
""" Start the scanner """
|
|
logging.info("Dirscanner starting up")
|
|
self.shutdown = False
|
|
|
|
while not self.shutdown:
|
|
# Use variable scan delay
|
|
x = max(self.dirscan_speed, 1)
|
|
while (x > 0) and not self.shutdown and not self.trigger:
|
|
time.sleep(1.0)
|
|
x = x - 1
|
|
|
|
self.trigger = False
|
|
if self.dirscan_speed and not self.shutdown:
|
|
self.scan()
|
|
|
|
def scan(self):
|
|
""" Do one scan of the watched folder """
|
|
|
|
def run_dir(folder, catdir):
|
|
try:
|
|
files = os.listdir(folder)
|
|
except OSError:
|
|
if not self.error_reported and not catdir:
|
|
logging.error(T("Cannot read Watched Folder %s"), filesystem.clip_path(folder))
|
|
self.error_reported = True
|
|
files = []
|
|
|
|
for filename in files:
|
|
path = os.path.join(folder, filename)
|
|
if os.path.isdir(path) or path in self.ignored or filename[0] == ".":
|
|
continue
|
|
|
|
if filesystem.get_ext(path) in VALID_NZB_FILES + VALID_ARCHIVES:
|
|
try:
|
|
stat_tuple = os.stat(path)
|
|
except OSError:
|
|
continue
|
|
else:
|
|
self.ignored[path] = 1
|
|
continue
|
|
|
|
if path in self.suspected:
|
|
if compare_stat_tuple(self.suspected[path], stat_tuple):
|
|
# Suspected file still has the same attributes
|
|
continue
|
|
else:
|
|
del self.suspected[path]
|
|
|
|
if stat_tuple.st_size > 0:
|
|
logging.info("Trying to import %s", path)
|
|
|
|
# Wait until the attributes are stable for 1 second, but give up after 3 sec
|
|
# This indicates that the file is fully written to disk
|
|
for n in range(3):
|
|
time.sleep(1.0)
|
|
try:
|
|
stat_tuple_tmp = os.stat(path)
|
|
except OSError:
|
|
continue
|
|
if compare_stat_tuple(stat_tuple, stat_tuple_tmp):
|
|
break
|
|
stat_tuple = stat_tuple_tmp
|
|
else:
|
|
# Not stable
|
|
continue
|
|
|
|
# Add the NZB's
|
|
res, _ = sabnzbd.add_nzbfile(path, catdir=catdir, keep=False)
|
|
if res < 0:
|
|
# Retry later, for example when we can't read the file
|
|
self.suspected[path] = stat_tuple
|
|
elif res == 0:
|
|
self.error_reported = False
|
|
else:
|
|
self.ignored[path] = 1
|
|
|
|
# Remove files from the bookkeeping that are no longer on the disk
|
|
clean_file_list(self.ignored, folder, files)
|
|
clean_file_list(self.suspected, folder, files)
|
|
|
|
if not self.busy:
|
|
self.busy = True
|
|
dirscan_dir = self.dirscan_dir
|
|
if dirscan_dir and not sabnzbd.PAUSED_ALL:
|
|
run_dir(dirscan_dir, None)
|
|
|
|
try:
|
|
dirscan_list = os.listdir(dirscan_dir)
|
|
except OSError:
|
|
if not self.error_reported:
|
|
logging.error(T("Cannot read Watched Folder %s"), filesystem.clip_path(dirscan_dir))
|
|
self.error_reported = True
|
|
dirscan_list = []
|
|
|
|
cats = config.get_categories()
|
|
for dd in dirscan_list:
|
|
dpath = os.path.join(dirscan_dir, dd)
|
|
if os.path.isdir(dpath) and dd.lower() in cats:
|
|
run_dir(dpath, dd.lower())
|
|
self.busy = False
|
|
|