forked from Weathermann/vcf2ics
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvcf_to_ics.py
More file actions
184 lines (156 loc) · 6.67 KB
/
Copy pathvcf_to_ics.py
File metadata and controls
184 lines (156 loc) · 6.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
#!/usr/bin/env python3
from dataclasses import dataclass
from pathlib import Path
from string import ascii_letters
from string import digits
import argparse
import random
import time
import sys
from datetime import datetime
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# CONFIGURATION
PROGRAM_NAME = "VCF to ICS"
PROGRAM_VERSION = "1.0"
@dataclass
class Vcard:
"""holds elements of a vcard as information for ics file"""
name: str
dt_birth_start: datetime | None = None
dt_birth_end: datetime | None = None
birth_start_str = str | None
uid: str | None = None
age: int | None = None
def set_birthday(self, bday):
# BDAY format:
# BDAY:2000-07-01T00:00:00
# or 2000-07-01
bday = bday.split("T")[0] # '2000-07-01T000000'
self.birth_start_str = bday.replace("-", "")
year, month, day = bday.split("-")
self.dt_birth_start = datetime(int(year), int(month), int(day))
self.dt_birth_end = datetime(int(time.strftime("%Y")), int(month), int(day)) # current year!
dt_diff = self.dt_birth_end - self.dt_birth_start
days = dt_diff.days + 1 # ein Tag zuwenig
self.age = int(days / 365)
def __str__(self):
return f"{self.name}: {self.dt_birth_start} {self.dt_birth_end} {self.age}"
@property
def entry(self):
list_block = [f"BEGIN:VEVENT", f"DTSTART:{self.birth_start_str}", f"SUMMARY:{self.name} ({self.age})",
"RRULE:FREQ=YEARLY", "DURATION:P1D", f"UID:{self.uid}", "END:VEVENT"]
entry = "\n".join(list_block)
return entry
def create_vcard(card: str) -> Vcard | None:
working_dict = {}
for line in card.split("\n"):
elems = line.split(":")
key = elems[0]
if key in ("BDAY", "N", "FN", "UID"):
working_dict[key] = "".join(elems[1:])
try:
bday = working_dict["BDAY"]
except KeyError:
return # not usable
try: # prefer "N" over "FN"
name = working_dict["N"] # separate parts
except KeyError:
try:
name = working_dict["FN"] # one element
except KeyError: # no information
return
else:
elems = name.split(";")
if elems[3]: # Dr.
new_name = f"{elems[3]} {elems[1]} {elems[0]}"
else:
new_name = f"{elems[1]} {elems[0]}"
name = new_name
vcard = Vcard(name)
vcard.set_birthday(bday)
try:
vcard.uid = working_dict["UID"]
except KeyError: # not a problem, can be generated later
birth_start_str = vcard.dt_birth_start.strftime("%Y%m%d")
vcard.uid = generate_uid(birth_start_str)
return vcard
def generate_uid(birth_start_str: str):
"""Unique ID
The UID purpose is to define a unique identifier across all the calendar components.
It is basically a random generated sequence that will assure the uniqueness of every
calendar component. This property is mandatory in defining calendar components.
Everyone must assure that this unique identifier is added at the Creation Time
of his calendar component, to be sure of the uniqueness of all the components.
Consequences of not doing it right
Because the UID globally identifies a calendar component, not having a good way of
generating this property can cause a lot of problems. Using a simple UID generation
algorithm might lead to overridden events with the same UID generated by others
using the same generation mechanism. Every creation of a calendar component with
a UID that is already contained by other component will lead in the end to the update
of the older component. Once generated, the calendar component should keep the same
UID all the time, so that no matter which part of the event is changed, the UID-Reference
is the same and the calendar can be updated until that event will be deleted. """
uid = ''.join([random.choice(list(ascii_letters + digits)) for _ in range(16)]) + "@VCFtoICS.com"
return f"{birth_start_str}-{uid}"
def process() -> list[str] | None:
# Read VCF file content
file_content = vcf_file.read_text(encoding="utf-8")
# Separate VCards
all_cards = file_content.split("END:VCARD")
print(f"{len(all_cards)} VCards found")
if not all_cards:
return None
formatted_entries = []
for card in all_cards:
vcard = create_vcard(card)
if not vcard:
continue
print(f"- {vcard.birth_start_str}\t{vcard.age}\t{vcard.name}")
formatted_entries.append(vcard.entry)
# for
print(f"\n{len(formatted_entries)} usable entries")
return formatted_entries
def write_ics_file(ics_file: Path, calendar_name: str, list_formatted_entries: list[str]):
with ics_file.open("w") as f:
# Write ICS calendar header
f.write(f"BEGIN:VCALENDAR\nPRODID:-//{PROGRAM_NAME}//NONSGML {calendar_name} V1.0//EN\n"
f"X-WR-CALNAME:{calendar_name}\nVERSION:2.0\n")
for entry in list_formatted_entries:
f.write(f"{entry}\n")
# Write ICS calendar footer
f.write("END:VCALENDAR\n")
print("\n->", ics_file)
if __name__ == "__main__":
# Command-line interface
argParser = argparse.ArgumentParser(description=f"{PROGRAM_NAME} {PROGRAM_VERSION}")
argParser.add_argument('-i', '--input', metavar='PATH', help='Input .vcf file path', required=True)
argParser.add_argument('-o', '--output', metavar='PATH', help='Output .ics file path', required=True)
argParser.add_argument('-n', '--name', metavar='NAME', help='Desired calendar name', required=True)
args = vars(argParser.parse_args())
vcf_file = Path(args["input"])
ics_file = Path(args["output"])
calendar_name = args["name"]
if not vcf_file.exists():
print("Invalid input file path:", vcf_file)
sys.exit(1)
formatted_entries = process()
if not formatted_entries:
sys.exit(1)
# Write ICS event:
write_ics_file(ics_file, calendar_name, formatted_entries)