Skip to content

Instantly share code, notes, and snippets.

@bitsnaps
Last active September 27, 2023 22:10
Show Gist options
  • Save bitsnaps/759e9a1363b0d63a329e16014d60021f to your computer and use it in GitHub Desktop.
Save bitsnaps/759e9a1363b0d63a329e16014d60021f to your computer and use it in GitHub Desktop.
Decrypt PSV file for mac
# 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
Copy link

albin01 commented Dec 6, 2021

Hi @bitsnaps, still not working:

 Traceback (most recent call last):
  File "psv_decryptor_new.py", line 143, in <module>
    course = read_json(file_path)
  File "psv_decryptor_new.py", line 90, in read_json
    course = Course(header['name'], header['id'], header['title'], header['level'], header['hasLearningCheck'])
KeyError: 'name'

in header object are arriving some records like this:
{'id': 'xxxxx', 'fullName': 'XXXXX', 'imageUrl': 'https://.......', 'numberOfCourses': 6}

@bitsnaps
Copy link
Author

bitsnaps commented Dec 6, 2021

@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