Skip to content

Instantly share code, notes, and snippets.

@MaxEtMoritz
Last active May 25, 2024 16:01
Show Gist options
  • Save MaxEtMoritz/dd4519a181823d6094ed3da0d6eec9da to your computer and use it in GitHub Desktop.
Save MaxEtMoritz/dd4519a181823d6094ed3da0d6eec9da to your computer and use it in GitHub Desktop.
Woov (https://woov.com/) event timetable to ical. Designed to run in NodeJS, but can be adapted for browser if needed.
const fs = require('fs');
const { exit } = require('process');
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const prompt = query => new Promise(resolve => rl.question(query, resolve));
const {sep} = require('path')
// TODO: The current color-coding does not work (first, it seems to be the same for every stage in woov; second, ical expects CSS3 named colors and not css hex string like woov provides)
// TODO: The schema provides the possibility to set artist overrides per show. Have never seen one being set, though. Thus not yet implemented.
const query = `
query ($eid: ID!, $after: String) {
event(eventId: $eid) {
name
shows(first: 100, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
rawId
start
end
artists {
name
imageUrl
links {
url
}
description
}
#artistOverrides {
# artistId
# name
# description
# image
# imageUrl
#}
stage {
name
color
# description
}
}
}
}
}
}
`;
const array_chunks = (array, chunk_size) => Array(Math.ceil(array.length / chunk_size)).fill().map((_, index) => index * chunk_size).map(begin => array.slice(begin, begin + chunk_size));
(async () => {
var flat = [];
let nextPage = true;
let endCursor = null;
const eventId = await prompt('enter your Woov Event ID (see Woov Invite link like https://gowoov.com/event/XXXXX): ');
let eventName = 'Event Timetable';
do {
let body = JSON.stringify({
query,
variables: {
eid: eventId,
after: endCursor
}
})
let response = await (
await fetch('https://api.woov.nl/graphql',{
method: 'POST',
body,
headers:{
'Content-Type': 'application/json'
}
})
).json();
if (response.errors) {
throw new Error(response.errors[0].message);
}
eventName = response.data.event.name;
nextPage = response.data.event.shows.pageInfo.hasNextPage;
endCursor = response.data.event.shows.pageInfo.endCursor;
flat.push(...response.data.event.shows.edges.map(e => e.node));
console.log('loaded', flat.length, 'shows.')
} while (nextPage);
var ics = `BEGIN:VCALENDAR\r
PRODID:${__filename.slice(__filename.lastIndexOf(sep))}\r
VERSION:2.0\r
CALSCALE:GREGORIAN\r
NAME:${eventName}\r
BEGIN:VEVENT\r
`;
ics += flat.map(n => {
let start = new Date(n.start);
let end = new Date(n.end);
let desc = n.artists[0].description ?? '';
if (n.artists[0].links.length > 0) {
desc += '\n\nLinks:\n';
desc += n.artists[0].links.map(l => l.url).join('\n');
}
function icalDate(jsDate) {
let result = '';
result += jsDate.getFullYear().toString().padStart(4, '0');
result += (jsDate.getMonth() + 1).toString().padStart(2, '0');
result += jsDate.getDate().toString().padStart(2, '0');
result += 'T' + jsDate.getHours().toString().padStart(2, '0');
result += jsDate.getMinutes().toString().padStart(2, '0');
result += jsDate.getSeconds().toString().padStart(2, '0');
return result;
}
let details = `UID:${n.rawId}\r
SUMMARY:${n.artists.map(a => a.name).join(' & ')}\r
DTSTAMP:${icalDate(start)}\r
DESCRIPTION:${array_chunks(desc.replace(/\r?\n/g, '\\n'),63).join('\r\n\t')}\r
DTSTART:${icalDate(start)}\r
DTEND:${icalDate(end)}\r
LOCATION:${n.stage.name}\r
CATEGORIES:${n.stage.name}\r
COLOR:${n.stage.color}`;
if(n.artists.flatMap(a=>a.links).length > 0){
details += '\r\n'+n.artists.flatMap(a=>a.links).map(l=>"ATTACH:"+l.url).join('\r\n')
}
if(n.artists[0].imageUrl){
details += `\r\nIMAGE;VALUE=URI:${n.artists[0].imageUrl}`
}
return details;
}).join(`\r
END:VEVENT\r
BEGIN:VEVENT\r
`);
ics += `\r
END:VEVENT\r
END:VCALENDAR\r
`;
fs.writeFileSync('timetable_2.ics', ics);
console.log('done')
exit(0)
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment