Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
When you import movies into iMovie 10 libraries, the file is always copied, wasting space and hindering editability. This script replaces the copy with a symlink, reclaiming disk space.
#!/usr/bin/env python3
#
# Usage: dedup-imovie-library.py LIBRARY ORIGINALS
#
# Public gist:
# https://gist.github.com/miguelmorin/92cd8b820fd0135bf0f7d5fea90dc3f1
#
# This code was inspired by this post and gist, and converted to Python
# https://benhollis.net/blog/2014/06/28/hard-linking-videos-to-save-space-in-your-imovie-library/
# https://gist.github.com/bhollis/6682c8b6b6357e2fedc5
#
# Goes through an iMovie 10 library and replaces all the "Original Media" movie
# and image files with symlinks to the actual original media, in order to conserve
# disk space.
#
# This assumes you've already imported the files into iMovie and waited for them
# all to be copied.
#
# You can adapt the global variable `PROJECTS_TO_SKIP` to avoid replacing media
# on some projects that you may be working on.
#
# This assumes that your iMovie library and originals folder are organized by
# the same event name (because sometimes a camera records similarly named
# DSC001.MOV and one needs a way to distinguish them). If the names are
# different, e.g. if you create two events titled "movie" then iMovie renames
# the second to "movie 1", you can adapt the global variable
# `SHOW_NAME_CORRESPONDENCE` to map the name of the iMovie event to the name of
# the folder with the original content.
#
# author: Miguel Morin
# copyright: public domain
# year: 2019
import doctest
import glob
import os
import pathlib
import shutil
import sys
FILE_SUFFIXES_LOWERCASE = [".mp4", ".mts", ".mov", ".jpg", ".jpeg", ".png"]
PROJECTS_TO_SKIP = [] # e.g., ["project 1", "project 2"]
SHOW_NAME_CORRESPONDENCE = {} # e.g. {"movie": "movie 1"}
def skip(f):
"""Returns a boolean for whether to skip a file depending on suffix.
>>> skip("abc.mp4")
False
>>> skip("ABC.JPEG")
False
>>> skip("abc.plist")
True
>>> skip("00114.MTS")
False
"""
suffix = pathlib.Path(f).suffix.lower()
return suffix not in FILE_SUFFIXES_LOWERCASE
def get_show_and_name(f):
"""
>>> show, name = get_show_and_name("/Volumes/video/iMovie Library.imovielibrary/my great show/Original Media/00117.mts")
>>> "my great show" == show
True
>>> "00117.mts" == name
True
>>> show, name = get_show_and_name("/Volumes/video/path/to/originals/my great show/00117.mts")
>>> "my great show" == show
True
>>> "00117.mts" == name
True
"""
path = pathlib.Path(f)
name = path.name.lower()
dirname = str(path.parents[0])
imovie = "iMovie Library.imovielibrary" in dirname
parent_dir = str(path.parents[2 if imovie else 1])
show = dirname.replace(parent_dir, "")
if imovie:
assert show.endswith("/Original Media"), f
show = show.replace("/Original Media", "")
assert show.startswith("/")
show = show[1:].lower()
if show in SHOW_NAME_CORRESPONDENCE:
show = SHOW_NAME_CORRESPONDENCE[show]
return show, name
def build_originals_dict(originals):
"""Go through the original directory to build a dictionary of filenames to paths."""
originals_dic = dict()
for f in glob.glob(os.path.join(originals, "**", "*.*"), recursive=True):
if skip(f):
continue
show, name = get_show_and_name(f)
originals_dic[(show, name)] = f
return originals_dic
def replace_files_with_symlinks(library, originals):
"""Go through the iMovie library and find the replacements."""
originals_dic = build_originals_dict(originals)
# List files recursively
for f in glob.glob(os.path.join(library, "**", "*.*"), recursive=True):
if skip(f) or os.path.islink(f):
continue
show, name = get_show_and_name(f)
if (show, name) in originals_dic:
target = originals_dic[(show, name)]
print("Replacing %s with %s" % (f, target))
os.unlink(f)
os.symlink(target, f)
else:
print("No original found for %s" % f)
def main():
args = sys.argv
assert 3 == len(args), "You need to pass 3 arguments"
library = args[1]
originals = args[2]
replace_files_with_symlinks(library = library, originals = originals)
if "__main__" == __name__:
r = doctest.testmod()
assert 0 == r.failed, "Problem: doc-tests do not pass!"
main()
@bmaupin

This comment has been minimized.

Copy link

@bmaupin bmaupin commented Feb 25, 2021

This is just comparing the filenames to find duplicates, right? Something like rdfind can actually scan the files to make sure they're the same, e.g.

rdfind -dryrun true -minsize 1048576 -makesymlinks true ~/Pictures/ ~/Movies/

https://apple.stackexchange.com/a/414495/22772

I do like that your script limits the file types. It's good to have options :)

@miguelmorin

This comment has been minimized.

Copy link
Owner Author

@miguelmorin miguelmorin commented Feb 26, 2021

@bmaupin: Yes, with two other advantages: it removes the duplicated file and creates a symlink in its place so the iMovie library is completely functional and all the projects still work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment