From e281f4e781560138629422c77b92727cf56fa8f2 Mon Sep 17 00:00:00 2001 From: Felipe Martin Date: Wed, 26 Sep 2018 23:44:04 +0200 Subject: [PATCH] In the beginning there was darkness --- Pipfile | 16 +++++ Pipfile.lock | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ porg.py | 112 +++++++++++++++++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 porg.py diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..02fcfd3 --- /dev/null +++ b/Pipfile @@ -0,0 +1,16 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pyexif = "*" +exifread = "*" +mutagen = "*" + +[dev-packages] +ipdb = "*" +yapf = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..01b108f --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,173 @@ +{ + "_meta": { + "hash": { + "sha256": "b9868bf53ffede6873397a5c3e4f890178f2f8e1c2c9b2cbcb408ff086e3e04e" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "exifread": { + "hashes": [ + "sha256:4aa9d227db5c4cd65d87520076140a6f84e33363e08e649b87c7afec6bab60ab", + "sha256:79e244f2eb466709029e8806fe5e2cdd557870c3db5f68954db0ef548d9320ad" + ], + "index": "pypi", + "version": "==2.1.2" + }, + "mutagen": { + "hashes": [ + "sha256:2ea9c900a05fa7f5f4c5bd9fc1475d7d576532e13b2f79b694452b997ff67200" + ], + "index": "pypi", + "version": "==1.41.1" + }, + "pyexif": { + "hashes": [ + "sha256:da2377f987fc221a91ec6d369296d1310c5e9239abe525786e380f97270ddebd" + ], + "index": "pypi", + "version": "==0.2.1" + } + }, + "develop": { + "appnope": { + "hashes": [ + "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", + "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.0" + }, + "backcall": { + "hashes": [ + "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", + "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" + ], + "version": "==0.1.0" + }, + "decorator": { + "hashes": [ + "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", + "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" + ], + "version": "==4.3.0" + }, + "ipdb": { + "hashes": [ + "sha256:7081c65ed7bfe7737f83fa4213ca8afd9617b42ff6b3f1daf9a3419839a2a00a" + ], + "index": "pypi", + "version": "==0.11" + }, + "ipython": { + "hashes": [ + "sha256:007dcd929c14631f83daff35df0147ea51d1af420da303fd078343878bd5fb62", + "sha256:b0f2ef9eada4a68ef63ee10b6dde4f35c840035c50fd24265f8052c98947d5a4" + ], + "version": "==6.5.0" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, + "jedi": { + "hashes": [ + "sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1", + "sha256:c254b135fb39ad76e78d4d8f92765ebc9bf92cbc76f49e97ade1d5f5121e1f6f" + ], + "version": "==0.12.1" + }, + "parso": { + "hashes": [ + "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", + "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24" + ], + "version": "==0.3.1" + }, + "pexpect": { + "hashes": [ + "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", + "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.6.0" + }, + "pickleshare": { + "hashes": [ + "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", + "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" + ], + "version": "==0.7.5" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381", + "sha256:3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4", + "sha256:858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917" + ], + "version": "==1.0.15" + }, + "ptyprocess": { + "hashes": [ + "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", + "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + ], + "version": "==0.6.0" + }, + "pygments": { + "hashes": [ + "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", + "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" + ], + "version": "==2.2.0" + }, + "simplegeneric": { + "hashes": [ + "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173" + ], + "version": "==0.8.1" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "traitlets": { + "hashes": [ + "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", + "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" + ], + "version": "==4.3.2" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "yapf": { + "hashes": [ + "sha256:b96815bd0bbd2ab290f2ae9e610756940b17a0523ef2f6b2d31da749fc395137", + "sha256:cebb6faf35c9027c08996c07831b8971f3d67c0eb615269f66dfd7e6815fdc2a" + ], + "index": "pypi", + "version": "==0.24.0" + } + } +} diff --git a/porg.py b/porg.py new file mode 100644 index 0000000..1d4e8b1 --- /dev/null +++ b/porg.py @@ -0,0 +1,112 @@ +from dataclasses import dataclass +from datetime import datetime +import hashlib +import mimetypes +import os.path + +import exifread +import mutagen + + +# Config +SOURCE_PATH = '/Volumes/MEDIA/Photos' +TARGET_PATH = '/Volumes/MEDIA/Pictures' + +# Globals +file_list = [] + + +@dataclass +class File: + path: str + + @property + def type(self): + """Retrieves the file mimetype by extension""" + if not getattr(self, '_type', False): + self._type, _ = mimetypes.guess_type(self.path) + if not self._type: + print(f"Can't guess type of file {self.path}") + return self._type + + @property + def is_image(self): + return 'image' in self.type + + @property + def is_video(self): + return 'video' in self.type + + @property + def exif(self): + """ + Retrieve EXIF data from the file and merge it with wathever mutagen finds in there for video files. + """ + if not getattr(self, '_exif', False): + self._exif = exifread.process_file(open(self.path, 'rb')) + if self.is_video: + self._exif.update(mutagen.File(self.path)) + return self._exif + + @property + def datetime(self): + """ + Retrieves original creation date for the picture trying exif data first, filename guessing and finally + modification date. Make sure your pictures are exported unmodified so the file attributes maintain their + original values for this to work. + """ + if self.is_image: + date, time = self.exif['EXIF DateTimeOriginal'].values.split() + return datetime(*(int(x) for x in date.split(':') + time.split(':'))) + + if self.is_video: + # Apple iPhone tag + try: + return datetime.strptime(self.exif.get('©day')[0], '%Y-%m-%dT%H:%M:%S%z') + except TypeError: + pass + + # Tag not found, try to guess datetime from filename + # Format: YYYY-MM-DD HH.MM.SS.ext + try: + name, _ = os.path.basename(self.path).rsplit('.', maxsplit=1) + date, time = name.split(' ') + return datetime(*(int(x) for x in date.split('-') + time.split('.'))) + except ValueError: + raise + + # Last resort, use file creation/modification date + stat = os.stat(self.path) + try: + return stat.st_birthtime + except AttributeError: + # Linux: No easy way to get creation dates here, + # so we'll settle for when its content was last modified. + return stat.st_mtime + + @property + def checksum(self): + if not getattr(self, '_checksum', False): + digest = hashlib.sha1() + with open(self.path, 'rb') as handler: + digest.update(handler.read()) + + self._checksum = digest.hexdigest() + return self._checksum + + +def read_path(): + for path, directories, files in os.walk(SOURCE_PATH): + for filename in files: + if not filename.startswith('.') and filename not in ['.', '..']: + yield File(path=os.path.join(path, filename)) + + +def get_target_path(fileobj): + return os.path.join(TARGET_PATH, str(fileobj.datetime.year), '%02d' % fileobj.datetime.month) + + +if __name__ == '__main__': + for fileobj in read_path(): + target_path = get_target_path(fileobj) + os.makedirs(target_path, exist_ok=True)