Foodmandu Scraper

A usercript to scrape Food Menu from Foodmandu!

// ==UserScript==
// @name         Foodmandu Scraper
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  A usercript to scrape Food Menu from Foodmandu!
// @author       kaley-bhai
// @match        https://foodmandu.com/Restaurant/Details/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=foodmandu.com
// @grant        none
// @require      https://code.jquery.com/jquery-3.6.4.min.js
// @license MIT
// ==/UserScript==

// This Userscript requires jquery to run, If you wanna run it as a Node JS application, you can use Cheerio

(function () {
  ("use strict");

  console.log("Foodmandu Scraper Script has started!");

  $(document).ready(function () {
    /** @type {Array} */
    const categoryItems = [];
    let dataDownloaded = false;
    /** @type {String} */
    const title = $("h1.text-white").text().trim().replace(/ /g, "_");
    /**
     * DOM Mutation Observer, With Angular and React,
     * Dynamic Content might be added after the page load/scroll, this tracks that
     *
     */
    observer = new MutationObserver(function (mutations) {
      clearTimeout(window.mutationTimeout);

      // Set a new timeout to wait for changes to settle
      window.mutationTimeout = setTimeout(function () {
        console.log("DOM changes settled");

        $(".list-header").each((index, element) => {
          const category = $(element).find("div:first-child").text().trim();

          $(element)
            .next("ul")
            .find("li")
            .each((i, e) => {
              const name = $(e).find(".small-title.ng-binding").text().trim();
              const price = $(e)
                .find(".menu__price span.ng-binding:not(.discount)")
                .text()
                .trim();
              const description = $(e).find(".small.dim").text().trim();

              categoryItems.push({
                category,
                name,
                description,
                price,
              });
            });
        });
        if (!dataDownloaded) {
          saveCategoryItems(categoryItems, title);
          dataDownloaded = true;
        }
        //downloadCSV(convertToCSV(categoryItems), 'foodmandu')
      }, 500);
    });
    //Log all the Menu Items
    console.log(categoryItems);

    // Observe changes to the entire document body
    observer.observe(document.body, { subtree: true, childList: true });
  });

  /**
   * Download the menu file to JSON
   *
   * @param {Array} data Menu Item Data to download
   * @param {String} name Name of the File to be saved as
   */
  function saveCategoryItems(data, name) {
    const jsonData = JSON.stringify(data, null, 2);
    const blob = new Blob([jsonData], { type: "application/json" });

    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = name + ".json";
    a.textContent = "Download Data";
    //Simulating a click to start download
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  }

  /**
   * Download the menu file to CSV
   * Currently Bug with the Key-Value Spacing
   *
   * @param {*} data
   */
  function saveCategoryItemsCSV(data) {
    let csvContent =
      "data:text/csv;charset=utf-8," + "Category,Name,Description,Price\n";

    // Convert data to CSV format // Concating err when desc is empty
    data.forEach((item) => {
      csvContent += `${item.category},${item.name},${item.description},${item.price}\n`;
    });

    const encodedUri = encodeURI(csvContent);

    const a = document.createElement("a");
    a.href = encodedUri;
    a.download = "foodmandu_data.csv";
    a.textContent = "Download Data";

    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  }

  /**
   * Function to convert the given Arrray of KV pair to CSV Format
   *
   * @param {Array} arr
   * @return {*}
   */
  function convertToCSV(arr) {
    const array = [Object.keys(arr[0])].concat(arr);

    return array
      .map((it) => {
        return Object.values(it).toString();
      })
      .join("\n");
  }
  /**
   * Function to download CVS, different from the fn: saveCategoryItemsCSV
   *
   * @param {*} data
   * @param {*} filename
   */
  function downloadCSV(data, filename) {
    const csvContent = convertToCSV(data);
    const blob = new Blob([csvContent], { type: "text/csv" });

    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = filename;

    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  }
})();