|
// ==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(); |
|
})(); |