Source code for imessagedb.attachment
import os
import urllib.parse
import re
import shutil
import ffmpeg
import heic2png
[docs]class Attachment:
""" Class for holding information about an attachment """
def __init__(self, database, rowid: str, filename: str, mime_type: str,
copy=False, copy_directory=None, home_directory=os.environ['HOME']) -> None:
"""
Parameters
----------
database : imessagedb.DB
An instance of a connected database
rowid, filename, mime_type : str
Fields from the database
copy : bool
Whether or not to copy the attachment, default is False
copy_directory : str
The directory to copy the attachment to, default is none
home_directory : str
Needed to know where to get the attachments from. The attachments in the database
are listed under ~/Library/Messages/Attachments, so need to be able to translate that
"""
self._database = database
self._rowid = rowid
self._filename = filename
self._mime_type = mime_type
self._copy = copy
self._copy_directory = copy_directory
self._home_directory = home_directory
self._destination_path = None
self._popup_type = None
self._conversion_type = None
self._skip = False
self._missing = False
self._needs_conversion = False
self._force = self._database.control.getboolean('force copy', False)
# The path is set to use ~, so replace it with the home directory
self._original_path = self._filename.replace('~', self._home_directory)
if not os.path.exists(self._original_path):
self._missing = True
return
if self._copy_directory is None or not os.path.isdir(self._copy_directory):
self._copy = False
if self._copy:
parts = self._original_path.split('/')
last = parts.pop()
penultimate = parts.pop()
self._destination_filename = f'{penultimate}-{last}'
self._process_mime_type() # We may need to do a conversion and therefore change the filename
if self._copy:
self._destination_path = f'{self._copy_directory}/{self._destination_filename}'
if len(self._destination_path) > 200: # Some filenames are too long
self._destination_filename = f'{self._destination_filename[:50]}---{self._destination_filename[-50:]}'
self._destination_path = f'{self._copy_directory}/{self._destination_filename}'
else:
self._destination_path = self._original_path
return
@property
def rowid(self) -> str:
""" Return the rowid """
return self._rowid
@property
def filename(self) -> str:
""" Return the filename of the attachment """
return self._filename
@property
def mime_type(self) -> str:
""" Return the mime_type of the attachment"""
return self._mime_type
@property
def copy(self) -> bool:
""" Return if the attachment should be copied """
return self._copy
@property
def destination_path(self) -> str:
""" Return the destination path the attachment will be copied to """
return self._destination_path
@property
def popup_type(self) -> str:
""" Return the popup type, one of [Audio, Video, Picture] """
return self._popup_type
@property
def conversion_type(self) -> str:
""" Return the conversion type, one of [Audio, HEIC, Video] """
return self._conversion_type
@property
def skip(self) -> bool:
""" Return if we need to skip this attachment """
return self._skip
@property
def missing(self) -> bool:
""" Return if the attachment is missing """
return self._missing
@property
def original_path(self) -> str:
""" Return the original path of the attachment """
return self._original_path
@property
def destination_filename(self) -> str:
""" Return the filename at the destination (not the whole path)"""
return self._destination_filename
@property
def html_path(self) -> str:
""" Return the html escaped path """
return urllib.parse.quote(self.destination_path)
@property
def link_path(self) -> str:
""" Return a link to the attachment """
return f"file://{urllib.parse.quote(self.destination_path)}"
[docs] def _process_mime_type(self) -> None:
""" Based on the mime_type, decide how to convert the file if necessary and how to rename it """
if self._mime_type is None:
# The only type of file that doesn't have a mime type that I am currently interested in are audio files
search_name = re.search(".caf$", self._filename)
if search_name:
self._popup_type = "Audio"
self._conversion_type = "Audio"
if self.copy:
self._destination_filename = f'{self._destination_filename}.mp3'
else:
self._skip = True # We don't care about this attachment
elif self._mime_type[0:5] == "image":
self._popup_type = "Picture"
if self.mime_type == 'image/heic':
self._conversion_type = "HEIC"
if self._copy:
self._destination_filename = f'{self._destination_filename}.png'
# Process audio
elif self._mime_type[0:5] == "audio":
self._popup_type = "Audio"
self._conversion_type = "Audio"
if self._copy:
self._destination_filename = f'{self._destination_filename}.mp3'
# Process Video
elif self._mime_type[0:5] == "video":
self._popup_type = "Video"
if self.mime_type != "video/mp4":
self._conversion_type = "Video"
if self.copy:
self._destination_filename = f'{self._destination_filename}.mp4'
[docs] def copy_attachment(self) -> None:
""" Copy the attachment """
# Skip the file copy if the copy already exists
if self._force or not os.path.exists(self._destination_path):
print(f"Copying {self._destination_filename}")
try:
shutil.copyfile(self._original_path, self._destination_path)
return
except Exception as exp:
print(f"Failed to copy {self._destination_filename}: {exp}")
return
else:
# If the file already exists, do nothing
return
[docs] def convert_heic_image(self, heic_location: str, png_location: str) -> None:
""" Convert a HEIC image to a PNG so it can be viewed in the browser """
# Don't do the expensive conversion if we've already converted it
if self._force or not os.path.exists(png_location):
try:
print(f"Converting {os.path.basename(png_location)}")
heic_image = heic2png.HEIC2PNG(heic_location)
heic_image.save(png_location)
return
except Exception as exp:
print(f'Failed to convert {heic_location} to {png_location}: {exp}')
return
else:
# If the file exists already, don't convert it
return
[docs] def convert_audio_video(self, original: str, converted: str) -> None:
""" Convert an audio or video file from an undisplayable source to something the browser can display"""
if self._force or not os.path.exists(converted):
try:
print(f"Converting {os.path.basename(converted)}")
stream = ffmpeg.input(original)
stream = ffmpeg.output(stream, converted)
stream = ffmpeg.overwrite_output(stream)
ffmpeg.run(stream, quiet=True)
return
except Exception as exp:
print(f'Failed to convert {original} to {converted}: {exp}')
return
else:
# If the file exists already, don't convert it
return