Last active
September 27, 2023 22:10
-
-
Save bitsnaps/759e9a1363b0d63a329e16014d60021f to your computer and use it in GitHub Desktop.
Decrypt PSV file for mac
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This script is inspired from: https://github.com/kigaita/PsvDecryptCore | |
# It allows you to decrypt psv video files for online developer training website | |
# The original code was written with .NET for Windows users, this one with Python tested for Mac users. | |
# *** Disclaimer **** | |
# Please only use it for your convenience so that you can watch the courses on your devices offline or for educational purposes. | |
# Piracy is strictly prohibited. Decrypted videos should not be uploaded to open servers, torrents, or other methods of mass distribution. Any consequences resulting from misuse of this tool are to be taken by the user. | |
# packages you may need to install: | |
# pip install python-magic | |
# pip install python-magic-bin | |
import sys, os, json, magic, hashlib, re | |
# check python version | |
if int(sys.version[0]) < 3: | |
raise Exception("Must be using Python 3") | |
class Course: | |
modules = [] | |
def __init__(self, name, id, title, level, hasLearningCheck): | |
self.name = name | |
self.id = id | |
self.title = title | |
self.level = level | |
self.hasLearningCheck = hasLearningCheck | |
def __str__(self): | |
return 'Course(Name: ' + self.name+', Nbr Modules: ' + str(len(self.modules))+')' #', Title: '+self.title | |
class Module: | |
index = 0 | |
clips = [] | |
def __init__(self, id, name, title, authorHandle, description, index): | |
self.id = id | |
self.title = title | |
self.name = name | |
self.authorHandle = authorHandle | |
self.description = description | |
self.index = index | |
def get_indexed_file(self): | |
return str(self.index) +'. '+ get_valid_filename(self.title) | |
def __str__(self): | |
return 'Module(id: ' + self.id+', name: ' + self.name+', Title: '+self.title+')' | |
class Clip: | |
def __init__(self, id, name, title, index): | |
self.id = id | |
self.name = name | |
self.title = title | |
self.index = index | |
def get_indexed_file(self): | |
return str(self.index) +'. '+ get_valid_filename(self.title)+'.mp4' | |
def get_file_name(self): | |
return str(self.id).lower().replace('-','')+'.psv' | |
def __str__(self): | |
return 'Clip(id: ' + self.id+', name: ' + self.name+', Title: '+self.title+')' | |
def get_clips(json_data): | |
clips = [] | |
for c in json_data: | |
clip = Clip(c['id'],c['name'],c['title'],c['index']) | |
clips.append(clip) | |
return clips | |
def get_modules(json_data): | |
modules = [] | |
i = 0 | |
if 'modules' in json_data: | |
json_modules = json_data['modules'] | |
for m in json_modules: | |
module = Module(m['id'],m['name'],m['title'],m['authorHandle'],m['description'], i) | |
module.clips = get_clips(m['clips']) | |
# print(module) | |
modules.append(module) | |
i = i + 1 | |
return modules | |
def read_json(filename): | |
course = None | |
collections = [] | |
# read json | |
with open(filename, mode='r', encoding='utf-8') as f: | |
try: | |
json_data = json.loads(f.read()) | |
except Exception as e: | |
raise Exception('Error in file: ' + filename) | |
sys.exit(2) | |
if 'header' in json_data: | |
header = json_data['header'] | |
course = Course(header['name'], header['id'], header['title'], header['level'], header['hasLearningCheck']) | |
elif 'collection' in json_data: | |
collection = json_data['collection'] | |
for col in collection: | |
if 'name' in col: | |
collections.append({'name':col['name'], 'id':col['id'], | |
'title':col['title'], 'level':col['level']}) | |
else: | |
if 'course' in col: | |
c = col['course'] | |
course = Course(c['name'], c['id'], c['title'], c['level'], c['hasLearningCheck']) | |
else: | |
print('Cannot find "course" key in collection') | |
continue | |
# add modules | |
if course: | |
course.modules = get_modules(json_data) | |
# print('Nbr of Collection: '+str(len(collections))) | |
return course | |
# image/jpeg | |
def get_file_type(filename): | |
file_type = magic.from_file(filename, mime=True) | |
return file_type | |
def get_valid_filename(s): | |
return re.sub('[^\w_.)( -]', ' -', s) | |
# return re.sub(r'(?u)[^-\w.]', '', s.strip().replace(' ', '_')) | |
def decrypt_psv(fpath, target_dir, target_fname): | |
target_file_path = os.path.join(target_dir, target_fname) | |
if os.path.exists(target_file_path): | |
os.remove(target_file_path) | |
print("Writing: %s" % target_file_path) | |
with open(target_file_path, "wb") as ofh: | |
for byte in bytearray(open(fpath, "rb").read()): | |
# ofh.write(chr(byte ^ 101)) # TypeError: a bytes-like object is required, not 'str' (it works with python2) | |
ofh.write(bytes(chr(byte ^ 101), 'iso_8859_1')) | |
# this directory should contains both: "fsCachedData" and "ClipDownloads" | |
source_dir = '/full/path/to/data/files' | |
target_dir = '/full/path/to/output/directory' | |
# loop through files | |
for root_dir, dirs, files in os.walk(source_dir): | |
if os.path.basename(root_dir)=='fsCachedData': | |
#Possible algorithms for filename (uuid4): str(uuid.uuid1()).upper() | |
for filename in files: | |
file_path = os.path.join(root_dir, filename) | |
if not get_file_type(file_path).startswith('image/'): | |
if not os.path.exists(file_path): | |
print('File: ', file_path, ' not found') | |
sys.exit(1) | |
course = read_json(file_path) | |
if course: | |
if not course.hasLearningCheck: | |
print(course.title,': DOES NOT HAVE LEARNING CHECK.') | |
continue | |
# Create course directory | |
course_path = os.path.join(target_dir, get_valid_filename(course.title)) | |
if not os.path.exists(course_path): | |
print('Creating course dir: ' + course_path) | |
os.mkdir(course_path) | |
# Loop through modules | |
for module in course.modules: | |
module_path = os.path.join(course_path, module.get_indexed_file()) | |
if not os.path.exists(module_path): | |
print('Creating module dir: ' + module_path) | |
os.mkdir(module_path) | |
# Loop through clips | |
for clip in module.clips: | |
clip_path = os.path.join(os.path.dirname(root_dir),os.path.join('ClipDownloads',clip.get_file_name())) | |
if os.path.exists(clip_path): | |
print('Course: ', course.title, ', Module: ', module.get_indexed_file()) | |
print("Clip: ", clip.get_indexed_file(), ', path: ', clip_path) | |
decrypt_psv(clip_path, module_path, clip.get_indexed_file()) | |
# else: | |
# print('clip not found: ', clip_path) |
@albin01 This means the script is -probably- no longer working for the newer versions, sorry I can't fix it now, since I don't have this version neither the downloaded course files, try to debug step by step and update the code accordingly, you may fix it easily if they didn't change mechanism of the encryption.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi @bitsnaps, still not working:
in header object are arriving some records like this:
{'id': 'xxxxx', 'fullName': 'XXXXX', 'imageUrl': 'https://.......', 'numberOfCourses': 6}