from dataclasses import dataclass from src.config.config import Configuration from src.rendering import GENERIC_FILE_MAPPING from src.rendering.markdown import ( render_markdown, read_raw_markdown, rendered_markdown_to_plain_text, ) from enum import Enum from PIL import Image, ExifTags from datetime import datetime import frontmatter @dataclass class ImageMetadata: width: int height: int alt: str exif: dict @dataclass class MarkdownMetadata: """ A class to represent metadata for a Markdown file. Attributes: ---------- frontmatter : dict A dictionary containing the front matter of the Markdown file. content : str The main content of the Markdown file. preview : str A preview or summary of the Markdown content. """ frontmatter: dict content: str preview: str @dataclass class FileMetadata: typeMeta: MarkdownMetadata | None @dataclass class TemplateFile: """ A class to represent a template file with its associated metadata. Attributes: ---------- name (str): The name of the file. path (str): The file path. proper_name (str): The proper name of the file. extension (str): The file extension. categories (list[str]): A list of categories associated with the file. date_modified (str): The date the file was last modified. date_created (str): The date the file was created. size_kb (int): The size of the file in kilobytes. metadata (ImageMetadata | FileMetadata | None): Metadata associated with the file, which can be either image metadata, file metadata, or None. dir_item_count (int): The number of items in the directory if the file is a directory. is_dir (bool): A flag indicating whether the file is a directory. """ name: str path: str proper_name: str extension: str categories: list[str] date_modified: str date_created: str size_kb: int metadata: ImageMetadata | FileMetadata | None dir_item_count: int is_dir: bool def format_date(timestamp): return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d") class TemplateHelpers: def __init__(self, config: Configuration): self.config: Configuration = config def _filter_hidden_files(self, files): return [f for f in files if not f.name.startswith("___")] def _build_metadata_for_file(self, path: str, categories: list[str] = []): file_path = self.config.content_dir / path for k in categories: if k == "image": img = Image.open(file_path) exif = img._getexif() # Conver exif to dict orientation = exif.get(274, 1) if exif else 1 width, height = img.width, img.height if orientation in [5, 6, 7, 8]: width, height = height, width exif = {} try: img = Image.open(file_path) exif_raw = img._getexif() if exif_raw: exif = { ExifTags.TAGS[k]: v for k, v in exif_raw.items() if k in ExifTags.TAGS } except Exception as e: print(f"Error processing image {file_path}: {e}") date_taken = exif.get("DateTimeOriginal") if not date_taken: date_taken = format_date(file_path.stat().st_ctime) return ImageMetadata( width=width, height=height, alt=file_path.name, exif=exif, ) elif k == "document": ret = None with open(file_path, "r") as fdoc: ret = FileMetadata(None) if file_path.suffix[1:].lower() == "md": ret.typeMeta = MarkdownMetadata({}, "", "") content, c_frontmatter, obj = render_markdown(file_path) ret.typeMeta.frontmatter = c_frontmatter ret.typeMeta.content = content ret.typeMeta.rawContent = read_raw_markdown(file_path) ret.typeMeta.rawText = rendered_markdown_to_plain_text( ret.typeMeta.content ) ret.typeMeta.preview = ret.typeMeta.rawText[:500] + "..." return ret return None def get_folder_contents(self, path: str = ""): """ Retrieve the contents of a folder and return a list of TemplateFile objects. Args: path (str): The relative path to the folder within the content directory. Defaults to an empty string, which refers to the root content directory. Returns: list: A list of TemplateFile objects representing the files and directories within the specified folder. The function performs the following steps: 1. Constructs the full path to the folder by combining the content directory with the provided path. 2. Retrieves all files and directories within the specified folder. 3. Iterates over each file and directory, creating a TemplateFile object with metadata such as name, path, proper name, extension, categories, date modified, date created, size in KB, metadata, directory item count, and whether it is a directory. 4. If the item is a file, it assigns categories based on the file extension using a predefined mapping. 5. Builds additional metadata for each file. 6. Filters out hidden files from the list. 7. Returns the list of TemplateFile objects. """ search_contnet_path = self.config.content_dir / path files = search_contnet_path.glob("*") ret = [] for f in files: t = TemplateFile( name=f.name, path=str(f.relative_to(self.config.content_dir)), proper_name=f.stem, extension=f.suffix.lower(), categories=[], date_modified=format_date(f.stat().st_mtime), date_created=format_date(f.stat().st_ctime), size_kb=f.stat().st_size / 1024, metadata=None, dir_item_count=len(list(f.glob("*"))) if f.is_dir() else 0, is_dir=f.is_dir(), ) if f.is_file(): for k, v in GENERIC_FILE_MAPPING.items(): if f.suffix[1:].lower() in v: t.categories.append(k) t.metadata = self._build_metadata_for_file(f, t.categories) if "image" in t.categories: # Adjust date_modified and date_created to be the date the image was taken from exif if available if t.metadata.exif and "DateTimeOriginal" in t.metadata.exif: t.date_modified = t.metadata.exif["DateTimeOriginal"] t.date_created = t.metadata.exif["DateTimeOriginal"] ret.append(t) ret = self._filter_hidden_files(ret) return ret def get_sibling_content_files(self, path: str = ""): """ Retrieves a list of sibling content files in the specified directory. Args: path (str): The relative path within the content directory to search for files. Defaults to an empty string, which means the root of the content directory. Returns: list: A list of tuples, where each tuple contains the file name and its relative path to the content directory. Only files that do not start with "___" are included. """ search_contnet_path = self.config.content_dir / path files = search_contnet_path.glob("*") return [ (file.name, str(file.relative_to(self.config.content_dir))) for file in files if file.is_file() and not file.name.startswith("___") ] def get_text_document_preview(self, path: str): """ Generates a preview of the text document located at the given path. This method reads the first 100 characters from the specified text file and returns it as a string. The file path is constructed by combining the content directory from the configuration with the provided path. Args: path (str): The relative path to the text document within the content directory. Returns: str: A string containing the first 100 characters of the text document. Raises: FileNotFoundError: If the file at the specified path does not exist. IOError: If an I/O error occurs while reading the file. """ file_path = self.config.content_dir / path with open(file_path, "r") as f: content = f.read(100) return content def get_sibling_content_folders(self, path: str = ""): """ Retrieves a list of sibling content folders within a specified directory. Args: path (str): A relative path from the content directory to search within. Defaults to an empty string, which means the search will be conducted in the content directory itself. Returns: list of tuple: A list of tuples where each tuple contains the folder name and its relative path to the content directory. Only directories that do not start with "___" are included. """ search_contnet_path = self.config.content_dir / path files = search_contnet_path.glob("*") return [ (file.name, str(file.relative_to(self.config.content_dir))) for file in files if file.is_dir() and not file.name.startswith("___") ]