Skip to content

Instantly share code, notes, and snippets.

@aidinabedi
Last active October 24, 2023 22:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aidinabedi/25b3b738af0092c3428a187dfc1ae3cf to your computer and use it in GitHub Desktop.
Save aidinabedi/25b3b738af0092c3428a187dfc1ae3cf to your computer and use it in GitHub Desktop.
Greasemonkey/Tampermonkey user-script to download your BlenderCon23 schedule as a calendar file (.ics)
// ==UserScript==
// @name BlenderCon23 Schedule Downloader
// @description Download your Blender Conference 2023 schedule as a calendar file (.ics)
// @author Mattias Nyberg & Aidin Abedi
// @match https://conference.blender.org/2023/schedule/*
// @icon https://conference.blender.org/static/conference_main/images/favicon.png
// @namespace https://gist.githubusercontent.com/aidinabedi/25b3b738af0092c3428a187dfc1ae3cf
// @version 1.8
// @updateURL https://gist.githubusercontent.com/aidinabedi/25b3b738af0092c3428a187dfc1ae3cf/raw/blendercon-schedule-downloader.user.js
// @downloadURL https://gist.githubusercontent.com/aidinabedi/25b3b738af0092c3428a187dfc1ae3cf/raw/blendercon-schedule-downloader.user.js
// ==/UserScript==
(function() {
function getScheduleAsICS(useFilters = true) {
const dataGetter = {
_cleanInnerText(el) {
return el.innerText.replace(/^\s+/im, "").replace(/\s+$/im, "");
},
getData() {
const eventElements = document.getElementsByClassName("event");
const toEndTime = (el, data) => {
const minutes = parseInt(el.innerText.replace("m"));
return new Date(data.time.getTime() + minutes * 60 * 1000)
}
const toStartTime = (el, _data) => {
const [dateStr, _] = el.getAttribute("data-day-hour").split(",");
const timeStr = this._cleanInnerText(el.parentElement.getElementsByClassName("time-header")[0]);
const [hour, minute] = timeStr.split(":").map(str => parseInt(str));
const [day, month, year] = dateStr.split("/").map(str => parseInt(str));
const startTime = new Date(2000 + year, month - 1, day, hour, minute);
return startTime;
};
const toIsFiltered = (el, _data) => !!el.getAttribute("data-filtered") || !!el.getAttribute("data-filtered-personal");
const toHref = (el, _data) => el.getAttribute("href");
const toId = (el, _data) => el.getAttribute("id").replace("#", "");
const toLower = (el, _data) => this._cleanInnerText(el).toLowerCase();
const toStatus = (el, _data) => !!el.getAttribute("data-is-checked");
const toText = (el, _data) => this._cleanInnerText(el);
const toSpeakers = (el, _data) => Array.from(el.children).map(c => this._cleanInnerText(c).replace(",", "").trim()).filter(s => s !== "");
const toTags = (el, _data) => Array.from(el.getElementsByClassName("badge")).map(c => this._cleanInnerText(c).toLowerCase());
const mappers = [
["event-category", "category", toLower],
["event-duration", "endTime", toEndTime],
["event-location", "location", toLower],
["event-name", "title", toText],
["going-star", "isGoing", toStatus],
["event-speakers", "speakers", toSpeakers],
["event-tags", "tags", toTags],
];
return Array.from(eventElements).map(
el => mappers.reduce((acc, [select, target, parser]) => {
const raw = el.getElementsByClassName(select)[0]
return {
...acc,
[target]: raw === undefined ? undefined : parser(raw, acc)
}
}, {
"id": toId(el),
"href": toHref(el),
"time": toStartTime(el),
"isFiltered": toIsFiltered(el)
})
);
},
_dateToDTSTAMP(date) {
const [
monthStr,
dayStr,
hourStr,
minuteStr,
secondStr
] = [
date.getUTCMonth() + 1,
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds()
].map(value => (value + "").padStart(2, "0"));
return `${date.getUTCFullYear()}${monthStr}${dayStr}T${hourStr}${minuteStr}${secondStr}Z`
},
_eventToVEVENT(event) {
const dtstart = this._dateToDTSTAMP(event.time);
const dtend = event.endTime ? this._dateToDTSTAMP(event.endTime) : undefined;
const categories = event.tags ? event.tags.map(t => t.toUpperCase()).join(",") : undefined;
const url = event.href ? `https://conference.blender.org${event.href}` : undefined;
const speakers = event.speakers ? event.speakers.join(", ") : undefined;
return [
"BEGIN:VEVENT",
`UID:${event.id}.2023@conference.blender.org`,
`DTSTAMP:${dtstart}`,
`DTSTART:${dtstart}`,
event.location ? `LOCATION:${event.location.toUpperCase()}` : undefined,
dtend ? `DTEND:${dtend}` : undefined,
categories ? `CATEGORIES:${categories}` : undefined,
url ? `URL:${url}` : undefined,
speakers ? `DESCRIPTION:Speakers: ${speakers}` : undefined,
`SUMMARY:${event.title}`,
"GEO:52.37001;04.88415",
"END:VEVENT"
].filter(item => !!item).join("\n");
},
_eventsToVCALENDAR(events) {
const vevents = events.map(
e => this._eventToVEVENT(e)
).join("\n");
return [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//hacksw/handcal//NONSGML v1.0//EN",
vevents,
"END:VCALENDAR"
].join("\n")
},
getDataAsICS(useEventFilters = true) {
const data = useEventFilters ? (
this.getData().filter(e => !e.isFiltered)
) : (
this.getData()
);
console.log(data)
return this._eventsToVCALENDAR(data);
}
}
return dataGetter.getDataAsICS(useFilters);
}
function downloadAsFile(data, filename) {
var link = document.createElement("a");
link.setAttribute('download', filename);
link.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(data));
document.body.appendChild(link);
link.click();
link.remove();
}
function addDownloadButton() {
const parent = document.getElementsByClassName("empty-time-cell")[0];
if (!parent) return;
const button = document.createElement("button");
button.style = "margin: 10px; color: hsl(var(--location-hue-studio), 50%, 50%);";
button.innerText = "Download";
button.addEventListener("click", (event) => {
const data = getScheduleAsICS();
downloadAsFile(data, "schedule.ics");
});
parent.appendChild(button);
}
addDownloadButton();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment