@wyteco/berkeley-ical
Version:
A command line tool to easily export your classes from the Berkeley Academic Guide to your calendar in iCal (.ics) format. Without having a student account lol!
151 lines • 6.51 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const commander_1 = require("commander");
const cli_progress_1 = __importDefault(require("cli-progress"));
const figlet_1 = __importDefault(require("figlet"));
const zod_1 = require("zod");
//
const ics_1 = require("./ics");
const parse_1 = require("./parse");
// ----------------------------------------------------------------------
const program = new commander_1.Command();
console.log(figlet_1.default.textSync('Berkeley iCal'));
console.log('');
program
.version('1.0.0')
.description('A command line tool to easily export your classes from the Berkeley Academic Guide to your calendar in iCal (.ics) format. Without having a student account lol!')
.option('-v, --verbose', 'Enable verbose mode')
.option('-o, --output <path>', 'Specify output path for the .ics file (default: current directory)', '.')
.arguments('<urls...>')
.action((urls) => {
return urls;
})
.parse(process.argv);
/**
* This stores all the unvalidated options provided by the user.
*/
const programOptions = program.opts();
const isVerbose = !!programOptions.verbose;
if (isVerbose) {
console.log('Verbose mode enabled.');
console.log('');
}
if (isVerbose) {
console.log('options', programOptions);
console.log('');
}
/**
* This stores all the unvalidated URLs provided by the user.
*/
const programArguments = program.args;
if (isVerbose) {
console.log('arguments', programArguments);
console.log('');
}
// ----------------------------------------------------------------------
/**
* Validate the output path provided by the user.
* Note that the default output path is the current directory `.`.
*/
if (!fs_1.default.existsSync(programOptions.output)) {
console.log(`Output path does not exist. Creating directory: "${programOptions.output}".`);
console.log('');
fs_1.default.mkdirSync(programOptions.output, { recursive: true });
}
if (!fs_1.default.statSync(programOptions.output).isDirectory()) {
console.error('The output path is not a valid directory!');
process.exit(1);
}
const validatedOutputPath = programOptions.output;
// ----------------------------------------------------------------------
/**
* Validate the URLs provided by the user.
* Every URL should be a valid URL, we do not just filter out the
* invalid ones.
*/
if (programArguments.length === 0) {
console.error('No URLs provided.');
process.exit(1);
}
const validatedUrls = programArguments.map((argument) => {
try {
return zod_1.z.string().url().parse(argument);
}
catch (error) {
console.error('Invalid URL provided:', argument);
process.exit(1);
}
});
// ----------------------------------------------------------------------
const run = () => __awaiter(void 0, void 0, void 0, function* () {
console.log('Fetching and parsing course data...');
console.log('This may take a while depending on the number of URLs provided.');
console.log('');
const progressBar = new cli_progress_1.default.SingleBar({
format: 'Progress [{bar}] {percentage}% | {value}/{total} URLs',
}, cli_progress_1.default.Presets.shades_classic);
progressBar.start(validatedUrls.length, 0);
/**
* Fetch and parse the course data from the provided URLs.
*/
const courses = yield Promise.all(validatedUrls.map((url) => __awaiter(void 0, void 0, void 0, function* () {
const course = yield (0, parse_1.fetchAndParseCourseData)(url);
progressBar.increment();
return course;
})));
progressBar.stop();
console.log('');
console.log('Fetched and parsed course data of all the URLs.');
console.log(`There is ${courses.length} course${courses.length === 1 ? '' : 's'}.`);
console.log('');
if (isVerbose) {
console.log('courses', courses);
console.log('');
}
// ----------------------------------------------------------------------
/**
* Generate the iCal (.ics) file from the parsed course data.
*/
const icsEventStrings = courses.map((course) => (0, ics_1.generateIcsEvent)(course));
const icsFileString = `BEGIN:VCALENDAR\nVERSION:2.0\n${icsEventStrings.join('\n')}\nEND:VCALENDAR`;
// ----------------------------------------------------------------------
/**
* Write the iCal (.ics) file to the output path.
* We append the current timestamp to the filename to ensure that the program
* can be run multiple times without overwriting the existing file.
* The timestamp is in the format "YYYY-MM-DDTHH-mm-ss" and generated based on ISO.
*/
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
let icsFilePath = `${validatedOutputPath}/berkeley-classes-${timestamp}.ics`;
/**
* This might be overengineering, but we'll add a counter to the filename
* if the file already exists. This is to avoid overwriting the existing file.
* We could also just throw an error, but this is more user-friendly.
*/
let filePathCounter = 1;
while (fs_1.default.existsSync(icsFilePath)) {
console.log(`The file "${icsFilePath}" already exists. Incrementing the counter to avoid overwriting the existing file...`);
icsFilePath = `${validatedOutputPath}/berkeley-classes-${timestamp}-${filePathCounter}.ics`;
filePathCounter++;
}
fs_1.default.writeFileSync(icsFilePath, icsFileString);
console.log(`iCal file generated at "${icsFilePath}".`);
console.log('Done!');
process.exit(0);
});
run();
//# sourceMappingURL=index.js.map