google maps saved lists are great. before I travel anywhere I fill up my lists with possible places to eat, drink, shop, and tour. I populate from sites like eater, michelin guide, timeout, and lonely planet.
populating these lists used to be a terribly manual ordeal. not anymore 😎
-
create a new bookmark
-
copy/paste the following code into the URL field
javascript:function preamble(){return'\nconst btnSaveSelector=\'[data-value*="Save"]\';let logPrefix="";function log(...args){console.log(logPrefix,...args)}function logPrefixSet(prefix){logPrefix=prefix}function rest(val){return new Promise((resolve=>setTimeout((()=>resolve(val)),500)))}function goToUrl(url){return log(`goToUrl ${url}`),new Promise((resolve=>fetch(url).then((res=>res.text())).then((data=>{document.open(),document.write(data),document.close()})).then((()=>resolve()))))}function observeUntilFound(_nodeObserve,nodesFindingSelectors){return log(`observeUntilFound ${nodesFindingSelectors}`),new Promise((resolve=>tryToFind(nodesFindingSelectors,resolve,Date.now())))}function tryToFind(nodesFindingSelectors,resolve,startTime){setTimeout((()=>{const nodesFinding=nodesFindingSelectors.map((selector=>document.querySelector(selector)));if(nodesFinding.every((node=>null!==node)))return void resolve(nodesFinding);const timeElapsedS=(Date.now()-startTime)/1e3;log(`🔎 ${nodesFindingSelectors}, ${timeElapsedS} s elapsed... `),tryToFind(nodesFindingSelectors,resolve,startTime)}),400)}function waitUntilClickable(btn){return log(`waitUntilClickable ${btn}`),new Promise((resolve=>checkClickable(btn,resolve,Date.now())))}function checkClickable(btn,resolve,startTime){setTimeout((()=>{if("function"==typeof btn.click)return void resolve(btn);const timeElapsedS=(Date.now()-startTime)/1e3;log(`🖱️❓ ${btn}, ${timeElapsedS} s elapsed...`),checkClickable(btn,resolve,startTime)}),200)}function addUrlToList(name,url,listName,note,loggingNote){return logPrefixSet(`[gmaps-add][${name}][${loggingNote}]`),goToUrl(url).then(rest).then((()=>observeUntilFound(document,[btnSaveSelector]))).then(rest).then((([btnSave])=>waitUntilClickable(btnSave))).then(rest).then((btnSave=>btnSave.click())).then(rest).then((()=>observeUntilFound(document.querySelector("#hovercard"),["#action-menu"]))).then((([menu])=>Array.from(menu.querySelectorAll("div")).find((div=>div.innerText===listName)))).then(rest).then((menuItemList=>{if("true"!==menuItemList.getAttribute("aria-checked"))return waitUntilClickable(menuItemList).then(rest).then((menuItemList=>menuItemList.click())).then(rest).then((()=>observeUntilFound(document.querySelector(\'[role="main"]\'),[`[aria-label="Add note in ${listName}"]`]))).then(rest).then((([btnAddNote])=>waitUntilClickable(btnAddNote))).then(rest).then((btnAddNote=>btnAddNote.click())).then(rest).then((()=>observeUntilFound(document.querySelector("#modal-dialog"),["#modal-dialog textarea"]))).then(rest).then((([textarea])=>{const btns=document.querySelectorAll("#modal-dialog button"),btnConfirm=Array.from(btns).find((btn=>"DONE"===btn.innerText));textarea.value=note,btnConfirm.click()})).then(rest);log(`already saved to ${listName}`)}))}'}function escapeSingleQuotes(str){return str.replace(/'/g,"\\'")}function genScript(data,listName){const dataCount=data.length,addUrlToListsStr=data.map((({name:name,url:url,note:note},index)=>{const loggingNote=`[${index+1} / ${dataCount}]`,noteSafe=escapeSingleQuotes(note);return`.then(() => addUrlToList("${name}", "${url}", "${listName}", '${noteSafe}', "${loggingNote}"))`})).join("\n");return`${preamble()}\n(() => Promise.resolve())()\n${addUrlToListsStr};`}function processCard(card){console.log(`card ${card}`);const nameEl=card.querySelector("h1");console.log(`nameEl ${nameEl}`);const name=nameEl.innerText;console.log(`name ${name}`);const descriptionEl=card.querySelector(".c-entry-content p");console.log(`descriptionEl ${descriptionEl}`);const description=descriptionEl.innerText;console.log(`description ${description}`);const servicesEls=Array.from(card.querySelectorAll(".services li a"));console.log(`servicesEls ${servicesEls}`);const gmapsEl=servicesEls.find((a=>"Open in Google Maps"===a.innerHTML));console.log(`gmapsEl ${gmapsEl}`);const gmapsRawLink=gmapsEl.href;console.log(`gmapsRawLink ${gmapsRawLink}`);const gmapsLink=gmapsRawLink.replace("https://google.com","").replace("https://www.google.com","");return console.log(`gmapsLink ${gmapsLink}`),{name:name,description:description,gmaps:gmapsLink}}function scrapeFromPage(){const eaterListTitleEl=document.querySelector(".c-mapstack__headline h1");console.log(`eaterListTitleEl ${eaterListTitleEl}`);const eaterListTitle=eaterListTitleEl.innerText;console.log(`eaterListTitle ${eaterListTitle}`);const updatedEl=document.querySelector('.c-mapstack__headline [data-ui="timestamp"]');console.log(`updatedEl ${updatedEl}`);const updated=updatedEl.innerText;console.log(`updated ${updated}`);const cardEls=Array.from(document.querySelectorAll("#content .c-mapstack__card"));console.log(`cardEls ${cardEls}`);const cards=cardEls.filter((card=>!["intro","newsletter","related-links","comments"].includes(card.getAttribute("data-slug"))));console.log(`cards ${cards}`);const cardData=cards.map(processCard);return console.log(`cardData ${cardData}`),[eaterListTitle,updated,cardData]}function noteForDatum(cardDatum,eaterListTitle,updated){return`Eater - ${eaterListTitle} (updated ${updated})\\n\\n${cardDatum.description}`}function cardDataToMapsData(cardData,eaterListTitle,updated){cardData.length;return cardData.map((cardDatum=>{const note=noteForDatum(cardDatum,eaterListTitle,updated),urlRaw=cardDatum.gmaps,url=(()=>{if(urlRaw.includes("?api=1"))return urlRaw;const[latLong]=urlRaw.split("/").slice(-1);return`https://www.google.com/maps/search/${cardDatum.name}/@${latLong}`})();return{name:cardDatum.name,url:url,note:note}}))}function bookmarklet(){const listName=prompt("please enter the exact name of the google maps list (must already exist!)"),[eaterListTitle,updated,cardData]=scrapeFromPage(),script=genScript(cardDataToMapsData(cardData,eaterListTitle,updated),listName);navigator.clipboard.writeText(script),alert("script copied to clipboard!")}bookmarklet(); ```
-
save the bookmark
setup.mp4
- open up google maps
- from the sidebar, go to "Saved" -> "Lists"
- either "+ New list" or find the name of a list you already have that you want to use (like "Want to go")
- navigate to an eater list (for example, the 38 best restaurants in new york city)
- click the bookmarklet you created in [setup]
- enter the name of the google maps list you picked
- a popup should appear saying "script copied to clipboard!"
- go back to google maps
- open up the console (ctrl+shift+i usually)
- paste into console
- hit enter and watch it run!
usage.mp4
- 🐞 high likelihood of bugs
- 🧑💻 relies on css selectors, so will break every time google deploys new UI for lists
- 📜 on longer lists tends to fail and get stuck towards the end of the list
- 🤔 (I suspect this is because when it navigates to a new restaurant it's not doing a full reload (only a partial one) and all previously loaded assets stick around and eventually overload something which causes the script to get stuck)
- ♻️ can refresh and re-paste (the script is smart enough to skip already saved places) but what I usually do is first paste it into a text editor and delete all the entries it already added
🙏 please submit bugs to github repo
- add other sources
- michelin guide
- timeout
- lonely planet
- atlas obscura
- suggest one!
- since we're already using
terser
we could use a proper build system and refactor to modules 🤷 - we could also switch to typescript 🤷
- (very ambitious) identify when stuck, or when css selector has changed, and allow user to hotfix it during execution (by clicking on an item ?)