Category PS4 CFW and Hacks       Thread starter PSXHAX       Start date Jul 13, 2019 at 1:25 AM       2,050       1            
Following the Final Fantasy XIV: Stormblood and recent Final Fantasy XIV: Shadowbringers Final Fantasy XIV Online PS4 expansion pack comes a FFXIV PS4 Screenshot Retimer Python Script by Skydeo to correct the file timestamps when transferring in-game screenshots from a PS4 to a flash drive. 📸 🖼

Download: / GIT

Below are further details from the, to quote: PS4/FFXIV Screenshot Retimer

I play a lot of FFXIV on the PS4, and take a lot of screenshots when doing so (over 5000 by the end of 4.0!). A few times a year I'll transfer these off my PS4 to save space and help me look at them, but while the datetime stamps are recorded in the file name¹, they aren't stored in the file metadata, instead just being whenever files were moved to a flash drive.

This means the screenshots don't sort correctly and it drives me crazy. How can I relive my adventure through Eorzea when the screenshots are out of order? This fixes the metadata using the datetime stored in the filename.

Example usage:
python ~/Desktop/PS4/FFXIV/Screenshots/ -e
I settled on using the -e flag to actually execute, but that might be a little overly cautious. There are a couple of other optional parameters you can read about using the -h flag (python -h).

There are two ways it assigns times, a fast method which works most of the time but doesn't assign file creation times on macOS² for some edge cases, and a slow method that does but requires the installation of macOS Developer Tools. Chances are these are already installed for most targets of this script.
  • ¹ 'Stored' in a non-sortable way using the stupid US MM-DD-YYYY notation.
  • ² I guess most UNIX systems don't care about file creation time, on access and modified times. The file creation time will be set if the modified time is before present.

Make it work with videos. For some unknown reason, there are at least 3 different formats of video file names over the years. Shouldn't be hard, but need to add flags and test.
import os
import re
import time
import timeit
import shutil
from pathlib import Path
from gooey import Gooey, GooeyParser

def retime_ps4_screenshots(input_dir, execute=False, rename=False, setfile=False, verbose=True):
  start_time = timeit.default_timer()
  input_dir = Path(input_dir)

  def create_dir_list(input_dir):
    p = Path(input_dir).iterdir()
    dir_list = [entry for entry in p
                if entry.is_file()
                and not'.')]
    return dir_list

  def correct_file_time(filename, datetime_string):
    time_struct = time.strptime(datetime_string, '%m/%d/%Y %H:%M:%S')
    seconds_since = int(time.mktime(time_struct))
    os.utime(filename, (seconds_since, seconds_since))

  def correct_file_time_setfile(filename, datetime_string):
    os.system('SetFile -dm "{}" {}'.format(datetime_string, filename.replace(' ', '\\ ')))

  if setfile:
    if not shutil.which('SetFile'):
      print('Creation time flag (-c) was used, but SetFile command cannot be found.')
      print('Install Xcode command line tools from')
    elif verbose:
      print(f'SetFile command found: {shutil.which("SetFile")}')

  # path = os.path.expanduser(input_dir)
  if verbose:
    print(f'Checking directory {input_dir}')
  dir_list = create_dir_list(input_dir)

  files = []
  videos = []
  for item in dir_list:
    if item.suffix.lower() in ['.jpg', '.png']:
      files.append(re.split(r' |_|\.', + [item])
    elif item.suffix.lower() == '.mp4':
      videos.append(re.split(r' |_|\.', + [item])

  ['Xander', 'Barabroda', '06', '18', '2017', '13', '37', '51', 'jpg']
  0 = First name
  1 = Last name
  2 = Month
  3 = Day
  4 = Year
  5 = Hour
  6 = Minute
  7 = Second
  8 = extension

  # Validate the files to build images and invalid files lists.
  images = []
  invalid_files = []

  if verbose:
    print('Checking files for invalid file names and extensions.')
  for f in files:
    dir_entry = f[-1]
    if len(f) != 10:
      if verbose:
        print('Invalid filename: \'{}\''.format(dir_entry))
    elif dir_entry.suffix not in ['.jpg', '.png', '.mp4']:
      if verbose:
        print('Invalid extension: \'{}\''.format(
  if verbose:
    if not len(invalid_files):
      print('None found.')

  if verbose:
    print('Retiming images.')

  for i in images:
    [first, last, month, day, year, hours, minutes, seconds, extension, original_filename] = i

    time_string = f'{month}/{day}/{year} {hours}:{minutes}:{seconds}'
    time_string_sane = f'{year}/{month}/{day} {hours}:{minutes}:{seconds}'
    new_filename = input_dir / f'{first} {last} {year}{month}{day}_{hours}{minutes}{seconds}.{extension}'

    if verbose == 2:
      print('\tRetiming to {}{}'.format(time_string_sane, '\n' if not rename else ''))

    if execute:
      if setfile:
        correct_file_time_setfile(original_filename, time_string)
      correct_file_time(original_filename, time_string)

    if rename:
      if verbose == 2:
        print('\tRenaming to \'{}\'\n'.format(new_filename))
      if execute:
        os.rename(original_filename, new_filename)

  end_time = timeit.default_timer()
  print('Completed in {0} seconds.'.format(round(end_time-start_time,2)))
  print('{} files {} {}{}.'.format(len(images), 'were' if execute else 'will be', 'renamed and retimed' if rename else 'retimed', ' using SetFile' if setfile else ''))

  if len(invalid_files) > 0:
    print('\nThe following {} {} {} processed. {}.'.format(len(invalid_files),'files' if len(invalid_files) != 1 else 'file','weren\'t' if execute else 'won\'t be', 'See above' if verbose else 'Run with the -v flag for more details'))
    for invalid_file in invalid_files:

  if not execute:
    print('DRY RUN COMPLETE. No files were modified. Run with -e flag to execute.')

def main():
  parser = GooeyParser(description='Correct the file creation and modified time on FFXIV screenshots exported from the PS4.')
  parser.add_argument('directory', metavar='Screenshot Folder', widget='DirChooser', help='Path to the directory containing the images.')
  parser.add_argument('-e', '--execute', action='store_true', help='Modifiy the files.')
  parser.add_argument('-r', '--rename', action='store_true', help='Rename the files to \'First Last\'')
  parser.add_argument('-c', '--creation_time', action='store_true', help='Use the macOS SetFile command to also set file creation time. It\'s much slower!')
  group = parser.add_mutually_exclusive_group()
  group.add_argument('-v', '--verbose', action='store_true', help='Display additional logging.')
  group.add_argument('-n', '--noisy', action='store_true', help='Display a huge amount of logging.')

  args = parser.parse_args()

  if args.noisy:
    args.verbose = True


if __name__ == '__main__':
Final Fantasy XIV (FFXIV) PS4 Screenshot Retimer Script by Skydeo.jpg
:idea: Reminder: Those without a Verified Badge yet on Discord to access the private areas we recommend Joining Us! Why? The waiting process takes a week for new Members, and there's a lot we're unable to share on public forums including the latest PS4 PKG Games. 🏴‍☠️


Recent Articles
SpecterDev Shares Low-Level Details on Porting MUSL to PS4
Since the OpenOrbis PlayStation 4 Toolchain release and related guides on his YouTube Channel, PS4 scene developer @SpecterDev of (Patreon) shared via Twitter a Blog Post detailing...
Sony's Days of Play 2020 Sale Offers Deals on Games and More
With Memorial Day weekend and the unofficial start of summer upon us things are heating up outside as well as in Sony's Days of Play 2020 sale with deals on PlayStation games, accessories, PS Now...
Super Console Wars 1.0 PS4 Homebrew Game PKG by Lapy & Acekone1!
A few weeks back we saw PS4-Xplorer File Manager Theme and Avatar Maker followed by a PS4-Xplorer 1.19 update, and today PlayStation 4 homebrew developer @Lapy returns via Twitter with designer...
Marvel's Iron Man PS VR Bundle & Free Marvel's Iron Man VR Demo
This July 3rd a new Marvel's Iron Man PS VR Bundle launches including the game itself, a PS Camera and two PS Move controllers alongside a free Marvel's Iron Man VR Demo available today for PS4...