Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save mahmoudimus/61dbd59f460ea9c3eded7bf7fa632ba4 to your computer and use it in GitHub Desktop.
Save mahmoudimus/61dbd59f460ea9c3eded7bf7fa632ba4 to your computer and use it in GitHub Desktop.
Chrome history tabs iphone history export syncedTabs chrome://history/syncedTabs
/**
* This script extracts synced tabs from Chrome’s History page and post-processes tab URLs.
*
* It traverses the page’s shadow DOM to locate the list of synced devices and then iterates over
* each device card to collect the device name and its open tabs (each with a title and URL).
*
* For tabs whose URLs are prefixed by Marvelous Suspender (i.e. URLs that start with a chrome-extension
* scheme and include "/suspended.html#"), the script extracts the actual URL from the "uri" query parameter.
*
* The URL processing is designed to be extensible. Simply add additional processor functions to the
* `urlPostProcessors` array to chain more modifications to the URLs.
*
* The resulting data is formatted into a JSON object with a "windows" array (each window representing a device),
* which can be used for session management tools like SessionBuddy. Finally, the JSON string is copied to the clipboard.
*/
(() => {
/**
* Apply an array of processor functions to a given value sequentially.
*
* @param {*} value - The initial value to process.
* @param {Array<Function>} processors - An array of functions that take the value and return a new value.
* @returns {*} - The processed value.
*/
const applyProcessors = (value, processors = []) =>
processors.reduce((result, processor) => processor(result), value);
/**
* Processor function to extract the actual URL from a suspended tab URL created by Marvelous Suspender.
*
* @param {string} url - The URL to process.
* @returns {string} - The processed URL.
*/
const extractSuspendedUri = (url) => {
const suspendedIndicator = '/suspended.html#';
if (url.startsWith("chrome-extension://") && url.includes(suspendedIndicator)) {
const hashIndex = url.indexOf("#");
if (hashIndex !== -1) {
// Get the fragment after '#' which should contain query parameters like ttl, pos, and uri.
const fragment = url.substring(hashIndex + 1);
const params = new URLSearchParams(fragment);
const actualUrl = params.get("uri");
if (actualUrl) {
try {
return decodeURIComponent(actualUrl);
} catch (e) {
return actualUrl;
}
}
}
}
return url;
};
// Array of URL post processor functions.
// Future processors can be added here to further transform URLs.
const urlPostProcessors = [
extractSuspendedUri,
// e.g., anotherProcessor,
];
// Retrieve the container holding the synced device list by traversing the shadow DOM.
const historyApp = document.getElementById("history-app");
if (!historyApp?.shadowRoot) {
console.error("Unable to access #history-app or its shadowRoot.");
return;
}
const syncedDevices = historyApp.shadowRoot.getElementById("synced-devices");
if (!syncedDevices?.shadowRoot) {
console.error("Unable to access #synced-devices or its shadowRoot.");
return;
}
const tabListContainer = syncedDevices.shadowRoot.getElementById("synced-device-list");
if (!tabListContainer) {
console.error("Unable to find #synced-device-list element.");
return;
}
// Object to hold devices and their associated tabs.
const devices = {};
// Loop through each child element in the synced-device list.
Array.from(tabListContainer.children).forEach((deviceCard) => {
// Process only elements with the expected tag name.
if (deviceCard.nodeName !== "HISTORY-SYNCED-DEVICE-CARD") return;
const cardShadow = deviceCard.shadowRoot;
if (!cardShadow) {
console.warn("A device card is missing its shadowRoot.");
return;
}
// Get the device name.
const deviceNameEl = cardShadow.getElementById("device-name");
if (!deviceNameEl) {
console.warn("Device card missing device-name element.");
return;
}
const deviceName = deviceNameEl.textContent.trim();
// Locate the container with the tab history items for this device.
const historyItemsContainer = cardShadow.getElementById("history-item-container");
if (!historyItemsContainer) {
console.warn(`No history items found for device "${deviceName}".`);
return;
}
// Select all anchor elements representing individual tabs.
const anchors = historyItemsContainer.querySelectorAll("div > a.website-link");
// Initialize the array for this device if it hasn't been created yet.
if (!devices[deviceName]) {
devices[deviceName] = [];
}
// Extract the title and URL (post-processed for suspended tabs) from each tab link.
anchors.forEach((anchor) => {
const rawUrl = anchor.getAttribute("href");
const processedUrl = applyProcessors(rawUrl, urlPostProcessors);
devices[deviceName].push({
nx_title: anchor.getAttribute("title"),
url: processedUrl,
});
});
});
// Build the final output structure.
const output = {
windows: Object.entries(devices).map(([deviceName, tabs]) => ({
focused: false,
nx_title: deviceName,
tabs: tabs,
})),
};
// Copy the formatted JSON string to the clipboard.
copy(JSON.stringify(output, null, 2));
console.log("Synced tabs JSON (with processed URLs) copied to clipboard.");
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment