Getting Journelly to Sync in France
1. Say no to iCloud
As part of the whole "Getting off of the American Tech Stack" thing, and eschewing iCloud as much as possible, I've run into a particular problem here in France.
Syncthing.
While Syncthing works for MacOS just fine in France, the App(s) which use Syncthing are unavailable from the Apple Store for iOS. Apparently it has something to do with the security forms having never been filled out. A few people have looked into it over the years but, let's be real, it's been six years. I doubt it'll happen anytime soon.
What do I want to do with Syncthing? I want to sync my Journelly files over to my Mac. For those who don't know, Journelly is a fantastic journaling app for the iPhone (and iPad). It works wondrously well. But it's made to primarily sync over iCloud.
The problem is that Journelly doesn't have the capability to store the files directly in the Nextcloud app. I've spoken to Xenodium about it, but he can't get it to currently work with the way iOS reads or writes data to files inside of the Nextcloud app. However, there is a solution.
Granted, it's not cheap. It's a paid app and you have the option of subscribing (which I hate), or buying the Pro version outright. I chose the latter option. But it was worth it.
It's called Filebrowser Pro and it lets you mount SMB shares and sync files directly to them (as well as perform a huge number of other tasks which, let's be frank, should be easily doable with iOS from the outset).
Now, I know some youngster is going to inevitably point out to me that this can be done with Apple Shortcuts, but I've tried and it just didn't work. If you're willing to actually create a shortcut and figure it out how to sync a folder directly to an SMB share on the iPhone, please let me know. Until then, I'll be using Filebrowser Pro.
With this, I can run a task which automatically sends the whole Journelly folder directly to my server, and Syncthing can then pull it down to my Mac. This way, I have Journelly available on the Mac with entries to import directly into my Denote Journal.
2. Importing to my Journal entries
I have an elisp function which imports the entries that have not yet been imported into the Denote Journal files which correspond to the correct dates. It imports them automatically on the bottom of each journal file and it works spectacularly well.
With this system, every few weeks or so, I can run the M-x my/import-journelly-entries command and presto, literally in a second or two, it's all done.
(defun my/journelly-initialize-tracking ()
"Initialize imported.org with ALL current Journelly headers.
Run this ONCE after the first import to prevent duplicates."
(interactive)
(let ((journelly-file "~/YOUR/PATH/TO/Journelly.org")
(imported-file "~/YOUR/PATH/TO/Journelly/imported.org")
(headers '()))
;; Extract all headers from Journelly.org
(with-temp-buffer
(insert-file-contents journelly-file)
(goto-char (point-min))
(while (re-search-forward "^\\* \\(\\[.*?\\] @ .*\\)$" nil t)
(push (match-string 1) headers)))
;; Write to imported.org
(with-temp-buffer
(dolist (header (reverse headers))
(insert "* " header "\n"))
(write-region (point-min) (point-max) imported-file))
(message "Initialized tracking file with %d entries" (length headers))))
;; Import Journelly entries to their respective journal date file
(defun my/journelly-get-imported-entries ()
"Read imported.org and return hash table of imported entry headers."
(let ((imported-file "~/YOUR/PATH/TO/Journelly/imported.org")
(imported-table (make-hash-table :test 'equal)))
(with-temp-buffer
(insert-file-contents imported-file)
(goto-char (point-min))
(while (re-search-forward "^\\* \\(\\[.*?\\] @ .*\\)$" nil t)
(puthash (match-string 1) t imported-table)))
imported-table))
(defun my/journelly-mark-as-imported (entry-header)
"Append entry header to imported.org tracking file."
(let ((imported-file "~/YOUR/PATH/TO/Journelly/imported.org"))
(with-temp-buffer
(insert-file-contents imported-file)
(goto-char (point-max))
(unless (bolp) (insert "\n"))
(insert "* " entry-header "\n")
(write-region (point-min) (point-max) imported-file))))
(defun my/import-journelly-entries ()
"Import new Journelly entries into their respective daily journal files.
Skips entries that have already been imported (tracked in imported.org)."
(interactive)
(let* ((journelly-file "~/YOUR/PATH/TO/Journelly.org")
(journal-base-dir "~/YOUR/PATH/TO/journal/")
(image-base-dir "~/YOUR/PATH/TO/org/images/journal/")
(entries-by-date (make-hash-table :test 'equal))
(imported-entries (my/journelly-get-imported-entries))
entry-count
new-entry-count)
;; Parse Journelly.org and group entries by date
(with-temp-buffer
(insert-file-contents journelly-file)
(goto-char (point-min))
(while (re-search-forward "^\\* \\(\\[\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) ... \\([0-9]\\{2\\}:[0-9]\\{2\\}\\)\\] @ \\(.*\\)\\)$" nil t)
(let* ((full-header (match-string 1))
(date (match-string 2))
(time (match-string 3))
(location (match-string 4))
(entry-start (line-beginning-position))
(entry-end (save-excursion
(if (re-search-forward "^\\* \\[" nil t)
(line-beginning-position)
(point-max))))
(entry-content (buffer-substring entry-start entry-end)))
;; Only process if not already imported
(unless (gethash full-header imported-entries)
(push (list :header full-header
:time time
:location location
:content entry-content)
(gethash date entries-by-date))))))
;; Process each date's entries
(setq entry-count 0)
(setq new-entry-count 0)
(maphash
(lambda (date entries)
(let* ((year (substring date 0 4))
(month (substring date 5 7))
(journal-dir (expand-file-name year journal-base-dir))
(image-dest-dir (expand-file-name (concat year "/" month) image-base-dir))
(journal-files (directory-files journal-dir t
(concat "^" (replace-regexp-in-string "-" "" date) "T.*\\.org$"))))
(when journal-files
(let ((journal-file (car journal-files)))
;; Create image directory if needed
(unless (file-directory-p image-dest-dir)
(make-directory image-dest-dir t))
;; Append to journal file
(with-current-buffer (find-file-noselect journal-file)
(goto-char (point-max))
;; Find or create "Journelly Entries" heading
(unless (save-excursion
(goto-char (point-min))
(re-search-forward "^\\* Journelly Entries$" nil t))
(insert "\n* Journelly Entries\n"))
(goto-char (point-max))
;; Insert each entry for this date
(dolist (entry (reverse entries))
(let* ((entry-header (plist-get entry :header))
(content (plist-get entry :content))
(processed-content
(with-temp-buffer
(insert content)
(goto-char (point-min))
;; Change heading level from * to **
(when (looking-at "^\\* ")
(replace-match "** "))
;; Strip timestamp brackets from heading
(when (re-search-forward "^\\*\\* \\[\\([^]]+\\)\\]" nil t)
(replace-match "** \\1"))
;; Process image links
(goto-char (point-min))
(while (re-search-forward "\\[\\[file:Journelly\\.org\\.assets/images/\\([^]]+\\)\\]\\]" nil t)
(let* ((image-name (match-string 1))
(source-image (expand-file-name
(concat "Journelly.org.assets/images/" image-name)
"~/YOUR/PATH/TO/Journelly/"))
(dest-image (expand-file-name image-name image-dest-dir))
(new-link (concat "[[file:" dest-image "]]")))
(when (file-exists-p source-image)
(copy-file source-image dest-image t))
(replace-match new-link)))
(buffer-string))))
(insert processed-content "\n")
(setq entry-count (1+ entry-count))
(setq new-entry-count (1+ new-entry-count))
;; Mark this entry as imported
(my/journelly-mark-as-imported entry-header)))
(save-buffer)
(kill-buffer))))))
entries-by-date)
(if (> new-entry-count 0)
(message "Imported %d new Journelly entries into journal files!" new-entry-count)
(message "No new Journelly entries to import - all up to date!"))))
Maybe this will help some people. I'm sure you can improve upon this as I used Claude to create it. However, before anyone chides me - I'm not a programmer, I don't have time to currently learn Elisp, and I did debug it with Claude and have tested this over the last few months. It does work and it works flawlessly. If you want to improve upon it, feel free.
– N.