From 3c78eb2d6abd0af3839367f13ffeeb3bc2d58717 Mon Sep 17 00:00:00 2001 From: Jon Scheiding Date: Mon, 7 Oct 2024 10:51:40 -0400 Subject: [PATCH 1/2] option to delete recordings after they are downloaded --- README.md | 19 +++++++++++-------- config_template.py | 3 +++ zoom_batch_downloader.py | 31 ++++++++++++++++++++++++++----- zoom_client.py | 16 +++++++++++----- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a193193..37c0d94 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,15 @@ Required Scopes for your server-to-server zoom app: - `cloud_recording:read:list_user_recordings:admin`. - `cloud_recording:read:list_recording_files:admin`. +- (Optional) `cloud_recording:delete:meeting_recording:admin`: if you want to enable `DELETE_AFTER_DOWNLOAD` - (Optional) `user:read:list_users:admin`: if you want the script to iterate over all users in the account (default behavior). -If you are using classic scopes then these would be: +If you are using classic scopes then these would be: + - `recording:read:admin` to download the recordings. +- `recording:write:admin` to delete recordings. - `user:read:admin` if you want the script to iterate over all users in the account. - + ## Instructions 1. Create a server-to-server app as specified above and activate it (no need to publish). @@ -28,14 +31,14 @@ If you are using classic scopes then these would be: 1. Install the requirements listed in `requirements.txt` using [pip](https://pip.pypa.io/en/stable/reference/requirement-specifiers/). - ```bash - python -m pip install -r requirements.txt - ``` + ```bash + python -m pip install -r requirements.txt + ``` 1. Run `zoom_batch_downloader.py`. - ```bash - python zoom_batch_downloader.py - ``` + ```bash + python zoom_batch_downloader.py + ``` Code written by Georg Kasmin, Lane Campbell, Sami Hassan and Aness Zurba. diff --git a/config_template.py b/config_template.py index 002438a..079a8ea 100644 --- a/config_template.py +++ b/config_template.py @@ -57,6 +57,9 @@ # Set to True for more verbose output. VERBOSE_OUTPUT = False +# Set to True to delete meetings that are successfully downloaded +DELETE_AFTER_DOWNLOAD = False + # Constants used for indicating size in bytes. B = 1 KB = 1024 * B diff --git a/zoom_batch_downloader.py b/zoom_batch_downloader.py index 7e335a6..287578e 100644 --- a/zoom_batch_downloader.py +++ b/zoom_batch_downloader.py @@ -64,7 +64,7 @@ def main(): CONFIG.END_DAY or monthrange(CONFIG.END_YEAR, CONFIG.END_MONTH)[1], ) - file_count, total_size, skipped_count = download_recordings( + file_count, total_size, skipped_count, deleted_count = download_recordings( get_users(), from_date, to_date ) @@ -74,6 +74,7 @@ def main(): f"{Style.BRIGHT}Downloaded {Fore.GREEN}{file_count}{Fore.RESET} files.", f"Total size: {Fore.GREEN}{total_size_str}{Fore.RESET}.{Style.RESET_ALL}", f"Skipped: {skipped_count} files.", + f"Deleted: {deleted_count} meetings.", ) @@ -130,7 +131,7 @@ def get_user_name(user_data): def download_recordings(users, from_date, to_date): - file_count, total_size, skipped_count = 0, 0, 0 + file_count, total_size, skipped_count, deleted_count = 0, 0, 0, 0 for user_email, user_name in users: user_description = get_user_description(user_email, user_name) @@ -142,20 +143,26 @@ def download_recordings(users, from_date, to_date): ) meetings = get_meetings(get_meeting_uuids(user_email, from_date, to_date)) - user_file_count, user_total_size, user_skipped_count = ( + user_file_count, user_total_size, user_skipped_count, user_downloaded = ( download_recordings_from_meetings(meetings, user_host_folder) ) utils.print_bright( "######################################################################" ) + + if CONFIG.DELETE_AFTER_DOWNLOAD: + utils.print_bright(f"Deleting {len(user_downloaded)} meetings from {user_description}") + delete_meetings(user_downloaded) + deleted_count += len(user_downloaded) + print() file_count += user_file_count total_size += user_total_size skipped_count += user_skipped_count - return (file_count, total_size, skipped_count) + return (file_count, total_size, skipped_count, deleted_count) def download_not_ready_files(): @@ -186,6 +193,15 @@ def get_user_host_folder(user_email): def date_to_str(date): return date.strftime("%Y-%m-%d") +def delete_meetings(meetings): + for meeting in meetings: + try: + url = f"https://api.zoom.us/v2/meetings/{meeting["uuid"]}/recordings" + client.delete(url) + except Exception as e: + utils.print_bright( + f"Logging error occurred while deleting recordings for meeting {meeting.get("uuid")}: {e}" + ) def get_meeting_uuids(user_email, start_date, end_date): meeting_uuids = [] @@ -246,6 +262,8 @@ def get_meetings(meeting_uuids): def download_recordings_from_meetings(meetings, host_folder): file_count, total_size, skipped_count = 0, 0, 0 + downloaded = [] + for meeting in meetings: if ( CONFIG.TOPICS @@ -308,7 +326,10 @@ def download_recordings_from_meetings(meetings, host_folder): else: skipped_count += 1 - return file_count, total_size, skipped_count + if skipped_count == 0: + downloaded.append(meeting) + + return file_count, total_size, skipped_count, downloaded def download_recording_file( diff --git a/zoom_client.py b/zoom_client.py index ed45c89..541597c 100644 --- a/zoom_client.py +++ b/zoom_client.py @@ -12,15 +12,21 @@ def __init__(self, account_id: str, client_id: str, client_secret: str, PAGE_SIZ self.cached_token = None def get(self, url): - return self._get_with_token(lambda t: requests.get(url=url, headers=self.get_headers(t))).json() + return self._req_with_token(lambda t: requests.get(url=url, headers=self.get_headers(t))).json() - def _get_with_token(self, get): + def delete(self, url): + resp = self._req_with_token(lambda t: requests.delete(url = url, headers=self.get_headers(t))) + if resp.status_code == 204: + return None + return resp.json() + + def _req_with_token(self, req): if self.cached_token: - response = get(self.cached_token) + response = req(self.cached_token) if not self.cached_token or response.status_code == 401: self.cached_token = self.fetch_token() - response = get(self.cached_token) + response = req(self.cached_token) if not response.ok: raise Exception(f'{response.status_code} {response.text}') @@ -86,4 +92,4 @@ def do_as_get(token): return test_response - self._get_with_token(lambda t: do_as_get(t)) \ No newline at end of file + self._req_with_token(lambda t: do_as_get(t)) \ No newline at end of file From 6b0ee30fe07d57dca7fabf253561318786b9e687 Mon Sep 17 00:00:00 2001 From: Jon Scheiding Date: Mon, 7 Oct 2024 13:20:43 -0400 Subject: [PATCH 2/2] option to name recordings with date prepended instead of appended --- config_template.py | 4 ++++ zoom_batch_downloader.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/config_template.py b/config_template.py index 079a8ea..dc7f654 100644 --- a/config_template.py +++ b/config_template.py @@ -50,6 +50,10 @@ # Note: One "meeting" can have multiple recording instances. GROUP_BY_RECORDING = False +# If True, recordings will be named with the meeting date at the beginning of the filename; +# otherwise it will be at the end. +NAME_RECORDINGS_DATE_FIRST = False + # If True, participant audio files will be downloaded as well. # This works when "Record a separate audio file of each participant" is enabled. INCLUDE_PARTICIPANT_AUDIO = True diff --git a/zoom_batch_downloader.py b/zoom_batch_downloader.py index 287578e..2b6a46b 100644 --- a/zoom_batch_downloader.py +++ b/zoom_batch_downloader.py @@ -295,9 +295,15 @@ def download_recordings_from_meetings(meetings, host_folder): recording_file.get("file_extension") or os.path.splitext(recording_file["file_name"])[1] ) - recording_name = utils.slugify( - f'{topic}__{recording_file["recording_start"]}' - ) + if CONFIG.NAME_RECORDINGS_DATE_FIRST: + recording_name = utils.slugify( + f'{recording_file["recording_start"]}__{topic}' + ) + else: + recording_name = utils.slugify( + f'{topic}__{recording_file["recording_start"]}' + ) + file_id = recording_file["id"] file_name_suffix = ( os.path.splitext(recording_file["file_name"])[0] + "__"