Google Maps Contributions Downloader

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

// ==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.2
// @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.getElementsByTagName("img")) {
      if (img.src.includes("-fo")) {
        let panoId = /AF1Q[^=]*/.exec(img.src)[0]
        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
    semaphore.add()
    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,atob(location.panoId.replaceAll(".", "")).slice(4)]]],[[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]
        })
				.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)
  }
})();