diff --git a/README.md b/README.md index 4e78d38..f53f20b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ical2csv +# ical2csv w/sorted events by start date ![alt text](images/ics.png) → ![alt text](images/python.png) → ![alt text](images/csv.png) @@ -14,11 +14,33 @@ Download the `ical2csv.py` file. * **Installation**: `pip install icalendar` * ***Python* 3** -## Usage +***Note:*** pip may be called pip3 on some systems with both python2 and python3 as options. + +## Usage of ical2csv Call the script and pass in the location of the ics file. -Ex: `python ical2csv event.ics` +Ex: `python ical2csv.py event.ics` + +# ical2txt w/sorted events by start date + +Like ical2csv.py, it parses an ics file and writes the output to a text-file. This is more of an agenda style. + +## Installation of ical2txt +Download the script or clone the project and get it from there. + +### Dependencies for ical2txt +* ***setuptools*** (just in case : pip3 install setuptools) +* ***BeautifulSoup4*** (pip3 install beautifulsoup4) +* ***icalendar*** (pip3 install icalendar) + * [**Homepage**](http://icalendar.readthedocs.org/) + * [**Code**](http://github.com/collective/icalendar) + +## Usage of ical2txt + +Call the script and pass in the location of the ics file. + +Ex: `python ical2txt.py event.ics` / `python3 ical2txt.py event.ics` ## Contributing @@ -30,10 +52,16 @@ Ex: `python ical2csv event.ics` ## Credits -Lead Developer - [Erik Cox](https://github.com/erikcox/) +Lead Developer - ical2csv - [Erik Cox](https://github.com/erikcox/) + +Developer - ical2txt - [Martin Møller](https://github.com/martinm76) Python 3 compatibility and improvements - [bozoslivehere](https://github.com/bozoslivehere/) +Logic and adjustments to sort events chronologically (Google Calendar doesn't do this in its export) - [Martin Møller](https://github.com/martinm76) + +Removal of HTML code from events (currently only ical2txt) - [Martin Møller](https://github.com/martinm76) + ## License The MIT License (MIT) diff --git a/ical2csv.py b/ical2csv.py old mode 100644 new mode 100755 index 7dd4389..464a288 --- a/ical2csv.py +++ b/ical2csv.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import sys import os.path from icalendar import Calendar @@ -33,6 +35,7 @@ def open_cal(): for component in gcal.walk(): event = CalendarEvent("event") + if component.get('TRANSP') == 'TRANSPARENT': continue #skip event that have not been accepted if component.get('SUMMARY') == None: continue #skip blank items event.summary = component.get('SUMMARY') event.uid = component.get('UID') @@ -64,8 +67,8 @@ def csv_write(icsfile): with open(csvfile, 'w') as myfile: wr = csv.writer(myfile, quoting=csv.QUOTE_ALL) wr.writerow(headers) - for event in events: - values = (event.summary.encode('utf-8'), event.uid, event.description.encode('utf-8'), event.location, event.start, event.end, event.url) + for event in sortedevents: + values = (event.summary.encode('utf8').decode(), event.uid, event.description.encode('uft8').decode(), event.location, event.start, event.end, event.url) wr.writerow(values) print("Wrote to ", csvfile, "\n") except IOError: @@ -84,5 +87,6 @@ def debug_event(class_name): print(class_name.url, "\n") open_cal() +sortedevents=sorted(events, key=lambda obj: obj.start) # Needed to sort events. They are not fully chronological in a Google Calendard export ... csv_write(filename) #debug_event(event) diff --git a/ical2txt.py b/ical2txt.py new file mode 100755 index 0000000..9cba6a3 --- /dev/null +++ b/ical2txt.py @@ -0,0 +1,157 @@ +#!/usr/bin/python3 + +import sys +import os.path +from icalendar import Calendar +import csv +from bs4 import BeautifulSoup +import warnings +from dateutil.parser import parse + +warnings.filterwarnings("ignore", category=UserWarning, module='bs4') # We don't want warnings about URL's. We just what the URL printed, if there. + +filename = sys.argv[1] +# TODO: use regex to get file extension (chars after last period), in case it's not exactly 3 chars. +file_extension = str(sys.argv[1])[-3:] +headers = ('Summary', 'UID', 'Description', 'Location', 'Start Time', 'End Time', 'URL') + +class CalendarEvent: + """Calendar event class""" + summary = '' + uid = '' + description = '' + location = '' + start = '' + end = '' + url = '' + + def __init__(self, name): + self.name = name + +events = [] + +def removehtml(html): + # Almost word for word copy from here: https://stackoverflow.com/questions/328356/extracting-text-from-html-file-using-python + + soup = BeautifulSoup(html, features="html.parser") + # kill all script and style elements + for script in soup(["script", "style"]): + script.extract() # remove it + + text = soup.get_text() # Get plain text + + # break into lines and remove leading and trailing space on each + lines = (line.strip() for line in text.splitlines()) + # break multi-headlines into a line each + chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) + # drop blank lines + text = '\n'.join(chunk for chunk in chunks if chunk) + + return text + + +def open_cal(): + if os.path.isfile(filename): + if file_extension == 'ics': + print("Extracting events from file:", filename, "\n") + f = open(sys.argv[1], 'rb') + gcal = Calendar.from_ical(f.read()) + + for component in gcal.walk(): + event = CalendarEvent("event") + if component.get('TRANSP') == 'TRANSPARENT': continue #skip event that have not been accepted + if component.get('SUMMARY') == None: continue #skip blank items + event.summary = component.get('SUMMARY') + event.uid = component.get('UID') + if component.get('DESCRIPTION') == None: continue #skip blank items + event.description = component.get('DESCRIPTION') + event.location = component.get('LOCATION') + if hasattr(component.get('dtstart'), 'dt'): + event.start = component.get('dtstart').dt + if hasattr(component.get('dtend'), 'dt'): + event.end = component.get('dtend').dt + + + event.url = component.get('URL') + events.append(event) + f.close() + else: + print("You entered ", filename, ". ") + print(file_extension.upper(), " is not a valid file format. Looking for an ICS file.") + exit(0) + else: + print("I can't find the file ", filename, ".") + print("Please enter an ics file located in the same folder as this script.") + exit(0) + + +def txt_write(icsfile): + txtfile = icsfile[:-3] + "txt" + prevdate="" + spent=0 + evcount=0 + evskip=0 + istart=0 + istop=4102441200.0 # The year 2100. Hopefully this will not be in use by then ... + if sys.argv[2] != '': + istart=parse(sys.argv[2]).timestamp() + if sys.argv[3] != '': + istop=parse(sys.argv[3]).timestamp() + print("Processing events :", end=" ") + try: + with open(txtfile, 'w') as myfile: + for event in sortedevents: + + if prevdate != event.start.strftime("%Y-%m-%d") and spent > 0: # Make a header for each day + if prevdate != '': # If you don't want a summary of the time spent added, comment this section. + th=divmod(spent, 3600)[0] + tm=divmod(spent, 3600)[1]/60 + myfile.write("\nTime Total: " + '{:02.0f}'.format(th) + ":" + '{:02.0f}'.format(tm) + "\n") + spent=0 + if event.start.timestamp() > istart and event.start.timestamp() < istop: + if prevdate != event.start.strftime("%Y-%m-%d"): # Make a header for each day + prevdate = event.start.strftime("%Y-%m-%d") + myfile.write("\nWorklog, " + prevdate + "\n===================\n") + + duration = event.end - event.start + ds = duration.total_seconds() + spent += ds + hours = divmod(ds, 3600)[0] + minutes = divmod(ds,3600)[1]/60 + description=removehtml(event.description.encode('utf-8').decode()) + values = event.start.strftime("%H:%M:%S") + " - " + event.end.strftime("%H:%M:%S") + " (" + '{:02.0f}'.format(hours) + ":" + '{:02.0f}'.format(minutes) + ") " + event.summary.encode('utf-8').decode() + if event.location != '': values = values + " [" + event.location + "]" # Only include location if there is one + + # Remove Google Meet and Skype Meeting part of description + trimmed=description.split('-::~')[0].split('......')[0] + #print("DescLen: " + str(len(description)) + " TrimmedLen: " + str(len(trimmed)) + " : " + trimmed) # For debugging + description=trimmed + if description != '': + values = values + "\n" + description + "\n" + myfile.write(values+"\n") + print("", end=".") + evcount+=1 + else: + print("", end="S") + evskip+=1 + + print("\n\nWrote " + str(evcount) + " events to ", txtfile, " and skipped ", str(evskip), " events\n") + except IOError: + print("Could not open file!") + exit(0) + + +def debug_event(class_name): + print("Contents of ", class_name.name, ":") + print(class_name.summary) + print(class_name.uid) + print(class_name.description) + print(class_name.location) + print(class_name.start) + print(class_name.end) + print(class_name.url, "\n") + +open_cal() +sortedevents=sorted(events, key=lambda obj: obj.start) +txt_write(filename) +#debug_event(event)