No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ical_fusion.py 6.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. #! /usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # vim:fenc=utf-8
  4. #
  5. # Copyright © 2017 Bogdan Cordier
  6. #
  7. # Distributed under terms of the MIT license.
  8. import datetime
  9. from icalendar import Calendar
  10. from pytz import timezone
  11. from dateutil.parser import parse
  12. from tkinter import Tk, filedialog, Listbox, Button, Entry, StringVar, \
  13. LabelFrame, BooleanVar, Frame, ttk, END, Checkbutton, messagebox
  14. local_timezone = timezone('Europe/Paris')
  15. ical_fields = ('SUMMARY', 'UID', 'LOCATION', 'CATEGORIES', 'DTSTART', 'DTEND')
  16. class GUI:
  17. def __init__(self):
  18. self.root = Tk()
  19. self.root.title('ICal Fusion')
  20. self.root.iconbitmap('@icon.xbm')
  21. self.create_filter_frame()
  22. self.files = []
  23. self.create_files_list_frame()
  24. self.create_duplicates_frame()
  25. self.btn_frame = Frame(self.root)
  26. self.create_button_frame()
  27. self.calendar = Calendar()
  28. self.root.mainloop()
  29. def create_filter_frame(self):
  30. filter_frame = LabelFrame(self.root, text='Filter')
  31. self.filter_type = ttk.Combobox(filter_frame, values=ical_fields)
  32. self.filter_type.current(0)
  33. self.filter_type.bind("<<ComboboxSelected>>", self.update_filter_cond)
  34. self.filter_type.state(('!disabled', 'readonly'))
  35. self.filter_type.grid(row=0)
  36. self.filter_cond = ttk.Combobox(filter_frame,
  37. values=('CONTAINS',
  38. 'EQUAL TO'))
  39. self.filter_cond.current(0)
  40. self.filter_cond.state(('!disabled', 'readonly'))
  41. self.filter_cond.grid(row=0, column=1)
  42. self.filter_value = StringVar()
  43. self.filter_entry = Entry(filter_frame,
  44. textvariable=self.filter_value,
  45. width=25,
  46. bg='white')
  47. self.filter_entry.grid(row=0, column=2)
  48. filter_frame.pack(fill='x', side='top')
  49. def update_filter_cond(self, *args):
  50. """ Update filter conditions on filter type selection.
  51. """
  52. if self.filter_type.get() in ('DTSTART', 'DTEND'):
  53. self.filter_cond['values'] = ('BEFORE', 'AFTER')
  54. else:
  55. self.filter_cond['values'] = ('CONTAINS', 'EQUAL TO')
  56. self.filter_cond.current(0)
  57. def create_files_list_frame(self):
  58. files_list_frame = LabelFrame(self.root, text='Files to merge')
  59. self.FilesList = Listbox(files_list_frame)
  60. self.FilesList.pack(side='left', fill='both', expand=1)
  61. files_list_frame.pack(fill='x')
  62. def create_duplicates_frame(self):
  63. frame = Frame(self.root)
  64. self.duplicates_check = BooleanVar()
  65. self.duplicates_filter = ttk.Combobox(frame, value=ical_fields)
  66. self.duplicates_filter.current(0)
  67. self.duplicates_filter.state(('!disabled', 'readonly'))
  68. self.duplicates_filter.pack(side='right')
  69. self.duplicates_cbox = Checkbutton(frame,
  70. variable=self.duplicates_check,
  71. text='Remove duplicates by')
  72. self.duplicates_cbox.pack(side='right')
  73. frame.pack(fill='x')
  74. def create_button_frame(self):
  75. Button(self.btn_frame, text='Add...',
  76. command=self.add_files).grid(row=0, column=0)
  77. Button(self.btn_frame, text='Merge',
  78. command=self.join_files).grid(row=0, column=1)
  79. self.btn_frame.pack(side='bottom')
  80. def add_files(self):
  81. files = filedialog.askopenfilenames(title="Load ICal files",
  82. filetypes=[('ICal files', '.ics'),
  83. ('all files', '.*')])
  84. for file in files:
  85. self.FilesList.insert(END, file)
  86. def filter(self, event):
  87. """Check if condition is met for a given event field"""
  88. value = self.filter_value.get()
  89. field = self.filter_type.get()
  90. condition = self.filter_cond.get()
  91. if field in ('DTSTART', 'DTEND'):
  92. try:
  93. value = parse(value)
  94. except ValueError:
  95. messagebox.showerror('Wrong value',
  96. 'Value is not recognized as a date')
  97. value = self.normalize_date(value)
  98. if condition == 'CONTAINS':
  99. if value in event.get(field):
  100. return True
  101. if condition == 'EQUAL TO':
  102. if value == event.get(field):
  103. return True
  104. if condition == 'BEFORE':
  105. if value > self.normalize_date(event.get(field).dt):
  106. return True
  107. if condition == 'AFTER':
  108. if value < self.normalize_date(event.get(field).dt):
  109. return True
  110. return False
  111. def normalize_date(self, date):
  112. """Ensure that date is a datetime object and is offset aware."""
  113. if not isinstance(date, datetime.datetime):
  114. date = datetime.datetime(date.year, date.month, date.day)
  115. if date.tzinfo is None or date.tzinfo.utcoffset(date) is None:
  116. date = local_timezone.localize(date)
  117. return date
  118. def join_files(self):
  119. if self.FilesList.get(0, END):
  120. ical = filedialog.asksaveasfilename(title='Save as...')
  121. self.checked_values = set()
  122. for file in self.FilesList.get(0, END):
  123. ics = open(file, 'r')
  124. cal = Calendar.from_ical(ics.read())
  125. ics.close()
  126. events = (co for co in cal.walk() if co.name == 'VEVENT')
  127. for event in events:
  128. if self.duplicates_check.get():
  129. field = self.duplicates_filter.get()
  130. value = event.get(field)
  131. if value in self.checked_values:
  132. break
  133. else:
  134. self.checked_values.add(value)
  135. if self.filter_value:
  136. if self.filter(event):
  137. self.calendar.add_component(event)
  138. else:
  139. if self.duplicates_cbox.getboolean():
  140. pass
  141. else:
  142. self.calendar.add_component(event)
  143. with open(ical, 'wb') as f:
  144. f.write(self.calendar.to_ical())
  145. messagebox.showinfo('Success', 'Files were successfully joined !')
  146. else:
  147. messagebox.showerror('No files', 'Please add files to merge...')
  148. if __name__ == '__main__':
  149. GUI()