diff --git a/SABnzbd.py b/SABnzbd.py index d076cda..58632ec 100755 --- a/SABnzbd.py +++ b/SABnzbd.py @@ -484,6 +484,11 @@ def print_modules(): else: logging.warning(Ta('unzip binary... NOT found!')) + if sabnzbd.newsunpack.SEVEN_COMMAND: + logging.info("7za binary... found (%s)", sabnzbd.newsunpack.SEVEN_COMMAND) + else: + logging.warning(Ta('7za binary... NOT found!')) + if not sabnzbd.WIN32: if sabnzbd.newsunpack.NICE_COMMAND: logging.info("nice binary... found (%s)", sabnzbd.newsunpack.NICE_COMMAND) @@ -1813,4 +1818,4 @@ if __name__ == '__main__': main() else: - main() \ No newline at end of file + main() diff --git a/interfaces/Config/templates/config_switches.tmpl b/interfaces/Config/templates/config_switches.tmpl index 1f95bdf..04931b4 100644 --- a/interfaces/Config/templates/config_switches.tmpl +++ b/interfaces/Config/templates/config_switches.tmpl @@ -156,72 +156,77 @@ 0 then 'checked="checked"' else ""#--> /> $T('explain-quick_check') -
+
"> 0 then 'checked="checked"' else ""#--> /> $T('explain-enable_unrar')
-
+
"> 0 then 'checked="checked"' else ""#--> /> $T('explain-enable_unzip')
-
+
"> + + 0 then 'checked="checked"' else ""#--> /> + $T('explain-enable_7zip') +
+
0 then 'checked="checked"' else ""#--> /> $T('explain-enable_filejoin')
-
+
0 then 'checked="checked"' else ""#--> /> $T('explain-ts_join')
-
+
0 then 'checked="checked"' else ""#--> /> $T('explain-enable_par_cleanup')
-
+
0 then 'checked="checked"' else ""#--> /> $T('explain-fail_on_crc')
-
+
0 then 'checked="checked"' else ""#--> /> $T('explain-safe_postproc')
-
+
0 then 'checked="checked"' else ""#--> /> $T('explain-sfv_check')
-
+
0 then 'checked="checked"' else ""#--> /> $T('explain-unpack_check')
-
"> +
"> 0 then 'checked="checked"' else ""#--> /> $T('explain-par2_multicore')
-
+
$T('explain-par_option')
-
"> +
"> /> $T('explain-nice')
-
"> +
"> /> $T('explain-ionice')
-
+
diff --git a/osx/7zip/7za b/osx/7zip/7za new file mode 100644 index 0000000..d6a55d9 Binary files /dev/null and b/osx/7zip/7za differ diff --git a/osx/7zip/License.txt b/osx/7zip/License.txt new file mode 100644 index 0000000..a6a7218 --- /dev/null +++ b/osx/7zip/License.txt @@ -0,0 +1,52 @@ + 7-Zip source code + ~~~~~~~~~~~~~~~~~ + License for use and distribution + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + 7-Zip Copyright (C) 1999-2010 Igor Pavlov. + + Licenses for files are: + + 1) CPP/7zip/Compress/Rar* files: GNU LGPL + unRAR restriction + 2) All other files: GNU LGPL + + The GNU LGPL + unRAR restriction means that you must follow both + GNU LGPL rules and unRAR restriction rules. + + + GNU LGPL information + -------------------- + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + + unRAR restriction + ----------------- + + The decompression engine for RAR archives was developed using source + code of unRAR program. + All copyrights to original unRAR code are owned by Alexander Roshal. + + The license for original unRAR code has the following restriction: + + The unRAR sources cannot be used to re-create the RAR compression algorithm, + which is proprietary. Distribution of modified unRAR sources in separate form + or as a part of other software is permitted, provided that it is clearly + stated in the documentation and source comments that the code may + not be used to develop a RAR (WinRAR) compatible archiver. + + + -- + Igor Pavlov diff --git a/package.py b/package.py index f0ad0a4..8b54338 100755 --- a/package.py +++ b/package.py @@ -327,6 +327,7 @@ data_files = [ 'win/par2/', 'win/unzip/', 'win/unrar/', + 'win/7zip/', 'icons/' ] @@ -418,7 +419,7 @@ if target == 'app': setup_requires=['py2app'], ) - # copy unrar & par2 binary to avoid striping + # copy unrar, 7zip & par2 binary to avoid striping os.system("mkdir dist/SABnzbd.app/Contents/Resources/osx>/dev/null") os.system("mkdir dist/SABnzbd.app/Contents/Resources/osx/par2>/dev/null") os.system("cp -pR osx/par2/ dist/SABnzbd.app/Contents/Resources/osx/par2>/dev/null") @@ -428,6 +429,8 @@ if target == 'app': os.system("cp -pR osx/unrar/unrar dist/SABnzbd.app/Contents/Resources/osx/unrar/ >/dev/null") else: os.system("cp -pR osx/unrar/unrar-leopard dist/SABnzbd.app/Contents/Resources/osx/unrar/unrar >/dev/null") + os.system("cp -pR osx/7zip/License.txt dist/SABnzbd.app/Contents/Resources/osx/7zip/ >/dev/null") + os.system("cp -pR osx/7zip/7za dist/SABnzbd.app/Contents/Resources/osx/7zip/ >/dev/null") os.system("cp icons/sabnzbd.ico dist/SABnzbd.app/Contents/Resources >/dev/null") os.system("cp README.rtf dist/SABnzbd.app/Contents/Resources/Credits.rtf >/dev/null") os.system("find dist/SABnzbd.app -name .git | xargs rm -rf") diff --git a/sabnzbd/cfg.py b/sabnzbd/cfg.py index 66dd47f..6f6ade9 100644 --- a/sabnzbd/cfg.py +++ b/sabnzbd/cfg.py @@ -87,6 +87,7 @@ start_paused = OptionBool('misc', 'start_paused', False) enable_unrar = OptionBool('misc', 'enable_unrar', True) enable_unzip = OptionBool('misc', 'enable_unzip', True) +enable_7zip = OptionBool('misc', 'enable_7zip', True) enable_filejoin = OptionBool('misc', 'enable_filejoin', True) enable_tsjoin = OptionBool('misc', 'enable_tsjoin', True) enable_par_cleanup = OptionBool('misc', 'enable_par_cleanup', True) diff --git a/sabnzbd/interface.py b/sabnzbd/interface.py index ba842c1..39e2673 100644 --- a/sabnzbd/interface.py +++ b/sabnzbd/interface.py @@ -1134,7 +1134,7 @@ SWITCH_LIST = \ 'ignore_samples', 'pause_on_post_processing', 'quick_check', 'nice', 'ionice', 'ssl_type', 'pre_script', 'pause_on_pwrar', 'ampm', 'sfv_check', 'folder_rename', 'unpack_check', 'quota_size', 'quota_day', 'quota_resume', 'quota_period', - 'pre_check', 'max_art_tries', 'max_art_opt' + 'pre_check', 'max_art_tries', 'max_art_opt', 'enable_7zip' ) #------------------------------------------------------------------------------ @@ -1154,6 +1154,9 @@ class ConfigSwitches(object): conf['nt'] = sabnzbd.WIN32 conf['have_nice'] = bool(sabnzbd.newsunpack.NICE_COMMAND) conf['have_ionice'] = bool(sabnzbd.newsunpack.IONICE_COMMAND) + conf['have_unrar'] = bool(sabnzbd.newsunpack.RAR_COMMAND) + conf['have_unzip'] = bool(sabnzbd.newsunpack.ZIP_COMMAND) + conf['have_7zip'] = bool(sabnzbd.newsunpack.SEVEN_COMMAND) for kw in SWITCH_LIST: conf[kw] = config.get_config('misc', kw)() diff --git a/sabnzbd/newsunpack.py b/sabnzbd/newsunpack.py index 682a200..989c582 100644 --- a/sabnzbd/newsunpack.py +++ b/sabnzbd/newsunpack.py @@ -60,6 +60,8 @@ TARGET_RE = re.compile(r'^(?:File|Target): "(.+)" -') EXTRACTFROM_RE = re.compile(r'^Extracting\sfrom\s(.+)') SPLITFILE_RE = re.compile(r'\.(\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) VOLPAR2_RE = re.compile(r'\.*vol[0-9]+\+[0-9]+\.par2', re.I) FULLVOLPAR2_RE = re.compile(r'(.*[^.])(\.*vol[0-9]+\+[0-9]+\.par2)', re.I) TS_RE = re.compile(r'\.(\d+)\.(ts$)', re.I) @@ -69,6 +71,7 @@ PAR2C_COMMAND = None RAR_COMMAND = None NICE_COMMAND = None ZIP_COMMAND = None +SEVEN_COMMAND = None IONICE_COMMAND = None RAR_PROBLEM = False CURL_COMMAND = None @@ -96,6 +99,8 @@ def find_programs(curdir): sabnzbd.newsunpack.PAR2_COMMAND = check(curdir, 'osx/par2/par2-classic') sabnzbd.newsunpack.RAR_COMMAND = check(curdir, 'osx/unrar/unrar') + if sabnzbd.DARWIN_INTEL: + sabnzbd.newsunpack.SEVEN_COMMAND = check(curdir, 'osx/7zip/7za') if sabnzbd.WIN32: if sabnzbd.WIN64 and cfg.allow_64bit_tools.get(): @@ -107,6 +112,7 @@ def find_programs(curdir): sabnzbd.newsunpack.RAR_COMMAND = check(curdir, 'win/unrar/UnRAR.exe') sabnzbd.newsunpack.PAR2C_COMMAND = check(curdir, 'win/par2/par2-classic.exe') sabnzbd.newsunpack.ZIP_COMMAND = check(curdir, 'win/unzip/unzip.exe') + sabnzbd.newsunpack.SEVEN_COMMAND = check(curdir, 'win/7zip/7za.exe') sabnzbd.newsunpack.CURL_COMMAND = check(curdir, 'lib/curl.exe') else: if not sabnzbd.newsunpack.PAR2_COMMAND: @@ -116,6 +122,7 @@ def find_programs(curdir): sabnzbd.newsunpack.NICE_COMMAND = find_on_path('nice') sabnzbd.newsunpack.IONICE_COMMAND = find_on_path('ionice') sabnzbd.newsunpack.ZIP_COMMAND = find_on_path('unzip') + sabnzbd.newsunpack.SEVEN_COMMAND = find_on_path('7za') if not (sabnzbd.WIN32 or sabnzbd.DARWIN): sabnzbd.newsunpack.RAR_PROBLEM = not unrar_check(sabnzbd.newsunpack.RAR_COMMAND) @@ -164,7 +171,7 @@ def SimpleRarExtract(rarfile, name): return ret, output #------------------------------------------------------------------------------ -def unpack_magic(nzo, workdir, workdir_complete, dele, one_folder, joinables, zips, rars, ts, depth=0): +def unpack_magic(nzo, workdir, workdir_complete, dele, one_folder, joinables, zips, rars, sevens, ts, depth=0): """ Do a recursive unpack from all archives in 'workdir' to 'workdir_complete' """ if depth > 5: @@ -174,9 +181,9 @@ def unpack_magic(nzo, workdir, workdir_complete, dele, one_folder, joinables, zi if depth == 1: # First time, ignore anything in workdir_complete - xjoinables, xzips, xrars, xts = build_filelists(workdir, None) + xjoinables, xzips, xrars, xsevens, xts = build_filelists(workdir, None) else: - xjoinables, xzips, xrars, xts = build_filelists(workdir, workdir_complete) + xjoinables, xzips, xrars, xsevens, xts = build_filelists(workdir, workdir_complete) rerun = False newfiles = [] @@ -214,6 +221,16 @@ def unpack_magic(nzo, workdir, workdir_complete, dele, one_folder, joinables, zi logging.info('Unzip finished on %s', workdir) nzo.set_action_line() + if cfg.enable_7zip(): + new_sevens = [seven for seven in xsevens if seven not in sevens] + if new_sevens: + rerun = True + logging.info('7za starting on %s', workdir) + if unseven(nzo, workdir, workdir_complete, dele, one_folder, new_sevens): + error = True + logging.info('7za finished on %s', workdir) + nzo.set_action_line() + if cfg.enable_tsjoin(): new_ts = [_ts for _ts in xts if _ts not in ts] if new_ts: @@ -228,7 +245,7 @@ def unpack_magic(nzo, workdir, workdir_complete, dele, one_folder, joinables, zi if rerun: z, y = unpack_magic(nzo, workdir, workdir_complete, dele, one_folder, - xjoinables, xzips, xrars, xts, depth) + xjoinables, xzips, xrars, xsevens, xts, depth) if z: error = z if y: @@ -748,12 +765,169 @@ def ZIP_Extract(zipfile, extraction_path, one_folder): startupinfo=stup, creationflags=creationflags) output = p.stdout.read() + logging.debug('unzip output: %s', output) ret = p.wait() return ret #------------------------------------------------------------------------------ +# 7Zip Functions +#------------------------------------------------------------------------------ + +def unseven(nzo, workdir, workdir_complete, delete, one_folder, sevens): + """ Unpack multiple sets '7z' of 7Zip files from 'workdir' to 'workdir_complete. + When 'delete' is set, originals will be deleted. + """ + i = 0 + unseven_failed = False + tms = time() + + # Find multi-volume sets, because 7zip will not provide actual set members + sets = {} + for seven in sevens: + name, ext = os.path.splitext(seven) + ext = ext.strip('.') + if not ext.isdigit(): + name = seven + ext = None + if name not in sets: + sets[name] = [] + if ext: + sets[name].append(ext) + + # Unpack each set + for seven in sets: + extensions = sets[seven] + logging.info("Starting extract on 7zip set/file: %s ", seven) + nzo.set_action_line(T('Unpacking'), '%s' % unicoder(seven)) + + if workdir_complete and seven.startswith(workdir): + extraction_path = workdir_complete + else: + extraction_path = os.path.split(seven)[0] + + res, msg = seven_extract(nzo, seven, extensions, extraction_path, one_folder, delete) + if res: + unseven_failed = True + nzo.set_unpack_info('Unpack', msg) + else: + i += 1 + + if not unseven_failed: + msg = T('%s files in %s') % (str(i), format_time_string(time() - tms)) + nzo.set_unpack_info('Unpack', msg) + + return unseven_failed + + +def seven_extract(nzo, sevenset, extensions, extraction_path, one_folder, delete): + """ Unpack single set 'sevenset' to 'extraction_path', + with password tries + Return fail==0(ok)/fail==1(error)/fail==2(wrong password), new_files, sevens + """ + + fail = 0 + if nzo.password: + passwords = [nzo.password] + else: + passwords = [] + pw_file = cfg.password_file.get_path() + if pw_file: + try: + pwf = open(pw_file, 'r') + passwords = pwf.read().split('\n') + # Remove empty lines and space-only passwords and remove surrounding spaces + passwords = [pw.strip('\r\n ') for pw in passwords if pw.strip('\r\n ')] + pwf.close() + logging.info('Read the passwords file %s', pw_file) + except IOError: + logging.info('Failed to read the passwords file %s', pw_file) + + if nzo.password: + # If an explicit password was set, add a retry without password, just in case. + passwords.append('') + elif not passwords or not nzo.encrypted: + # If we're not sure about encryption, start with empty password + # and make sure we have at least the empty password + passwords.insert(0, '') + + for password in passwords: + if password: + logging.debug('Trying 7zip with password "%s"', password) + msg = T('Trying 7zip with password "%s"') % unicoder(password) + nzo.fail_msg = msg + nzo.set_unpack_info('Unpack', msg) + fail, msg = seven_extract_core(sevenset, extensions, extraction_path, one_folder, delete, password) + if fail != 2: + break + + nzo.fail_msg = '' + if fail == 2: + logging.error('%s (%s)', Ta('Unpacking failed, archive requires a password'), latin1(os.path.split(sevenset)[1])) + return fail, msg + + +def seven_extract_core(sevenset, extensions, extraction_path, one_folder, delete, password): + """ Unpack single 7Z set 'sevenset' to 'extraction_path' + Return fail==0(ok)/fail==1(error)/fail==2(wrong password), message + """ + msg = None + if one_folder: + method = 'e' # Unpack without folders + else: + method = 'x' # Unpack with folders + if sabnzbd.WIN32 or sabnzbd.DARWIN: + case = '-ssc-' # Case insensitive + else: + case = '-ssc' # Case sensitive + if password: + password = '-p%s' % password + else: + password = '-p' + + + if len(extensions) > 0: + name = '%s.001' % sevenset + else: + name = sevenset + + if not os.path.exists(name): + return 1, T('7ZIP set "%s" is incomplete, cannot unpack') % unicoder(sevenset) + + command = [SEVEN_COMMAND, method, '-y', '-aou', case, password, + '-o%s' % extraction_path, name] + + stup, need_shell, command, creationflags = build_command(command) + + p = subprocess.Popen(command, shell=need_shell, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + startupinfo=stup, creationflags=creationflags) + + output = p.stdout.read() + logging.debug('7za output: %s', output) + + ret = p.wait() + + if ret == 0 and delete: + if extensions: + for ext in extensions: + path = '%s.%s' % (sevenset, ext) + try: + os.remove(path) + except: + logging.warning(Ta('Deleting %s failed!'), latin1(path)) + else: + try: + os.remove(sevenset) + except: + logging.warning(Ta('Deleting %s failed!'), latin1(sevenset)) + + # Always return an error message, even when return code is 0 + return ret, T('Could not unpack %s') % unicoder(sevenset) + + +#------------------------------------------------------------------------------ # PAR2 Functions #------------------------------------------------------------------------------ @@ -786,7 +960,7 @@ def par2_repair(parfile_nzf, nzo, workdir, setname): nzo.set_action_line(T('Repair'), T('Starting Repair')) logging.info('Scanning "%s"', parfile) - joinables, zips, rars, ts = build_filelists(workdir, None, check_rar=False) + joinables, zips, rars, sevens, ts = build_filelists(workdir, None, check_rar=False) finished, readd, pars, datafiles, used_joinables = PAR_Verify(parfile, parfile_nzf, nzo, setname, joinables) @@ -1235,7 +1409,7 @@ def build_filelists(workdir, workdir_complete, check_rar=True): """ Build filelists, if workdir_complete has files, ignore workdir. Optionally test content to establish RAR-ness """ - joinables, zips, rars, filelist = ([], [], [], []) + joinables, zips, rars, sevens, filelist = ([], [], [], [], []) if workdir_complete: for root, dirs, files in os.walk(workdir_complete): @@ -1247,10 +1421,13 @@ def build_filelists(workdir, workdir_complete, check_rar=True): for _file in files: filelist.append(os.path.join(root, _file)) + sevens = [f for f in filelist if SEVENZIP_RE.search(f)] + sevens.extend([f for f in filelist if SEVENMULTI_RE.search(f)]) + if check_rar: - joinables = [f for f in filelist if SPLITFILE_RE.search(f) and not is_rarfile(f)] + joinables = [f for f in filelist if f not in sevens and SPLITFILE_RE.search(f) and not is_rarfile(f)] else: - joinables = [f for f in filelist if SPLITFILE_RE.search(f)] + joinables = [f for f in filelist if f not in sevens and SPLITFILE_RE.search(f)] zips = [f for f in filelist if ZIP_RE.search(f)] @@ -1259,14 +1436,15 @@ def build_filelists(workdir, workdir_complete, check_rar=True): else: rars = [f for f in filelist if RAR_RE.search(f)] - ts = [f for f in filelist if TS_RE.search(f) and f not in joinables] + ts = [f for f in filelist if TS_RE.search(f) and f not in joinables and f not in sevens] 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, ts) + return (joinables, zips, rars, sevens, ts) def QuickCheck(set, nzo): diff --git a/sabnzbd/postproc.py b/sabnzbd/postproc.py index 2f94224..1e39001 100644 --- a/sabnzbd/postproc.py +++ b/sabnzbd/postproc.py @@ -340,7 +340,7 @@ def process_job(nzo): #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, (), (), (), ()) + unpack_error, newfiles = unpack_magic(nzo, workdir, tmp_workdir_complete, flag_delete, one_folder, (), (), (), (), ()) logging.info("unpack_magic finished on %s", filename) else: nzo.set_unpack_info('Unpack', T('No post-processing because of failed verification')) diff --git a/sabnzbd/skintext.py b/sabnzbd/skintext.py index fb98a1d..fcec52e 100644 --- a/sabnzbd/skintext.py +++ b/sabnzbd/skintext.py @@ -365,6 +365,8 @@ SKIN_TEXT = { 'explain-enable_unrar' : TT('Enable built-in unrar functionality.'), 'opt-enable_unzip' : TT('Enable Unzip'), 'explain-enable_unzip' : TT('Enable built-in unzip functionality.'), + 'opt-enable_7zip' : TT('Enable 7zip'), + 'explain-enable_7zip' : TT('Enable built-in 7zip functionality.'), 'opt-enable_filejoin' : TT('Enable Filejoin'), 'explain-enable_filejoin' : TT('Join files ending in .001, .002 etc. into one file.'), 'opt-enable_tsjoin' : TT('Enable TS Joining'), diff --git a/win/7zip/7za.exe b/win/7zip/7za.exe new file mode 100644 index 0000000..7f6bf86 Binary files /dev/null and b/win/7zip/7za.exe differ diff --git a/win/7zip/license.txt b/win/7zip/license.txt new file mode 100644 index 0000000..530ff36 --- /dev/null +++ b/win/7zip/license.txt @@ -0,0 +1,29 @@ + 7-Zip Command line version + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + License for use and distribution + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + 7-Zip Copyright (C) 1999-2010 Igor Pavlov. + + 7za.exe is distributed under the GNU LGPL license + + Notes: + You can use 7-Zip on any computer, including a computer in a commercial + organization. You don't need to register or pay for 7-Zip. + + + GNU LGPL information + -------------------- + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You can receive a copy of the GNU Lesser General Public License from + http://www.gnu.org/