Last active
February 8, 2025 18:47
-
-
Save mahmoudimus/61dbd59f460ea9c3eded7bf7fa632ba4 to your computer and use it in GitHub Desktop.
Chrome history tabs iphone history export syncedTabs chrome://history/syncedTabs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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