Google Maps Contributions Downloader

Download all the photospheres of a specific Google Maps contributor as a GeoGuessr json

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name Google Maps Contributions Downloader
// @namespace gmcd
// @description Download all the photospheres of a specific Google Maps contributor as a GeoGuessr json
// @version 0.3
// @match https://www.google.com/*
// @run-at document-start
// @license MIT
// ==/UserScript==

(function() {
  locations = []
  semaphore = null
  n = 0
  label = null
  lastUpdate = 0
  function onLoad() {
    h1 = document.querySelector("h1[jsaction='pane.profile-stats.showStats; keydown:pane.profile-stats.showStats']")
    
    div = document.createElement("div")
    div.style = "display: flex; flex-direction: horizontal; align-items: center;"
    
    h1.parentNode.insertBefore(div, h1)
    div.appendChild(h1)
    
    button = document.createElement("button")
    button.innerText = "💾"
    button.classList = h1.classList
    button.addEventListener("click", onClick)
    div.appendChild(button)
    
    label = document.createElement("label")
    label.classList = h1.classList
    div.appendChild(label)
    
    semaphore = new Semaphore(1000)
  }
	window.addEventListener("load", onLoad)

  function onClick() {
    locations = []
    
    for (let img of document.querySelectorAll("button[data-photo-id] img")) {
      if (img.src.includes("-fo")) {
        let panoId = img.closest("button").dataset.photoId.split(":")[1]
        if (panoId.length == 44) {
          panoId = btoa("\b\n\x12," + panoId)
        } else if (panoId.length == 43) {
          panoId = btoa("\b\n\x12+" + panoId).replaceAll("=", "")
        } else if (panoId.length == 42) {
          panoId = btoa("\b\n\x12+" + panoId).replaceAll("=", "")
        }
        locations.push({ lat: 0, lng:0, panoId: panoId })
      }
    }
    
    n = 0
    label.innerText = 0 + "/" + locations.length
    lastUpdate = 0
    for (let location of locations) {
      semaphore.add(getMetadata, location)
    }
  }
  
  class Semaphore {
    constructor(max = 1) {
      this.max = max
      this.compteur = 0
      this.liste = []
    }
 
    add(f, ...args) {
      return new Promise((resolve, reject) => {
        this.liste.push({
          f, args, resolve, reject
        })
        this.next()
      })
    }
 
    next() {
      if (this.liste.length > 0 && this.compteur < this.max) {
        let { f, args, resolve, reject } = this.liste.shift()
        this.compteur++
        f(...args)
          .then(resolve)
          .catch(reject)
          .finally(() => {
            this.compteur--
            this.next()
          })
      }
    }
  }
  
  function getMetadata(location) {
    return new Promise((resolve, reject) => {
      let url = "https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/GetMetadata"
      let init = {
        method: "POST",
        headers: {
          "Content-Type": "application/json+protobuf",
          "x-user-agent": "grpc-web-javascript/0.1"
        },
        body: JSON.stringify([["apiv3"],[],[[[10, location.panoId]]],[[1, 2, 3, 4, 6, 8]]])
      }
      fetch(url, init)
        .then((response) => {
          return response.json()
        })
        .then((response) => {
          location.lat = response[1][0][5][0][1][0][2]
          location.lng = response[1][0][5][0][1][0][3]
          location.panoId = btoa("\x08\x0A\x12" + String.fromCharCode(location.panoId.length) + location.panoId).replaceAll("=", "")
        })
				.catch((response) => {
        	console.error(response)
				})
				.finally(() => {
					resolve()
        
          n++
          if (Date.now() - lastUpdate > 1000) {
            label.innerText = n + "/" + locations.length
            lastUpdate = Date.now()
          }
 
          if (n == locations.length) {
            label.innerText = null
            downloadJSON()
          }
				})
    })
  }
  
  function downloadJSON() {
    let file = new Blob([JSON.stringify(locations)], { type: "application/json" })
    let link = document.createElement("a")
    link.target= "_blank"
    link.href = URL.createObjectURL(file)
    link.download = document.querySelector("h1[jsaction='pane.profile-stats.showStats; keydown:pane.profile-stats.showStats']").innerText + ".json"
    link.click()
    URL.revokeObjectURL(link.href)
  }
})();