Skip to content

Instantly share code, notes, and snippets.

@apeyroux
Created September 29, 2024 07:59
Show Gist options
  • Save apeyroux/422f26a2ef94def68f7ecf0cfd438955 to your computer and use it in GitHub Desktop.
Save apeyroux/422f26a2ef94def68f7ecf0cfd438955 to your computer and use it in GitHub Desktop.
import os
import shutil
import re
import logging
import argparse
from urllib.parse import unquote
from datetime import datetime # Importation de datetime pour la gestion des dates
# Configuration du logger
logging.basicConfig(filename='migration_log.txt', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Extensions d'images prises en charge
IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg']
# Chemins par défaut pour les dossiers Obsidian et NotePlan
obsidian_folder_default = "/chemin/vers/ton/dossier/Obsidian"
noteplan_folder_default = "/chemin/vers/ton/dossier/NotePlan"
obsidian_assets_folder_default = os.path.join(obsidian_folder_default, "assets")
class FileHandler:
@staticmethod
def list_markdown_files(folder):
"""Liste tous les fichiers Markdown dans un dossier"""
return [f for f in os.listdir(folder) if f.endswith(".md")]
@staticmethod
def is_image(file_name):
"""Détermine si un fichier est une image en fonction de son extension"""
return any(file_name.lower().endswith(ext) for ext in IMAGE_EXTENSIONS)
@staticmethod
def copy_attachment(attachment_name, obsidian_assets_folder, noteplan_attachments_folder):
"""Recherche et copie une pièce jointe du dossier Obsidian (y compris les sous-dossiers) vers NotePlan"""
# Décoder le nom de fichier encodé en URL
decoded_attachment_name = unquote(attachment_name)
for root, _, files in os.walk(obsidian_assets_folder):
if decoded_attachment_name in files:
source_path = os.path.join(root, decoded_attachment_name)
destination_path = os.path.join(noteplan_attachments_folder, decoded_attachment_name)
shutil.copy(source_path, destination_path)
logging.info(f"Pièce jointe copiée : {decoded_attachment_name} vers {noteplan_attachments_folder}")
return True
logging.warning(f"Pièce jointe manquante : {decoded_attachment_name} non trouvée dans {obsidian_assets_folder}")
return False
class ContentConverter:
@staticmethod
def remove_yaml_header(content):
"""Supprime les en-têtes YAML dans les fichiers daily notes"""
# Recherche et suppression de l'en-tête entre les `---` au début
return re.sub(r'^---.*?---\n', '', content, flags=re.DOTALL)
@staticmethod
def convert_links(content, attachments_folder_name):
"""Convertit les liens internes Obsidian vers le format NotePlan, en tenant compte des fichiers avec espaces"""
def replace_link(match):
file_name = match.group(1).strip()
# Décoder le nom du fichier encodé en URL
file_name_decoded = unquote(file_name)
return f'![{"image" if FileHandler.is_image(file_name_decoded) else "file"}]({attachments_folder_name}/{file_name_decoded})'
return re.sub(r'!\[\[(.+?)\]\]', replace_link, content)
class Migration:
def __init__(self, obsidian_folder, noteplan_folder, obsidian_assets_folder, batch_size, migrate_attachments, is_daily):
self.obsidian_folder = obsidian_folder
self.noteplan_folder = noteplan_folder
self.obsidian_assets_folder = obsidian_assets_folder
self.batch_size = batch_size
self.migrate_attachments = migrate_attachments
self.is_daily = is_daily
@staticmethod
def convert_daily_filename(filename):
"""Convertit les noms de fichiers daily notes du format dd-mm-yyyy.md au format yyyymmdd.md"""
try:
# Extraire la date du nom de fichier avec le format dd-mm-yyyy
date_obj = datetime.strptime(filename[:10], '%d-%m-%Y')
# Reformater la date au format yyyymmdd
return date_obj.strftime('%Y%m%d') + ".md"
except ValueError:
# Si le fichier ne correspond pas au format attendu, retourner le nom original
return filename
def migrate_files_in_batches(self):
"""Migration par lots des fichiers Markdown et pièces jointes d'Obsidian vers NotePlan"""
markdown_files = FileHandler.list_markdown_files(self.obsidian_folder)
total_files = len(markdown_files)
logging.info(f"Nombre total de fichiers Markdown à migrer : {total_files}")
if not os.path.exists(self.noteplan_folder):
os.makedirs(self.noteplan_folder)
for i in range(0, total_files, self.batch_size):
batch_files = markdown_files[i:i + self.batch_size]
logging.info(f"Traitement du lot {i//self.batch_size + 1} : {len(batch_files)} fichiers")
for file in batch_files:
self.migrate_single_file(file)
def migrate_single_file(self, file):
"""Gère la migration d'un fichier Markdown unique"""
title = os.path.splitext(file)[0]
if self.is_daily:
# Si c'est une daily note, convertit le nom du fichier au format yyyymmdd.md
new_file_name = self.convert_daily_filename(file)
else:
new_file_name = file
obsidian_file_path = os.path.join(self.obsidian_folder, file)
noteplan_file_path = os.path.join(self.noteplan_folder, new_file_name)
# Lire le contenu du fichier Markdown
with open(obsidian_file_path, 'r', encoding='utf-8') as f:
content = f.read()
if self.is_daily:
# Retirer l'en-tête YAML dans les daily notes
content = ContentConverter.remove_yaml_header(content)
attachments_copied = 0
if self.migrate_attachments:
attachments = re.findall(r'!\[\[(.+?)\]\]', content)
if attachments:
# Utiliser le nouveau nom de fichier pour les daily notes
noteplan_attachments_folder = os.path.join(self.noteplan_folder, f"{os.path.splitext(new_file_name)[0]}_attachments")
os.makedirs(noteplan_attachments_folder, exist_ok=True)
for attachment in attachments:
if FileHandler.copy_attachment(attachment, self.obsidian_assets_folder, noteplan_attachments_folder):
attachments_copied += 1
# Convertir les liens, mais ne pas ajouter de titre pour les daily notes
if not self.is_daily:
content = f"# {title}\n\n" + content # Ajouter le titre pour les fichiers non daily
converted_content = ContentConverter.convert_links(content, f"{os.path.splitext(new_file_name)[0]}_attachments")
# Sauvegarder le fichier converti
with open(noteplan_file_path, 'w', encoding='utf-8') as f:
f.write(converted_content)
logging.info(f"Fichier migré : {file} avec {attachments_copied}/{len(attachments)} pièces jointes copiées")
def main():
# Interface CLI
parser = argparse.ArgumentParser(description="Migration de fichiers Markdown d'Obsidian vers NotePlan")
parser.add_argument('--obsidian_folder', type=str, default=obsidian_folder_default, help="Dossier source Obsidian")
parser.add_argument('--noteplan_folder', type=str, default=noteplan_folder_default, help="Dossier destination NotePlan")
parser.add_argument('--obsidian_assets_folder', type=str, default=obsidian_assets_folder_default, help="Dossier des assets (pièces jointes) Obsidian")
parser.add_argument('--batch_size', type=int, default=10, help="Nombre de fichiers à traiter par lot")
parser.add_argument('--migrate_attachments', type=bool, default=True, help="Activer ou désactiver la migration des pièces jointes")
parser.add_argument('--daily', action='store_true', help="Traiter les notes quotidiennes et convertir les fichiers au format yyyymmdd.md")
args = parser.parse_args()
# Initialisation et lancement de la migration
migration = Migration(
obsidian_folder=args.obsidian_folder,
noteplan_folder=args.noteplan_folder,
obsidian_assets_folder=args.obsidian_assets_folder,
batch_size=args.batch_size,
migrate_attachments=args.migrate_attachments,
is_daily=args.daily
)
migration.migrate_files_in_batches()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment