| a18bfc9 by Aurélien Bompard at 2010-04-11 |
1 |
#!/usr/bin/env python |
| 153a071 by Aurélien Bompard at 2010-04-14 |
2 |
# vim: set fileencoding=utf-8 tabstop=4 shiftwidth=4 expandtab smartindent: |
| f5600b8 by Aurélien Bompard at 2010-05-12 |
3 |
u""" |
| 7581434 by Aurélien Bompard at 2011-04-03 |
4 |
|
|
5 |
Birthdays from VCard |
|
6 |
-------------------- |
|
7 |
|
| a18bfc9 by Aurélien Bompard at 2010-04-11 |
8 |
Creates an ICalendar file (ICS) from the birthdays in a VCard file (VCF). |
|
9 |
|
| 7581434 by Aurélien Bompard at 2011-04-03 |
10 |
.. :Authors: |
|
11 |
Aurélien Bompard <aurelien@bompard.org> <http://aurelien.bompard.org> |
| a18bfc9 by Aurélien Bompard at 2010-04-11 |
12 |
|
| 7581434 by Aurélien Bompard at 2011-04-03 |
13 |
.. :License: |
|
14 |
GNU GPL v3 or later |
| a18bfc9 by Aurélien Bompard at 2010-04-11 |
15 |
""" |
|
16 |
|
|
17 |
import sys |
|
18 |
import os, vobject, datetime, time |
|
19 |
import re |
|
20 |
|
|
21 |
|
|
22 |
#event_tpl = """\r |
|
23 |
#BEGIN:VEVENT\r |
|
24 |
#UID:%(uid)s\r |
|
25 |
#SUMMARY:Anniv. %(name)s\r |
|
26 |
#CATEGORIES:Anniversaire\r |
|
27 |
#DTSTART:%(date)sT060000Z\r |
|
28 |
#DTEND:%(date)sT160000Z\r |
|
29 |
#DESCRIPTION:%(desc)s\r |
|
30 |
#BEGIN:VALARM\r |
|
31 |
#ACTION:\r |
|
32 |
#TRIGGER;VALUE=DURATION:PT0S\r |
|
33 |
#END:VALARM\r |
|
34 |
#END:VEVENT\r |
|
35 |
#""" |
|
36 |
#event_tpl = """\r |
|
37 |
#BEGIN:VEVENT\r |
|
38 |
#UID:ANNIV-%(uid)s\r |
|
39 |
#SUMMARY:Anniv. %(name)s\r |
|
40 |
#CATEGORIES:Anniversaire\r |
|
41 |
#DESCRIPTION:%(desc)s\r |
|
42 |
#DTSTART:%(date)sT060000Z\r |
|
43 |
#DTEND:%(date)sT160000Z\r |
|
44 |
#END:VEVENT\r |
|
45 |
#""" |
|
46 |
event_dict = { |
| df70fe1 by Aurélien Bompard at 2012-01-07 |
47 |
"UID": "ANNIV-%(cleanname)s", |
| a18bfc9 by Aurélien Bompard at 2010-04-11 |
48 |
"SUMMARY": "Anniv. %(name)s", |
|
49 |
"CATEGORIES": "Anniversaire", |
|
50 |
"DESCRIPTION": "%(desc)s", |
|
51 |
# "DTSTART": "%(date)sT060000Z", |
|
52 |
# "DTEND": "%(date)sT160000Z", |
|
53 |
"DTSTART;VALUE=DATE": "%(date)s", |
|
54 |
"DTEND;VALUE=DATE": "%(day_after)s", |
|
55 |
"LAST-MODIFIED": "%(mtime)s", |
|
56 |
"DTSTAMP": "%(mtime)s", |
|
57 |
"CREATED": "%(mtime)s", |
|
58 |
} |
|
59 |
#RRULE:FREQ=YEARLY |
|
60 |
#CREATED:%(now)s\r |
|
61 |
#LAST-MODIFIED:%(now)s\r |
|
62 |
#DTSTAMP:%(now)s\r |
|
63 |
|
|
64 |
|
|
65 |
class UTC(datetime.tzinfo): |
|
66 |
def utcoffset(self, dt): |
|
67 |
return datetime.timedelta(0) |
|
68 |
def tzname(self, dt): |
|
69 |
return "UTC" |
|
70 |
def dst(self, dt): |
|
71 |
return datetime.timedelta(0) |
|
72 |
|
|
73 |
re_datetime = re.compile("(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(Z)?") |
|
74 |
re_date = re.compile("(\d\d\d\d)-(\d\d)-(\d\d)") |
|
75 |
def stringToDateTime(s, tzinfo=None): |
| 7581434 by Aurélien Bompard at 2011-04-03 |
76 |
#Returns datetime.datetime object. |
| a18bfc9 by Aurélien Bompard at 2010-04-11 |
77 |
if re_datetime.match(s): |
|
78 |
mo = re_datetime.match(s) |
|
79 |
year = int(mo.group(1)) |
|
80 |
month = int(mo.group(2)) |
|
81 |
day = int(mo.group(3)) |
|
82 |
hour = int(mo.group(4)) |
|
83 |
minute = int(mo.group(5)) |
|
84 |
second = int(mo.group(6)) |
|
85 |
if mo.group(7): |
|
86 |
tzinfo = UTC() |
|
87 |
return datetime.datetime(year, month, day, hour, minute, second, 0, tzinfo) |
|
88 |
elif re_date.match(s): |
|
89 |
mo = re_date.match(s) |
|
90 |
year = int(mo.group(1)) |
|
91 |
month = int(mo.group(2)) |
|
92 |
day = int(mo.group(3)) |
|
93 |
return datetime.datetime(year, month, day, 0, 0, 0, 0, tzinfo) |
|
94 |
else: |
|
95 |
print s |
|
96 |
raise ValueError("Can't parse the date: %s" % s) |
|
97 |
|
|
98 |
|
|
99 |
|
|
100 |
def makeICS(vcf_in): |
|
101 |
# Create the calendar |
|
102 |
cal = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n\r\n" |
|
103 |
events = getICS(vcf_in) |
|
104 |
cal += "\r\n\r\n".join(events) |
|
105 |
cal += "\r\n\r\nEND:VCALENDAR\r\n" |
|
106 |
return cal |
|
107 |
|
|
108 |
def getEvents(vcf_in): |
|
109 |
# Filter out unparsable lines |
|
110 |
addresses = "" |
|
111 |
for line in vcf_in: |
|
112 |
if line.startswith("X-messaging"): |
|
113 |
continue |
|
114 |
addresses += line |
|
115 |
|
|
116 |
# Create the calendar |
|
117 |
today = datetime.date.today() |
|
118 |
#now = datetime.datetime.utcfromtimestamp(time.mktime(time.localtime())) # timezone-aware |
|
119 |
now = datetime.datetime.now() - datetime.timedelta(hours=2) |
|
120 |
items = [] |
|
121 |
for address in vobject.readComponents(addresses): |
|
122 |
name = address.fn.value |
| df70fe1 by Aurélien Bompard at 2012-01-07 |
123 |
#uid = address.uid.value |
| a18bfc9 by Aurélien Bompard at 2010-04-11 |
124 |
bday = None |
|
125 |
for child in address.getChildren(): |
|
126 |
if child.name == "BDAY": |
|
127 |
bday = stringToDateTime(child.value).date() |
|
128 |
elif child.name == "NICKNAME": |
|
129 |
name = child.value |
|
130 |
if bday is None: |
|
131 |
continue |
|
132 |
bday_now = bday.replace(year=today.year) |
|
133 |
day_after = bday_now + datetime.timedelta(days=1) |
|
134 |
desc = "%s - %s years old" % (bday.year, today.year-bday.year) |
| 5808a2d by Aurélien Bompard at 2012-01-04 |
135 |
if hasattr(address, "rev"): |
|
136 |
mtime = address.rev.value.replace("-","").replace(":","") |
|
137 |
else: |
|
138 |
mtime = now.strftime("%Y%m%dT%H%M%SZ") |
| a18bfc9 by Aurélien Bompard at 2010-04-11 |
139 |
event = {} |
|
140 |
for t_key, t_value in event_dict.iteritems(): |
| df70fe1 by Aurélien Bompard at 2012-01-07 |
141 |
e_value = t_value % {#"uid": uid, |
| a18bfc9 by Aurélien Bompard at 2010-04-11 |
142 |
"name": name, |
| df70fe1 by Aurélien Bompard at 2012-01-07 |
143 |
"cleanname": re.sub("[^\w]", "", name), |
| a18bfc9 by Aurélien Bompard at 2010-04-11 |
144 |
"date": bday_now.strftime("%Y%m%d"), |
|
145 |
"day_after": day_after.strftime("%Y%m%d"), |
|
146 |
"desc": desc, |
|
147 |
"now": now.strftime("%Y%m%dT%H%M%SZ"), |
| 5808a2d by Aurélien Bompard at 2012-01-04 |
148 |
"mtime": mtime, |
| a18bfc9 by Aurélien Bompard at 2010-04-11 |
149 |
} |
|
150 |
event[t_key] = e_value.encode("utf-8") |
|
151 |
items.append(event) |
|
152 |
return items |
|
153 |
|
|
154 |
def getICS(vcf_in): |
|
155 |
events = getEvents(vcf_in) |
|
156 |
items = [] |
|
157 |
for event in events: |
|
158 |
item = ["BEGIN:VEVENT"] |
|
159 |
item.extend( [ "%s:%s" % (key, value) for key, value in event.iteritems() ] ) |
|
160 |
item.append("END:VEVENT") |
|
161 |
items.append("\r\n".join(item)) |
|
162 |
return items |
|
163 |
|
|
164 |
|
|
165 |
if __name__ == "__main__": |
|
166 |
#addresses_path = "%s/.kde/share/apps/kabc/std.vcf"%os.getenv("HOME") |
|
167 |
addresses_path = sys.argv[1] |
|
168 |
#anniv_path = "/tmp/annivs.ics" |
|
169 |
anniv_path = sys.argv[2] |
|
170 |
addresses_file = open(addresses_path) |
|
171 |
cal = makeICS(addresses_file) |
|
172 |
addresses_file.close() |
|
173 |
annivs = open(anniv_path, "w") |
|
174 |
annivs.write(cal) |
|
175 |
annivs.close() |
|
176 |
|