def test_clip_path_non_win(self): # Shouldn't have any effect on platforms other than Windows assert filesystem.clip_path(r"\\?\UNC\test") == r"\\?\UNC\test" assert filesystem.clip_path(r"\\?\F:\test") == r"\\?\F:\test" assert filesystem.clip_path(r"\\test") == r"\\test" assert filesystem.clip_path(r"F:\test") == r"F:\test" assert filesystem.clip_path("/test/dir") == "/test/dir"
def rename(self, files, current_path): """ Renaming Date file """ logging.debug("Renaming Date file") # find the master file to rename for file in files: if is_full_path(file): filepath = os.path.normpath(file) else: filepath = os.path.normpath(os.path.join(current_path, file)) if os.path.exists(filepath): size = os.stat(filepath).st_size if size > cfg.movie_rename_limit.get_int(): if "sample" not in file: self.fname, ext = os.path.splitext(os.path.split(file)[1]) newname = "%s%s" % (self.filename_set, ext) newname = newname.replace("%fn", self.fname) newpath = os.path.join(current_path, newname) if not os.path.exists(newpath): try: logging.debug("Rename: %s to %s", filepath, newpath) renamer(filepath, newpath) except: logging.error( T("Failed to rename: %s to %s"), clip_path(current_path), clip_path(newpath) ) logging.info("Traceback: ", exc_info=True) rename_similar(current_path, ext, self.filename_set, ()) break
def move(self, workdir_complete): ok = True if self.type == "movie": move_to_parent = True # check if we should leave the files inside an extra folder if cfg.movie_extra_folders(): # if there is a folder in the download, leave it in an extra folder move_to_parent = not check_for_folder(workdir_complete) if move_to_parent: workdir_complete, ok = move_to_parent_folder(workdir_complete) else: workdir_complete, ok = move_to_parent_folder(workdir_complete) if not ok: return workdir_complete, False path, part = os.path.split(workdir_complete) if "%fn" in part and self.sorter.fname: old = workdir_complete workdir_complete = os.path.join(path, part.replace("%fn", self.sorter.fname)) workdir_complete = get_unique_path(workdir_complete, create_dir=False) try: renamer(old, workdir_complete) except: logging.error(T("Cannot create directory %s"), clip_path(workdir_complete)) workdir_complete = old ok = False return workdir_complete, ok
def rename_similar(folder, skip_ext, name, skipped_files): """ Rename all other files in the 'folder' hierarchy after 'name' and move them to the root of 'folder'. Files having extension 'skip_ext' will be moved, but not renamed. Don't touch files in list `skipped_files` """ logging.debug('Give files in set "%s" matching names.', name) folder = os.path.normpath(folder) skip_ext = skip_ext.lower() for root, dirs, files in os.walk(folder): for f in files: path = os.path.join(root, f) if path in skipped_files: continue org, ext = os.path.splitext(f) if ext.lower() == skip_ext: # Move file, but do not rename newpath = os.path.join(folder, f) else: # Move file and rename newname = "%s%s" % (name, ext) newname = newname.replace("%fn", org) newpath = os.path.join(folder, newname) if path != newpath: newpath = get_unique_filename(newpath) try: logging.debug("Rename: %s to %s", path, newpath) renamer(path, newpath) except: logging.error(T("Failed to rename similar file: %s to %s"), clip_path(path), clip_path(newpath)) logging.info("Traceback: ", exc_info=True) cleanup_empty_directories(folder)
def rename(self, files, current_path): """ Rename for Series """ logging.debug("Renaming Series") largest = (None, None, 0) def to_filepath(f, current_path): if is_full_path(f): filepath = os.path.normpath(f) else: filepath = os.path.normpath(os.path.join(current_path, f)) return filepath # Create a generator of filepaths, ignore sample files and excluded files (vobs ect) filepaths = ( (file, to_filepath(file, current_path)) for file in files if not RE_SAMPLE.search(file) and get_ext(file) not in EXCLUDED_FILE_EXTS ) # Find the largest existing file for file, fp in filepaths: # If for some reason the file no longer exists, skip if not os.path.exists(fp): continue size = os.stat(fp).st_size f_file, f_fp, f_size = largest if size > f_size: largest = (file, fp, size) file, filepath, size = largest # >20MB if filepath and size > 20971520: self.fname, self.ext = os.path.splitext(os.path.split(file)[1]) newname = "%s%s" % (self.filename_set, self.ext) # Replace %fn with the original filename newname = newname.replace("%fn", self.fname) newpath = os.path.join(current_path, newname) # Replace %ext with extension newpath = newpath.replace("%ext", self.ext) try: logging.debug("Rename: %s to %s", filepath, newpath) renamer(filepath, newpath) except: logging.error(T("Failed to rename: %s to %s"), clip_path(current_path), clip_path(newpath)) logging.info("Traceback: ", exc_info=True) rename_similar(current_path, self.ext, self.filename_set, ()) else: logging.debug("Nothing to rename, %s", files)
def get_values(self): """ Collect and construct all the values needed for path replacement """ try: # - Show Name self.get_shownames() # - Season self.get_seasons() # - Episode Number self.get_episodes() # - Episode Name self.get_showdescriptions() return True except: logging.error(T("Error getting TV info (%s)"), clip_path(self.original_job_name)) logging.info("Traceback: ", exc_info=True) return False
def remove_samples(path): """Remove all files that match the sample pattern Skip deleting if it matches all files or there is only 1 file """ files_to_delete = [] nr_files = 0 for root, _dirs, files in os.walk(path): for file_to_match in files: nr_files += 1 if RE_SAMPLE.search(file_to_match): files_to_delete.append(os.path.join(root, file_to_match)) # Make sure we skip false-positives if len(files_to_delete) < nr_files: for path in files_to_delete: try: logging.info("Removing unwanted sample file %s", path) remove_file(path) except: logging.error(T("Removing %s failed"), clip_path(path)) logging.info("Traceback: ", exc_info=True) else: logging.info("Skipping sample-removal, false-positive")
def cleanup_list(wdir, skip_nzb): """Remove all files whose extension matches the cleanup list, optionally ignoring the nzb extension """ if cfg.cleanup_list(): try: files = os.listdir(wdir) except: files = () for filename in files: path = os.path.join(wdir, filename) if os.path.isdir(path): cleanup_list(path, skip_nzb) else: if on_cleanup_list(filename, skip_nzb): try: logging.info("Removing unwanted file %s", path) remove_file(path) except: logging.error(T("Removing %s failed"), clip_path(path)) logging.info("Traceback: ", exc_info=True) if files: # If directories only contained unwanted files, remove them cleanup_empty_directories(wdir)
def create_unrar_instance(self): """ Start the unrar instance using the user's options """ # Generate extraction path and save for post-proc if not self.unpack_dir_info: try: self.unpack_dir_info = prepare_extraction_path(self.nzo) except: # Prevent fatal crash if directory creation fails self.abort() return # Get the information extraction_path, _, _, one_folder, _ = self.unpack_dir_info # Set options if self.nzo.password: password_command = "-p%s" % self.nzo.password else: password_command = "-p-" if one_folder or cfg.flat_unpack(): action = "e" else: action = "x" # The first NZF self.rarfile_nzf = self.have_next_volume() # Ignore if maybe this set is not there any more # This can happen due to race/timing issues when creating the sets if not self.rarfile_nzf: return # Generate command rarfile_path = os.path.join(self.nzo.downpath, self.rarfile_nzf.filename) if sabnzbd.WIN32: # For Unrar to support long-path, we need to cricumvent Python's list2cmdline # See: https://github.com/sabnzbd/sabnzbd/issues/1043 command = [ "%s" % sabnzbd.newsunpack.RAR_COMMAND, action, "-vp", "-idp", "-o+", "-ai", password_command, "%s" % clip_path(rarfile_path), "%s\\" % long_path(extraction_path), ] else: # Don't use "-ai" (not needed for non-Windows) command = [ "%s" % sabnzbd.newsunpack.RAR_COMMAND, action, "-vp", "-idp", "-o+", password_command, "%s" % rarfile_path, "%s/" % extraction_path, ] if cfg.ignore_unrar_dates(): command.insert(3, "-tsm-") # Let's start from the first one! self.cur_volume = 1 # Need to disable buffer to have direct feedback self.active_instance = build_and_run_command(command, flatten_command=True, bufsize=0) # Add to runners ACTIVE_UNPACKERS.append(self) # Doing the first logging.info("DirectUnpacked volume %s for %s", self.cur_volume, self.cur_setname)
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)
def run(self): while 1: # Set NzbObject and NzbFile objects to None so references # from this thread do not keep the objects alive (see #1628) nzo = nzf = None nzo, nzf, file_done = self.queue.get() if not nzo: logging.info("Shutting down") break if nzf: # Check if enough disk space is free after each file is done # If not enough space left, pause downloader and send email if file_done and not sabnzbd.Downloader.paused: freespace = diskspace(force=True) full_dir = None required_space = (cfg.download_free.get_float() + nzf.bytes) / GIGI if freespace["download_dir"][1] < required_space: full_dir = "download_dir" # Enough space in download_dir, check complete_dir complete_free = cfg.complete_free.get_float() if complete_free > 0 and not full_dir: required_space = 0 if cfg.direct_unpack(): required_space = (complete_free + nzo.bytes_downloaded) / GIGI else: # Continue downloading until 95% complete before checking if nzo.bytes_tried > (nzo.bytes - nzo.bytes_par2) * 0.95: required_space = (complete_free + nzo.bytes) / GIGI if required_space and freespace["complete_dir"][1] < required_space: full_dir = "complete_dir" if full_dir: logging.warning(T("Too little diskspace forcing PAUSE")) # Pause downloader, but don't save, since the disk is almost full! sabnzbd.Downloader.pause() if cfg.fulldisk_autoresume(): sabnzbd.Scheduler.plan_diskspace_resume(full_dir, required_space) sabnzbd.emailer.diskfull_mail() # Prepare filepath filepath = nzf.prepare_filepath() if filepath: logging.debug("Decoding part of %s", filepath) try: self.assemble(nzf, file_done) except IOError as err: # If job was deleted or in active post-processing, ignore error if not nzo.deleted and not nzo.is_gone() and not nzo.pp_active: # 28 == disk full => pause downloader if err.errno == 28: logging.error(T("Disk full! Forcing Pause")) else: logging.error(T("Disk error on creating file %s"), clip_path(filepath)) # Log traceback logging.info("Traceback: ", exc_info=True) # Pause without saving sabnzbd.Downloader.pause() continue except: logging.error(T("Fatal error in Assembler"), exc_info=True) break # Continue after partly written data if not file_done: continue # Clean-up admin data logging.info("Decoding finished %s", filepath) nzf.remove_admin() # Do rar-related processing if rarfile.is_rarfile(filepath): # Encryption and unwanted extension detection rar_encrypted, unwanted_file = check_encrypted_and_unwanted_files(nzo, filepath) if rar_encrypted: if cfg.pause_on_pwrar() == 1: logging.warning( T( 'Paused job "%s" because of encrypted RAR file (if supplied, all passwords were tried)' ), nzo.final_name, ) nzo.pause() else: logging.warning( T( 'Aborted job "%s" because of encrypted RAR file (if supplied, all passwords were tried)' ), nzo.final_name, ) nzo.fail_msg = T("Aborted, encryption detected") sabnzbd.NzbQueue.end_job(nzo) if unwanted_file: # Don't repeat the warning after a user override of an unwanted extension pause if nzo.unwanted_ext == 0: logging.warning( T('In "%s" unwanted extension in RAR file. Unwanted file is %s '), nzo.final_name, unwanted_file, ) logging.debug(T("Unwanted extension is in rar file %s"), filepath) if cfg.action_on_unwanted_extensions() == 1 and nzo.unwanted_ext == 0: logging.debug("Unwanted extension ... pausing") nzo.unwanted_ext = 1 nzo.pause() if cfg.action_on_unwanted_extensions() == 2: logging.debug("Unwanted extension ... aborting") nzo.fail_msg = T("Aborted, unwanted extension detected") sabnzbd.NzbQueue.end_job(nzo) # Add to direct unpack nzo.add_to_direct_unpacker(nzf) elif par2file.is_parfile(filepath): # Parse par2 files, cloaked or not nzo.handle_par2(nzf, filepath) filter_output, reason = nzo_filtered_by_rating(nzo) if filter_output == 1: logging.warning( T('Paused job "%s" because of rating (%s)'), nzo.final_name, reason, ) nzo.pause() elif filter_output == 2: logging.warning( T('Aborted job "%s" because of rating (%s)'), nzo.final_name, reason, ) nzo.fail_msg = T("Aborted, rating filter matched (%s)") % reason sabnzbd.NzbQueue.end_job(nzo) else: sabnzbd.NzbQueue.remove(nzo.nzo_id, cleanup=False) sabnzbd.PostProcessor.process(nzo)
def test_clip_path_win(self): assert filesystem.clip_path(r"\\?\UNC\test") == r"\\test" assert filesystem.clip_path(r"\\?\F:\test") == r"F:\test"
def rename(self, _files, current_path): """ Rename for Generic files """ logging.debug("Renaming Generic file") def filter_files(_file, current_path): if is_full_path(_file): filepath = os.path.normpath(_file) else: filepath = os.path.normpath(os.path.join(current_path, _file)) if os.path.exists(filepath): size = os.stat(filepath).st_size if ( size >= cfg.movie_rename_limit.get_int() and not RE_SAMPLE.search(_file) and get_ext(_file) not in EXCLUDED_FILE_EXTS ): return True return False # remove any files below the limit from this list files = [_file for _file in _files if filter_files(_file, current_path)] length = len(files) # Single File Handling if length == 1: file = files[0] if is_full_path(file): filepath = os.path.normpath(file) else: filepath = os.path.normpath(os.path.join(current_path, file)) if os.path.exists(filepath): self.fname, ext = os.path.splitext(os.path.split(file)[1]) newname = "%s%s" % (self.filename_set, ext) newname = newname.replace("%fn", self.fname) newpath = os.path.join(current_path, newname) try: logging.debug("Rename: %s to %s", filepath, newpath) renamer(filepath, newpath) except: logging.error(T("Failed to rename: %s to %s"), clip_path(filepath), clip_path(newpath)) logging.info("Traceback: ", exc_info=True) rename_similar(current_path, ext, self.filename_set, ()) # Sequence File Handling # if there is more than one extracted file check for CD1/1/A in the title elif self.extra: matched_files = check_for_multiple(files) # rename files marked as in a set if matched_files: logging.debug("Renaming a series of generic files (%s)", matched_files) renamed = list(matched_files.values()) for index, file in matched_files.items(): filepath = os.path.join(current_path, file) renamed.append(filepath) self.fname, ext = os.path.splitext(os.path.split(file)[1]) name = "%s%s" % (self.filename_set, self.extra) name = name.replace("%1", str(index)).replace("%fn", self.fname) name = name + ext newpath = os.path.join(current_path, name) try: logging.debug("Rename: %s to %s", filepath, newpath) renamer(filepath, newpath) except: logging.error(T("Failed to rename: %s to %s"), clip_path(filepath), clip_path(newpath)) logging.info("Traceback: ", exc_info=True) rename_similar(current_path, ext, self.filename_set, renamed) else: logging.debug("Movie files not in sequence %s", _files)
def process_job(nzo): """ Process one job """ start = time.time() # keep track of whether we can continue all_ok = True # keep track of par problems par_error = False # keep track of any unpacking errors unpack_error = False # Signal empty download, for when 'empty_postproc' is enabled empty = False nzb_list = [] # These need to be initialized in case of a crash workdir_complete = "" script_log = "" script_line = "" # Get the job flags nzo.save_attribs() flag_repair, flag_unpack, flag_delete = nzo.repair_opts # Normalize PP if flag_delete: flag_unpack = True if flag_unpack: flag_repair = True # Get the NZB name filename = nzo.final_name # Download-processes can mark job as failed if nzo.fail_msg: nzo.status = Status.FAILED nzo.save_attribs() all_ok = False par_error = True unpack_error = 1 try: # Get the folder containing the download result workdir = nzo.downpath tmp_workdir_complete = None # if no files are present (except __admin__), fail the job if all_ok and len(globber(workdir)) < 2: if nzo.precheck: _, ratio = nzo.check_availability_ratio() emsg = T( "Download might fail, only %s of required %s available" ) % (ratio, cfg.req_completion_rate()) else: emsg = T("Download failed - Not on your server(s)") empty = True emsg += " - https://sabnzbd.org/not-complete" nzo.fail_msg = emsg nzo.set_unpack_info("Fail", emsg) nzo.status = Status.FAILED # do not run unpacking or parity verification flag_repair = flag_unpack = False all_ok = cfg.empty_postproc() and empty if not all_ok: par_error = True unpack_error = 1 script = nzo.script logging.info( "Starting Post-Processing on %s => Repair:%s, Unpack:%s, Delete:%s, Script:%s, Cat:%s", filename, flag_repair, flag_unpack, flag_delete, script, nzo.cat, ) # Set complete dir to workdir in case we need to abort workdir_complete = workdir # Send post-processing notification notifier.send_notification(T("Post-processing"), nzo.final_name, "pp", nzo.cat) # Par processing, if enabled if all_ok and flag_repair: par_error, re_add = parring(nzo, workdir) if re_add: # Try to get more par files return False # If we don't need extra par2, we can disconnect if sabnzbd.nzbqueue.NzbQueue.do.actives( grabs=False) == 0 and cfg.autodisconnect(): # This was the last job, close server connections sabnzbd.downloader.Downloader.do.disconnect() # Sanitize the resulting files if sabnzbd.WIN32: sanitize_files_in_folder(workdir) # Check if user allows unsafe post-processing if flag_repair and cfg.safe_postproc(): all_ok = all_ok and not par_error if all_ok: # Fix encodings fix_unix_encoding(workdir) # Use dirs generated by direct-unpacker if nzo.direct_unpacker and nzo.direct_unpacker.unpack_dir_info: ( tmp_workdir_complete, workdir_complete, file_sorter, one_folder, marker_file, ) = nzo.direct_unpacker.unpack_dir_info else: # Generate extraction path tmp_workdir_complete, workdir_complete, file_sorter, one_folder, marker_file = prepare_extraction_path( nzo) newfiles = [] # Run Stage 2: Unpack if flag_unpack: # Set the current nzo status to "Extracting...". Used in History nzo.status = Status.EXTRACTING logging.info("Running unpack_magic on %s", filename) unpack_error, newfiles = unpack_magic(nzo, workdir, tmp_workdir_complete, flag_delete, one_folder, (), (), (), (), ()) logging.info("Unpacked files %s", newfiles) if sabnzbd.WIN32: # Sanitize the resulting files newfiles = sanitize_files_in_folder(tmp_workdir_complete) logging.info("Finished unpack_magic on %s", filename) if cfg.safe_postproc(): all_ok = all_ok and not unpack_error if all_ok: # Move any (left-over) files to destination nzo.status = Status.MOVING nzo.set_action_line(T("Moving"), "...") for root, _dirs, files in os.walk(workdir): if not root.endswith(JOB_ADMIN): for file_ in files: path = os.path.join(root, file_) new_path = path.replace(workdir, tmp_workdir_complete) ok, new_path = move_to_path(path, new_path) if new_path: newfiles.append(new_path) if not ok: nzo.set_unpack_info( "Unpack", T("Failed moving %s to %s") % (path, new_path)) all_ok = False break # Set permissions right set_permissions(tmp_workdir_complete) if all_ok and marker_file: del_marker(os.path.join(tmp_workdir_complete, marker_file)) remove_from_list(marker_file, newfiles) if all_ok: # Remove files matching the cleanup list cleanup_list(tmp_workdir_complete, skip_nzb=True) # Check if this is an NZB-only download, if so redirect to queue # except when PP was Download-only if flag_repair: nzb_list = nzb_redirect(tmp_workdir_complete, nzo.final_name, nzo.pp, script, nzo.cat, nzo.priority) else: nzb_list = None if nzb_list: nzo.set_unpack_info("Download", T("Sent %s to queue") % nzb_list) cleanup_empty_directories(tmp_workdir_complete) else: # Full cleanup including nzb's cleanup_list(tmp_workdir_complete, skip_nzb=False) script_output = "" script_ret = 0 if not nzb_list: # Give destination its final name if cfg.folder_rename() and tmp_workdir_complete and not one_folder: if not all_ok: # Rename failed folders so they are easy to recognize workdir_complete = tmp_workdir_complete.replace( "_UNPACK_", "_FAILED_") workdir_complete = get_unique_path(workdir_complete, create_dir=False) try: newfiles = rename_and_collapse_folder( tmp_workdir_complete, workdir_complete, newfiles) except: logging.error( T('Error renaming "%s" to "%s"'), clip_path(tmp_workdir_complete), clip_path(workdir_complete), ) logging.info("Traceback: ", exc_info=True) # Better disable sorting because filenames are all off now file_sorter.sort_file = None if empty: job_result = -1 else: job_result = int(par_error) + int(bool(unpack_error)) * 2 if cfg.ignore_samples(): remove_samples(workdir_complete) # TV/Movie/Date Renaming code part 2 - rename and move files to parent folder if all_ok and file_sorter.sort_file: if newfiles: file_sorter.rename(newfiles, workdir_complete) workdir_complete, ok = file_sorter.move(workdir_complete) else: workdir_complete, ok = file_sorter.rename_with_ext( workdir_complete) if not ok: nzo.set_unpack_info("Unpack", T("Failed to move files")) all_ok = False if cfg.deobfuscate_final_filenames() and all_ok and not nzb_list: # deobfuscate the filenames logging.info("Running deobfuscate") deobfuscate.deobfuscate_list(newfiles, nzo.final_name) # Run the user script script_path = make_script_path(script) if (all_ok or not cfg.safe_postproc()) and ( not nzb_list) and script_path: # Set the current nzo status to "Ext Script...". Used in History nzo.status = Status.RUNNING nzo.set_action_line(T("Running script"), script) nzo.set_unpack_info("Script", T("Running user script %s") % script, unique=True) script_log, script_ret = external_processing( script_path, nzo, clip_path(workdir_complete), nzo.final_name, job_result) script_line = get_last_line(script_log) if script_log: script_output = nzo.nzo_id if script_line: nzo.set_unpack_info("Script", script_line, unique=True) else: nzo.set_unpack_info("Script", T("Ran %s") % script, unique=True) else: script = "" script_line = "" script_ret = 0 # Maybe bad script result should fail job if script_ret and cfg.script_can_fail(): script_error = True all_ok = False nzo.fail_msg = T("Script exit code is %s") % script_ret else: script_error = False # Email the results if (not nzb_list) and cfg.email_endjob(): if (cfg.email_endjob() == 1) or (cfg.email_endjob() == 2 and (unpack_error or par_error or script_error)): emailer.endjob( nzo.final_name, nzo.cat, all_ok, workdir_complete, nzo.bytes_downloaded, nzo.fail_msg, nzo.unpack_info, script, script_log, script_ret, ) if script_output: # Can do this only now, otherwise it would show up in the email if script_ret: script_ret = "Exit(%s) " % script_ret else: script_ret = "" if len(script_log.rstrip().split("\n")) > 1: nzo.set_unpack_info( "Script", '%s%s <a href="./scriptlog?name=%s">(%s)</a>' % (script_ret, script_line, encoding.xml_name(script_output), T("More")), unique=True, ) else: # No '(more)' button needed nzo.set_unpack_info("Script", "%s%s " % (script_ret, script_line), unique=True) # Cleanup again, including NZB files if all_ok: cleanup_list(workdir_complete, False) # Force error for empty result all_ok = all_ok and not empty # Update indexer with results if cfg.rating_enable(): if nzo.encrypted > 0: Rating.do.update_auto_flag(nzo.nzo_id, Rating.FLAG_ENCRYPTED) if empty: hosts = [ s.host for s in sabnzbd.downloader.Downloader.do.nzo_servers(nzo) ] if not hosts: hosts = [None] for host in hosts: Rating.do.update_auto_flag(nzo.nzo_id, Rating.FLAG_EXPIRED, host) except: logging.error(T("Post Processing Failed for %s (%s)"), filename, T("see logfile")) logging.info("Traceback: ", exc_info=True) nzo.fail_msg = T("PostProcessing was aborted (%s)") % T("see logfile") notifier.send_notification(T("Download Failed"), filename, "failed", nzo.cat) nzo.status = Status.FAILED par_error = True all_ok = False if cfg.email_endjob(): emailer.endjob( nzo.final_name, nzo.cat, all_ok, clip_path(workdir_complete), nzo.bytes_downloaded, nzo.fail_msg, nzo.unpack_info, "", "", 0, ) if all_ok: # If the folder only contains one file OR folder, have that as the path # Be aware that series/generic/date sorting may move a single file into a folder containing other files workdir_complete = one_file_or_folder(workdir_complete) workdir_complete = os.path.normpath(workdir_complete) # Clean up the NZO data try: nzo.purge_data(delete_all_data=all_ok) except: logging.error(T("Cleanup of %s failed."), nzo.final_name) logging.info("Traceback: ", exc_info=True) # Use automatic retry link on par2 errors and encrypted/bad RARs if par_error or unpack_error in (2, 3): try_alt_nzb(nzo) # Show final status in history if all_ok: notifier.send_notification(T("Download Completed"), filename, "complete", nzo.cat) nzo.status = Status.COMPLETED else: notifier.send_notification(T("Download Failed"), filename, "failed", nzo.cat) nzo.status = Status.FAILED # Log the overall time taken for postprocessing postproc_time = int(time.time() - start) # Create the history DB instance history_db = database.HistoryDB() # Add the nzo to the database. Only the path, script and time taken is passed # Other information is obtained from the nzo history_db.add_history_db(nzo, workdir_complete, postproc_time, script_log, script_line) # Purge items history_db.auto_history_purge() # The connection is only used once, so close it here history_db.close() sabnzbd.history_updated() return True
def run(self): while 1: job = self.queue.get() if not job: logging.info("Shutting down") break nzo, nzf, file_done = job if nzf: # Check if enough disk space is free after each file is done # If not enough space left, pause downloader and send email if (file_done and diskspace(force=True)["download_dir"][1] < (cfg.download_free.get_float() + nzf.bytes) / GIGI): # Only warn and email once if not sabnzbd.downloader.Downloader.do.paused: logging.warning( T("Too little diskspace forcing PAUSE")) # Pause downloader, but don't save, since the disk is almost full! sabnzbd.downloader.Downloader.do.pause() sabnzbd.emailer.diskfull_mail() # Abort all direct unpackers, just to be sure sabnzbd.directunpacker.abort_all() # Prepare filepath filepath = nzf.prepare_filepath() if filepath: logging.debug("Decoding part of %s", filepath) try: self.assemble(nzf, file_done) except IOError as err: # If job was deleted or in active post-processing, ignore error if not nzo.deleted and not nzo.is_gone( ) and not nzo.pp_active: # 28 == disk full => pause downloader if err.errno == 28: logging.error(T("Disk full! Forcing Pause")) else: logging.error( T("Disk error on creating file %s"), clip_path(filepath)) # Log traceback logging.info("Traceback: ", exc_info=True) # Pause without saving sabnzbd.downloader.Downloader.do.pause() continue except: logging.error(T("Fatal error in Assembler"), exc_info=True) break # Continue after partly written data if not file_done: continue # Clean-up admin data logging.info("Decoding finished %s", filepath) nzf.remove_admin() # Do rar-related processing if rarfile.is_rarfile(filepath): # Encryption and unwanted extension detection rar_encrypted, unwanted_file = check_encrypted_and_unwanted_files( nzo, filepath) if rar_encrypted: if cfg.pause_on_pwrar() == 1: logging.warning( T('Paused job "%s" because of encrypted RAR file (if supplied, all passwords were tried)' ), nzo.final_name, ) nzo.pause() else: logging.warning( T('Aborted job "%s" because of encrypted RAR file (if supplied, all passwords were tried)' ), nzo.final_name, ) nzo.fail_msg = T( "Aborted, encryption detected") sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo) if unwanted_file: logging.warning( T('In "%s" unwanted extension in RAR file. Unwanted file is %s ' ), nzo.final_name, unwanted_file, ) logging.debug( T("Unwanted extension is in rar file %s"), filepath) if cfg.action_on_unwanted_extensions( ) == 1 and nzo.unwanted_ext == 0: logging.debug("Unwanted extension ... pausing") nzo.unwanted_ext = 1 nzo.pause() if cfg.action_on_unwanted_extensions() == 2: logging.debug( "Unwanted extension ... aborting") nzo.fail_msg = T( "Aborted, unwanted extension detected") sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo) # Add to direct unpack nzo.add_to_direct_unpacker(nzf) elif par2file.is_parfile(filepath): # Parse par2 files, cloaked or not nzo.handle_par2(nzf, filepath) filter_output, reason = nzo_filtered_by_rating(nzo) if filter_output == 1: logging.warning( T('Paused job "%s" because of rating (%s)'), nzo.final_name, reason, ) nzo.pause() elif filter_output == 2: logging.warning( T('Aborted job "%s" because of rating (%s)'), nzo.final_name, reason, ) nzo.fail_msg = T( "Aborted, rating filter matched (%s)") % reason sabnzbd.nzbqueue.NzbQueue.do.end_job(nzo) else: sabnzbd.nzbqueue.NzbQueue.do.remove(nzo.nzo_id, add_to_history=False, cleanup=False) PostProcessor.do.process(nzo)
def test_empty(self): assert filesystem.clip_path(None) is None assert filesystem.long_path(None) is None
def process_nzb_archive_file( filename, path, pp=None, script=None, cat=None, catdir=None, keep=False, priority=None, nzbname=None, reuse=None, nzo_info=None, dup_check=True, url=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, 0==OK, 1==Ignore """ nzo_ids = [] if catdir is None: catdir = cat filename, cat = name_to_cat(filename, catdir) # Returns -1==Error/Retry, 0==OK, 1==Ignore 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=pp, script=script, nzb=data, cat=cat, url=url, priority=priority, nzbname=nzbname, nzo_info=nzo_info, reuse=reuse, dup_check=dup_check, ) 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.remove(nzo_id, delete_all_data=False) nzo.nzo_id = nzo_id nzo_id = None nzo_ids.append(sabnzbd.NzbQueue.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) else: zf.close() status = 1 return status, nzo_ids
def test_nothing_to_clip_win(self): assert filesystem.clip_path(r"\\test") == r"\\test" assert filesystem.clip_path(r"F:\test") == r"F:\test" assert filesystem.clip_path("/test/dir") == "/test/dir"
def process_single_nzb( filename, path, pp=None, script=None, cat=None, catdir=None, keep=False, priority=None, nzbname=None, reuse=None, nzo_info=None, dup_check=True, url=None, 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 """ 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 filename = filename.replace(".nzb.gz", ".nzb") nzb_reader_handler = gzip.GzipFile elif check_bytes == b"BZ": # bz2 file or bz2 in disguise filename = filename.replace(".nzb.bz2", ".nzb") nzb_reader_handler = bz2.BZ2File else: 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 OSError: logging.warning(T("Cannot read %s"), filesystem.clip_path(path)) logging.info("Traceback: ", exc_info=True) return -2, nzo_ids if filename: filename, cat = name_to_cat(filename, 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 = get_filename(filename) try: nzo = nzbstuff.NzbObject( filename, pp=pp, script=script, nzb=data, cat=cat, url=url, priority=priority, nzbname=nzbname, nzo_info=nzo_info, reuse=reuse, dup_check=dup_check, ) if not nzo.password: nzo.password = password except TypeError: # Duplicate, ignore if nzo_id: sabnzbd.NzbQueue.remove(nzo_id) nzo = None except ValueError: # Empty 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"), filename, 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.remove(nzo_id, delete_all_data=False) nzo.nzo_id = nzo_id nzo_ids.append(sabnzbd.NzbQueue.add(nzo, quiet=reuse)) nzo.update_rating() try: if not keep: filesystem.remove_file(path) except OSError: # Job was still added to the queue, so throw error but don't report failed add logging.error(T("Error removing %s"), filesystem.clip_path(path)) logging.info("Traceback: ", exc_info=True) return 0, nzo_ids
def build_history_info(nzo, workdir_complete="", postproc_time=0, script_output="", script_line="", series_info=False): """ Collects all the information needed for the database """ completed = int(time.time()) pp = _PP_LOOKUP.get(opts_to_pp(*nzo.repair_opts), "X") if script_output: # Compress the output of the script script_output = sqlite3.Binary(zlib.compress(utob(script_output))) download_time = nzo.nzo_info.get("download_time", 0) url_info = nzo.nzo_info.get("details", "") or nzo.nzo_info.get( "more_info", "") # Get the dictionary containing the stages and their unpack process # Pack the dictionary up into a single string # Stage Name is separated by ::: stage lines by ; and stages by \r\n lines = [] for key, results in nzo.unpack_info.items(): lines.append("%s:::%s" % (key, ";".join(results))) stage_log = "\r\n".join(lines) # Reuse the old 'report' column to indicate a URL-fetch report = "future" if nzo.futuretype else "" # Analyze series info only when job is finished series = "" if series_info: seriesname, season, episode, _ = sabnzbd.newsunpack.analyse_show( nzo.final_name) if seriesname and season and episode: series = "%s/%s/%s" % (seriesname.lower(), season, episode) return ( completed, nzo.final_name, nzo.filename, nzo.cat, pp, nzo.script, report, nzo.url, nzo.status, nzo.nzo_id, clip_path(workdir_complete), clip_path(nzo.downpath), script_output, script_line, download_time, postproc_time, stage_log, nzo.bytes_downloaded, nzo.fail_msg, url_info, nzo.bytes_downloaded, series, nzo.md5sum, nzo.password, )
def get_clipped_path(self): """ Return clipped full absolute path """ return clip_path(self.get_path())