-
-
Save bitsnaps/759e9a1363b0d63a329e16014d60021f to your computer and use it in GitHub Desktop.
# 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) |
does it decrypt psv videos on MAC ? any explanation how to run this ?
Yep, it does; just run the script python psv_decryptor.py
after updating these variables (on lines: 126, 127):
source_dir = '/full/path/to/data/files'
target_dir = '/full/path/to/output/directory'
the source_dir
directory should contains tow directories: "fsCachedData" and "ClipDownloads" which you can find on your user's application settings, make sure you're using python3.
Have you downloaded the course locally into your disk?
this directory should contains the meta data of the course, if yes, you're probably using an older (or newer) version.
yes, of course, I have downloaded the courses locally and the offline player version is 1.12 (261) , which version this works on?
honestly I don't remember (I lost it), but this version seems to be old, have you tried to upgrade?
If it doesn't work, try to find the location of the meta data file (it's just a JSON file), then update the path accordingly.
no, actually it's the latest version. maybe I need a specific version for this to work. if you can check which version do you have that works fine with this decryptor it would be good.
Sorry I don't have the app anymore, maybe you could search for the meta data directory by looking at into files content (e.g. try executing this command in the app directory: grep -s -r "durationInMilliseconds" .
to find out), there is a big chance that the algorithm of encryption has been changed, so even if you find that directory the code won't work unless you do some upgrade, you'll need to investigate.
the encryption has changed I see that even the database structure has changed. thank you anyway bro Ibrahim .
I don't see the folder fsCachedData , and after installing libmagic and all looks good it doesn't do anything, I did run it on the terminal and on IntelliJ IDEA it shows instantly: Process finished with exit code 0 , see the attached picture for the folders
fsCacheData - present in Caches directory - /Users/muni/Library/Caches/com.pluralsight.pluralsight-mac/fsCachedData.
Copy the fsCacheData from caches directory to the source_dir and the COMMAND. It will work fine.
Scripts working 100%. Kudos to @bitsnaps
I don't see the folder fsCachedData , and after installing libmagic and all looks good it doesn't do anything, I did run it on the terminal and on IntelliJ IDEA it shows instantly: Process finished with exit code 0 , see the attached picture for the folders
fsCacheData - present in Caches directory - /Users/muni/Library/Caches/com.pluralsight.pluralsight-mac/fsCachedData.
Copy the fsCacheData from caches directory to the source_dir and the COMMAND. It will work fine.
Scripts working 100%. Kudos to @bitsnaps
before selling my mac a copied all the files but i forgot to copy the fsCacheData from my mac. but i have the videos clip directory and other directories. So how can I decrypt these videos?
before selling my mac a copied all the files but i forgot to copy the fsCacheData from my mac. but i have the videos clip directory and other directories. So how can I decrypt these videos?
I don't see any way to decrypt without this directory, it contains the IDs of the videos, try to re-download the list (only the first video of each course), then see if it fills the fsCacheData
with meta data.
Cant find the directory ClipDownloads , has something changed?
Cant find the directory ClipDownloads , has something changed?
I'm not sure, they may have changed its name, you can still look for *.psv
file and update the path accordingly: find ~ -type f -name '*.psv'
.
Cant find the directory ClipDownloads , has something changed?
I'm not sure, they may have changed its name, you can still look for
*.psv
file and update the path accordingly:find ~ -type f -name '*.psv'
.
Thank you , looks like the files are in two different places now. copied them to a single folder and it works.
Is there a way to skip broken and move to next, i now get the following error after it decrypts a few, even tried re-downloading the said file, no luck.
Traceback (most recent call last):
File "ps.py", line 83, in read_json
json_data = json.loads(f.read())
File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/codecs.py", line 322, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 3131: invalid start byte
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "ps.py", line 136, in
course = read_json(file_path)
File "ps.py", line 85, in read_json
raise Exception('Error in file: ' + filename)
Exception: Error in file: /Users/user/Downloads/com.pluralsight.pluralsight-mac/fsCachedData/.DS_Store
Exception: Error in file: /Users/user/Downloads/com.pluralsight.pluralsight-mac/fsCachedData/.DS_Store
Looks like you're trying to parse .DS_Store
file, I guess you can avoid this issue by remove those files intead find . -name ".DS_Store" -delete
Exception: Error in file: /Users/user/Downloads/com.pluralsight.pluralsight-mac/fsCachedData/.DS_Store
Looks like you're trying to parse
.DS_Store
file, I guess you can avoid this issue by remove those files inteadfind . -name ".DS_Store" -delete
Thanks, i was able to fix this by deleting the file.
Is there a way to skip if exists and also delete from source if already exists in destination. I was able to do this but not sure if its right:
def decrypt_psv(fpath, target_dir, target_fname):
target_file_path = os.path.join(target_dir, target_fname)
while not 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'))
# else
# print("File already exists skipping: %s" % target_file_path)
Is still this script working or it needs to be updated ?
@albin01 I'm not sure, just try it and let us know.
@bitsnaps I've tried running but i got this error :
Traceback (most recent call last):
File "psv_decryptor_new.py", line 136, in <module>
course = read_json(file_path)
File "psv_decryptor_new.py", line 98, in read_json
c = col['course']
KeyError: 'course'
@albin01 I've just updated the script; so now it checks for the existence of the key, the code may not work if collection's structure has changed, please check if all files exist in the expected location then try again and let me know if that works.
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}
@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.
does it decrypt psv videos on MAC ? any explanation how to run this ?