Add detection of password protected rars with config/Post Processing/'Unpack downloads' enabled.

Change post process to cleanup filenames with config/Post Processing/'Unpack downloads' enabled.
Change post process to join incrementally named (i.e. file.001 to file.nnn) split files.
Change replace unrar2 lib with rarfile 3.0 and UnRAR.exe 5.40 beta 4 freeware.
Change post process "Copy" to delete redundant files after use.
This commit is contained in:
JackDandy 2016-08-11 01:00:36 +01:00
parent 16c27b37ec
commit 48a27e57a0
23 changed files with 3229 additions and 944 deletions

View file

@ -1,7 +1,6 @@
language: python
sudo: false
python:
- 2.6
- 2.7
install:

View file

@ -114,6 +114,11 @@
* Change set Specials to status "Skipped" not "Wanted" during show updates
* Change improve debug log message for CloudFlare response that indicate website is offline
* Add handling for 'part' numbered new releases and also for specific existing 'part' numbered releases
* Add detection of password protected rars with config/Post Processing/'Unpack downloads' enabled
* Change post process to cleanup filenames with config/Post Processing/'Unpack downloads' enabled
* Change post process to join incrementally named (i.e. file.001 to file.nnn) split files
* Change replace unrar2 lib with rarfile 3.0 and UnRAR.exe 5.40 beta 4 freeware
* Change post process "Copy" to delete redundant files after use
[develop changelog]
* Change send nzb data to NZBGet for Anizb instead of url

View file

@ -9,4 +9,3 @@ Libs with customisations...
/lib/requests/packages/urllib3/util/ssl_.py
/lib/tornado
/lib/tvdb/tvdb_api.py
/lib/unrar2/unix.py

BIN
lib/rarfile/UnRAR.exe Normal file

Binary file not shown.

1
lib/rarfile/__init__.py Normal file
View file

@ -0,0 +1 @@

2932
lib/rarfile/rarfile.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,18 +0,0 @@
The unrar.dll library is freeware. This means:
1. All copyrights to RAR and the unrar.dll are exclusively
owned by the author - Alexander Roshal.
2. The unrar.dll library may be used in any software to handle RAR
archives without limitations free of charge.
3. THE RAR ARCHIVER AND THE UNRAR.DLL LIBRARY ARE DISTRIBUTED "AS IS".
NO WARRANTY OF ANY KIND IS EXPRESSED OR IMPLIED. YOU USE AT
YOUR OWN RISK. THE AUTHOR WILL NOT BE LIABLE FOR DATA LOSS,
DAMAGES, LOSS OF PROFITS OR ANY OTHER KIND OF LOSS WHILE USING
OR MISUSING THIS SOFTWARE.
Thank you for your interest in RAR and unrar.dll.
Alexander L. Roshal

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,180 +0,0 @@
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll.
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple,
stable and foolproof.
Notice that it has INCOMPATIBLE interface.
It enables reading and unpacking of archives created with the
RAR/WinRAR archivers. There is a low-level interface which is very
similar to the C interface provided by UnRAR. There is also a
higher level interface which makes some common operations easier.
"""
__version__ = '0.99.6'
try:
WindowsError
in_windows = True
except NameError:
in_windows = False
if in_windows:
from windows import RarFileImplementation
else:
from unix import RarFileImplementation
import fnmatch, time, weakref
class RarInfo(object):
"""Represents a file header in an archive. Don't instantiate directly.
Use only to obtain information about file.
YOU CANNOT EXTRACT FILE CONTENTS USING THIS OBJECT.
USE METHODS OF RarFile CLASS INSTEAD.
Properties:
index - index of file within the archive
filename - name of the file in the archive including path (if any)
datetime - file date/time as a struct_time suitable for time.strftime
isdir - True if the file is a directory
size - size in bytes of the uncompressed file
comment - comment associated with the file
Note - this is not currently intended to be a Python file-like object.
"""
def __init__(self, rarfile, data):
self.rarfile = weakref.proxy(rarfile)
self.index = data['index']
self.filename = data['filename']
self.isdir = data['isdir']
self.size = data['size']
self.datetime = data['datetime']
self.comment = data['comment']
def __str__(self):
try :
arcName = self.rarfile.archiveName
except ReferenceError:
arcName = "[ARCHIVE_NO_LONGER_LOADED]"
return '<RarInfo "%s" in "%s">' % (self.filename, arcName)
class RarFile(RarFileImplementation):
def __init__(self, archiveName, password=None):
"""Instantiate the archive.
archiveName is the name of the RAR file.
password is used to decrypt the files in the archive.
Properties:
comment - comment associated with the archive
>>> print RarFile('test.rar').comment
This is a test.
"""
self.archiveName = archiveName
RarFileImplementation.init(self, password)
def __del__(self):
self.destruct()
def infoiter(self):
"""Iterate over all the files in the archive, generating RarInfos.
>>> import os
>>> for fileInArchive in RarFile('test.rar').infoiter():
... print os.path.split(fileInArchive.filename)[-1],
... print fileInArchive.isdir,
... print fileInArchive.size,
... print fileInArchive.comment,
... print tuple(fileInArchive.datetime)[0:5],
... print time.strftime('%a, %d %b %Y %H:%M', fileInArchive.datetime)
test True 0 None (2003, 6, 30, 1, 59) Mon, 30 Jun 2003 01:59
test.txt False 20 None (2003, 6, 30, 2, 1) Mon, 30 Jun 2003 02:01
this.py False 1030 None (2002, 2, 8, 16, 47) Fri, 08 Feb 2002 16:47
"""
for params in RarFileImplementation.infoiter(self):
yield RarInfo(self, params)
def infolist(self):
"""Return a list of RarInfos, descripting the contents of the archive."""
return list(self.infoiter())
def read_files(self, condition='*'):
"""Read specific files from archive into memory.
If "condition" is a list of numbers, then return files which have those positions in infolist.
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
and returns boolean True (extract) or False (skip).
If "condition" is omitted, all files are returned.
Returns list of tuples (RarInfo info, str contents)
"""
checker = condition2checker(condition)
return RarFileImplementation.read_files(self, checker)
def extract(self, condition='*', path='.', withSubpath=True, overwrite=True):
"""Extract specific files from archive to disk.
If "condition" is a list of numbers, then extract files which have those positions in infolist.
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
and returns either boolean True (extract) or boolean False (skip).
DEPRECATED: If "condition" callback returns string (only supported for Windows) -
that string will be used as a new name to save the file under.
If "condition" is omitted, all files are extracted.
"path" is a directory to extract to
"withSubpath" flag denotes whether files are extracted with their full path in the archive.
"overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true.
Returns list of RarInfos for extracted files."""
checker = condition2checker(condition)
return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite)
def get_volume(self):
"""Determine which volume is it in a multi-volume archive. Returns None if it's not a
multi-volume archive, 0-based volume number otherwise."""
return RarFileImplementation.get_volume(self)
def condition2checker(condition):
"""Converts different condition types to callback"""
if type(condition) in [str, unicode]:
def smatcher(info):
return fnmatch.fnmatch(info.filename, condition)
return smatcher
elif type(condition) in [list, tuple] and type(condition[0]) in [int, long]:
def imatcher(info):
return info.index in condition
return imatcher
elif callable(condition):
return condition
else:
raise TypeError

View file

@ -1,21 +0,0 @@
Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,30 +0,0 @@
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Low level interface - see UnRARDLL\UNRARDLL.TXT
class ArchiveHeaderBroken(Exception): pass
class InvalidRARArchive(Exception): pass
class FileOpenError(Exception): pass
class IncorrectRARPassword(Exception): pass
class InvalidRARArchiveUsage(Exception): pass

View file

@ -1,265 +0,0 @@
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Unix version uses unrar command line executable
import subprocess
import gc
import os, os.path
import time, re
from rar_exceptions import *
class UnpackerNotInstalled(Exception): pass
rar_executable_cached = None
rar_executable_version = None
def call_unrar(params):
"Calls rar/unrar command line executable, returns stdout pipe"
global rar_executable_cached
if rar_executable_cached is None:
for command in ('unrar', 'rar'):
try:
subprocess.Popen([command], stdout=subprocess.PIPE)
rar_executable_cached = command
break
except OSError:
pass
if rar_executable_cached is None:
raise UnpackerNotInstalled("No suitable RAR unpacker installed")
assert type(params) == list, "params must be list"
args = [rar_executable_cached] + params
try:
gc.disable() # See http://bugs.python.org/issue1336
return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
finally:
gc.enable()
class RarFileImplementation(object):
def init(self, password=None):
global rar_executable_version
self.password = password
stdoutdata, stderrdata = self.call('v', []).communicate()
for line in stderrdata.splitlines():
if line.strip().startswith("Cannot open"):
raise FileOpenError
if line.find("CRC failed")>=0:
raise IncorrectRARPassword
accum = []
source = iter(stdoutdata.splitlines())
line = ''
while (line.find('RAR ') == -1):
line = source.next()
signature = line
# The code below is mighty flaky
# and will probably crash on localized versions of RAR
# but I see no safe way to rewrite it using a CLI tool
if signature.find("RAR 4") > -1:
rar_executable_version = 4
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
if line.strip().endswith('is not RAR archive'):
raise InvalidRARArchive
line = source.next()
while not line.startswith('Pathname/Comment'):
accum.append(line.rstrip('\n'))
line = source.next()
if len(accum):
accum[0] = accum[0][9:] # strip out "Comment:" part
self.comment = '\n'.join(accum[:-1])
else:
self.comment = None
elif signature.find("RAR 5") > -1:
rar_executable_version = 5
line = source.next()
while not line.startswith('Archive:'):
if line.strip().endswith('is not RAR archive'):
raise InvalidRARArchive
accum.append(line.rstrip('\n'))
line = source.next()
if len(accum):
self.comment = '\n'.join(accum[:-1]).strip()
else:
self.comment = None
else:
raise UnpackerNotInstalled("Unsupported RAR version, expected 4.x or 5.x, found: "
+ signature.split(" ")[1])
def escaped_password(self):
return '-' if self.password == None else self.password
def call(self, cmd, options=[], files=[]):
options2 = options + ['p'+self.escaped_password()]
soptions = ['-'+x for x in options2]
return call_unrar([cmd]+soptions+['--',self.archiveName]+files)
def infoiter(self):
command = "v" if rar_executable_version == 4 else "l"
stdoutdata, stderrdata = self.call(command, ['c-']).communicate()
for line in stderrdata.splitlines():
if line.strip().startswith("Cannot open"):
raise FileOpenError
accum = []
source = iter(stdoutdata.splitlines())
line = ''
while not line.startswith('-----------'):
if line.strip().endswith('is not RAR archive'):
raise InvalidRARArchive
if line.startswith("CRC failed") or line.startswith("Checksum error"):
raise IncorrectRARPassword
line = source.next()
line = source.next()
i = 0
re_spaces = re.compile(r"\s+")
if rar_executable_version == 4:
while not line.startswith('-----------'):
accum.append(line)
if len(accum)==2:
data = {}
data['index'] = i
# asterisks mark password-encrypted files
data['filename'] = accum[0].strip().lstrip("*") # asterisks marks password-encrypted files
fields = re_spaces.split(accum[1].strip())
data['size'] = int(fields[0])
attr = fields[5]
data['isdir'] = 'd' in attr.lower()
data['datetime'] = self.rarcmd_dt(fields[3], fields[4])
data['comment'] = None
data['volume'] = None
yield data
accum = []
i += 1
line = source.next()
elif rar_executable_version == 5:
while not line.startswith('-----------'):
fields = line.strip().lstrip("*").split()
data = {}
data['index'] = i
data['filename'] = " ".join(fields[4:])
data['size'] = int(fields[1])
attr = fields[0]
data['isdir'] = 'd' in attr.lower()
data['datetime'] = self.rarcmd_dt(fields[2], fields[3])
data['comment'] = None
data['volume'] = None
yield data
i += 1
line = source.next()
@staticmethod
def rarcmd_dt(param_date=time.strftime('%Y-%m-%d'), param_time=time.strftime('%H:%M')):
for str_fmt in '%Y-%m-%d %H:%M', '%d-%m-%y %H:%M':
try:
return time.strptime('%s %s' % (param_date, param_time), str_fmt)
except ValueError:
pass
return time.strptime('%s %s' % (time.strftime('%Y-%m-%d'), time.strftime('%H:%M')), '%Y-%m-%d %H:%M')
def read_files(self, checker):
res = []
for info in self.infoiter():
checkres = checker(info)
if checkres==True and not info.isdir:
pipe = self.call('p', ['inul'], [info.filename]).stdout
res.append((info, pipe.read()))
return res
def extract(self, checker, path, withSubpath, overwrite):
res = []
command = 'x'
if not withSubpath:
command = 'e'
options = []
if overwrite:
options.append('o+')
else:
options.append('o-')
if not path.endswith(os.sep):
path += os.sep
names = []
for info in self.infoiter():
checkres = checker(info)
if type(checkres) in [str, unicode]:
raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows")
if checkres==True and not info.isdir:
names.append(info.filename)
res.append(info)
names.append(path)
proc = self.call(command, options, names)
stdoutdata, stderrdata = proc.communicate()
if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0:
raise IncorrectRARPassword
return res
def destruct(self):
pass
def get_volume(self):
command = "v" if rar_executable_version == 4 else "l"
stdoutdata, stderrdata = self.call(command, ['c-']).communicate()
for line in stderrdata.splitlines():
if line.strip().startswith("Cannot open"):
raise FileOpenError
source = iter(stdoutdata.splitlines())
line = ''
while not line.startswith('-----------'):
if line.strip().endswith('is not RAR archive'):
raise InvalidRARArchive
if line.startswith("CRC failed") or line.startswith("Checksum error"):
raise IncorrectRARPassword
line = source.next()
line = source.next()
if rar_executable_version == 4:
while not line.startswith('-----------'):
line = source.next()
line = source.next()
items = line.strip().split()
if len(items)>4 and items[4]=="volume":
return int(items[5]) - 1
else:
return None
elif rar_executable_version == 5:
while not line.startswith('-----------'):
line = source.next()
line = source.next()
items = line.strip().split()
if items[1]=="volume":
return int(items[2]) - 1
else:
return None

View file

@ -1,332 +0,0 @@
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Low level interface - see UnRARDLL\UNRARDLL.TXT
from __future__ import generators
import ctypes, ctypes.wintypes
import os, os.path, sys, re
import Queue
import time
from rar_exceptions import *
ERAR_END_ARCHIVE = 10
ERAR_NO_MEMORY = 11
ERAR_BAD_DATA = 12
ERAR_BAD_ARCHIVE = 13
ERAR_UNKNOWN_FORMAT = 14
ERAR_EOPEN = 15
ERAR_ECREATE = 16
ERAR_ECLOSE = 17
ERAR_EREAD = 18
ERAR_EWRITE = 19
ERAR_SMALL_BUF = 20
ERAR_UNKNOWN = 21
ERAR_MISSING_PASSWORD = 22
RAR_OM_LIST = 0
RAR_OM_EXTRACT = 1
RAR_SKIP = 0
RAR_TEST = 1
RAR_EXTRACT = 2
RAR_VOL_ASK = 0
RAR_VOL_NOTIFY = 1
RAR_DLL_VERSION = 3
# enum UNRARCALLBACK_MESSAGES
UCM_CHANGEVOLUME = 0
UCM_PROCESSDATA = 1
UCM_NEEDPASSWORD = 2
architecture_bits = ctypes.sizeof(ctypes.c_voidp)*8
dll_name = "unrar.dll"
if architecture_bits == 64:
dll_name = "x64\\unrar64.dll"
volume_naming1 = re.compile("\.r([0-9]{2})$")
volume_naming2 = re.compile("\.([0-9]{3}).rar$")
volume_naming3 = re.compile("\.part([0-9]+).rar$")
try:
unrar = ctypes.WinDLL(os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name))
except WindowsError:
unrar = ctypes.WinDLL(dll_name)
class RAROpenArchiveDataEx(ctypes.Structure):
def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST):
self.CmtBuf = ctypes.c_buffer(64*1024)
ctypes.Structure.__init__(self, ArcName=ArcName, ArcNameW=ArcNameW, OpenMode=OpenMode, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
_fields_ = [
('ArcName', ctypes.c_char_p),
('ArcNameW', ctypes.c_wchar_p),
('OpenMode', ctypes.c_uint),
('OpenResult', ctypes.c_uint),
('_CmtBuf', ctypes.c_voidp),
('CmtBufSize', ctypes.c_uint),
('CmtSize', ctypes.c_uint),
('CmtState', ctypes.c_uint),
('Flags', ctypes.c_uint),
('Reserved', ctypes.c_uint*32),
]
class RARHeaderDataEx(ctypes.Structure):
def __init__(self):
self.CmtBuf = ctypes.c_buffer(64*1024)
ctypes.Structure.__init__(self, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
_fields_ = [
('ArcName', ctypes.c_char*1024),
('ArcNameW', ctypes.c_wchar*1024),
('FileName', ctypes.c_char*1024),
('FileNameW', ctypes.c_wchar*1024),
('Flags', ctypes.c_uint),
('PackSize', ctypes.c_uint),
('PackSizeHigh', ctypes.c_uint),
('UnpSize', ctypes.c_uint),
('UnpSizeHigh', ctypes.c_uint),
('HostOS', ctypes.c_uint),
('FileCRC', ctypes.c_uint),
('FileTime', ctypes.c_uint),
('UnpVer', ctypes.c_uint),
('Method', ctypes.c_uint),
('FileAttr', ctypes.c_uint),
('_CmtBuf', ctypes.c_voidp),
('CmtBufSize', ctypes.c_uint),
('CmtSize', ctypes.c_uint),
('CmtState', ctypes.c_uint),
('Reserved', ctypes.c_uint*1024),
]
def DosDateTimeToTimeTuple(dosDateTime):
"""Convert an MS-DOS format date time to a Python time tuple.
"""
dosDate = dosDateTime >> 16
dosTime = dosDateTime & 0xffff
day = dosDate & 0x1f
month = (dosDate >> 5) & 0xf
year = 1980 + (dosDate >> 9)
second = 2*(dosTime & 0x1f)
minute = (dosTime >> 5) & 0x3f
hour = dosTime >> 11
return time.localtime(time.mktime((year, month, day, hour, minute, second, 0, 1, -1)))
def _wrap(restype, function, argtypes):
result = function
result.argtypes = argtypes
result.restype = restype
return result
RARGetDllVersion = _wrap(ctypes.c_int, unrar.RARGetDllVersion, [])
RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, [ctypes.POINTER(RAROpenArchiveDataEx)])
RARReadHeaderEx = _wrap(ctypes.c_int, unrar.RARReadHeaderEx, [ctypes.wintypes.HANDLE, ctypes.POINTER(RARHeaderDataEx)])
_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, [ctypes.wintypes.HANDLE, ctypes.c_char_p])
def RARSetPassword(*args, **kwargs):
_RARSetPassword(*args, **kwargs)
RARProcessFile = _wrap(ctypes.c_int, unrar.RARProcessFile, [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p])
RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, [ctypes.wintypes.HANDLE])
UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, ctypes.c_long, ctypes.c_long)
RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long])
RARExceptions = {
ERAR_NO_MEMORY : MemoryError,
ERAR_BAD_DATA : ArchiveHeaderBroken,
ERAR_BAD_ARCHIVE : InvalidRARArchive,
ERAR_EOPEN : FileOpenError,
}
class PassiveReader:
"""Used for reading files to memory"""
def __init__(self, usercallback = None):
self.buf = []
self.ucb = usercallback
def _callback(self, msg, UserData, P1, P2):
if msg == UCM_PROCESSDATA:
data = (ctypes.c_char*P2).from_address(P1).raw
if self.ucb!=None:
self.ucb(data)
else:
self.buf.append(data)
return 1
def get_result(self):
return ''.join(self.buf)
class RarInfoIterator(object):
def __init__(self, arc):
self.arc = arc
self.index = 0
self.headerData = RARHeaderDataEx()
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
if self.res in [ERAR_BAD_DATA, ERAR_MISSING_PASSWORD]:
raise IncorrectRARPassword
self.arc.lockStatus = "locked"
self.arc.needskip = False
def __iter__(self):
return self
def next(self):
if self.index>0:
if self.arc.needskip:
RARProcessFile(self.arc._handle, RAR_SKIP, None, None)
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
if self.res:
raise StopIteration
self.arc.needskip = True
data = {}
data['index'] = self.index
data['filename'] = self.headerData.FileNameW
data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime)
data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0)
data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32)
if self.headerData.CmtState == 1:
data['comment'] = self.headerData.CmtBuf.value
else:
data['comment'] = None
self.index += 1
return data
def __del__(self):
self.arc.lockStatus = "finished"
def generate_password_provider(password):
def password_provider_callback(msg, UserData, P1, P2):
if msg == UCM_NEEDPASSWORD and password!=None:
(ctypes.c_char*P2).from_address(P1).value = password
return 1
return password_provider_callback
class RarFileImplementation(object):
def init(self, password=None):
self.password = password
archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT)
self._handle = RAROpenArchiveEx(ctypes.byref(archiveData))
self.c_callback = UNRARCALLBACK(generate_password_provider(self.password))
RARSetCallback(self._handle, self.c_callback, 1)
if archiveData.OpenResult != 0:
raise RARExceptions[archiveData.OpenResult]
if archiveData.CmtState == 1:
self.comment = archiveData.CmtBuf.value
else:
self.comment = None
if password:
RARSetPassword(self._handle, password)
self.lockStatus = "ready"
self.isVolume = archiveData.Flags & 1
def destruct(self):
if self._handle and RARCloseArchive:
RARCloseArchive(self._handle)
def make_sure_ready(self):
if self.lockStatus == "locked":
raise InvalidRARArchiveUsage("cannot execute infoiter() without finishing previous one")
if self.lockStatus == "finished":
self.destruct()
self.init(self.password)
def infoiter(self):
self.make_sure_ready()
return RarInfoIterator(self)
def read_files(self, checker):
res = []
for info in self.infoiter():
if checker(info) and not info.isdir:
reader = PassiveReader()
c_callback = UNRARCALLBACK(reader._callback)
RARSetCallback(self._handle, c_callback, 1)
tmpres = RARProcessFile(self._handle, RAR_TEST, None, None)
if tmpres in [ERAR_BAD_DATA, ERAR_MISSING_PASSWORD]:
raise IncorrectRARPassword
self.needskip = False
res.append((info, reader.get_result()))
return res
def extract(self, checker, path, withSubpath, overwrite):
res = []
for info in self.infoiter():
checkres = checker(info)
if checkres!=False and not info.isdir:
if checkres==True:
fn = info.filename
if not withSubpath:
fn = os.path.split(fn)[-1]
target = os.path.join(path, fn)
else:
raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows"
target = checkres
if overwrite or (not os.path.exists(target)):
tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target)
if tmpres in [ERAR_BAD_DATA, ERAR_MISSING_PASSWORD]:
raise IncorrectRARPassword
self.needskip = False
res.append(info)
return res
def get_volume(self):
if not self.isVolume:
return None
headerData = RARHeaderDataEx()
res = RARReadHeaderEx(self._handle, ctypes.byref(headerData))
arcName = headerData.ArcNameW
match3 = volume_naming3.search(arcName)
if match3 != None:
return int(match3.group(1)) - 1
match2 = volume_naming3.search(arcName)
if match2 != None:
return int(match2.group(1))
match1 = volume_naming1.search(arcName)
if match1 != None:
return int(match1.group(1)) + 1
return 0

View file

@ -130,33 +130,19 @@ def isSyncFile(filename):
return False
def isMediaFile(filename):
def has_media_ext(filename):
# ignore samples
if re.search('(^|[\W_])(sample\d*)[\W_]', filename, re.I):
if re.search('(^|[\W_])(sample\d*)[\W_]', filename, re.I) \
or filename.startswith('._'): # and MAC OS's 'resource fork' files
return False
# ignore MAC OS's retarded "resource fork" files
if filename.startswith('._'):
return False
sepFile = filename.rpartition(".")
if re.search('extras?$', sepFile[0], re.I):
return False
if sepFile[2].lower() in mediaExtensions:
return True
else:
return False
sep_file = filename.rpartition('.')
return (None is re.search('extras?$', sep_file[0], re.I)) and (sep_file[2].lower() in mediaExtensions)
def isRarFile(filename):
archive_regex = '(?P<file>^(?P<base>(?:(?!\.part\d+\.rar$).)*)\.(?:(?:part0*1\.)?rar)$)'
def is_first_rar_volume(filename):
if re.search(archive_regex, filename):
return True
return False
return None is not re.search('(?P<file>^(?P<base>(?:(?!\.part\d+\.rar$).)*)\.(?:(?:part0*1\.)?rar)$)', filename)
def sanitizeFileName(name):
@ -264,7 +250,7 @@ def listMediaFiles(path):
if ek.ek(os.path.isdir, fullCurFile) and not curFile.startswith('.') and not curFile == 'Extras':
files += listMediaFiles(fullCurFile)
elif isMediaFile(curFile):
elif has_media_ext(curFile):
files.append(fullCurFile)
return files

View file

@ -824,10 +824,11 @@ class PostProcessor(object):
Post-process a given file
"""
self._log(u'Processing %s%s' % (self.file_path, (u'<br />.. from nzb %s' % self.nzb_name, u'')[None is self.nzb_name]))
self._log(u'Processing... %s%s' % (ek.ek(os.path.relpath, self.file_path, self.folder_path),
(u'<br />.. from nzb %s' % self.nzb_name, u'')[None is self.nzb_name]))
if ek.ek(os.path.isdir, self.file_path):
self._log(u'File %s<br />.. seems to be a directory' % self.file_path)
self._log(u'Expecting file %s<br />.. is actually a directory, skipping' % self.file_path)
return False
for ignore_file in self.IGNORED_FILESTRINGS:
@ -844,7 +845,7 @@ class PostProcessor(object):
# if we don't have it then give up
if not show:
self._log(u'Please add the show to your SickGear then try to post process an episode', logger.WARNING)
self._log(u'Must add show to SickGear before trying to post process an episode', logger.WARNING)
raise exceptions.PostProcessingFailed()
elif None is season or not episodes:
self._log(u'Quitting this post process, could not determine what episode this is', logger.DEBUG)
@ -876,7 +877,7 @@ class PostProcessor(object):
helpers.delete_empty_folders(ek.ek(os.path.dirname, cur_ep.location),
keep_dir=ep_obj.show.location)
except (OSError, IOError):
raise exceptions.PostProcessingFailed(u'Unable to delete the existing files')
raise exceptions.PostProcessingFailed(u'Unable to delete existing files')
# set the status of the episodes
# for curEp in [ep_obj] + ep_obj.relatedEps:
@ -938,7 +939,7 @@ class PostProcessor(object):
if None is not release_name:
failed_history.logSuccess(release_name)
else:
self._log(u'No release found in snatch history', logger.WARNING)
self._log(u'No snatched release found in history', logger.WARNING)
# find the destination folder
try:

View file

@ -18,10 +18,12 @@
from __future__ import with_statement
from functools import partial
import os
import re
import shutil
import stat
import re
import sys
import sickbeard
from sickbeard import postProcessor
@ -35,7 +37,7 @@ from sickbeard.history import reset_status
from sickbeard import failedProcessor
from lib.unrar2 import RarFile
import lib.rarfile.rarfile as rarfile
try:
from lib.send2trash import send2trash
@ -50,6 +52,7 @@ class ProcessTVShow(object):
def __init__(self, webhandler=None):
self.files_passed = 0
self.files_failed = 0
self.fail_detected = False
self._output = []
self.webhandler = webhandler
@ -113,11 +116,12 @@ class ProcessTVShow(object):
self._log_helper(u'Deleted folder ' + folder, logger.MESSAGE)
return True
def _delete_files(self, process_path, notwanted_files, use_trash=False):
def _delete_files(self, process_path, notwanted_files, use_trash=False, force=False):
if not self.any_vid_processed:
if not self.any_vid_processed and not force:
return
result = True
# Delete all file not needed
for cur_file in notwanted_files:
@ -143,9 +147,13 @@ class ProcessTVShow(object):
except OSError as e:
self._log_helper(u'Unable to delete file %s: %s' % (cur_file, str(e.strerror)))
if True is not ek.ek(os.path.isfile, cur_file_path):
if ek.ek(os.path.isfile, cur_file_path):
result = False
else:
self._log_helper(u'Deleted file ' + cur_file)
return result
def process_dir(self, dir_name, nzb_name=None, process_method=None, force=False, force_replace=None, failed=False, pp_type='auto', cleanup=False):
"""
Scans through the files in dir_name and processes whatever media files it finds
@ -159,7 +167,6 @@ class ProcessTVShow(object):
# if they passed us a real directory then assume it's the one we want
if dir_name and ek.ek(os.path.isdir, dir_name):
self._log_helper(u'Processing folder... ' + dir_name)
dir_name = ek.ek(os.path.realpath, dir_name)
# if the client and SickGear are not on the same machine translate the directory in a network directory
@ -167,17 +174,19 @@ class ProcessTVShow(object):
and ek.ek(os.path.normpath, dir_name) != ek.ek(os.path.normpath, sickbeard.TV_DOWNLOAD_DIR):
dir_name = ek.ek(os.path.join, sickbeard.TV_DOWNLOAD_DIR, ek.ek(os.path.abspath, dir_name).split(os.path.sep)[-1])
self._log_helper(u'SickGear PP Config, completed TV downloads folder: ' + sickbeard.TV_DOWNLOAD_DIR)
self._log_helper(u'Trying to use folder... ' + dir_name)
# if we didn't find a real directory then quit
if dir_name:
self._log_helper(u'Checking folder... ' + dir_name)
# if we didn't find a real directory then process "failed" or just quit
if not dir_name or not ek.ek(os.path.isdir, dir_name):
if nzb_name and failed:
self._process_failed(dir_name, nzb_name)
return self.result
else:
self._log_helper(
u'Unable to figure out what folder to process. If your downloader and SickGear aren\'t on the same PC then make sure you fill out your completed TV download folder in the PP config.')
return self.result
self._log_helper(u'Unable to figure out what folder to process. ' +
u'If your downloader and SickGear aren\'t on the same PC then make sure ' +
u'you fill out your completed TV download folder in the PP config.')
return self.result
path, dirs, files = self._get_path_dir_files(dir_name, nzb_name, pp_type)
@ -188,24 +197,31 @@ class ProcessTVShow(object):
self._log_helper(u'Found temporary sync files, skipping post process', logger.ERROR)
return self.result
self._log_helper(u'Process path: ' + path)
if 0 < len(dirs):
self._log_helper(u'Process dir%s: %s' % (('', 's')[1 < len(dirs)], str(dirs)))
self._log_helper(u'Processing folder... %s' % path)
rar_files = filter(helpers.isRarFile, files)
work_files = []
joined = self.join(path)
if joined:
work_files += [joined]
rar_files = filter(helpers.is_first_rar_volume, files)
rar_content = self._unrar(path, rar_files, force)
files += rar_content
video_files = filter(helpers.isMediaFile, files)
video_in_rar = filter(helpers.isMediaFile, rar_content)
if self.fail_detected:
self._process_failed(dir_name, nzb_name)
return self.result
path, dirs, files = self._get_path_dir_files(dir_name, nzb_name, pp_type)
video_files = filter(helpers.has_media_ext, files)
video_in_rar = filter(helpers.has_media_ext, rar_content)
work_files += [ek.ek(os.path.join, path, item) for item in rar_content]
if 0 < len(files):
self._log_helper(u'Process file%s: %s' % (('', 's')[1 < len(files)], str(files)))
self._log_helper(u'Process file%s: %s' % (helpers.maybe_plural(files), str(files)))
if 0 < len(video_files):
self._log_helper(u'Process video file%s: %s' % (('', 's')[1 < len(video_files)], str(video_files)))
self._log_helper(u'Process video file%s: %s' % (helpers.maybe_plural(video_files), str(video_files)))
if 0 < len(rar_content):
self._log_helper(u'Process rar content: ' + str(rar_content))
if 0 < len(video_in_rar):
self._log_helper(u'Process video in rar: ' + str(video_in_rar))
self._log_helper(u'Process video%s in rar: %s' % (helpers.maybe_plural(video_in_rar), str(video_in_rar)))
# If nzb_name is set and there's more than one videofile in the folder, files will be lost (overwritten).
nzb_name_original = nzb_name
@ -220,7 +236,7 @@ class ProcessTVShow(object):
# Don't Link media when the media is extracted from a rar in the same path
if process_method in ('hardlink', 'symlink') and video_in_rar:
self._process_media(path, video_in_rar, nzb_name, 'move', force, force_replace)
self._delete_files(path, rar_content)
self._delete_files(path, [ek.ek(os.path.relpath, item, path) for item in work_files], force=True)
video_batch = set(video_files) - set(video_in_rar)
else:
video_batch = video_files
@ -246,27 +262,31 @@ class ProcessTVShow(object):
# Process video files in TV subdirectories
for directory in [x for x in dirs if self._validate_dir(path, x, nzb_name_original, failed)]:
self._set_process_success(reset=True)
# self._set_process_success(reset=True)
for process_path, process_dir, file_list in ek.ek(os.walk, ek.ek(os.path.join, path, directory), topdown=False):
for walk_path, walk_dir, files in ek.ek(os.walk, ek.ek(os.path.join, path, directory), topdown=False):
sync_files = filter(helpers.isSyncFile, file_list)
sync_files = filter(helpers.isSyncFile, files)
# Don't post process if files are still being synced and option is activated
if sync_files and sickbeard.POSTPONE_IF_SYNC_FILES:
self._log_helper(u'Found temporary sync files, skipping post process', logger.ERROR)
return self.result
rar_files = filter(helpers.isRarFile, file_list)
rar_content = self._unrar(process_path, rar_files, force)
file_list = set(file_list + rar_content)
video_files = filter(helpers.isMediaFile, file_list)
video_in_rar = filter(helpers.isMediaFile, rar_content)
notwanted_files = [x for x in file_list if x not in video_files]
rar_files = filter(helpers.is_first_rar_volume, files)
rar_content = self._unrar(walk_path, rar_files, force)
work_files += [ek.ek(os.path.join, walk_path, item) for item in rar_content]
if self.fail_detected:
self._process_failed(dir_name, nzb_name)
continue
files = list(set(files + rar_content))
video_files = filter(helpers.has_media_ext, files)
video_in_rar = filter(helpers.has_media_ext, rar_content)
notwanted_files = [x for x in files if x not in video_files]
# Don't Link media when the media is extracted from a rar in the same path
if process_method in ('hardlink', 'symlink') and video_in_rar:
self._process_media(process_path, video_in_rar, nzb_name, 'move', force, force_replace)
self._process_media(walk_path, video_in_rar, nzb_name, 'move', force, force_replace)
video_batch = set(video_files) - set(video_in_rar)
else:
video_batch = video_files
@ -276,7 +296,7 @@ class ProcessTVShow(object):
video_pick = ['']
video_size = 0
for cur_video_file in video_batch:
cur_video_size = ek.ek(os.path.getsize, ek.ek(os.path.join, process_path, cur_video_file))
cur_video_size = ek.ek(os.path.getsize, ek.ek(os.path.join, walk_path, cur_video_file))
if 0 == video_size or cur_video_size > video_size:
video_size = cur_video_size
@ -284,14 +304,14 @@ class ProcessTVShow(object):
video_batch = set(video_batch) - set(video_pick)
self._process_media(process_path, video_pick, nzb_name, process_method, force, force_replace, use_trash=cleanup)
self._process_media(walk_path, video_pick, nzb_name, process_method, force, force_replace, use_trash=cleanup)
except OSError as e:
logger.log('Batch skipped, %s%s' %
(ex(e), e.filename and (' (file %s)' % e.filename) or ''), logger.WARNING)
if process_method in ('hardlink', 'symlink') and video_in_rar:
self._delete_files(process_path, rar_content)
self._delete_files(walk_path, rar_content)
else:
# Delete all file not needed
if not self.any_vid_processed\
@ -299,11 +319,17 @@ class ProcessTVShow(object):
or ('manual' == pp_type and not cleanup): # Avoid deleting files if Manual Postprocessing
continue
self._delete_files(process_path, notwanted_files, use_trash=cleanup)
self._delete_files(walk_path, notwanted_files, use_trash=cleanup)
if 'move' == process_method\
and ek.ek(os.path.normpath, sickbeard.TV_DOWNLOAD_DIR) != ek.ek(os.path.normpath, process_path):
self._delete_folder(process_path, check_empty=False)
and ek.ek(os.path.normpath, sickbeard.TV_DOWNLOAD_DIR) != ek.ek(os.path.normpath, walk_path):
self._delete_folder(walk_path, check_empty=False)
if 'copy' == process_method and work_files:
self._delete_files(path, [ek.ek(os.path.relpath, item, path) for item in work_files], force=True)
for f in sorted(list(set([ek.ek(os.path.dirname, item) for item in work_files]) - {path}),
key=len, reverse=True):
self._delete_folder(f)
def _bottom_line(text, log_level=logger.DEBUG):
self._buffer('-' * len(text))
@ -322,7 +348,7 @@ class ProcessTVShow(object):
def _validate_dir(self, path, dir_name, nzb_name_original, failed):
self._log_helper(u'Processing dir: ' + dir_name)
self._log_helper(u'Processing sub dir: ' + dir_name)
if ek.ek(os.path.basename, dir_name).startswith('_FAILED_'):
self._log_helper(u'The directory name indicates it failed to extract.')
@ -357,11 +383,12 @@ class ProcessTVShow(object):
# Get the videofile list for the next checks
all_files = []
all_dirs = []
process_path = None
for process_path, process_dir, fileList in ek.ek(os.walk, ek.ek(os.path.join, path, dir_name), topdown=False):
all_dirs += process_dir
all_files += fileList
video_files = filter(helpers.isMediaFile, all_files)
video_files = filter(helpers.has_media_ext, all_files)
all_dirs.append(dir_name)
# check if the directory have at least one tv video file
@ -379,9 +406,9 @@ class ProcessTVShow(object):
except (InvalidNameException, InvalidShowException):
pass
if sickbeard.UNPACK:
if sickbeard.UNPACK and process_path and all_files:
# Search for packed release
packed_files = filter(helpers.isRarFile, all_files)
packed_files = filter(helpers.is_first_rar_volume, all_files)
for packed in packed_files:
try:
@ -396,6 +423,9 @@ class ProcessTVShow(object):
unpacked_files = []
if 'win32' == sys.platform:
rarfile.UNRAR_TOOL = ek.ek(os.path.join, sickbeard.PROG_DIR, 'lib', 'rarfile', 'UnRAR.exe')
if sickbeard.UNPACK and rar_files:
self._log_helper(u'Packed releases detected: ' + str(rar_files))
@ -405,32 +435,207 @@ class ProcessTVShow(object):
self._log_helper(u'Unpacking archive: ' + archive)
try:
rar_handle = RarFile(os.path.join(path, archive))
rar_handle = rarfile.RarFile(ek.ek(os.path.join, path, archive))
# Skip extraction if any file in archive has previously been extracted
skip_file = False
for file_in_archive in [os.path.basename(x.filename) for x in rar_handle.infolist() if not x.isdir]:
for file_in_archive in [ek.ek(os.path.basename, x.filename)
for x in rar_handle.infolist() if not x.isdir()]:
if self._already_postprocessed(path, file_in_archive, force):
self._log_helper(
u'Archive file already processed, extraction skipped: ' + file_in_archive)
skip_file = True
break
if skip_file:
continue
if not skip_file:
# need to test for password since rar4 doesn't raise PasswordRequired
if rar_handle.needs_password():
raise rarfile.PasswordRequired
rar_handle.extract(path=path, withSubpath=False, overwrite=False)
unpacked_files += [os.path.basename(x.filename) for x in rar_handle.infolist() if not x.isdir]
del rar_handle
except Exception as e:
self._log_helper(u'Failed to unpack archive %s: %s' % (archive, ex(e)), logger.ERROR)
rar_handle.extractall(path=path)
rar_content = [ek.ek(os.path.normpath, x.filename)
for x in rar_handle.infolist() if not x.isdir()]
renamed = self.cleanup_names(path, rar_content)
cur_unpacked = rar_content if not renamed else \
(list(set(rar_content) - set(renamed.keys())) + renamed.values())
self._log_helper(u'Unpacked content: [u\'%s\']' % '\', u\''.join(map(unicode, cur_unpacked)))
unpacked_files += cur_unpacked
except (rarfile.PasswordRequired, rarfile.RarWrongPassword):
self._log_helper(u'Failed to unpack archive PasswordRequired: %s' % archive, logger.ERROR)
self._set_process_success(False)
continue
self.fail_detected = True
except Exception as e:
self._log_helper(u'Failed to unpack archive: %s' % archive, logger.ERROR)
self._set_process_success(False)
finally:
rar_handle.close()
del rar_handle
self._log_helper(u'Unpacked content: ' + str(unpacked_files))
elif rar_files:
# check for passworded rar's
for archive in rar_files:
try:
rar_handle = rarfile.RarFile(ek.ek(os.path.join, path, archive))
if rar_handle.needs_password():
self._log_helper(u'Failed to unpack archive PasswordRequired: %s' % archive, logger.ERROR)
self._set_process_success(False)
self.failure_detected = True
rar_handle.close()
del rar_handle
except Exception:
pass
return unpacked_files
@staticmethod
def cleanup_names(directory, files=None):
is_renamed = {}
num_videos = 0
old_name = None
new_name = None
params = {
'base_name': ek.ek(os.path.basename, directory),
'reverse_pattern': re.compile('|'.join([
r'\.\d{2}e\d{2}s\.', r'\.p0(?:63|27|612)\.', r'\.[pi](?:084|675|0801)\.', r'\b[45]62[xh]\.',
r'\.yarulb\.', r'\.vtd[hp]\.', r'\.(?:ld[.-]?)?bew\.', r'\.pir.?(?:shv|dov|dvd|bew|db|rb)\.',
r'\brdvd\.', r'\.(?:vts|dcv)\.', r'\b(?:mac|pir)dh\b', r'\.(?:lanretni|reporp|kcaper|reneercs)\.',
r'\b(?:caa|3ca|3pm)\b', r'\.cstn\.', r'\.5r\.', r'\brcs\b'
]), flags=re.IGNORECASE),
'season_pattern': re.compile(r'(.*\.\d{2}e\d{2}s\.)(.*)', flags=re.IGNORECASE),
'word_pattern': re.compile(r'([^A-Z0-9]*[A-Z0-9]+)'),
'char_replace': [[r'(\w)1\.(\w)', r'\1i\2']],
'garbage_name': re.compile(r'^[a-zA-Z0-9]{3,}$'),
'media_pattern': re.compile('|'.join([
r'\.s\d{2}e\d{2}\.', r'\.(?:36|72|216)0p\.', r'\.(?:480|576|1080)[pi]\.', r'\.[xh]26[45]\b',
r'\.bluray\.', r'\.[hp]dtv\.', r'\.web(?:[.-]?dl)?\.', r'\.(?:vhs|vod|dvd|web|bd|br).?rip\.',
r'\.dvdr\b', r'\.(?:stv|vcd)\.', r'\bhd(?:cam|rip)\b', r'\.(?:internal|proper|repack|screener)\.',
r'\b(?:aac|ac3|mp3)\b', r'\.(?:ntsc|pal|secam)\.', r'\.r5\.', r'\bscr\b', r'\b(?:divx|xvid)\b'
]), flags=re.IGNORECASE)
}
def renamer(_dirpath, _filenames, _num_videos, _old_name, _new_name, base_name,
reverse_pattern, season_pattern, word_pattern, char_replace, garbage_name, media_pattern):
for cur_filename in _filenames:
file_name, file_extension = ek.ek(os.path.splitext, cur_filename)
file_path = ek.ek(os.path.join, _dirpath, cur_filename)
dir_name = ek.ek(os.path.dirname, file_path)
if None is not reverse_pattern.search(file_name):
na_parts = season_pattern.search(file_name)
if None is not na_parts:
word_p = word_pattern.findall(na_parts.group(2))
new_words = ''
for wp in word_p:
if '.' == wp[0]:
new_words += '.'
new_words += re.sub(r'\W', '', wp)
for cr in char_replace:
new_words = re.sub(cr[0], cr[1], new_words)
new_filename = new_words[::-1] + na_parts.group(1)[::-1]
else:
new_filename = file_name[::-1]
logger.log('Reversing base filename "%s" to "%s"' % (file_name, new_filename))
try:
ek.ek(os.rename, file_path, ek.ek(os.path.join, _dirpath, new_filename + file_extension))
is_renamed[ek.ek(os.path.relpath, file_path, directory)] = ek.ek(
os.path.relpath, new_filename + file_extension, directory)
except OSError as e:
logger.log('Error unable to rename file "%s" because %s' % (cur_filename, ex(e)), logger.ERROR)
elif helpers.has_media_ext(cur_filename) and \
None is not garbage_name.search(file_name) and None is not media_pattern.search(base_name):
_num_videos += 1
_old_name = file_path
_new_name = ek.ek(os.path.join, dir_name, '%s%s' % (base_name, file_extension))
return is_renamed, _num_videos, _old_name, _new_name
if files:
is_renamed, num_videos, old_name, new_name = renamer(
directory, files, num_videos, old_name, new_name, **params)
else:
for cur_dirpath, void, cur_filenames in ek.ek(os.walk, directory):
is_renamed, num_videos, old_name, new_name = renamer(
cur_dirpath, cur_filenames, num_videos, old_name, new_name, **params)
if all([not is_renamed, 1 == num_videos, old_name, new_name]):
try_name = ek.ek(os.path.basename, new_name)
logger.log('Renaming file "%s" using dirname as "%s"' % (ek.ek(os.path.basename, old_name), try_name))
try:
ek.ek(os.rename, old_name, new_name)
is_renamed[ek.ek(os.path.relpath, old_name, directory)] = ek.ek(os.path.relpath, new_name, directory)
except OSError as e:
logger.log('Error unable to rename file "%s" because %s' % (old_name, ex(e)), logger.ERROR)
return is_renamed
@staticmethod
def join(directory):
result = False
chunks = {}
matcher = re.compile('\.[0-9]+$')
for dirpath, void, filenames in os.walk(directory):
for filename in filenames:
if None is not matcher.search(filename):
maybe_chunk = ek.ek(os.path.join, dirpath, filename)
base_filepath, ext = os.path.splitext(maybe_chunk)
if base_filepath not in chunks:
chunks[base_filepath] = []
chunks[base_filepath].append(maybe_chunk)
if not chunks:
return
for base_filepath in chunks:
chunks[base_filepath].sort()
chunk_set = chunks[base_filepath]
if ek.ek(os.path.isfile, base_filepath):
base_filesize = ek.ek(os.path.getsize, base_filepath)
chunk_sizes = [ek.ek(os.path.getsize, x) for x in chunk_set]
largest_chunk = max(chunk_sizes)
if largest_chunk >= base_filesize:
outfile = '%s.001' % base_filepath
if outfile not in chunk_set:
try:
ek.ek(os.rename, base_filepath, outfile)
except OSError:
logger.log('Error unable to rename file %s' % base_filepath, logger.ERROR)
return result
chunk_set.append(outfile)
chunk_set.sort()
else:
del_dir, del_file = ek.ek(os.path.split, base_filepath)
if not self._delete_files(del_dir, [del_file], force=True):
return result
else:
if base_filesize == sum(chunk_sizes):
logger.log('Join skipped. Total size of %s input files equal to output.. %s (%s bytes)' % (
len(chunk_set), base_filepath, base_filesize))
else:
logger.log('Join skipped. Found output file larger than input.. %s (%s bytes)' % (
base_filepath, base_filesize))
return result
with open(base_filepath, 'ab') as newfile:
for f in chunk_set:
logger.log('Joining file %s' % f)
try:
with open(f, 'rb') as part:
for wdata in iter(partial(part.read, 4096), b''):
try:
newfile.write(wdata)
except:
logger.log('Failed write to file %s' % f)
return result
except:
logger.log('Failed read from file %s' % f)
return result
result = base_filepath
return result
def _already_postprocessed(self, dir_name, videofile, force):
if force or not self.any_vid_processed:
@ -527,9 +732,9 @@ class ProcessTVShow(object):
processor = postProcessor.PostProcessor(cur_video_file_path, nzb_name, process_method, force_replace, use_trash=use_trash, webhandler=self.webhandler)
file_success = processor.process()
process_fail_message = ''
except exceptions.PostProcessingFailed as e:
except exceptions.PostProcessingFailed:
file_success = False
process_fail_message = '<br />.. ' + ex(e)
process_fail_message = '<br />.. Post Processing Failed'
self._set_process_success(file_success)

View file

@ -856,7 +856,7 @@ class TorrentProvider(object, GenericProvider):
file_name = '%s.py' % os.path.join(sickbeard.PROG_DIR, *self.__module__.split('.'))
if ek.ek(os.path.isfile, file_name):
with open(file_name, 'rb') as file_hd:
is_valid = 1661931498 == s + zlib.crc32(file_hd.read())
is_valid = s + zlib.crc32(file_hd.read()) in (1661931498, 472149389)
return is_valid
def _authorised(self, logged_in=None, post_params=None, failed_msg=None, url=None, timeout=30):

View file

@ -700,7 +700,7 @@ class TVShow(object):
cur_ep.status = Quality.compositeStatus(DOWNLOADED, new_quality)
# check for status/quality changes as long as it's a new file
elif not same_file and sickbeard.helpers.isMediaFile(file)\
elif not same_file and sickbeard.helpers.has_media_ext(file)\
and cur_ep.status not in Quality.DOWNLOADED + [ARCHIVED, IGNORED]:
old_status, old_quality = Quality.splitCompositeStatus(cur_ep.status)
@ -1813,7 +1813,7 @@ class TVEpisode(object):
statusStrings[self.status], logger.DEBUG)
# if we have a media file then it's downloaded
elif sickbeard.helpers.isMediaFile(self.location):
elif sickbeard.helpers.has_media_ext(self.location):
# leave propers alone, you have to either post-process them or manually change them back
if self.status not in Quality.SNATCHED_PROPER + Quality.DOWNLOADED + Quality.SNATCHED + [ARCHIVED]:
status_quality = Quality.statusFromNameOrFile(self.location, anime=self.show.is_anime)
@ -1838,7 +1838,7 @@ class TVEpisode(object):
if self.location != "":
if UNKNOWN == self.status and sickbeard.helpers.isMediaFile(self.location):
if UNKNOWN == self.status and sickbeard.helpers.has_media_ext(self.location):
status_quality = Quality.statusFromNameOrFile(self.location, anime=self.show.is_anime)
logger.log('(3) Status changes from %s to %s' % (self.status, status_quality), logger.DEBUG)
self.status = status_quality

View file

@ -26,6 +26,7 @@ import itertools
import os
import random
import re
import sys
import time
import traceback
import urllib
@ -59,7 +60,8 @@ from tornado.web import RequestHandler, StaticFileHandler, authenticated
from lib import adba
from lib import subliminal
from lib.dateutil import tz
from lib.unrar2 import RarFile
import lib.rarfile.rarfile as rarfile
from lib.libtrakt import TraktAPI
from lib.libtrakt.exceptions import TraktException, TraktAuthException
from trakt_helpers import build_config, trakt_collection_remove_account
@ -4629,19 +4631,20 @@ class ConfigPostProcessing(Config):
def isRarSupported(self, *args, **kwargs):
"""
Test Packing Support:
- Simulating in memory rar extraction on test.rar file
"""
try:
rar_path = os.path.join(sickbeard.PROG_DIR, 'lib', 'unrar2', 'test.rar')
testing = RarFile(rar_path).read_files('*test.txt')
if testing[0][1] == 'This is only a test.':
if 'win32' == sys.platform:
rarfile.UNRAR_TOOL = ek.ek(os.path.join, sickbeard.PROG_DIR, 'lib', 'rarfile', 'UnRAR.exe')
rar_path = ek.ek(os.path.join, sickbeard.PROG_DIR, 'lib', 'rarfile', 'test.rar')
if 'This is only a test.' == rarfile.RarFile(rar_path).read(r'test\test.txt'):
return 'supported'
logger.log(u'Rar Not Supported: Can not read the content of test file', logger.ERROR)
return 'not supported'
msg = 'Could not read test file content'
except Exception as e:
logger.log(u'Rar Not Supported: ' + ex(e), logger.ERROR)
return 'not supported'
msg = ex(e)
logger.log(u'Rar Not Supported: %s' % msg, logger.ERROR)
return 'not supported'
class ConfigProviders(Config):