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/