r""" ======================================================================== Combine all calendar files in a folder into one new calendar file. Part of Frigcal 4.1.0, quixotely.com/Frigcal, with same terms of use. Overview: If you have made multiple calendars in the app or are using more than one ICS file from other sources, this script can be run to combine the calendars into a single calendar and ICS file. All events in all the source folder's calendars are added to a new single calendar file. This makes it slightly simpler to add new events because the default calendar for new-event adds is automatically set to the sole calendar if there is just one. The default can also be set manually in Settings. This script is primarily meant for use on Windows, macOS, and Linux PCs. It may not be able to access calendar folders on Android, subject to hosting apps' permissions, and it is not available in Android installs of the Frigcal app (see online host in next section). Usage: 1) Get this script's source-code file. Either download this file from www.quixotely.com/Frigcal or locate it in the "tools" subfolder of the app's install folder on PCs. 2) Get Python 3.X (3.12 or later recommended). On PCs, get it from www.python.org/downloads or other. On Android, use an app that runs Python scripts, such as Termux or PyDroid 3. 3) Run this script with one of the following Python command lines: - On Windows, type this in Command Prompt or PowerShell: py -3 combine-calendars.py - On macOS and Linux, type this in Terminal: python3 combine-calendars.py As usual, you must either first use "cd" to change to the folder containing this script, or include the script's folder path before its name in the command. On Windows: cd folder\containing\this\script py -3 combine-calendars.py py -3 folder\containing\this\script\combine-calendars.py 4) In the console where you're running the script's command line, reply to prompts asking for your source calendars folder, and the folder and name of the new combined calendar. Full example: $ cd folder\containing\this\script $ py -3 combine-calendars.py Path to your source calendar folder? C:\Users\me\Calendar Path to folder for your new combined calendar? combo-folder Name of the new combined calendar (.ics optional)? combined Done: 7 calendars combined in combo-folder/combined.ics. Tip: you can verify results by comparing the number of events in calendar-file stats for loads of the source and target folders. Change folders in Settings to load each. You can use "." for any asked folder path to refer to the folder in which this script is run, and the source folder may be given in the command line itself, in which case only the new folder and calendar are asked (this allows tab completions for source): $ python3 combine-calendars.py /Users/me/MY-STUFF/Calendar Path to folder for your new combined calendar? . Name of the new combined calendar (.ics optional)? combined 5) After running this script, your combined folder will have a ".ics" file with all combined events. Load it in a new run of Frigcal by selecting its folder in the app's Settings screen. This script does not change or delete source-folder calendars. ======================================================================== """ import sys, os, glob # import icalendar, pytz # not needed for text-parsing approach #--------------- # Get run inputs #--------------- if len(sys.argv) > 1: source_folder = sys.argv[1] else: source_folder = input('Path to your source calendar folder? ') target_folder = input('Path to folder for your new combined calendar? ') target_file = input('Name of the new combined calendar (.ics optional)? ') if not target_file.endswith('.ics'): target_file += '.ics' #---------------- # Validate inputs #---------------- if not os.path.exists(source_folder): print('Error - source folder does not exist: cancelling script') sys.exit(1) if not os.path.exists(target_folder): try: os.makedirs(target_folder) except: print('Error - cannot create target folder: cancelling script') sys.exit(1) icsfiles = glob.glob(os.path.join(source_folder, '*.ics')) icsfiles = [f for f in icsfiles if not f.startswith('._')] # macos junk if not icsfiles: print('Error - no calendars in source folder: cancelling script') sys.exit(1) #------------------ # Combine calendars #------------------ # This could load calendars with code borrowed from storage.py, # but it's much easier to parse event text out of ICS text files, # from start of first event up to the end-calendar terminator. combotext = '' numconverts = 0 event_header_line = 'BEGIN:VEVENT\n' calendar_end_line = 'END:VCALENDAR\n' for icsfile in icsfiles: icspath = os.path.join(source_folder, icsfile) fileobj = open(icspath, mode='r', encoding='utf8') filetext = fileobj.read() fileobj.close() startevents = filetext.find(event_header_line) # first if startevents == -1: print(f'Skipping calendar with no events: {icsfile}') continue endevents = filetext.find(calendar_end_line) if startevents == -1: print(f'Skipping calendar with no ending line: {icsfile}') continue elif filetext[endevents:] != calendar_end_line: print(f'Skipping calendar with text after ending: {icsfile}') continue eventstext = filetext[startevents : endevents] combotext += eventstext numconverts += 1 #----------------------- # Save combined calendar #----------------------- # caveat: hardcoded FC version (2.0 is iCal) fullcalendartext = \ f"""BEGIN:VCALENDAR VERSION:2.0 PRODID:Frigcal 4.1.0 {combotext}END:VCALENDAR""" try: combopath = os.path.join(target_folder, target_file) combofile = open(combopath, mode='w', encoding='utf8') combofile.write(fullcalendartext) combofile.close() except: print(f'Error - could not create new calendar: {combopath}') else: print(f'\nDone: {numconverts} calendars combined in {combopath}.\n') print('Tip: you can verify results by comparing the number of\n' 'events in calendar-file stats for loads of the source and\n' 'target folders. Change folders in Settings to load each.') # and compare source/target #events after folder changes and loads in app