Skip to content

Instantly share code, notes, and snippets.

@zsarge
Created June 29, 2025 03:17
Show Gist options
  • Save zsarge/175ec7ea6513b62c7e9b8acfac1d705f to your computer and use it in GitHub Desktop.
Save zsarge/175ec7ea6513b62c7e9b8acfac1d705f to your computer and use it in GitHub Desktop.
Export Google Recorder Recordings With Metadata

Google Recorder Export with metadata

Motivation: Google Recorder currently does not make it easy to export recordings with associated metadata.

I have a significant number of recordings, but if I use the default "share" feature, then only the titles are exported.

Notably, the titles of the recordings do not include the dates by default.

If you want to export all recordings with the dates they were created and Google's speech-to-text, I recommend Google Takeout.

However, Google Takeout does not include the locations of the recordings.

As such, I've written two quick scripts:

  • A User Script to capture a selection of recordings with associated metadata
    • There's no fancy export method; I just copied the data from the console.log({metadataElements});
  • A python script to download the recordings as uuids

This was enough for my use case, but this is certianly not a polished solution.

I hope it may serve as an example approach, for anyone in a similar situation.

"""
This program takes the list of uuids gathered by the metadata export script,
and then downloads the appropriate recordings. It depends on the nonstandard libraries
browser_cookie3 for taking cookies from firefox and tqdm for a progress display.
"""
import json
import browser_cookie3
import requests
import os
from tqdm import tqdm
from time import sleep
DOWNLOAD_DIR = 'audio-downloads'
SECONDS_BETWEEN_DOWLOADS = 2
cookiejar = browser_cookie3.firefox(domain_name='google.com')
# https://stackoverflow.com/a/16696317
def download_file(url: str, local_filename: str):
# NOTE the stream=True parameter below
with requests.get(url, stream=True, cookies=cookiejar) as r:
r.raise_for_status()
with open(os.path.join(DOWNLOAD_DIR, local_filename), 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
# If you have chunk encoded response uncomment if
# and set chunk_size parameter to None.
#if chunk:
f.write(chunk)
def download_recorder_uuid(uuid: str):
url = f'https://usercontent.recorder.google.com/download/playback/{uuid}?download=true'
local_filename = f'{uuid}.m4a'
download_file(url, local_filename)
with open('list-of-recordings-to-migrate.txt') as f:
recordings = json.loads(f.read())
for recording in tqdm(recordings):
uuid = recording['pathname'].replace('/', '')
download_recorder_uuid(uuid)
sleep(SECONDS_BETWEEN_DOWLOADS)
// ==UserScript==
// @name Google Recorder Export
// @namespace https://recorder.google.com
// @version 2025-06-28
// @description Export the files from Google Recorder with metadata.
// @author You
// @match https://recorder.google.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant none
// ==/UserScript==
function processRecordingElement(recording) {
recording.click();
const metadata = recording.querySelector('recorder-sidebar-item').shadowRoot.querySelector('.top-section recorder-metadata').shadowRoot.querySelector('.container');
const nestedMetadata = metadata.querySelector('.subtitle .wrapper .flex-overflow-wrapper');
const title = metadata.querySelector('.title-wrapper').textContent.trim();
const recordingLocation = nestedMetadata.querySelector('.location')?.textContent || null;
const date = nestedMetadata.querySelector('span[aria-label]').textContent;
const regex = /\b(1[0-2]|0?[1-9]):[0-5][0-9]\s?(AM|PM)\b/i; // for timestamp
const match = title.match(regex);
let time = null;
if (match) {
time = match[0];
}
const fullMetadataString = nestedMetadata.parentElement.textContent;
const pathname = window.location.pathname;
const url = window.location.href;
return {title, recordingLocation, time, date, fullMetadataString, pathname, url};
}
function processAllMetadata() {
console.log("Google Recorder Exporter Loading...");
// drill down through the shadow dom
const items = document.querySelector('recorder-main')
.ki.querySelector('.container recorder-sidebar')
.shadowRoot.querySelector('.container .sidebar recorder-sidebar-items')
.shadowRoot.querySelector('.container .items');
const libraryList = items.querySelector('.library-list');
const loadMoreButton = items.querySelector('.load-more');
const loadMore = () => loadMoreButton.click();
const getNumberOfRecordings = () => libraryList.getElementsByClassName("item").length;
let userConfirmed = confirm("Do you want to load all user recordings? Saying no will just export what you have.");
if (userConfirmed) {
function loadAllRecordings(delay = 1000) {
let lastAmount = 0;
let currentAmount = getNumberOfRecordings();
function checkAndLoad() {
lastAmount = currentAmount;
loadMore();
setTimeout(() => {
currentAmount = getNumberOfRecordings();
if (currentAmount > lastAmount) {
checkAndLoad(); // Keep loading
} else {
console.log("All recordings loaded. Found:", getNumberOfRecordings());
}
}, delay);
}
checkAndLoad();
}
loadAllRecordings();
}
// now, process the recordings we've loaded:
const recordingElements = libraryList.getElementsByClassName("item");
const metadataElements = [];
const iterationDepth = getNumberOfRecordings();
function printAll() {
console.log({metadataElements});
// console.log('stringified:');
// console.log(JSON.stringify(metadataElements));
}
function gatherMetadata(i = 0, delay = 2000) {
console.log({i});
const metadata = processRecordingElement(recordingElements[i]);
metadataElements.push(metadata);
console.log({metadata});
printAll();
setTimeout(() => {
if (i < iterationDepth) {
gatherMetadata(i + 1); // Keep loading
} else {
console.log("All recordings loaded. Found:", getNumberOfRecordings());
printAll();
}
}, delay);
}
gatherMetadata();
}
(function() {
'use strict';
// wait 5 seconds, then process the metadata
window.addEventListener('load', () => setTimeout(processAllMetadata, 5000));
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment