NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name VelibRideExtract // @namespace https://openuserjs.org//users/codexus // @version 0.1 // @description Extract your Velib rides into CSV // @license GPL-3.0-or-later // @author codexus // @match https://www.velib-metropole.fr/fr/private/account* // @grant GM_notification // @grant window.focus // @grant document // @copyright 2018, codexus (https://openuserjs.org//users/codexus) // ==/UserScript== function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) }; function to_csv({ list, order = null, sep = '\t' }) { order = order || Object.keys(list[0]); let date_replacer = x => (x) ? x.toISOString().replace('T', ' ').replace(/\.[0-9]{3}Z/g, '') : ''; let csv = [order.join(sep)]; for (let o of list) { let row = []; for (let k of order) { if (k.toLowerCase().includes("date")) row.push(date_replacer(o[k])) else row.push(o[k]) }; csv.push(row.join(sep)) } return csv.join('\n') } function write_file({ name, content }) { let a = document.createElement("a"); a.href = `data:text,${content}`; //content a.download = name; //file name a.click(); } function flatten(data) { var result = {}; function recurse(cur, prop) { if (Object(cur) !== cur) { result[prop] = cur; } else if (Array.isArray(cur)) { for (var i = 0, l = cur.length; i < l; i++) recurse(cur[i], prop + "[" + i + "]"); if (l == 0) result[prop] = []; } else { var isEmpty = true; for (var p in cur) { isEmpty = false; recurse(cur[p], prop ? prop + "." + p : p); } if (isEmpty && prop) result[prop] = {}; } } recurse(data, ""); return result; } function notify({ text, title, timeout, onclick, image }) { GM_notification({ text: text, title: 'Velib Extract' || title, timeout: 3000 || timeout, image: 'https://i.stack.imgur.com/geLPT.png' || image, onclick: function () { console.log("Notice clicked."); window.focus(); } || onclick }) } function clean_ride(ride) { ride = flatten(ride) for (let key in ride) { if (key.toLowerCase().includes('date') && ride[key]) { try { let d = new Date(ride[key]); if (!isNaN(d)) ride[key] = d; } catch (e) {} } } return ride } async function fetch_page({ limit, offset }) { notify({ text: `Downloading items #${offset}-${limit+offset} (excl.)` }) try { let url = `https://www.velib-metropole.fr/webapi/private/getCourseList?limit=${limit}&offset=${offset}`; let data = await fetch(url).then(r => r.json()); if (data.actionStatus.status == 'SUCCESS') return data else throw new Error(`Error in downloading page ${limit} ${offset}`); } catch (err) { console.log(err); return {} }; } async function fetch_all({ limit = 10, wait = 500, max = 0 }) { let p1 = await fetch_page({ limit: limit, offset: 0 }), n_rides = p1.paging.totalNumberOfRecords, n_pages = Math.ceil(n_rides / limit), all_pages = [p1]; for (let i = 1; i <= Math.min(max, n_pages); i++) { all_pages.push( sleep(wait * i) .then(() => fetch_page({ limit: limit, offset: limit * i })) ); } let data = await Promise.all(all_pages) .then(list => [].concat.apply([], list.map(r => r.walletOperations || [])) .map(clean_ride) ); notify({ text: `Fetched ${data.length} rides` }) let csv = to_csv({ list: data, sep: ";" }) write_file({ name: "mes_trajets.csv", content: csv }) }; fetch_all({ limit: 100, max: 40 });