|
|
|
#!/usr/bin/python3 -OO
|
|
|
|
# Copyright 2007-2021 The SABnzbd-Team <team@sabnzbd.org>
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or
|
|
|
|
# modify it under the terms of the GNU General Public License
|
|
|
|
# as published by the Free Software Foundation; either version 2
|
|
|
|
# of the License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program; if not, write to the Free Software
|
|
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
Deobfuscation post-processing script:
|
|
|
|
|
|
|
|
Will check in the completed job folder if maybe there are par2 files,
|
|
|
|
for example "rename.par2", and use those to rename the files.
|
|
|
|
If there is no "rename.par2" available, it will rename the largest
|
|
|
|
file to the job-name in the queue.
|
|
|
|
|
|
|
|
NOTES:
|
|
|
|
1) To use this script you need Python installed on your system and
|
|
|
|
select "Add to path" during its installation. Select this folder in
|
|
|
|
Config > Folders > Scripts Folder and select this script for each job
|
|
|
|
you want it used for, or link it to a category in Config > Categories.
|
|
|
|
2) Beware that files on the 'Cleanup List' are removed before
|
|
|
|
scripts are called and if any of them happen to be required by
|
|
|
|
the found par2 file, it will fail.
|
|
|
|
3) If there are multiple larger (>40MB) files, then the script will not
|
|
|
|
rename anything, since it could be a multi-pack.
|
|
|
|
4) If you want to modify this script, make sure to copy it out
|
|
|
|
of this directory, or it will be overwritten when SABnzbd is updated.
|
|
|
|
5) Feedback or bugs in this script can be reported in on our forum:
|
|
|
|
https://forums.sabnzbd.org/viewforum.php?f=9
|
|
|
|
|
|
|
|
|
|
|
|
Improved by P1nGu1n
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import time
|
|
|
|
import fnmatch
|
|
|
|
import struct
|
|
|
|
import hashlib
|
|
|
|
|
|
|
|
|
|
|
|
# Are we being called from SABnzbd?
|
|
|
|
if not os.environ.get("SAB_VERSION"):
|
|
|
|
print("This script needs to be called from SABnzbd as post-processing script.")
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
# Files to exclude and minimal file size for renaming
|
|
|
|
EXCLUDED_FILE_EXTS = (".vob", ".bin")
|
|
|
|
MIN_FILE_SIZE = 40 * 1024 * 1024
|
|
|
|
|
|
|
|
# see: http://parchive.sourceforge.net/docs/specifications/parity-volume-spec/article-spec.html
|
|
|
|
STRUCT_PACKET_HEADER = struct.Struct(
|
|
|
|
"<"
|
|
|
|
"8s" # Magic sequence
|
|
|
|
"Q" # Length of the entire packet (including header), must be multiple of 4
|
|
|
|
"16s" # MD5 Hash of packet
|
|
|
|
"16s" # Recovery Set ID
|
|
|
|
"16s" # Packet type
|
|
|
|
)
|
|
|
|
|
|
|
|
PACKET_TYPE_FILE_DESC = "PAR 2.0\x00FileDesc"
|
|
|
|
STRUCT_FILE_DESC_PACKET = struct.Struct(
|
|
|
|
"<"
|
|
|
|
"16s" # File ID
|
|
|
|
"16s" # MD5 hash of the entire file
|
|
|
|
"16s" # MD5 hash of the first 16KiB of the file
|
|
|
|
"Q" # Length of the file
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Supporting functions
|
|
|
|
def print_splitter():
|
|
|
|
"""Simple helper function"""
|
|
|
|
print("\n------------------------\n")
|
|
|
|
|
|
|
|
|
|
|
|
def decode_par2(parfile):
|
|
|
|
result = False
|
|
|
|
dirname = os.path.dirname(parfile)
|
|
|
|
with open(parfile, "rb") as parfileToDecode:
|
|
|
|
while True:
|
|
|
|
header = parfileToDecode.read(STRUCT_PACKET_HEADER.size)
|
|
|
|
if not header:
|
|
|
|
break # file fully read
|
|
|
|
|
|
|
|
(_, packetLength, _, _, packet_type) = STRUCT_PACKET_HEADER.unpack(header)
|
|
|
|
body_length = packetLength - STRUCT_PACKET_HEADER.size
|
|
|
|
|
|
|
|
# only process File Description packets
|
|
|
|
if packet_type != PACKET_TYPE_FILE_DESC:
|
|
|
|
# skip this packet
|
|
|
|
parfileToDecode.seek(body_length, os.SEEK_CUR)
|
|
|
|
continue
|
|
|
|
|
|
|
|
chunck = parfileToDecode.read(STRUCT_FILE_DESC_PACKET.size)
|
|
|
|
(_, _, hash16k, filelength) = STRUCT_FILE_DESC_PACKET.unpack(chunck)
|
|
|
|
|
|
|
|
# filename makes up for the rest of the packet, padded with null characters
|
|
|
|
target_name = parfileToDecode.read(body_length - STRUCT_FILE_DESC_PACKET.size).rstrip(b"\0")
|
|
|
|
target_path = os.path.join(dirname, target_name)
|
|
|
|
|
|
|
|
# file already exists, skip it
|
|
|
|
if os.path.exists(target_path):
|
|
|
|
print("File already exists: %s" % target_name)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# find and rename file
|
|
|
|
src_path = find_file(dirname, filelength, hash16k)
|
|
|
|
if src_path is not None:
|
|
|
|
os.rename(src_path, target_path)
|
|
|
|
print("Renamed file from %s to %s" % (os.path.basename(src_path), target_name))
|
|
|
|
result = True
|
|
|
|
else:
|
|
|
|
print("No match found for: %s" % target_name)
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def find_file(dirname, filelength, hash16k):
|
|
|
|
for fn in os.listdir(dirname):
|
|
|
|
filepath = os.path.join(dirname, fn)
|
|
|
|
|
|
|
|
# check if the size matches as an indication
|
|
|
|
if os.path.getsize(filepath) != filelength:
|
|
|
|
continue
|
|
|
|
|
|
|
|
with open(filepath, "rb") as fileToMatch:
|
|
|
|
data = fileToMatch.read(16 * 1024)
|
|
|
|
m = hashlib.md5()
|
|
|
|
m.update(data)
|
|
|
|
|
|
|
|
# compare hash to confirm the match
|
|
|
|
if m.digest() == hash16k:
|
|
|
|
return filepath
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# Run main program
|
|
|
|
print_splitter()
|
|
|
|
print("SABnzbd version: ", os.environ["SAB_VERSION"])
|
|
|
|
print("Job location: ", os.environ["SAB_COMPLETE_DIR"])
|
|
|
|
print_splitter()
|
|
|
|
|
|
|
|
# Search for par2 files
|
|
|
|
matches = []
|
|
|
|
for root, dirnames, filenames in os.walk(os.environ["SAB_COMPLETE_DIR"]):
|
|
|
|
for filename in fnmatch.filter(filenames, "*.par2"):
|
|
|
|
matches.append(os.path.join(root, filename))
|
|
|
|
print("Found file:", os.path.join(root, filename))
|
|
|
|
|
|
|
|
# Found any par2 files we can use?
|
|
|
|
run_renamer = True
|
|
|
|
if not matches:
|
|
|
|
print("No par2 files found to process.")
|
|
|
|
|
|
|
|
# Run par2 from SABnzbd on them
|
|
|
|
for par2_file in matches:
|
|
|
|
# Analyse data and analyse result
|
|
|
|
print_splitter()
|
|
|
|
if decode_par2(par2_file):
|
|
|
|
print("Recursive repair/verify finished.")
|
|
|
|
run_renamer = False
|
|
|
|
else:
|
|
|
|
print("Recursive repair/verify did not complete!")
|
|
|
|
|
|
|
|
# No matches? Then we try to rename the largest file to the job-name
|
|
|
|
if run_renamer:
|
|
|
|
print_splitter()
|
|
|
|
print("Trying to see if there are large files to rename")
|
|
|
|
print_splitter()
|
|
|
|
|
|
|
|
# If there are more larger files, we don't rename
|
|
|
|
largest_file = None
|
|
|
|
for root, dirnames, filenames in os.walk(os.environ["SAB_COMPLETE_DIR"]):
|
|
|
|
for filename in filenames:
|
|
|
|
full_path = os.path.join(root, filename)
|
|
|
|
file_size = os.path.getsize(full_path)
|
|
|
|
# Do we count this file?
|
|
|
|
if file_size > MIN_FILE_SIZE and os.path.splitext(filename)[1].lower() not in EXCLUDED_FILE_EXTS:
|
|
|
|
# Did we already found one?
|
|
|
|
if largest_file:
|
|
|
|
print("Found:", largest_file)
|
|
|
|
print("Found:", full_path)
|
|
|
|
print_splitter()
|
|
|
|
print("Found multiple larger files, aborting.")
|
|
|
|
largest_file = None
|
|
|
|
break
|
|
|
|
largest_file = full_path
|
|
|
|
|
|
|
|
# Found something large enough?
|
|
|
|
if largest_file:
|
|
|
|
# We don't need to do any cleaning of dir-names
|
|
|
|
# since SABnzbd already did that!
|
|
|
|
new_name = "%s%s" % (
|
|
|
|
os.path.join(os.environ["SAB_COMPLETE_DIR"], os.environ["SAB_FINAL_NAME"]),
|
|
|
|
os.path.splitext(largest_file)[1].lower(),
|
|
|
|
)
|
|
|
|
print("Renaming %s to %s" % (largest_file, new_name))
|
|
|
|
|
|
|
|
# With retries for Windows
|
|
|
|
for r in range(3):
|
|
|
|
try:
|
|
|
|
os.rename(largest_file, new_name)
|
|
|
|
print("Renaming done!")
|
|
|
|
break
|
|
|
|
except:
|
|
|
|
time.sleep(1)
|
|
|
|
else:
|
|
|
|
print("No par2 files or large files found")
|
|
|
|
|
|
|
|
# Note about the new option
|
|
|
|
print(
|
|
|
|
"The features of Deobfuscate.py are now integrated into SABnzbd! "
|
|
|
|
+ "Just enable 'Deobfuscate final filenames' in Config - Switches. "
|
|
|
|
+ "Don't forget to disable this script when you enable the new option!"
|
|
|
|
+ "This script will be removed in the next version of SABnzbd."
|
|
|
|
)
|
|
|
|
|
|
|
|
# Always exit with success-code
|
|
|
|
sys.exit(0)
|