mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
185 lines
8.2 KiB
Python
185 lines
8.2 KiB
Python
#!/usr/bin/env python
|
|
# encoding: utf-8
|
|
|
|
from __future__ import absolute_import, unicode_literals
|
|
from . import Entry
|
|
from . import Journal
|
|
from . import __title__ # 'jrnl'
|
|
from . import __version__
|
|
import os
|
|
import re
|
|
from datetime import datetime
|
|
import time
|
|
import fnmatch
|
|
import plistlib
|
|
import pytz
|
|
import uuid
|
|
import tzlocal
|
|
from xml.parsers.expat import ExpatError
|
|
import socket
|
|
import platform
|
|
|
|
|
|
class DayOne(Journal.Journal):
|
|
"""A special Journal handling DayOne files"""
|
|
|
|
# InvalidFileException was added to plistlib in Python3.4
|
|
PLIST_EXCEPTIONS = (ExpatError, plistlib.InvalidFileException) if hasattr(plistlib, "InvalidFileException") else ExpatError
|
|
|
|
def __init__(self, **kwargs):
|
|
self.entries = []
|
|
self._deleted_entries = []
|
|
super(DayOne, self).__init__(**kwargs)
|
|
|
|
def open(self):
|
|
filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))]
|
|
filenames = []
|
|
for root, dirnames, f in os.walk(self.config['journal']):
|
|
for filename in fnmatch.filter(f, '*.doentry'):
|
|
filenames.append(os.path.join(root, filename))
|
|
self.entries = []
|
|
for filename in filenames:
|
|
with open(filename, 'rb') as plist_entry:
|
|
try:
|
|
dict_entry = plistlib.readPlist(plist_entry)
|
|
except self.PLIST_EXCEPTIONS:
|
|
pass
|
|
else:
|
|
try:
|
|
timezone = pytz.timezone(dict_entry['Time Zone'])
|
|
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
|
|
timezone = tzlocal.get_localzone()
|
|
date = dict_entry['Creation Date']
|
|
date = date + timezone.utcoffset(date, is_dst=False)
|
|
raw = dict_entry['Entry Text']
|
|
sep = re.search("\n|[\?!.]+ +\n?", raw)
|
|
title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "")
|
|
entry = Entry.Entry(self, date, title, body, starred=dict_entry["Starred"])
|
|
entry.uuid = dict_entry["UUID"]
|
|
entry.tags = [self.config['tagsymbols'][0] + tag for tag in dict_entry.get("Tags", [])]
|
|
"""Extended DayOne attributes"""
|
|
try:
|
|
entry.creator_device_agent = dict_entry['Creator']['Device Agent']
|
|
except:
|
|
pass
|
|
try:
|
|
entry.creator_generation_date = dict_entry['Creator']['Generation Date']
|
|
except:
|
|
pass
|
|
try:
|
|
entry.creator_host_name = dict_entry['Creator']['Host Name']
|
|
except:
|
|
pass
|
|
try:
|
|
entry.creator_os_agent = dict_entry['Creator']['OS Agent']
|
|
except:
|
|
pass
|
|
try:
|
|
entry.creator_software_agent = dict_entry['Creator']['Software Agent']
|
|
except:
|
|
pass
|
|
self.entries.append(entry)
|
|
self.sort()
|
|
return self
|
|
|
|
def write(self):
|
|
"""Writes only the entries that have been modified into plist files."""
|
|
for entry in self.entries:
|
|
if entry.modified:
|
|
utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple()))
|
|
filename = os.path.join(self.config['journal'], "entries", entry.uuid.upper() + ".doentry")
|
|
|
|
if not hasattr(entry, "uuid"):
|
|
entry.uuid = uuid.uuid1().hex
|
|
if not hasattr(entry, "creator_device_agent"):
|
|
entry.creator_device_agent = '' # iPhone/iPhone5,3
|
|
if not hasattr(entry, "creator_generation_date"):
|
|
entry.creator_generation_date = utc_time
|
|
if not hasattr(entry, "creator_host_name"):
|
|
entry.creator_host_name = socket.gethostname()
|
|
if not hasattr(entry, "creator_os_agent"):
|
|
entry.creator_os_agent = '{} {}'.format(platform.system(), platform.release())
|
|
if not hasattr(entry, "creator_software_agent"):
|
|
entry.creator_software_agent = '{} {}'.format(__title__, __version__)
|
|
|
|
entry_plist = {
|
|
'Creation Date': utc_time,
|
|
'Starred': entry.starred if hasattr(entry, 'starred') else False,
|
|
'Entry Text': entry.title + "\n" + entry.body,
|
|
'Time Zone': str(tzlocal.get_localzone()),
|
|
'UUID': entry.uuid.upper(),
|
|
'Tags': [tag.strip(self.config['tagsymbols']).replace("_", " ") for tag in entry.tags],
|
|
'Creator': {'Device Agent': entry.creator_device_agent,
|
|
'Generation Date': entry.creator_generation_date,
|
|
'Host Name': entry.creator_host_name,
|
|
'OS Agent': entry.creator_os_agent,
|
|
'Sofware Agent': entry.creator_software_agent}
|
|
}
|
|
plistlib.writePlist(entry_plist, filename)
|
|
for entry in self._deleted_entries:
|
|
filename = os.path.join(self.config['journal'], "entries", entry.uuid + ".doentry")
|
|
os.remove(filename)
|
|
|
|
def editable_str(self):
|
|
"""Turns the journal into a string of entries that can be edited
|
|
manually and later be parsed with eslf.parse_editable_str."""
|
|
return "\n".join(["# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries])
|
|
|
|
def parse_editable_str(self, edited):
|
|
"""Parses the output of self.editable_str and updates it's entries."""
|
|
# Method: create a new list of entries from the edited text, then match
|
|
# UUIDs of the new entries against self.entries, updating the entries
|
|
# if the edited entries differ, and deleting entries from self.entries
|
|
# if they don't show up in the edited entries anymore.
|
|
date_length = len(datetime.today().strftime(self.config['timeformat']))
|
|
|
|
# Initialise our current entry
|
|
entries = []
|
|
current_entry = None
|
|
|
|
for line in edited.splitlines():
|
|
# try to parse line as UUID => new entry begins
|
|
line = line.rstrip()
|
|
m = re.match("# *([a-f0-9]+) *$", line.lower())
|
|
if m:
|
|
if current_entry:
|
|
entries.append(current_entry)
|
|
current_entry = Entry.Entry(self)
|
|
current_entry.modified = False
|
|
current_entry.uuid = m.group(1).lower()
|
|
else:
|
|
try:
|
|
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
|
|
if line.endswith("*"):
|
|
current_entry.starred = True
|
|
line = line[:-1]
|
|
current_entry.title = line[date_length + 1:]
|
|
current_entry.date = new_date
|
|
except ValueError:
|
|
if current_entry:
|
|
current_entry.body += line + "\n"
|
|
|
|
# Append last entry
|
|
if current_entry:
|
|
entries.append(current_entry)
|
|
|
|
# Now, update our current entries if they changed
|
|
for entry in entries:
|
|
entry.parse_tags()
|
|
matched_entries = [e for e in self.entries if e.uuid.lower() == entry.uuid]
|
|
if matched_entries:
|
|
# This entry is an existing entry
|
|
match = matched_entries[0]
|
|
if match != entry:
|
|
self.entries.remove(match)
|
|
entry.modified = True
|
|
self.entries.append(entry)
|
|
else:
|
|
# This entry seems to be new... save it.
|
|
entry.modified = True
|
|
self.entries.append(entry)
|
|
# Remove deleted entries
|
|
edited_uuids = [e.uuid for e in entries]
|
|
self._deleted_entries = [e for e in self.entries if e.uuid not in edited_uuids]
|
|
self.entries[:] = [e for e in self.entries if e.uuid in edited_uuids]
|
|
return entries
|