├── screenshot.png ├── screenshot_with_bg.jpg ├── .gitignore ├── readme.md └── main.py /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robbyHuelsi/macOSVoiceMemosExporter/HEAD/screenshot.png -------------------------------------------------------------------------------- /screenshot_with_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robbyHuelsi/macOSVoiceMemosExporter/HEAD/screenshot_with_bg.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | venv/ 3 | __pycache__/ 4 | 5 | # Mac Gedoens 6 | .DS_Store 7 | 8 | # PyCharm 9 | .idea/ -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # macOS Voice Memos Exporter 2 | Python project to export audio files from macOS Voice Memos app with right filename and date created 3 | ![Screenshot](screenshot.png) 4 | 5 | Since Apple has forgotten to implement a serious export function to the Voice Memos app, this project will help you. 6 | You can export all or selected memos as audio files. The names of the files correspond to the labels of the memos. 7 | The recording date of the memos can be found in the creation date of the files and can be also added to the file name. 8 | 9 | ## Parameters 10 | #### Database File Path 11 | Use `-d` or `--db_path` to specify the path to the database Voice Memo App uses to store information about the memos. 12 | 13 | Default: `~/Library/Application Support/com.apple.voicememos/Recordings/CloudRecordings.db` 14 | 15 | If you don't use iCloud Sync for Voice Memos, this path could be also interesting for you: 16 | `~/Library/Application Support/com.apple.voicememos/Recordings/Recordings.db` (not proved) 17 | 18 | #### Export Folder Path 19 | Use `-e` or `--export_path` to change the export folder path. 20 | 21 | Defaut: `~/Voice Memos Export` 22 | 23 | #### Export All Memos 24 | Add the flag `-a` or `--all` to export all memos at once instead instead of deciding for each memo whether it should be exported or not. 25 | 26 | #### Add Date to File Name 27 | Add the flag `--date_in_name` to add the recording date at the beginning of the file name. 28 | 29 | #### Date Format for File Name 30 | If you use the flag `--date_in_name` you can modify the date format with `--date_in_name_format`. 31 | 32 | Default: `%Y-%m-%d-%H-%M-%S_` ➔ 2019-12-06-22-31-11_ 33 | 34 | #### Prevent to Open Finder 35 | Use the flag `--no_finder` to avoid opening a finder window to view exported memos. 36 | 37 | ### Example 38 | `python main.py -e ~/Music/memos -a --date_in_name --date_in_name_format "%Y-%m-%d "` 39 | 40 | 41 | ## Disclaimer: 42 | No liability for damage to the memo database, library folder, or anywhere else in the file system. 43 | Create a backup (in particular of `~/Library`) before using this tool. 44 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import argparse 4 | import os 5 | import sqlite3 6 | from datetime import datetime, timedelta 7 | import time 8 | from shutil import copyfile 9 | from sqlite3 import Error 10 | import sys 11 | import tty 12 | import termios 13 | import subprocess 14 | 15 | 16 | def create_connection(db_file): 17 | """ 18 | create a database connection to the SQLite database specified by the db_file 19 | :param db_file: database file 20 | :return: Connection object or None 21 | """ 22 | conn = None 23 | try: 24 | conn = sqlite3.connect(db_file) 25 | except Error as e: 26 | print(e) 27 | 28 | return conn 29 | 30 | 31 | def get_all_memos(conn): 32 | """ 33 | Query wanted rows in the table ZCLOUDRECORDING 34 | :param conn: the Connection object 35 | :return: rows 36 | """ 37 | cur = conn.cursor() 38 | cur.execute("SELECT ZDATE, ZDURATION, ZCUSTOMLABEL, ZPATH FROM ZCLOUDRECORDING ORDER BY ZDATE") 39 | 40 | return cur.fetchall() 41 | 42 | 43 | def main(): 44 | # Define default paths 45 | _db_path_default = os.path.join(os.path.expanduser("~"), "Library", "Application Support", 46 | "com.apple.voicememos", "Recordings", "CloudRecordings.db") 47 | _export_path_default = os.path.join(os.path.expanduser("~"), "Voice Memos Export") 48 | 49 | # Setting up arguments and --help 50 | parser = argparse.ArgumentParser(description='Export audio files from macOS Voice Memo App ' + 51 | 'with right filename and date created.') 52 | parser.add_argument("-d", "--db_path", type=str, 53 | help="define path to database of Voice Memos.app", 54 | default=_db_path_default) 55 | parser.add_argument("-e", "--export_path", type=str, 56 | help="define path to folder for exportation", 57 | default=_export_path_default) 58 | parser.add_argument("-a", "--all", action="store_true", 59 | help="export everything at once instead of step by step") 60 | parser.add_argument("--date_in_name", action="store_true", 61 | help="include date in file name") 62 | parser.add_argument("--date_in_name_format", type=str, 63 | help="define the format of the date in file name (if --date_in_name active)", 64 | default="%Y-%m-%d-%H-%M-%S_") 65 | parser.add_argument("--no_finder", action="store_true", 66 | help="prevent to open finder window to show exported memos") 67 | args = parser.parse_args() 68 | 69 | # Define name and width of columns 70 | _cols = [{"n": "Date", 71 | "w": 19}, 72 | {"n": "Duration", 73 | "w": 11}, 74 | {"n": "Old Path", 75 | "w": 32}, 76 | {"n": "New Path", 77 | "w": 60}, 78 | {"n": "Status", 79 | "w": 12}] 80 | 81 | # offset between datetime starts to count (1.1.1970) and Apple starts to count (1.1.2001) 82 | _dt_offset = 978307200.825232 83 | 84 | def getWidth(name): 85 | """ 86 | get width of column called by name 87 | :param name: name of column 88 | :return: width 89 | """ 90 | for c in _cols: 91 | if c["n"] == name: 92 | return c["w"] 93 | return False 94 | 95 | def helper_str(seperator): 96 | """ 97 | create a helper string for printing table row 98 | Example: helper_str(" | ").format(...) 99 | :param seperator: string to symbol column boundary 100 | :return: helper string like: "{0:10} | {1:50}" 101 | """ 102 | return seperator.join(["{" + str(i) + ":" + str(c["w"]) + "}" for i, c in enumerate(_cols)]) 103 | 104 | def body_row(content_list): 105 | """ 106 | create a string for a table body row 107 | :param content_list: list of cells in this row 108 | :return: table body row string 109 | """ 110 | return "│ " + helper_str(" │ ").format(*content_list) + " │" 111 | 112 | # Check permission 113 | if not os.access(args.db_path, os.R_OK): 114 | print("No permission to read database file. ({})".format(args.db_path)) 115 | exit() 116 | 117 | # create a database connection and load rows 118 | conn = create_connection(args.db_path) 119 | if not conn: 120 | exit() 121 | with conn: 122 | rows = get_all_memos(conn) 123 | if not rows: 124 | exit() 125 | 126 | # create export folder if it doesn't exist 127 | try: 128 | os.stat(args.export_path) 129 | except: 130 | os.mkdir(args.export_path) 131 | 132 | # Print intro and table header 133 | print() 134 | if not args.all: 135 | print("Press ENTER to export the memo shown in the current row or ESC to go to next memo.") 136 | print("Do not press other keys.") 137 | print() 138 | print("┌─" + helper_str("─┬─").format(*["─" * c["w"] for c in _cols]) + "─┐") 139 | print("│ " + helper_str(" │ ").format(*[c["n"] for c in _cols]) + " │") 140 | print("├─" + helper_str("─┼─").format(*["─" * c["w"] for c in _cols]) + "─┤") 141 | 142 | # iterate over memos found in database 143 | for row in rows: 144 | 145 | # get information from database and modify them for exportation 146 | date = datetime.fromtimestamp(row[0] + _dt_offset) 147 | date_str = date.strftime("%d.%m.%Y %H:%M:%S") 148 | duration_str = str(timedelta(seconds=row[1])) 149 | duration_str = duration_str[:duration_str.rfind(".") + 3] if "." in duration_str else duration_str + ".00" 150 | duration_str = "0" + duration_str if len(duration_str) == 10 else duration_str 151 | label = row[2].encode('ascii', 'ignore').decode("ascii").replace("/", "_") 152 | path_old = row[3] if row[3] else "" 153 | if path_old: 154 | path_new = label + path_old[path_old.rfind("."):] 155 | path_new = date.strftime(args.date_in_name_format) + path_new if args.date_in_name else path_new 156 | path_new = os.path.join(args.export_path, path_new) 157 | else: 158 | path_new = "" 159 | if len(path_old) < getWidth("Old Path") - 3: 160 | path_old_short = path_old 161 | else: 162 | path_old_short = "..." + path_old[-getWidth("Old Path") + 3:] 163 | if len(path_new) < getWidth("New Path") - 3: 164 | path_new_short = path_new 165 | else: 166 | path_new_short = "..." + path_new[-getWidth("New Path") + 3:] 167 | 168 | # print body row and wait for keys (if needed) 169 | if not path_old: 170 | print(body_row((date_str, duration_str, path_old_short, path_new_short, "No File"))) 171 | else: 172 | if args.all: 173 | key = 10 174 | else: 175 | key = 0 176 | print(body_row((date_str, duration_str, path_old_short, path_new_short, "Export?")), end="\r") 177 | fd = sys.stdin.fileno() 178 | old = termios.tcgetattr(fd) 179 | new = termios.tcgetattr(fd) 180 | new[3] = new[3] & ~termios.ECHO 181 | termios.tcsetattr(fd, termios.TCSADRAIN, new) 182 | tty.setcbreak(sys.stdin) 183 | while key not in (10, 27): 184 | try: 185 | key = ord(sys.stdin.read(1)) 186 | # print("Key: {}".format(key)) 187 | finally: 188 | termios.tcsetattr(fd, termios.TCSADRAIN, old) 189 | 190 | # copy file and modify file times if this memo should be exported 191 | if key == 10: 192 | copyfile(path_old, path_new) 193 | mod_time = time.mktime(date.timetuple()) 194 | os.utime(path_new, (mod_time, mod_time)) 195 | print(body_row((date_str, duration_str, path_old_short, path_new_short, "Exported!"))) 196 | 197 | # skip this memo if desired 198 | elif key == 27: 199 | print(body_row((date_str, duration_str, path_old_short, path_new_short, "Not Exported"))) 200 | 201 | # print bottom table border and closing statement 202 | print("└─" + helper_str("─┴─").format(*["─" * c["w"] for c in _cols]) + "─┘") 203 | print() 204 | print("Done. Memos exported to: {}".format(args.export_path)) 205 | print() 206 | 207 | # open finder if desired 208 | if not args.no_finder: 209 | subprocess.Popen(["open", args.export_path]) 210 | 211 | 212 | if __name__ == '__main__': 213 | main() 214 | --------------------------------------------------------------------------------