File: Frigcal/code/Frigcal--source/tools/combine-calendars/combine-calendars.py

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