Skip to content

Instantly share code, notes, and snippets.

@bitsnaps
Last active September 27, 2023 22:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • 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)
@abdogomri
Copy link

does it decrypt psv videos on MAC ? any explanation how to run this ?

@bitsnaps
Copy link
Author

bitsnaps commented Jul 25, 2020

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.

@abdogomri
Copy link

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
Capture d’écran 2020-07-25 à 14 56 41

@bitsnaps
Copy link
Author

bitsnaps commented Jul 25, 2020

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.

@abdogomri
Copy link

abdogomri commented Jul 25, 2020

yes, of course, I have downloaded the courses locally and the offline player version is 1.12 (261) , which version this works on?

@bitsnaps
Copy link
Author

bitsnaps commented Jul 25, 2020

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.

@abdogomri
Copy link

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.

@bitsnaps
Copy link
Author

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.

@abdogomri
Copy link

the encryption has changed I see that even the database structure has changed. thank you anyway bro Ibrahim .

@muni2explore
Copy link

muni2explore commented Nov 29, 2020

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
Capture d’écran 2020-07-25 à 14 56 41

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

@Crispy-rw
Copy link

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
Capture d’écran 2020-07-25 à 14 56 41

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?
Screenshot from 2020-12-18 11-38-57

@bitsnaps
Copy link
Author

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.

@cloudhubzawsaz
Copy link

Cant find the directory ClipDownloads , has something changed?

@bitsnaps
Copy link
Author

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'.

@cloudhubzawsaz
Copy link

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.

@cloudhubzawsaz
Copy link

cloudhubzawsaz commented Dec 25, 2020

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

@bitsnaps
Copy link
Author

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

@cloudhubzawsaz
Copy link

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

Thanks, i was able to fix this by deleting the file.

@cloudhubzawsaz
Copy link

cloudhubzawsaz commented Dec 26, 2020

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)

@albin01
Copy link

albin01 commented Dec 6, 2021

Is still this script working or it needs to be updated ?

@bitsnaps
Copy link
Author

bitsnaps commented Dec 6, 2021

@albin01 I'm not sure, just try it and let us know.

@albin01
Copy link

albin01 commented Dec 6, 2021

@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'

@bitsnaps
Copy link
Author

bitsnaps commented Dec 6, 2021

@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.

@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