65
CHANGELOG.md
@ -5,38 +5,63 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
|
||||
|
||||
## [2.12.0] - Unreleased (Develop Branch)
|
||||
## [2.13.0] - Unreleased (Develop Branch - Please add your contributions to this release.)
|
||||
|
||||
_This release is scheduled to be released on 2020-07-01._
|
||||
_This release is scheduled to be released on 2020-10-01._
|
||||
|
||||
### Added
|
||||
|
||||
- Compliments Module - Add Advice API (https://api.adviceslip.com/) Option
|
||||
|
||||
- Added prettier for an even cleaner codebase
|
||||
|
||||
- Hide Sunrise/Sunset in Weather module
|
||||
|
||||
### Updated
|
||||
|
||||
- Cleaned up alert module code
|
||||
- Cleaned up check_config code
|
||||
- Replaced grunt-based linters with their non-grunt equivalents
|
||||
- Switch to most of the eslint:recommended rules and fix warnings
|
||||
- Replaced insecure links with https ones
|
||||
- Cleaned up all "no-undef" warnings from eslint
|
||||
- Added location title wrapping for calendar module
|
||||
- Change incorrect weather.js default properties.
|
||||
|
||||
### Deleted
|
||||
|
||||
- Removed truetype (ttf) fonts
|
||||
### Fixed
|
||||
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast depending on the endpoint (forecast/daily or forecast)". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Fix calendar display. Account for current timezone. [#2068](https://github.com/MichMich/MagicMirror/issues/2068)
|
||||
- Fix logLevel being set before loading config.
|
||||
- Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MichMich/MagicMirror/issues/2072)
|
||||
|
||||
## [2.12.0] - 2020-07-01
|
||||
|
||||
Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryanzzhu, @chamakura, @DarthBrento, @Ekristoffe, @khassel, @Legion2, @ndom91, @radokristof, @rejas, @XBCreepinJesus & @ZoneMR.
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- Added option to config the level of logging.
|
||||
- Added prettier for an even cleaner codebase.
|
||||
- Hide Sunrise/Sunset in Weather module.
|
||||
- Hide Sunrise/Sunset in Current Weather module.
|
||||
- Added Met Office DataHub (UK) provider.
|
||||
|
||||
### Updated
|
||||
|
||||
- Cleaned up alert module code.
|
||||
- Cleaned up check_config code.
|
||||
- Replaced grunt-based linters with their non-grunt equivalents.
|
||||
- Switch to most of the eslint:recommended rules and fix warnings.
|
||||
- Replaced insecure links with https ones.
|
||||
- Cleaned up all "no-undef" warnings from eslint.
|
||||
- Added location title wrapping for calendar module.
|
||||
- Updated the BG translation.
|
||||
|
||||
### Deleted
|
||||
|
||||
- Removed truetype (ttf) fonts.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The broken modules due to Socket.io change from last release [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
|
||||
- Add backward compatibility for old module code in socketclient.js [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
|
||||
- Support multiple instances of calendar module with different config [#1109](https://github.com/MichMich/MagicMirror/issues/1109)
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast" [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- The broken modules due to Socket.io change from last release. [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
|
||||
- Add backward compatibility for old module code in socketclient.js. [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
|
||||
- Support multiple instances of calendar module with different config. [#1109](https://github.com/MichMich/MagicMirror/issues/1109)
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928)
|
||||
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050)
|
||||
- Updated ical library to latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926)
|
||||
|
||||
## [2.11.0] - 2020-04-01
|
||||
|
||||
|
@ -28,6 +28,7 @@ var config = {
|
||||
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
|
||||
|
||||
language: "en",
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
// serverOnly: true/false/"local" ,
|
||||
|
48
js/app.js
@ -5,20 +5,18 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var Log = require(__dirname + "/logger.js");
|
||||
var Server = require(__dirname + "/server.js");
|
||||
var Utils = require(__dirname + "/utils.js");
|
||||
var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js");
|
||||
var path = require("path");
|
||||
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
// add timestamps in front of log messages
|
||||
require("console-stamp")(console, "yyyy-mm-dd HH:MM:ss.l");
|
||||
|
||||
// Get version number.
|
||||
global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version;
|
||||
console.log("Starting MagicMirror: v" + global.version);
|
||||
Log.log("Starting MagicMirror: v" + global.version);
|
||||
|
||||
// global absolute root path
|
||||
global.root_path = path.resolve(__dirname + "/../");
|
||||
@ -36,10 +34,10 @@ if (process.env.MM_PORT) {
|
||||
// The next part is here to prevent a major exception when there
|
||||
// is no internet connection. This could probable be solved better.
|
||||
process.on("uncaughtException", function (err) {
|
||||
console.log("Whoops! There was an uncaught exception...");
|
||||
console.error(err);
|
||||
console.log("MagicMirror will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
|
||||
console.log("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
|
||||
Log.error("Whoops! There was an uncaught exception...");
|
||||
Log.error(err);
|
||||
Log.error("MagicMirror will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
|
||||
Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
|
||||
});
|
||||
|
||||
/* App - The core app.
|
||||
@ -54,7 +52,7 @@ var App = function () {
|
||||
* argument callback function - The callback function.
|
||||
*/
|
||||
var loadConfig = function (callback) {
|
||||
console.log("Loading config ...");
|
||||
Log.log("Loading config ...");
|
||||
var defaults = require(__dirname + "/defaults.js");
|
||||
|
||||
// For this check proposed to TestSuite
|
||||
@ -72,11 +70,11 @@ var App = function () {
|
||||
callback(config);
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT") {
|
||||
console.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
||||
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
||||
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
|
||||
console.error(Utils.colors.error("WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: " + e.stack));
|
||||
Log.error(Utils.colors.error("WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: " + e.stack));
|
||||
} else {
|
||||
console.error(Utils.colors.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e));
|
||||
Log.error(Utils.colors.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e));
|
||||
}
|
||||
callback(defaults);
|
||||
}
|
||||
@ -94,7 +92,7 @@ var App = function () {
|
||||
}
|
||||
});
|
||||
if (usedDeprecated.length > 0) {
|
||||
console.warn(Utils.colors.warn("WARNING! Your config is using deprecated options: " + usedDeprecated.join(", ") + ". Check README and CHANGELOG for more up-to-date ways of getting the same functionality."));
|
||||
Log.warn(Utils.colors.warn("WARNING! Your config is using deprecated options: " + usedDeprecated.join(", ") + ". Check README and CHANGELOG for more up-to-date ways of getting the same functionality."));
|
||||
}
|
||||
};
|
||||
|
||||
@ -119,7 +117,7 @@ var App = function () {
|
||||
fs.accessSync(helperPath, fs.R_OK);
|
||||
} catch (e) {
|
||||
loadModule = false;
|
||||
console.log("No helper found for module: " + moduleName + ".");
|
||||
Log.log("No helper found for module: " + moduleName + ".");
|
||||
}
|
||||
|
||||
if (loadModule) {
|
||||
@ -127,11 +125,11 @@ var App = function () {
|
||||
var m = new Module();
|
||||
|
||||
if (m.requiresVersion) {
|
||||
console.log("Check MagicMirror version for node helper '" + moduleName + "' - Minimum version: " + m.requiresVersion + " - Current version: " + global.version);
|
||||
Log.log("Check MagicMirror version for node helper '" + moduleName + "' - Minimum version: " + m.requiresVersion + " - Current version: " + global.version);
|
||||
if (cmpVersions(global.version, m.requiresVersion) >= 0) {
|
||||
console.log("Version is ok!");
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
console.log("Version is incorrect. Skip module: '" + moduleName + "'");
|
||||
Log.log("Version is incorrect. Skip module: '" + moduleName + "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -152,7 +150,7 @@ var App = function () {
|
||||
* argument module string - The name of the module (including subpath).
|
||||
*/
|
||||
var loadModules = function (modules, callback) {
|
||||
console.log("Loading module helpers ...");
|
||||
Log.log("Loading module helpers ...");
|
||||
|
||||
var loadNextModule = function () {
|
||||
if (modules.length > 0) {
|
||||
@ -163,7 +161,7 @@ var App = function () {
|
||||
});
|
||||
} else {
|
||||
// All modules are loaded
|
||||
console.log("All module helpers loaded.");
|
||||
Log.log("All module helpers loaded.");
|
||||
callback();
|
||||
}
|
||||
};
|
||||
@ -204,6 +202,8 @@ var App = function () {
|
||||
loadConfig(function (c) {
|
||||
config = c;
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
var modules = [];
|
||||
|
||||
for (var m in config.modules) {
|
||||
@ -215,7 +215,7 @@ var App = function () {
|
||||
|
||||
loadModules(modules, function () {
|
||||
var server = new Server(config, function (app, io) {
|
||||
console.log("Server started ...");
|
||||
Log.log("Server started ...");
|
||||
|
||||
for (var h in nodeHelpers) {
|
||||
var nodeHelper = nodeHelpers[h];
|
||||
@ -224,7 +224,7 @@ var App = function () {
|
||||
nodeHelper.start();
|
||||
}
|
||||
|
||||
console.log("Sockets connected & modules started ...");
|
||||
Log.log("Sockets connected & modules started ...");
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback(config);
|
||||
@ -255,7 +255,7 @@ var App = function () {
|
||||
* this.stop() is called by app.on("before-quit"... in `electron.js`
|
||||
*/
|
||||
process.on("SIGINT", () => {
|
||||
console.log("[SIGINT] Received. Shutting down server...");
|
||||
Log.log("[SIGINT] Received. Shutting down server...");
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000); // Force quit after 3 seconds
|
||||
@ -266,7 +266,7 @@ var App = function () {
|
||||
/* We also need to listen to SIGTERM signals so we stop everything when we are asked to stop by the OS.
|
||||
*/
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("[SIGTERM] Received. Shutting down server...");
|
||||
Log.log("[SIGTERM] Received. Shutting down server...");
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 3000); // Force quit after 3 seconds
|
||||
|
@ -1,14 +1,10 @@
|
||||
/* Magic Mirror
|
||||
*
|
||||
* Checker configuration file
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena
|
||||
* https://rodrigoramirez.com
|
||||
* Check the configuration file for errors
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*
|
||||
*/
|
||||
|
||||
const Linter = require("eslint").Linter;
|
||||
const linter = new Linter();
|
||||
|
||||
@ -17,6 +13,7 @@ const fs = require("fs");
|
||||
|
||||
const rootPath = path.resolve(__dirname + "/../");
|
||||
const config = require(rootPath + "/.eslintrc.json");
|
||||
const Log = require(rootPath + "/js/logger.js");
|
||||
const Utils = require(rootPath + "/js/utils.js");
|
||||
|
||||
/* getConfigFile()
|
||||
@ -34,23 +31,24 @@ function getConfigFile() {
|
||||
|
||||
function checkConfigFile() {
|
||||
const configFileName = getConfigFile();
|
||||
|
||||
// Check if file is present
|
||||
if (fs.existsSync(configFileName) === false) {
|
||||
console.error(Utils.colors.error("File not found: "), configFileName);
|
||||
return;
|
||||
Log.error(Utils.colors.error("File not found: "), configFileName);
|
||||
throw new Error("No config file present!");
|
||||
}
|
||||
|
||||
// check permission
|
||||
try {
|
||||
fs.accessSync(configFileName, fs.F_OK);
|
||||
} catch (e) {
|
||||
console.log(Utils.colors.error(e));
|
||||
return;
|
||||
Log.log(Utils.colors.error(e));
|
||||
throw new Error("No permission to access config file!");
|
||||
}
|
||||
|
||||
// Validate syntax of the configuration file.
|
||||
// In case the there errors show messages and
|
||||
// return
|
||||
console.info(Utils.colors.info("Checking file... "), configFileName);
|
||||
Log.info(Utils.colors.info("Checking file... "), configFileName);
|
||||
|
||||
// I'm not sure if all ever is utf-8
|
||||
fs.readFile(configFileName, "utf-8", function (err, data) {
|
||||
if (err) {
|
||||
@ -58,12 +56,14 @@ function checkConfigFile() {
|
||||
}
|
||||
const messages = linter.verify(data, config);
|
||||
if (messages.length === 0) {
|
||||
console.log("Your configuration file doesn't contain syntax errors :)");
|
||||
Log.log("Your configuration file doesn't contain syntax errors :)");
|
||||
return true;
|
||||
} else {
|
||||
// In case the there errors show messages and return
|
||||
messages.forEach((error) => {
|
||||
console.log("Line", error.line, "col", error.column, error.message);
|
||||
Log.log("Line", error.line, "col", error.column, error.message);
|
||||
});
|
||||
throw new Error("Wrong syntax in config file!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
"use strict";
|
||||
|
||||
const electron = require("electron");
|
||||
const core = require(__dirname + "/app.js");
|
||||
const core = require("./app.js");
|
||||
const Log = require("./logger.js");
|
||||
|
||||
// Config
|
||||
var config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
@ -86,7 +87,7 @@ function createWindow() {
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
app.on("ready", function () {
|
||||
console.log("Launching application.");
|
||||
Log.log("Launching application.");
|
||||
createWindow();
|
||||
});
|
||||
|
||||
@ -110,7 +111,7 @@ app.on("activate", function () {
|
||||
* core.stop() is called by process.on("SIGINT"... in `app.js`
|
||||
*/
|
||||
app.on("before-quit", (event) => {
|
||||
console.log("Shutting down server...");
|
||||
Log.log("Shutting down server...");
|
||||
event.preventDefault();
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
|
@ -182,7 +182,7 @@ var Loader = (function () {
|
||||
}
|
||||
};
|
||||
script.onerror = function () {
|
||||
console.error("Error on loading script:", fileName);
|
||||
Log.error("Error on loading script:", fileName);
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
@ -202,7 +202,7 @@ var Loader = (function () {
|
||||
}
|
||||
};
|
||||
stylesheet.onerror = function () {
|
||||
console.error("Error on loading stylesheet:", fileName);
|
||||
Log.error("Error on loading stylesheet:", fileName);
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
|
32
js/logger.js
@ -1,13 +1,25 @@
|
||||
/* Magic Mirror
|
||||
* Logger
|
||||
* Log
|
||||
*
|
||||
* This logger is very simple, but needs to be extended.
|
||||
* This system can eventually be used to push the log messages to an external target.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Log = (function () {
|
||||
return {
|
||||
(function (root, factory) {
|
||||
if (typeof exports === "object") {
|
||||
// add timestamps in front of log messages
|
||||
require("console-stamp")(console, "yyyy-mm-dd HH:MM:ss.l");
|
||||
|
||||
// Node, CommonJS-like
|
||||
module.exports = factory(root.config);
|
||||
} else {
|
||||
// Browser globals (root is window)
|
||||
root.Log = factory(root.config);
|
||||
}
|
||||
})(this, function (config) {
|
||||
let logLevel = {
|
||||
info: Function.prototype.bind.call(console.info, console),
|
||||
log: Function.prototype.bind.call(console.log, console),
|
||||
error: Function.prototype.bind.call(console.error, console),
|
||||
@ -19,4 +31,16 @@ const Log = (function () {
|
||||
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
|
||||
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
|
||||
};
|
||||
})();
|
||||
|
||||
logLevel.setLogLevel = function (newLevel) {
|
||||
if (newLevel) {
|
||||
Object.keys(logLevel).forEach(function (key, index) {
|
||||
if (!newLevel.includes(key.toLocaleUpperCase())) {
|
||||
logLevel[key] = function () {};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return logLevel;
|
||||
});
|
||||
|
@ -477,6 +477,9 @@ var MM = (function () {
|
||||
init: function () {
|
||||
Log.info("Initializing MagicMirror.");
|
||||
loadConfig();
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
Translator.loadCoreTranslations(config.language);
|
||||
Loader.loadModules();
|
||||
},
|
||||
|
@ -5,20 +5,21 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Class = require("./class.js");
|
||||
const Log = require("./logger.js");
|
||||
const express = require("express");
|
||||
|
||||
var NodeHelper = Class.extend({
|
||||
init: function () {
|
||||
console.log("Initializing new module helper ...");
|
||||
Log.log("Initializing new module helper ...");
|
||||
},
|
||||
|
||||
loaded: function (callback) {
|
||||
console.log("Module helper loaded: " + this.name);
|
||||
Log.log("Module helper loaded: " + this.name);
|
||||
callback();
|
||||
},
|
||||
|
||||
start: function () {
|
||||
console.log("Starting module helper: " + this.name);
|
||||
Log.log("Starting module helper: " + this.name);
|
||||
},
|
||||
|
||||
/* stop()
|
||||
@ -28,7 +29,7 @@ var NodeHelper = Class.extend({
|
||||
*
|
||||
*/
|
||||
stop: function () {
|
||||
console.log("Stopping module helper: " + this.name);
|
||||
Log.log("Stopping module helper: " + this.name);
|
||||
},
|
||||
|
||||
/* socketNotificationReceived(notification, payload)
|
||||
@ -38,7 +39,7 @@ var NodeHelper = Class.extend({
|
||||
* argument payload mixed - The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
console.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
|
||||
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
|
||||
},
|
||||
|
||||
/* setName(name)
|
||||
@ -92,7 +93,7 @@ var NodeHelper = Class.extend({
|
||||
var self = this;
|
||||
self.io = io;
|
||||
|
||||
console.log("Connecting socket for: " + this.name);
|
||||
Log.log("Connecting socket for: " + this.name);
|
||||
var namespace = this.name;
|
||||
io.of(namespace).on("connection", function (socket) {
|
||||
// add a catch all event.
|
||||
@ -107,7 +108,7 @@ var NodeHelper = Class.extend({
|
||||
// register catch all.
|
||||
socket.on("*", function (notification, payload) {
|
||||
if (notification !== "*") {
|
||||
//console.log('received message in namespace: ' + namespace);
|
||||
//Log.log('received message in namespace: ' + namespace);
|
||||
self.socketNotificationReceived(notification, payload);
|
||||
}
|
||||
});
|
||||
|
11
js/server.js
@ -4,14 +4,15 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var express = require("express");
|
||||
var app = require("express")();
|
||||
var path = require("path");
|
||||
var ipfilter = require("express-ipfilter").IpFilter;
|
||||
var fs = require("fs");
|
||||
var helmet = require("helmet");
|
||||
var Utils = require(__dirname + "/utils.js");
|
||||
|
||||
var Log = require("./logger.js");
|
||||
var Utils = require("./utils.js");
|
||||
|
||||
var Server = function (config, callback) {
|
||||
var port = config.port;
|
||||
@ -31,12 +32,12 @@ var Server = function (config, callback) {
|
||||
}
|
||||
var io = require("socket.io")(server);
|
||||
|
||||
console.log("Starting server on port " + port + " ... ");
|
||||
Log.log("Starting server on port " + port + " ... ");
|
||||
|
||||
server.listen(port, config.address ? config.address : "localhost");
|
||||
|
||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||
console.info(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
Log.info(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
}
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
@ -44,7 +45,7 @@ var Server = function (config, callback) {
|
||||
if (err === undefined) {
|
||||
return next();
|
||||
}
|
||||
console.log(err.message);
|
||||
Log.log(err.message);
|
||||
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
||||
});
|
||||
});
|
||||
|
@ -282,7 +282,6 @@ Module.register("calendar", {
|
||||
timeWrapper = document.createElement("td");
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
//console.log(event.today);
|
||||
var now = new Date();
|
||||
// Define second, minute, hour, and day variables
|
||||
var oneSecond = 1000; // 1,000 milliseconds
|
||||
@ -373,7 +372,6 @@ Module.register("calendar", {
|
||||
}
|
||||
}
|
||||
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
|
||||
//console.log(event);
|
||||
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
}
|
||||
@ -469,6 +467,7 @@ Module.register("calendar", {
|
||||
var calendar = this.calendarData[c];
|
||||
for (var e in calendar) {
|
||||
var event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||
|
||||
if (event.endDate < now) {
|
||||
continue;
|
||||
}
|
||||
|
@ -4,28 +4,29 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const ical = require("./vendor/ical.js");
|
||||
const Log = require("../../../js/logger.js");
|
||||
const ical = require("ical");
|
||||
const moment = require("moment");
|
||||
const request = require("request");
|
||||
|
||||
var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) {
|
||||
var self = this;
|
||||
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNumberOfDays, auth, includePastEvents) {
|
||||
const self = this;
|
||||
|
||||
var reloadTimer = null;
|
||||
var events = [];
|
||||
let reloadTimer = null;
|
||||
let events = [];
|
||||
|
||||
var fetchFailedCallback = function () {};
|
||||
var eventsReceivedCallback = function () {};
|
||||
let fetchFailedCallback = function () {};
|
||||
let eventsReceivedCallback = function () {};
|
||||
|
||||
/* fetchCalendar()
|
||||
* Initiates calendar fetch.
|
||||
*/
|
||||
var fetchCalendar = function () {
|
||||
const fetchCalendar = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
|
||||
var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
var opts = {
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const opts = {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
||||
},
|
||||
@ -40,42 +41,40 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
} else {
|
||||
opts.auth = {
|
||||
user: auth.user,
|
||||
pass: auth.pass
|
||||
pass: auth.pass,
|
||||
sendImmediately: auth.method !== "digest"
|
||||
};
|
||||
|
||||
if (auth.method === "digest") {
|
||||
opts.auth.sendImmediately = false;
|
||||
} else {
|
||||
opts.auth.sendImmediately = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ical.fromURL(url, opts, function (err, data) {
|
||||
request(url, opts, function (err, r, requestData) {
|
||||
if (err) {
|
||||
fetchFailedCallback(self, err);
|
||||
scheduleTimer();
|
||||
return;
|
||||
} else if (r.statusCode !== 200) {
|
||||
fetchFailedCallback(self, r.statusCode + ": " + r.statusMessage);
|
||||
scheduleTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(data);
|
||||
var newEvents = [];
|
||||
const data = ical.parseICS(requestData);
|
||||
const newEvents = [];
|
||||
|
||||
// limitFunction doesn't do much limiting, see comment re: the dates array in rrule section below as to why we need to do the filtering ourselves
|
||||
var limitFunction = function (date, i) {
|
||||
const limitFunction = function (date, i) {
|
||||
return true;
|
||||
};
|
||||
|
||||
var eventDate = function (event, time) {
|
||||
const eventDate = function (event, time) {
|
||||
return event[time].length === 8 ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
};
|
||||
|
||||
for (var e in data) {
|
||||
var event = data[e];
|
||||
var now = new Date();
|
||||
var today = moment().startOf("day").toDate();
|
||||
var future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
var past = today;
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day").toDate();
|
||||
const future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
let past = today;
|
||||
|
||||
if (includePastEvents) {
|
||||
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
|
||||
@ -83,7 +82,7 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
|
||||
// FIXME: Ugly fix to solve the facebook birthday issue.
|
||||
// Otherwise, the recurring events only show the birthday for next year.
|
||||
var isFacebookBirthday = false;
|
||||
let isFacebookBirthday = false;
|
||||
if (typeof event.uid !== "undefined") {
|
||||
if (event.uid.indexOf("@facebook.com") !== -1) {
|
||||
isFacebookBirthday = true;
|
||||
@ -91,13 +90,13 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
}
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
var startDate = eventDate(event, "start");
|
||||
var endDate;
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
var dur = moment.duration(event.duration);
|
||||
endDate = startDate.clone().add(dur);
|
||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
endDate = startDate;
|
||||
@ -106,20 +105,20 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the duration f the event for use with recurring events.
|
||||
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
// calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
var title = getTitleFromEvent(event);
|
||||
const title = getTitleFromEvent(event);
|
||||
|
||||
var excluded = false,
|
||||
let excluded = false,
|
||||
dateFilter = null;
|
||||
|
||||
for (var f in excludedEvents) {
|
||||
var filter = excludedEvents[f],
|
||||
for (let f in excludedEvents) {
|
||||
let filter = excludedEvents[f],
|
||||
testTitle = title.toLowerCase(),
|
||||
until = null,
|
||||
useRegex = false,
|
||||
@ -162,16 +161,19 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
}
|
||||
|
||||
if (excluded) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
var location = event.location || false;
|
||||
var geo = event.geo || false;
|
||||
var description = event.description || false;
|
||||
const location = event.location || false;
|
||||
const geo = event.geo || false;
|
||||
const description = event.description || false;
|
||||
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
var rule = event.rrule;
|
||||
var addedEvents = 0;
|
||||
const rule = event.rrule;
|
||||
let addedEvents = 0;
|
||||
|
||||
const pastMoment = moment(past);
|
||||
const futureMoment = moment(future);
|
||||
|
||||
// can cause problems with e.g. birthdays before 1900
|
||||
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
|
||||
@ -180,28 +182,21 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
}
|
||||
|
||||
// For recurring events, get the set of start dates that fall within the range
|
||||
// of dates we"re looking for.
|
||||
// of dates we're looking for.
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
var pastLocal = moment(past).subtract(past.getTimezoneOffset(), "minutes").toDate();
|
||||
var futureLocal = moment(future).subtract(future.getTimezoneOffset(), "minutes").toDate();
|
||||
var datesLocal = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
var dates = datesLocal.map(function (dateLocal) {
|
||||
var date = moment(dateLocal).add(dateLocal.getTimezoneOffset(), "minutes").toDate();
|
||||
return date;
|
||||
});
|
||||
const pastLocal = pastMoment.subtract(past.getTimezoneOffset(), "minutes").toDate();
|
||||
const futureLocal = futureMoment.subtract(future.getTimezoneOffset(), "minutes").toDate();
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it"s possible for us to have a specific recurrence that
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
// we"ll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||
// because the logic below will filter out any recurrences that don"t actually belong within
|
||||
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||
// because the logic below will filter out any recurrences that don't actually belong within
|
||||
// our display range.
|
||||
// Would be great if there was a better way to handle this.
|
||||
if (event.recurrences !== undefined) {
|
||||
var pastMoment = moment(past);
|
||||
var futureMoment = moment(future);
|
||||
|
||||
for (var r in event.recurrences) {
|
||||
for (let r in event.recurrences) {
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don"t double-add those events.
|
||||
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
|
||||
@ -211,31 +206,25 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
}
|
||||
|
||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||
for (var d in dates) {
|
||||
var date = dates[d];
|
||||
for (let d in dates) {
|
||||
const date = dates[d];
|
||||
// ical.js started returning recurrences and exdates as ISOStrings without time information.
|
||||
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
|
||||
// (see https://github.com/peterbraden/ical.js/pull/84 )
|
||||
var dateKey = date.toISOString().substring(0, 10);
|
||||
var curEvent = event;
|
||||
var showRecurrence = true;
|
||||
|
||||
// Stop parsing this event's recurrences if we've already found maximumEntries worth of recurrences.
|
||||
// (The logic below would still filter the extras, but the check is simple since we're already tracking the count)
|
||||
if (addedEvents >= maximumEntries) {
|
||||
break;
|
||||
}
|
||||
const dateKey = date.toISOString().substring(0, 10);
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
startDate = moment(date);
|
||||
|
||||
// For each date that we"re checking, it"s possible that there is a recurrence override for that one day.
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
startDate = moment(curEvent.start);
|
||||
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||
}
|
||||
// If there"s no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
@ -246,7 +235,7 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
|
||||
var recurrenceTitle = getTitleFromEvent(curEvent);
|
||||
const recurrenceTitle = getTitleFromEvent(curEvent);
|
||||
|
||||
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
|
||||
// it to the event list.
|
||||
@ -258,7 +247,7 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true && addedEvents < maximumEntries) {
|
||||
if (showRecurrence === true) {
|
||||
addedEvents++;
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
@ -275,43 +264,41 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
}
|
||||
// end recurring event parsing
|
||||
} else {
|
||||
// console.log("Single event ...");
|
||||
// Single event.
|
||||
var fullDayEvent = isFacebookBirthday ? true : isFullDayEvent(event);
|
||||
const fullDayEvent = isFacebookBirthday ? true : isFullDayEvent(event);
|
||||
|
||||
if (includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
if (endDate < past) {
|
||||
//console.log("Past event is too far in the past. So skip: " + title);
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// It's not a fullday event, and it is in the past, so skip.
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
//console.log("It's not a fullday event, and it is in the past. So skip: " + title);
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a fullday event, and it is before today, So skip.
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
//console.log("It's a fullday event, and it is before today. So skip: " + title);
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||
if (startDate > future) {
|
||||
//console.log("It exceeds the maximumNumberOfDays limit. So skip: " + title);
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeFilterApplies(now, endDate, dateFilter)) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
// adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
|
||||
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
|
||||
if (fullDayEvent && startDate <= today) {
|
||||
startDate = moment(today);
|
||||
}
|
||||
|
||||
// Every thing is good. Add it to the list.
|
||||
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: startDate.format("x"),
|
||||
@ -324,15 +311,13 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newEvents.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
//console.log(newEvents);
|
||||
|
||||
events = newEvents.slice(0, maximumEntries);
|
||||
events = newEvents;
|
||||
|
||||
self.broadcastEvents();
|
||||
scheduleTimer();
|
||||
@ -342,8 +327,7 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
/* scheduleTimer()
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
var scheduleTimer = function () {
|
||||
//console.log('Schedule update timer.');
|
||||
const scheduleTimer = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchCalendar();
|
||||
@ -357,14 +341,14 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
*
|
||||
* return bool - The event is a fullday event.
|
||||
*/
|
||||
var isFullDayEvent = function (event) {
|
||||
const isFullDayEvent = function (event) {
|
||||
if (event.start.length === 8 || event.start.dateOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var start = event.start || 0;
|
||||
var startDate = new Date(start);
|
||||
var end = event.end || 0;
|
||||
const start = event.start || 0;
|
||||
const startDate = new Date(start);
|
||||
const end = event.end || 0;
|
||||
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
|
||||
// Is 24 hours, and starts on the middle of the night.
|
||||
return true;
|
||||
@ -382,11 +366,11 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
*
|
||||
* return bool - The event should be filtered out
|
||||
*/
|
||||
var timeFilterApplies = function (now, endDate, filter) {
|
||||
const timeFilterApplies = function (now, endDate, filter) {
|
||||
if (filter) {
|
||||
var until = filter.split(" "),
|
||||
const until = filter.split(" "),
|
||||
value = parseInt(until[0]),
|
||||
increment = until[1].slice("-1") === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
|
||||
increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
|
||||
filterUntil = moment(endDate.format()).subtract(value, increment);
|
||||
|
||||
return now < filterUntil.format("x");
|
||||
@ -402,8 +386,8 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
*
|
||||
* return string - The title of the event, or "Event" if no title is found.
|
||||
*/
|
||||
var getTitleFromEvent = function (event) {
|
||||
var title = "Event";
|
||||
const getTitleFromEvent = function (event) {
|
||||
let title = "Event";
|
||||
if (event.summary) {
|
||||
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
|
||||
} else if (event.description) {
|
||||
@ -413,7 +397,7 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
return title;
|
||||
};
|
||||
|
||||
var testTitleByFilter = function (title, filter, useRegex, regexFlags) {
|
||||
const testTitleByFilter = function (title, filter, useRegex, regexFlags) {
|
||||
if (useRegex) {
|
||||
// Assume if leading slash, there is also trailing slash
|
||||
if (filter[0] === "/") {
|
||||
@ -442,7 +426,7 @@ var CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntr
|
||||
* Broadcast the existing events.
|
||||
*/
|
||||
this.broadcastEvents = function () {
|
||||
//console.log('Broadcasting ' + events.length + ' events.');
|
||||
Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
|
||||
eventsReceivedCallback(self);
|
||||
};
|
||||
|
||||
|
@ -5,24 +5,23 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
|
||||
var CalendarFetcher = require("./calendarfetcher.js");
|
||||
|
||||
var url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
|
||||
// var url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first)
|
||||
var fetchInterval = 60 * 60 * 1000;
|
||||
var maximumEntries = 10;
|
||||
var maximumNumberOfDays = 365;
|
||||
var user = "magicmirror";
|
||||
var pass = "MyStrongPass";
|
||||
var auth = {
|
||||
const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
|
||||
//const url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first)
|
||||
const fetchInterval = 60 * 60 * 1000;
|
||||
const maximumEntries = 10;
|
||||
const maximumNumberOfDays = 365;
|
||||
const user = "magicmirror";
|
||||
const pass = "MyStrongPass";
|
||||
const auth = {
|
||||
user: user,
|
||||
pass: pass
|
||||
};
|
||||
|
||||
console.log("Create fetcher ...");
|
||||
|
||||
var fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maximumNumberOfDays, auth);
|
||||
const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maximumNumberOfDays, auth);
|
||||
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
console.log(fetcher.events());
|
||||
|
@ -5,25 +5,22 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var NodeHelper = require("node_helper");
|
||||
var validUrl = require("valid-url");
|
||||
var CalendarFetcher = require("./calendarfetcher.js");
|
||||
const NodeHelper = require("node_helper");
|
||||
const validUrl = require("valid-url");
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
const Log = require("../../../js/logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
var events = [];
|
||||
|
||||
Log.log("Starting node helper for: " + this.name);
|
||||
this.fetchers = [];
|
||||
|
||||
console.log("Starting node helper for: " + this.name);
|
||||
},
|
||||
|
||||
// Override socketNotificationReceived method.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "ADD_CALENDAR") {
|
||||
//console.log('ADD_CALENDAR: ');
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id);
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id);
|
||||
}
|
||||
},
|
||||
|
||||
@ -34,8 +31,7 @@ module.exports = NodeHelper.create({
|
||||
* attribute url string - URL of the news feed.
|
||||
* attribute reloadInterval number - Reload interval in milliseconds.
|
||||
*/
|
||||
|
||||
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, identifier) {
|
||||
createFetcher: function (url, fetchInterval, excludedEvents, maximumNumberOfDays, auth, broadcastPastEvents, identifier) {
|
||||
var self = this;
|
||||
|
||||
if (!validUrl.isUri(url)) {
|
||||
@ -45,13 +41,10 @@ module.exports = NodeHelper.create({
|
||||
|
||||
var fetcher;
|
||||
if (typeof self.fetchers[identifier + url] === "undefined") {
|
||||
console.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents);
|
||||
Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumNumberOfDays, auth, broadcastPastEvents);
|
||||
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
//console.log('Broadcast events.');
|
||||
//console.log(fetcher.events());
|
||||
|
||||
self.sendSocketNotification("CALENDAR_EVENTS", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
@ -60,7 +53,7 @@ module.exports = NodeHelper.create({
|
||||
});
|
||||
|
||||
fetcher.onError(function (fetcher, error) {
|
||||
console.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
|
||||
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
|
||||
self.sendSocketNotification("FETCH_ERROR", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
@ -70,7 +63,7 @@ module.exports = NodeHelper.create({
|
||||
|
||||
self.fetchers[identifier + url] = fetcher;
|
||||
} else {
|
||||
//console.log('Use existing news fetcher for url: ' + url);
|
||||
Log.log("Use existing calendar fetcher for url: " + url);
|
||||
fetcher = self.fetchers[identifier + url];
|
||||
fetcher.broadcastEvents();
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "8.9"
|
||||
install: npm install
|
178
modules/default/calendar/vendor/ical.js/LICENSE
vendored
@ -1,178 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
13
modules/default/calendar/vendor/ical.js/NOTICE
vendored
@ -1,13 +0,0 @@
|
||||
Copyright 2012 Peter Braden
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const ical = require('ical');
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function (err, data) {
|
||||
for (let k in data) {
|
||||
if (data.hasOwnProperty(k)) {
|
||||
var ev = data[k];
|
||||
if (data[k].type == 'VEVENT') {
|
||||
console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -1,118 +0,0 @@
|
||||
var ical = require('./node-ical')
|
||||
var moment = require('moment')
|
||||
|
||||
var data = ical.parseFile('./examples/example_rrule.ics');
|
||||
|
||||
// Complicated example demonstrating how to handle recurrence rules and exceptions.
|
||||
|
||||
for (var k in data) {
|
||||
|
||||
// When dealing with calendar recurrences, you need a range of dates to query against,
|
||||
// because otherwise you can get an infinite number of calendar events.
|
||||
var rangeStart = moment("2017-01-01");
|
||||
var rangeEnd = moment("2017-12-31");
|
||||
|
||||
|
||||
var event = data[k]
|
||||
if (event.type === 'VEVENT') {
|
||||
|
||||
var title = event.summary;
|
||||
var startDate = moment(event.start);
|
||||
var endDate = moment(event.end);
|
||||
|
||||
// Calculate the duration of the event for use with recurring events.
|
||||
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
// Simple case - no recurrences, just print out the calendar event.
|
||||
if (typeof event.rrule === 'undefined')
|
||||
{
|
||||
console.log('title:' + title);
|
||||
console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('duration:' + moment.duration(duration).humanize());
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Complicated case - if an RRULE exists, handle multiple recurrences of the event.
|
||||
else if (typeof event.rrule !== 'undefined')
|
||||
{
|
||||
// For recurring events, get the set of event start dates that fall within the range
|
||||
// of dates we're looking for.
|
||||
var dates = event.rrule.between(
|
||||
rangeStart.toDate(),
|
||||
rangeEnd.toDate(),
|
||||
true,
|
||||
function(date, i) {return true;}
|
||||
)
|
||||
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. One way to handle this is
|
||||
// to add *all* recurrence override entries into the set of dates that we check, and then later
|
||||
// filter out any recurrences that don't actually belong within our range.
|
||||
if (event.recurrences != undefined)
|
||||
{
|
||||
for (var r in event.recurrences)
|
||||
{
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don't double-add those events.
|
||||
if (moment(new Date(r)).isBetween(rangeStart, rangeEnd) != true)
|
||||
{
|
||||
dates.push(new Date(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through the set of date entries to see which recurrences should be printed.
|
||||
for(var i in dates) {
|
||||
|
||||
var date = dates[i];
|
||||
var curEvent = event;
|
||||
var showRecurrence = true;
|
||||
var curDuration = duration;
|
||||
|
||||
startDate = moment(date);
|
||||
|
||||
// Use just the date of the recurrence to look up overrides and exceptions (i.e. chop off time information)
|
||||
var dateLookupKey = date.toISOString().substring(0, 10);
|
||||
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if ((curEvent.recurrences != undefined) && (curEvent.recurrences[dateLookupKey] != undefined))
|
||||
{
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateLookupKey];
|
||||
startDate = moment(curEvent.start);
|
||||
curDuration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||
}
|
||||
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
else if ((curEvent.exdate != undefined) && (curEvent.exdate[dateLookupKey] != undefined))
|
||||
{
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
// Set the the title and the end date from either the regular event or the recurrence override.
|
||||
var recurrenceTitle = curEvent.summary;
|
||||
endDate = moment(parseInt(startDate.format("x")) + curDuration, 'x');
|
||||
|
||||
// If this recurrence ends before the start of the date range, or starts after the end of the date range,
|
||||
// don't process it.
|
||||
if (endDate.isBefore(rangeStart) || startDate.isAfter(rangeEnd)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
|
||||
console.log('title:' + recurrenceTitle);
|
||||
console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('duration:' + moment.duration(curDuration).humanize());
|
||||
console.log();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,40 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:ical
|
||||
X-WR-TIMEZONE:US/Central
|
||||
X-WR-CALDESC:
|
||||
BEGIN:VEVENT
|
||||
UID:98765432-ABCD-DCBB-999A-987765432123
|
||||
DTSTART;TZID=US/Central:20170601T090000
|
||||
DTEND;TZID=US/Central:20170601T170000
|
||||
DTSTAMP:20170727T044436Z
|
||||
EXDATE;TZID=US/Central:20170706T090000,20170713T090000,20170720T090000,20
|
||||
170803T090000
|
||||
LAST-MODIFIED:20170727T044435Z
|
||||
RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20170814T045959Z;BYDAY=TH
|
||||
SEQUENCE:0
|
||||
SUMMARY:Recurring weekly meeting from June 1 - Aug 14 (except July 6, July 13, July 20, Aug 3)
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:98765432-ABCD-DCBB-999A-987765432123
|
||||
RECURRENCE-ID;TZID=US/Central:20170629T090000
|
||||
DTSTART;TZID=US/Central:20170703T090000
|
||||
DTEND;TZID=US/Central:20170703T120000
|
||||
DTSTAMP:20170727T044436Z
|
||||
LAST-MODIFIED:20170216T143445Z
|
||||
SEQUENCE:0
|
||||
SUMMARY:Last meeting in June moved to Monday July 3 and shortened to half day
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:12354454-ABCD-DCBB-999A-2349872354897
|
||||
DTSTART;TZID=US/Central:20171201T130000
|
||||
DTEND;TZID=US/Central:20171201T150000
|
||||
DTSTAMP:20170727T044436Z
|
||||
LAST-MODIFIED:20170727T044435Z
|
||||
SEQUENCE:0
|
||||
SUMMARY:Single event on Dec 1
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
452
modules/default/calendar/vendor/ical.js/ical.js
vendored
@ -1,452 +0,0 @@
|
||||
(function(name, definition) {
|
||||
|
||||
/****************
|
||||
* A tolerant, minimal icalendar parser
|
||||
* (http://tools.ietf.org/html/rfc5545)
|
||||
*
|
||||
* <peterbraden@peterbraden.co.uk>
|
||||
* **************/
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = definition();
|
||||
} else if (typeof define === 'function' && typeof define.amd === 'object'){
|
||||
define(definition);
|
||||
} else {
|
||||
this[name] = definition();
|
||||
}
|
||||
|
||||
}('ical', function(){
|
||||
|
||||
// Unescape Text re RFC 4.3.11
|
||||
var text = function(t){
|
||||
t = t || "";
|
||||
return (t
|
||||
.replace(/\\\,/g, ',')
|
||||
.replace(/\\\;/g, ';')
|
||||
.replace(/\\[nN]/g, '\n')
|
||||
.replace(/\\\\/g, '\\')
|
||||
)
|
||||
}
|
||||
|
||||
var parseParams = function(p){
|
||||
var out = {}
|
||||
for (var i = 0; i<p.length; i++){
|
||||
if (p[i].indexOf('=') > -1){
|
||||
var segs = p[i].split('=');
|
||||
|
||||
out[segs[0]] = parseValue(segs.slice(1).join('='));
|
||||
|
||||
}
|
||||
}
|
||||
return out || sp
|
||||
}
|
||||
|
||||
var parseValue = function(val){
|
||||
if ('TRUE' === val)
|
||||
return true;
|
||||
|
||||
if ('FALSE' === val)
|
||||
return false;
|
||||
|
||||
var number = Number(val);
|
||||
if (!isNaN(number))
|
||||
return number;
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
var storeValParam = function (name) {
|
||||
return function (val, curr) {
|
||||
var current = curr[name];
|
||||
if (Array.isArray(current)) {
|
||||
current.push(val);
|
||||
return curr;
|
||||
}
|
||||
|
||||
if (current != null) {
|
||||
curr[name] = [current, val];
|
||||
return curr;
|
||||
}
|
||||
|
||||
curr[name] = val;
|
||||
return curr
|
||||
}
|
||||
}
|
||||
|
||||
var storeParam = function (name) {
|
||||
return function (val, params, curr) {
|
||||
var data;
|
||||
if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) {
|
||||
data = { params: parseParams(params), val: text(val) }
|
||||
}
|
||||
else
|
||||
data = text(val)
|
||||
|
||||
return storeValParam(name)(data, curr);
|
||||
}
|
||||
}
|
||||
|
||||
var addTZ = function (dt, params) {
|
||||
var p = parseParams(params);
|
||||
|
||||
if (params && p){
|
||||
dt.tz = p.TZID
|
||||
}
|
||||
|
||||
return dt
|
||||
}
|
||||
|
||||
var dateParam = function(name){
|
||||
return function (val, params, curr) {
|
||||
|
||||
var newDate = text(val);
|
||||
|
||||
|
||||
if (params && params[0] === "VALUE=DATE") {
|
||||
// Just Date
|
||||
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val);
|
||||
if (comps !== null) {
|
||||
// No TZ info - assume same timezone as this computer
|
||||
newDate = new Date(
|
||||
comps[1],
|
||||
parseInt(comps[2], 10)-1,
|
||||
comps[3]
|
||||
);
|
||||
|
||||
newDate = addTZ(newDate, params);
|
||||
newDate.dateOnly = true;
|
||||
|
||||
// Store as string - worst case scenario
|
||||
return storeValParam(name)(newDate, curr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//typical RFC date-time format
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val);
|
||||
if (comps !== null) {
|
||||
if (comps[7] == 'Z'){ // GMT
|
||||
newDate = new Date(Date.UTC(
|
||||
parseInt(comps[1], 10),
|
||||
parseInt(comps[2], 10)-1,
|
||||
parseInt(comps[3], 10),
|
||||
parseInt(comps[4], 10),
|
||||
parseInt(comps[5], 10),
|
||||
parseInt(comps[6], 10 )
|
||||
));
|
||||
// TODO add tz
|
||||
} else {
|
||||
newDate = new Date(
|
||||
parseInt(comps[1], 10),
|
||||
parseInt(comps[2], 10)-1,
|
||||
parseInt(comps[3], 10),
|
||||
parseInt(comps[4], 10),
|
||||
parseInt(comps[5], 10),
|
||||
parseInt(comps[6], 10)
|
||||
);
|
||||
}
|
||||
|
||||
newDate = addTZ(newDate, params);
|
||||
}
|
||||
|
||||
|
||||
// Store as string - worst case scenario
|
||||
return storeValParam(name)(newDate, curr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var geoParam = function(name){
|
||||
return function(val, params, curr){
|
||||
storeParam(val, params, curr)
|
||||
var parts = val.split(';');
|
||||
curr[name] = {lat:Number(parts[0]), lon:Number(parts[1])};
|
||||
return curr
|
||||
}
|
||||
}
|
||||
|
||||
var categoriesParam = function (name) {
|
||||
var separatorPattern = /\s*,\s*/g;
|
||||
return function (val, params, curr) {
|
||||
storeParam(val, params, curr)
|
||||
if (curr[name] === undefined)
|
||||
curr[name] = val ? val.split(separatorPattern) : []
|
||||
else
|
||||
if (val)
|
||||
curr[name] = curr[name].concat(val.split(separatorPattern))
|
||||
return curr
|
||||
}
|
||||
}
|
||||
|
||||
// EXDATE is an entry that represents exceptions to a recurrence rule (ex: "repeat every day except on 7/4").
|
||||
// The EXDATE entry itself can also contain a comma-separated list, so we make sure to parse each date out separately.
|
||||
// There can also be more than one EXDATE entries in a calendar record.
|
||||
// Since there can be multiple dates, we create an array of them. The index into the array is the ISO string of the date itself, for ease of use.
|
||||
// i.e. You can check if ((curr.exdate != undefined) && (curr.exdate[date iso string] != undefined)) to see if a date is an exception.
|
||||
// NOTE: This specifically uses date only, and not time. This is to avoid a few problems:
|
||||
// 1. The ISO string with time wouldn't work for "floating dates" (dates without timezones).
|
||||
// ex: "20171225T060000" - this is supposed to mean 6 AM in whatever timezone you're currently in
|
||||
// 2. Daylight savings time potentially affects the time you would need to look up
|
||||
// 3. Some EXDATE entries in the wild seem to have times different from the recurrence rule, but are still excluded by calendar programs. Not sure how or why.
|
||||
// These would fail any sort of sane time lookup, because the time literally doesn't match the event. So we'll ignore time and just use date.
|
||||
// ex: DTSTART:20170814T140000Z
|
||||
// RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
|
||||
// EXDATE:20171219T060000
|
||||
// Even though "T060000" doesn't match or overlap "T1400000Z", it's still supposed to be excluded? Odd. :(
|
||||
// TODO: See if this causes any problems with events that recur multiple times a day.
|
||||
var exdateParam = function (name) {
|
||||
return function (val, params, curr) {
|
||||
var separatorPattern = /\s*,\s*/g;
|
||||
curr[name] = curr[name] || [];
|
||||
var dates = val ? val.split(separatorPattern) : [];
|
||||
dates.forEach(function (entry) {
|
||||
var exdate = new Array();
|
||||
dateParam(name)(entry, params, exdate);
|
||||
|
||||
if (exdate[name])
|
||||
{
|
||||
if (typeof exdate[name].toISOString === 'function') {
|
||||
curr[name][exdate[name].toISOString().substring(0, 10)] = exdate[name];
|
||||
} else {
|
||||
console.error("No toISOString function in exdate[name]", exdate[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return curr;
|
||||
}
|
||||
}
|
||||
|
||||
// RECURRENCE-ID is the ID of a specific recurrence within a recurrence rule.
|
||||
// TODO: It's also possible for it to have a range, like "THISANDPRIOR", "THISANDFUTURE". This isn't currently handled.
|
||||
var recurrenceParam = function (name) {
|
||||
return dateParam(name);
|
||||
}
|
||||
|
||||
var addFBType = function (fb, params) {
|
||||
var p = parseParams(params);
|
||||
|
||||
if (params && p){
|
||||
fb.type = p.FBTYPE || "BUSY"
|
||||
}
|
||||
|
||||
return fb;
|
||||
}
|
||||
|
||||
var freebusyParam = function (name) {
|
||||
return function(val, params, curr){
|
||||
var fb = addFBType({}, params);
|
||||
curr[name] = curr[name] || []
|
||||
curr[name].push(fb);
|
||||
|
||||
storeParam(val, params, fb);
|
||||
|
||||
var parts = val.split('/');
|
||||
|
||||
['start', 'end'].forEach(function (name, index) {
|
||||
dateParam(name)(parts[index], params, fb);
|
||||
});
|
||||
|
||||
return curr;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
|
||||
objectHandlers : {
|
||||
'BEGIN' : function(component, params, curr, stack){
|
||||
stack.push(curr)
|
||||
|
||||
return {type:component, params:params}
|
||||
}
|
||||
|
||||
, 'END' : function(component, params, curr, stack){
|
||||
// prevents the need to search the root of the tree for the VCALENDAR object
|
||||
if (component === "VCALENDAR") {
|
||||
//scan all high level object in curr and drop all strings
|
||||
var key,
|
||||
obj;
|
||||
|
||||
for (key in curr) {
|
||||
if(curr.hasOwnProperty(key)) {
|
||||
obj = curr[key];
|
||||
if (typeof obj === 'string') {
|
||||
delete curr[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return curr
|
||||
}
|
||||
|
||||
var par = stack.pop()
|
||||
|
||||
if (curr.uid)
|
||||
{
|
||||
// If this is the first time we run into this UID, just save it.
|
||||
if (par[curr.uid] === undefined)
|
||||
{
|
||||
par[curr.uid] = curr;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we have multiple ical entries with the same UID, it's either going to be a
|
||||
// modification to a recurrence (RECURRENCE-ID), and/or a significant modification
|
||||
// to the entry (SEQUENCE).
|
||||
|
||||
// TODO: Look into proper sequence logic.
|
||||
|
||||
if (curr.recurrenceid === undefined)
|
||||
{
|
||||
// If we have the same UID as an existing record, and it *isn't* a specific recurrence ID,
|
||||
// not quite sure what the correct behaviour should be. For now, just take the new information
|
||||
// and merge it with the old record by overwriting only the fields that appear in the new record.
|
||||
var key;
|
||||
for (key in curr) {
|
||||
par[curr.uid][key] = curr[key];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// If we have recurrence-id entries, list them as an array of recurrences keyed off of recurrence-id.
|
||||
// To use - as you're running through the dates of an rrule, you can try looking it up in the recurrences
|
||||
// array. If it exists, then use the data from the calendar object in the recurrence instead of the parent
|
||||
// for that day.
|
||||
|
||||
// NOTE: Sometimes the RECURRENCE-ID record will show up *before* the record with the RRULE entry. In that
|
||||
// case, what happens is that the RECURRENCE-ID record ends up becoming both the parent record and an entry
|
||||
// in the recurrences array, and then when we process the RRULE entry later it overwrites the appropriate
|
||||
// fields in the parent record.
|
||||
|
||||
if (curr.recurrenceid != null)
|
||||
{
|
||||
|
||||
// TODO: Is there ever a case where we have to worry about overwriting an existing entry here?
|
||||
|
||||
// Create a copy of the current object to save in our recurrences array. (We *could* just do par = curr,
|
||||
// except for the case that we get the RECURRENCE-ID record before the RRULE record. In that case, we
|
||||
// would end up with a shared reference that would cause us to overwrite *both* records at the point
|
||||
// that we try and fix up the parent record.)
|
||||
var recurrenceObj = new Object();
|
||||
var key;
|
||||
for (key in curr) {
|
||||
recurrenceObj[key] = curr[key];
|
||||
}
|
||||
|
||||
if (recurrenceObj.recurrences != undefined) {
|
||||
delete recurrenceObj.recurrences;
|
||||
}
|
||||
|
||||
|
||||
// If we don't have an array to store recurrences in yet, create it.
|
||||
if (par[curr.uid].recurrences === undefined) {
|
||||
par[curr.uid].recurrences = new Array();
|
||||
}
|
||||
|
||||
// Save off our cloned recurrence object into the array, keyed by date but not time.
|
||||
// We key by date only to avoid timezone and "floating time" problems (where the time isn't associated with a timezone).
|
||||
// TODO: See if this causes a problem with events that have multiple recurrences per day.
|
||||
if (typeof curr.recurrenceid.toISOString === 'function') {
|
||||
par[curr.uid].recurrences[curr.recurrenceid.toISOString().substring(0,10)] = recurrenceObj;
|
||||
} else {
|
||||
console.error("No toISOString function in curr.recurrenceid", curr.recurrenceid);
|
||||
}
|
||||
}
|
||||
|
||||
// One more specific fix - in the case that an RRULE entry shows up after a RECURRENCE-ID entry,
|
||||
// let's make sure to clear the recurrenceid off the parent field.
|
||||
if ((par[curr.uid].rrule != undefined) && (par[curr.uid].recurrenceid != undefined))
|
||||
{
|
||||
delete par[curr.uid].recurrenceid;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
par[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID
|
||||
|
||||
return par
|
||||
}
|
||||
|
||||
, 'SUMMARY' : storeParam('summary')
|
||||
, 'DESCRIPTION' : storeParam('description')
|
||||
, 'URL' : storeParam('url')
|
||||
, 'UID' : storeParam('uid')
|
||||
, 'LOCATION' : storeParam('location')
|
||||
, 'DTSTART' : dateParam('start')
|
||||
, 'DTEND' : dateParam('end')
|
||||
, 'EXDATE' : exdateParam('exdate')
|
||||
,' CLASS' : storeParam('class')
|
||||
, 'TRANSP' : storeParam('transparency')
|
||||
, 'GEO' : geoParam('geo')
|
||||
, 'PERCENT-COMPLETE': storeParam('completion')
|
||||
, 'COMPLETED': dateParam('completed')
|
||||
, 'CATEGORIES': categoriesParam('categories')
|
||||
, 'FREEBUSY': freebusyParam('freebusy')
|
||||
, 'DTSTAMP': dateParam('dtstamp')
|
||||
, 'CREATED': dateParam('created')
|
||||
, 'LAST-MODIFIED': dateParam('lastmodified')
|
||||
, 'RECURRENCE-ID': recurrenceParam('recurrenceid')
|
||||
|
||||
},
|
||||
|
||||
|
||||
handleObject : function(name, val, params, ctx, stack, line){
|
||||
var self = this
|
||||
|
||||
if(self.objectHandlers[name])
|
||||
return self.objectHandlers[name](val, params, ctx, stack, line)
|
||||
|
||||
//handling custom properties
|
||||
if(name.match(/X\-[\w\-]+/) && stack.length > 0) {
|
||||
//trimming the leading and perform storeParam
|
||||
name = name.substring(2);
|
||||
return (storeParam(name))(val, params, ctx, stack, line);
|
||||
}
|
||||
|
||||
return storeParam(name.toLowerCase())(val, params, ctx);
|
||||
},
|
||||
|
||||
|
||||
parseICS : function(str){
|
||||
var self = this
|
||||
var lines = str.split(/\r?\n/)
|
||||
var ctx = {}
|
||||
var stack = []
|
||||
|
||||
for (var i = 0, ii = lines.length, l = lines[0]; i<ii; i++, l=lines[i]){
|
||||
//Unfold : RFC#3.1
|
||||
while (lines[i+1] && /[ \t]/.test(lines[i+1][0])) {
|
||||
l += lines[i+1].slice(1)
|
||||
i += 1
|
||||
}
|
||||
|
||||
var kv = l.split(":")
|
||||
|
||||
if (kv.length < 2){
|
||||
// Invalid line - must have k&v
|
||||
continue;
|
||||
}
|
||||
|
||||
// Although the spec says that vals with colons should be quote wrapped
|
||||
// in practise nobody does, so we assume further colons are part of the
|
||||
// val
|
||||
var value = kv.slice(1).join(":")
|
||||
, kp = kv[0].split(";")
|
||||
, name = kp[0]
|
||||
, params = kp.slice(1)
|
||||
|
||||
ctx = self.handleObject(name, value, params, ctx, stack, l) || {}
|
||||
}
|
||||
|
||||
// type and params are added to the list of items, get rid of them.
|
||||
delete ctx.type
|
||||
delete ctx.params
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
}
|
||||
}))
|
@ -1,8 +0,0 @@
|
||||
module.exports = require('./ical')
|
||||
|
||||
var node = require('./node-ical')
|
||||
|
||||
// Copy node functions across to exports
|
||||
for (var i in node){
|
||||
module.exports[i] = node[i]
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
var ical = require('./ical')
|
||||
, request = require('request')
|
||||
, fs = require('fs')
|
||||
|
||||
exports.fromURL = function(url, opts, cb){
|
||||
if (!cb)
|
||||
return;
|
||||
request(url, opts, function(err, r, data){
|
||||
if (err)
|
||||
{
|
||||
return cb(err, null);
|
||||
}
|
||||
else if (r.statusCode != 200)
|
||||
{
|
||||
return cb(r.statusCode + ": " + r.statusMessage, null);
|
||||
}
|
||||
|
||||
cb(undefined, ical.parseICS(data));
|
||||
})
|
||||
}
|
||||
|
||||
exports.parseFile = function(filename){
|
||||
return ical.parseICS(fs.readFileSync(filename, 'utf8'))
|
||||
}
|
||||
|
||||
|
||||
var rrule = require('rrule').RRule
|
||||
|
||||
function getLocaleISOString(date) {
|
||||
var year = date.getFullYear().toString(10).padStart(4,'0');
|
||||
var month = (date.getMonth() + 1).toString(10).padStart(2,'0');
|
||||
var day = date.getDate().toString(10).padStart(2,'0');
|
||||
var hour = date.getHours().toString(10).padStart(2,'0');
|
||||
var minute = date.getMinutes().toString(10).padStart(2,'0');
|
||||
var second = date.getSeconds().toString(10).padStart(2,'0');
|
||||
|
||||
return `${year}${month}${day}T${hour}${minute}${second}Z`;
|
||||
}
|
||||
|
||||
ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){
|
||||
curr.rrule = line;
|
||||
return curr
|
||||
}
|
||||
var originalEnd = ical.objectHandlers['END'];
|
||||
ical.objectHandlers['END'] = function (val, params, curr, stack) {
|
||||
// Recurrence rules are only valid for VEVENT, VTODO, and VJOURNAL.
|
||||
// More specifically, we need to filter the VCALENDAR type because we might end up with a defined rrule
|
||||
// due to the subtypes.
|
||||
if ((val === "VEVENT") || (val === "VTODO") || (val === "VJOURNAL")) {
|
||||
if (curr.rrule) {
|
||||
var rule = curr.rrule.replace('RRULE:', '');
|
||||
if (rule.indexOf('DTSTART') === -1) {
|
||||
|
||||
if (curr.start.length === 8) {
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(curr.start);
|
||||
if (comps) {
|
||||
curr.start = new Date(comps[1], comps[2] - 1, comps[3]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (typeof curr.start.toISOString === 'function') {
|
||||
try {
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
rule += ';DTSTART=' + getLocaleISOString(curr.start);
|
||||
} catch (error) {
|
||||
console.error("ERROR when trying to convert to ISOString", error);
|
||||
}
|
||||
} else {
|
||||
console.error("No toISOString function in curr.start", curr.start);
|
||||
}
|
||||
}
|
||||
curr.rrule = rrule.fromString(rule);
|
||||
}
|
||||
}
|
||||
return originalEnd.call(this, val, params, curr, stack);
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "ical",
|
||||
"version": "0.5.0",
|
||||
"main": "index.js",
|
||||
"description": "A tolerant, minimal icalendar parser",
|
||||
"keywords": [
|
||||
"ical",
|
||||
"ics",
|
||||
"calendar"
|
||||
],
|
||||
"homepage": "https://github.com/peterbraden/ical.js",
|
||||
"author": "Peter Braden <peterbraden@peterbraden.co.uk> (peterbraden.co.uk)",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/peterbraden/ical.js.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"request": "^2.88.0",
|
||||
"rrule": "2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vows": "0.8.2",
|
||||
"underscore": "1.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "./node_modules/vows/bin/vows ./test/test.js"
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
# ical.js #
|
||||
(Formerly node-ical)
|
||||
|
||||
[](https://travis-ci.org/peterbraden/ical.js)
|
||||
|
||||
A tolerant, minimal icalendar parser for javascript/node
|
||||
(http://tools.ietf.org/html/rfc5545)
|
||||
|
||||
|
||||
|
||||
## Install - Node.js ##
|
||||
|
||||
ical.js is availble on npm:
|
||||
|
||||
npm install ical
|
||||
|
||||
|
||||
|
||||
## API ##
|
||||
|
||||
ical.parseICS(str)
|
||||
|
||||
Parses a string with an ICS File
|
||||
|
||||
var data = ical.parseFile(filename)
|
||||
|
||||
Reads in the specified iCal file, parses it and returns the parsed data
|
||||
|
||||
ical.fromURL(url, options, function(err, data) {} )
|
||||
|
||||
Use the request library to fetch the specified URL (```opts``` gets passed on to the ```request()``` call), and call the function with the result (either an error or the data).
|
||||
|
||||
|
||||
|
||||
## Example 1 - Print list of upcoming node conferences (see example.js)
|
||||
```javascript
|
||||
'use strict';
|
||||
|
||||
const ical = require('ical');
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function (err, data) {
|
||||
for (let k in data) {
|
||||
if (data.hasOwnProperty(k)) {
|
||||
var ev = data[k];
|
||||
if (data[k].type == 'VEVENT') {
|
||||
console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Recurrences and Exceptions ##
|
||||
Calendar events with recurrence rules can be significantly more complicated to handle correctly. There are three parts to handling them:
|
||||
|
||||
1. rrule - the recurrence rule specifying the pattern of recurring dates and times for the event.
|
||||
2. recurrences - an optional array of event data that can override specific occurrences of the event.
|
||||
3. exdate - an optional array of dates that should be excluded from the recurrence pattern.
|
||||
|
||||
See example_rrule.js for an example of handling recurring calendar events.
|
500
modules/default/calendar/vendor/ical.js/test/test.js
vendored
@ -1,500 +0,0 @@
|
||||
/****
|
||||
* Tests
|
||||
*
|
||||
*
|
||||
***/
|
||||
process.env.TZ = 'America/San_Francisco';
|
||||
var ical = require('../index')
|
||||
|
||||
var vows = require('vows')
|
||||
, assert = require('assert')
|
||||
, _ = require('underscore')
|
||||
|
||||
vows.describe('node-ical').addBatch({
|
||||
'when parsing test1.ics (node conferences schedule from lanyrd.com, modified)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test1.ics')
|
||||
}
|
||||
|
||||
,'we get 9 events': function (topic) {
|
||||
var events = _.select(_.values(topic), function(x){ return x.type==='VEVENT'})
|
||||
assert.equal (events.length, 9);
|
||||
}
|
||||
|
||||
,'event 47f6e' : {
|
||||
topic: function(events){
|
||||
return _.select(_.values(events),
|
||||
function(x){
|
||||
return x.uid ==='47f6ea3f28af2986a2192fa39a91fa7d60d26b76'})[0]
|
||||
}
|
||||
,'is in fort lauderdale' : function(topic){
|
||||
assert.equal(topic.location, "Fort Lauderdale, United States")
|
||||
}
|
||||
,'starts Tue, 29 Nov 2011' : function(topic){
|
||||
assert.equal(topic.start.toDateString(), new Date(2011,10,29).toDateString())
|
||||
}
|
||||
}
|
||||
, 'event 480a' : {
|
||||
topic: function(events){
|
||||
return _.select(_.values(events),
|
||||
function(x){
|
||||
return x.uid ==='480a3ad48af5ed8965241f14920f90524f533c18'})[0]
|
||||
}
|
||||
, 'has a summary (invalid colon handling tolerance)' : function(topic){
|
||||
assert.equal(topic.summary, '[Async]: Everything Express')
|
||||
}
|
||||
, 'has a date only start datetime' : function(topic){
|
||||
assert.equal(topic.start.dateOnly, true)
|
||||
}
|
||||
, 'has a date only end datetime' : function(topic){
|
||||
assert.equal(topic.end.dateOnly, true)
|
||||
}
|
||||
}
|
||||
, 'event d4c8' :{
|
||||
topic : function(events){
|
||||
return _.select(_.values(events),
|
||||
function(x){
|
||||
return x.uid === 'd4c826dfb701f611416d69b4df81caf9ff80b03a'})[0]
|
||||
}
|
||||
, 'has a start datetime' : function(topic){
|
||||
assert.equal(topic.start.toDateString(), new Date(Date.UTC(2011, 2, 12, 20, 0, 0)).toDateString())
|
||||
}
|
||||
}
|
||||
|
||||
, 'event sdfkf09fsd0 (Invalid Date)' :{
|
||||
topic : function(events){
|
||||
return _.select(_.values(events),
|
||||
function(x){
|
||||
return x.uid === 'sdfkf09fsd0'})[0]
|
||||
}
|
||||
, 'has a start datetime' : function(topic){
|
||||
assert.equal(topic.start, "Next Year")
|
||||
}
|
||||
}
|
||||
}
|
||||
, 'with test2.ics (testing ical features)' : {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test2.ics')
|
||||
}
|
||||
, 'todo item uid4@host1.com' : {
|
||||
topic : function(items){
|
||||
return items['uid4@host1.com']
|
||||
}
|
||||
, 'is a VTODO' : function(topic){
|
||||
assert.equal(topic.type, 'VTODO')
|
||||
}
|
||||
}
|
||||
, 'vfreebusy' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.type === 'VFREEBUSY';
|
||||
})[0];
|
||||
}
|
||||
, 'has a URL' : function(topic) {
|
||||
assert.equal(topic.url, 'http://www.host.com/calendar/busytime/jsmith.ifb');
|
||||
}
|
||||
}
|
||||
, 'vfreebusy first freebusy' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.type === 'VFREEBUSY';
|
||||
})[0].freebusy[0];
|
||||
}
|
||||
, 'has undefined type defaulting to busy' : function(topic) {
|
||||
assert.equal(topic.type, "BUSY");
|
||||
}
|
||||
, 'has an start datetime' : function(topic) {
|
||||
assert.equal(topic.start.getFullYear(), 1998);
|
||||
assert.equal(topic.start.getUTCMonth(), 2);
|
||||
assert.equal(topic.start.getUTCDate(), 14);
|
||||
assert.equal(topic.start.getUTCHours(), 23);
|
||||
assert.equal(topic.start.getUTCMinutes(), 30);
|
||||
}
|
||||
, 'has an end datetime' : function(topic) {
|
||||
assert.equal(topic.end.getFullYear(), 1998);
|
||||
assert.equal(topic.end.getUTCMonth(), 2);
|
||||
assert.equal(topic.end.getUTCDate(), 15);
|
||||
assert.equal(topic.end.getUTCHours(), 00);
|
||||
assert.equal(topic.end.getUTCMinutes(), 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
, 'with test3.ics (testing tvcountdown.com)' : {
|
||||
topic: function() {
|
||||
return ical.parseFile('./test/test3.ics');
|
||||
}
|
||||
, 'event -83' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.uid === '20110505T220000Z-83@tvcountdown.com';
|
||||
})[0];
|
||||
}
|
||||
, 'has a start datetime' : function(topic) {
|
||||
assert.equal(topic.start.getFullYear(), 2011);
|
||||
assert.equal(topic.start.getMonth(), 4);
|
||||
}
|
||||
, 'has an end datetime' : function(topic) {
|
||||
assert.equal(topic.end.getFullYear(), 2011);
|
||||
assert.equal(topic.end.getMonth(), 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test4.ics (testing tripit.com)' : {
|
||||
topic: function() {
|
||||
return ical.parseFile('./test/test4.ics');
|
||||
}
|
||||
, 'event c32a5...' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.uid === 'c32a5eaba2354bb29e012ec18da827db90550a3b@tripit.com';
|
||||
})[0];
|
||||
}
|
||||
, 'has a start datetime' : function(topic) {
|
||||
assert.equal(topic.start.getFullYear(), 2011);
|
||||
assert.equal(topic.start.getMonth(), 09);
|
||||
assert.equal(topic.start.getDate(), 11);
|
||||
}
|
||||
|
||||
, 'has a summary' : function(topic){
|
||||
// escaped commas and semicolons should be replaced
|
||||
assert.equal(topic.summary, 'South San Francisco, CA, October 2011;')
|
||||
|
||||
}
|
||||
|
||||
, 'has a description' : function(topic){
|
||||
var desired = 'John Doe is in South San Francisco, CA from Oct 11 ' +
|
||||
'to Oct 13, 2011\nView and/or edit details in TripIt : http://www.tripit.c' +
|
||||
'om/trip/show/id/23710889\nTripIt - organize your travel at http://www.trip' +
|
||||
'it.com\n'
|
||||
assert.equal(topic.description, desired)
|
||||
|
||||
}
|
||||
|
||||
, 'has a geolocation' : function(topic){
|
||||
assert.ok(topic.geo, 'no geo param')
|
||||
assert.equal(topic.geo.lat, 37.654656)
|
||||
assert.equal(topic.geo.lon, -122.40775)
|
||||
}
|
||||
|
||||
, 'has transparency' : function(topic){
|
||||
assert.equal(topic.transparency, 'TRANSPARENT')
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
, 'with test5.ics (testing meetup.com)' : {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test5.ics')
|
||||
}
|
||||
, 'event nsmxnyppbfc@meetup.com' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.uid === 'event_nsmxnyppbfc@meetup.com';
|
||||
})[0];
|
||||
}
|
||||
, 'has a start' : function(topic){
|
||||
assert.equal(topic.start.tz, 'America/Phoenix')
|
||||
assert.equal(topic.start.toISOString(), new Date(2011, 10, 09, 19, 0,0).toISOString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test6.ics (testing assembly.org)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test6.ics')
|
||||
}
|
||||
, 'event with no ID' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events), function(x) {
|
||||
return x.summary === 'foobar Summer 2011 starts!';
|
||||
})[0];
|
||||
}
|
||||
, 'has a start' : function(topic){
|
||||
assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 12, 0,0).toISOString())
|
||||
}
|
||||
}
|
||||
, 'event with rrule' :{
|
||||
topic: function(events){
|
||||
return _.select(_.values(events), function(x){
|
||||
return x.summary === "foobarTV broadcast starts"
|
||||
})[0];
|
||||
}
|
||||
, "Has an RRULE": function(topic){
|
||||
assert.notEqual(topic.rrule, undefined);
|
||||
}
|
||||
, "RRule text": function(topic){
|
||||
assert.equal(topic.rrule.toText(), "every 5 weeks on Monday, Friday until January 30, 2013")
|
||||
}
|
||||
}
|
||||
}
|
||||
, 'with test7.ics (testing dtstart of rrule)' :{
|
||||
topic: function() {
|
||||
return ical.parseFile('./test/test7.ics');
|
||||
},
|
||||
'recurring yearly event (14 july)': {
|
||||
topic: function(events){
|
||||
var ev = _.values(events)[0];
|
||||
return ev.rrule.between(new Date(2013, 0, 1), new Date(2014, 0, 1));
|
||||
},
|
||||
'dt start well set': function(topic) {
|
||||
assert.equal(topic[0].toDateString(), new Date(2013, 6, 14).toDateString());
|
||||
}
|
||||
}
|
||||
}
|
||||
, "with test 8.ics (VTODO completion)": {
|
||||
topic: function() {
|
||||
return ical.parseFile('./test/test8.ics');
|
||||
},
|
||||
'grabbing VTODO task': {
|
||||
topic: function(topic) {
|
||||
return _.values(topic)[0];
|
||||
},
|
||||
'task completed': function(task){
|
||||
assert.equal(task.completion, 100);
|
||||
assert.equal(task.completed.toISOString(), new Date(2013, 06, 16, 10, 57, 45).toISOString());
|
||||
}
|
||||
}
|
||||
}
|
||||
, "with test 9.ics (VEVENT with VALARM)": {
|
||||
topic: function() {
|
||||
return ical.parseFile('./test/test9.ics');
|
||||
},
|
||||
'grabbing VEVENT task': {
|
||||
topic: function(topic) {
|
||||
return _.values(topic)[0];
|
||||
},
|
||||
'task completed': function(task){
|
||||
assert.equal(task.summary, "Event with an alarm");
|
||||
}
|
||||
}
|
||||
}
|
||||
, 'with test 11.ics (VEVENT with custom properties)': {
|
||||
topic: function() {
|
||||
return ical.parseFile('./test10.ics');
|
||||
},
|
||||
'grabbing custom properties': {
|
||||
topic: function(topic) {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'with test10.ics': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test10.ics');
|
||||
},
|
||||
|
||||
'when categories present': {
|
||||
topic: function (t) {return _.values(t)[0]},
|
||||
|
||||
'should be a list': function (e) {
|
||||
assert(e.categories instanceof [].constructor);
|
||||
},
|
||||
|
||||
'should contain individual category values': function (e) {
|
||||
assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']);
|
||||
}
|
||||
},
|
||||
|
||||
'when categories present with trailing whitespace': {
|
||||
topic: function (t) {return _.values(t)[1]},
|
||||
|
||||
'should contain individual category values without whitespace': function (e) {
|
||||
assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']);
|
||||
}
|
||||
},
|
||||
|
||||
'when categories present but empty': {
|
||||
topic: function (t) {return _.values(t)[2]},
|
||||
|
||||
'should be an empty list': function (e) {
|
||||
assert.deepEqual(e.categories, []);
|
||||
}
|
||||
},
|
||||
|
||||
'when categories present but singular': {
|
||||
topic: function (t) {return _.values(t)[3]},
|
||||
|
||||
'should be a list of single item': function (e) {
|
||||
assert.deepEqual(e.categories, ['lonely-cat']);
|
||||
}
|
||||
},
|
||||
|
||||
'when categories present on multiple lines': {
|
||||
topic: function (t) {return _.values(t)[4]},
|
||||
|
||||
'should contain the category values in an array': function (e) {
|
||||
assert.deepEqual(e.categories, ['cat1', 'cat2', 'cat3']);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'with test11.ics (testing zimbra freebusy)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test11.ics');
|
||||
},
|
||||
|
||||
'freebusy params' : {
|
||||
topic: function(events) {
|
||||
return _.values(events)[0];
|
||||
}
|
||||
, 'has a URL' : function(topic) {
|
||||
assert.equal(topic.url, 'http://mail.example.com/yvr-2a@example.com/20140416');
|
||||
}
|
||||
, 'has an ORGANIZER' : function(topic) {
|
||||
assert.equal(topic.organizer, 'mailto:yvr-2a@example.com');
|
||||
}
|
||||
, 'has an start datetime' : function(topic) {
|
||||
assert.equal(topic.start.getFullYear(), 2014);
|
||||
assert.equal(topic.start.getMonth(), 3);
|
||||
}
|
||||
, 'has an end datetime' : function(topic) {
|
||||
assert.equal(topic.end.getFullYear(), 2014);
|
||||
assert.equal(topic.end.getMonth(), 6);
|
||||
}
|
||||
}
|
||||
, 'freebusy busy events' : {
|
||||
topic: function(events) {
|
||||
return _.select(_.values(events)[0].freebusy, function(x) {
|
||||
return x.type === 'BUSY';
|
||||
})[0];
|
||||
}
|
||||
, 'has an start datetime' : function(topic) {
|
||||
assert.equal(topic.start.getFullYear(), 2014);
|
||||
assert.equal(topic.start.getMonth(), 3);
|
||||
assert.equal(topic.start.getUTCHours(), 15);
|
||||
assert.equal(topic.start.getUTCMinutes(), 15);
|
||||
}
|
||||
, 'has an end datetime' : function(topic) {
|
||||
assert.equal(topic.end.getFullYear(), 2014);
|
||||
assert.equal(topic.end.getMonth(), 3);
|
||||
assert.equal(topic.end.getUTCHours(), 19);
|
||||
assert.equal(topic.end.getUTCMinutes(), 00);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test12.ics (testing recurrences and exdates)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test12.ics')
|
||||
}
|
||||
, 'event with rrule': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '0000001';
|
||||
})[0];
|
||||
}
|
||||
, "Has an RRULE": function (topic) {
|
||||
assert.notEqual(topic.rrule, undefined);
|
||||
}
|
||||
, "Has summary Treasure Hunting": function (topic) {
|
||||
assert.equal(topic.summary, 'Treasure Hunting');
|
||||
}
|
||||
, "Has two EXDATES": function (topic) {
|
||||
assert.notEqual(topic.exdate, undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2015, 06, 08, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2015, 06, 10, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
}
|
||||
, "Has a RECURRENCE-ID override": function (topic) {
|
||||
assert.notEqual(topic.recurrences, undefined);
|
||||
assert.notEqual(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.equal(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString().substring(0, 10)].summary, 'More Treasure Hunting');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test13.ics (testing recurrence-id before rrule)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test13.ics')
|
||||
}
|
||||
, 'event with rrule': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '6m2q7kb2l02798oagemrcgm6pk@google.com';
|
||||
})[0];
|
||||
}
|
||||
, "Has an RRULE": function (topic) {
|
||||
assert.notEqual(topic.rrule, undefined);
|
||||
}
|
||||
, "Has summary 'repeated'": function (topic) {
|
||||
assert.equal(topic.summary, 'repeated');
|
||||
}
|
||||
, "Has a RECURRENCE-ID override": function (topic) {
|
||||
assert.notEqual(topic.recurrences, undefined);
|
||||
assert.notEqual(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.equal(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString().substring(0, 10)].summary, 'bla bla');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test14.ics (testing comma-separated exdates)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test14.ics')
|
||||
}
|
||||
, 'event with comma-separated exdate': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '98765432-ABCD-DCBB-999A-987765432123';
|
||||
})[0];
|
||||
}
|
||||
, "Has summary 'Example of comma-separated exdates'": function (topic) {
|
||||
assert.equal(topic.summary, 'Example of comma-separated exdates');
|
||||
}
|
||||
, "Has four comma-separated EXDATES": function (topic) {
|
||||
assert.notEqual(topic.exdate, undefined);
|
||||
// Verify the four comma-separated EXDATES are there
|
||||
assert.notEqual(topic.exdate[new Date(2017, 6, 6, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 6, 17, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 6, 20, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 7, 3, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
// Verify an arbitrary date isn't there
|
||||
assert.equal(topic.exdate[new Date(2017, 4, 5, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test14.ics (testing exdates with bad times)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test14.ics')
|
||||
}
|
||||
, 'event with exdates with bad times': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '1234567-ABCD-ABCD-ABCD-123456789012';
|
||||
})[0];
|
||||
}
|
||||
, "Has summary 'Example of exdate with bad times'": function (topic) {
|
||||
assert.equal(topic.summary, 'Example of exdate with bad times');
|
||||
}
|
||||
, "Has two EXDATES even though they have bad times": function (topic) {
|
||||
assert.notEqual(topic.exdate, undefined);
|
||||
// Verify the two EXDATES are there, even though they have bad times
|
||||
assert.notEqual(topic.exdate[new Date(2017, 11, 18, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 11, 19, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'url request errors': {
|
||||
topic : function () {
|
||||
ical.fromURL('http://255.255.255.255/', {}, this.callback);
|
||||
}
|
||||
, 'are passed back to the callback' : function (err, result) {
|
||||
assert.instanceOf(err, Error);
|
||||
if (!err){
|
||||
console.log(">E:", err, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).export(module)
|
||||
|
||||
|
||||
//ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics',
|
||||
// {},
|
||||
// function(err, data){
|
||||
// console.log("OUT:", data)
|
||||
// })
|
@ -1,78 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//lanyrd.com//Lanyrd//EN
|
||||
X-ORIGINAL-URL:http://lanyrd.com/topics/nodejs/nodejs.ics
|
||||
X-WR-CALNAME;CHARSET=utf-8:Node.js conferences
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:Dyncon 2011
|
||||
LOCATION;CHARSET=utf-8:Stockholm, Sweden
|
||||
URL:http://lanyrd.com/2011/dyncon/
|
||||
UID:d4c826dfb701f611416d69b4df81caf9ff80b03a
|
||||
DTSTART:20110312T200000Z
|
||||
DTEND;VALUE=DATE:20110314
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:[Async]: Everything Express
|
||||
LOCATION;CHARSET=utf-8:Brighton, United Kingdom
|
||||
URL:http://lanyrd.com/2011/asyncjs-express/
|
||||
UID:480a3ad48af5ed8965241f14920f90524f533c18
|
||||
DTSTART;VALUE=DATE:20110324
|
||||
DTEND;VALUE=DATE:20110325
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:JSConf US 2011
|
||||
LOCATION;CHARSET=utf-8:Portland, United States
|
||||
URL:http://lanyrd.com/2011/jsconf/
|
||||
UID:ed334cc85db5ebdff5ff5a630a7a48631a677dbe
|
||||
DTSTART;VALUE=DATE:20110502
|
||||
DTEND;VALUE=DATE:20110504
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:NodeConf 2011
|
||||
LOCATION;CHARSET=utf-8:Portland, United States
|
||||
URL:http://lanyrd.com/2011/nodeconf/
|
||||
UID:25169a7b1ba5c248278f47120a40878055dc8c15
|
||||
DTSTART;VALUE=DATE:20110505
|
||||
DTEND;VALUE=DATE:20110506
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:BrazilJS
|
||||
LOCATION;CHARSET=utf-8:Fortaleza, Brazil
|
||||
URL:http://lanyrd.com/2011/braziljs/
|
||||
UID:dafee3be83624f3388c5635662229ff11766bb9c
|
||||
DTSTART;VALUE=DATE:20110513
|
||||
DTEND;VALUE=DATE:20110515
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:Falsy Values
|
||||
LOCATION;CHARSET=utf-8:Warsaw, Poland
|
||||
URL:http://lanyrd.com/2011/falsy-values/
|
||||
UID:73cad6a09ac4e7310979c6130f871d17d990b5ad
|
||||
DTSTART;VALUE=DATE:20110518
|
||||
DTEND;VALUE=DATE:20110521
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:nodecamp.eu
|
||||
LOCATION;CHARSET=utf-8:Cologne, Germany
|
||||
URL:http://lanyrd.com/2011/nodecampde/
|
||||
UID:b728a5fdb5f292b6293e4a2fd97a1ccfc69e9d6f
|
||||
DTSTART;VALUE=DATE:20110611
|
||||
DTEND;VALUE=DATE:20110613
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:Rich Web Experience 2011
|
||||
LOCATION;CHARSET=utf-8:Fort Lauderdale, United States
|
||||
URL:http://lanyrd.com/2011/rich-web-experience/
|
||||
UID:47f6ea3f28af2986a2192fa39a91fa7d60d26b76
|
||||
DTSTART;VALUE=DATE:20111129
|
||||
DTEND;VALUE=DATE:20111203
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY;CHARSET=utf-8:Foobar
|
||||
UID:sdfkf09fsd0
|
||||
DTSTART;VALUE=DATE:Next Year
|
||||
DTEND;VALUE=DATE:20111203
|
||||
END:VEVENT
|
||||
|
||||
END:VCALENDAR
|
@ -1,34 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
UID:1
|
||||
SUMMARY:Event with a category
|
||||
DESCRIPTION:Details for an event with a category
|
||||
CATEGORIES:cat1,cat2,cat3
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:2
|
||||
SUMMARY:Event with a category
|
||||
DESCRIPTION:Details for an event with a category
|
||||
CATEGORIES:cat1 , cat2, cat3
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:3
|
||||
SUMMARY:Event with a category
|
||||
DESCRIPTION:Details for an event with a category
|
||||
CATEGORIES:
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:4
|
||||
SUMMARY:Event with a category
|
||||
DESCRIPTION:Details for an event with a category
|
||||
CATEGORIES:lonely-cat
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:5
|
||||
SUMMARY:Event with a category
|
||||
DESCRIPTION:Details for an event with a category
|
||||
CATEGORIES:cat1
|
||||
CATEGORIES:cat2
|
||||
CATEGORIES:cat3
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
@ -1,41 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:Zimbra-Calendar-Provider
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
BEGIN:VFREEBUSY
|
||||
ORGANIZER:mailto:yvr-2a@example.com
|
||||
DTSTAMP:20140516T235436Z
|
||||
DTSTART:20140415T235436Z
|
||||
DTEND:20140717T235436Z
|
||||
URL:http://mail.example.com/yvr-2a@example.com/20140416
|
||||
FREEBUSY;FBTYPE=BUSY:20140416T151500Z/20140416T190000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140416T195500Z/20140416T231500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140417T193000Z/20140417T203000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140421T210000Z/20140421T213000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140423T180000Z/20140423T190000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140423T200000Z/20140423T210000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140423T223500Z/20140423T231500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140424T155000Z/20140424T165500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140424T170000Z/20140424T183000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140424T195000Z/20140424T230000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140425T144500Z/20140425T161500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140425T180000Z/20140425T194500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140425T223000Z/20140425T230000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140428T151500Z/20140428T163000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140428T170000Z/20140428T173000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140428T195500Z/20140428T213000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140428T231000Z/20140428T234000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140429T152500Z/20140429T170000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140429T180000Z/20140429T183000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140429T201500Z/20140429T230000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140430T162500Z/20140430T165500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140430T180000Z/20140430T190000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140501T170000Z/20140501T173000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140501T175000Z/20140501T190000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140501T232000Z/20140501T235000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140502T163500Z/20140502T173000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140505T165500Z/20140505T173000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140505T201500Z/20140505T203000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20140505T210000Z/20140505T213000Z
|
||||
END:VFREEBUSY
|
||||
END:VCALENDAR
|
@ -1,19 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
UID:0000001
|
||||
SUMMARY:Treasure Hunting
|
||||
DTSTART;TZID=America/Los_Angeles:20150706T120000
|
||||
DTEND;TZID=America/Los_Angeles:20150706T130000
|
||||
RRULE:FREQ=DAILY;COUNT=10
|
||||
EXDATE;TZID=America/Los_Angeles:20150708T120000
|
||||
EXDATE;TZID=America/Los_Angeles:20150710T120000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:0000001
|
||||
SUMMARY:More Treasure Hunting
|
||||
LOCATION:The other island
|
||||
DTSTART;TZID=America/Los_Angeles:20150709T150000
|
||||
DTEND;TZID=America/Los_Angeles:20150707T160000
|
||||
RECURRENCE-ID;TZID=America/Los_Angeles:20150707T120000
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
@ -1,57 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:ical
|
||||
X-WR-TIMEZONE:Europe/Kiev
|
||||
X-WR-CALDESC:
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Kiev
|
||||
X-LIC-LOCATION:Europe/Kiev
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0300
|
||||
TZNAME:EEST
|
||||
DTSTART:19700329T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0300
|
||||
TZOFFSETTO:+0200
|
||||
TZNAME:EET
|
||||
DTSTART:19701025T040000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Europe/Kiev:20160826T140000
|
||||
DTEND;TZID=Europe/Kiev:20160826T150000
|
||||
DTSTAMP:20160825T061505Z
|
||||
UID:6m2q7kb2l02798oagemrcgm6pk@google.com
|
||||
RECURRENCE-ID;TZID=Europe/Kiev:20160826T140000
|
||||
CREATED:20160823T125221Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20160823T130320Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:bla bla
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Europe/Kiev:20160825T140000
|
||||
DTEND;TZID=Europe/Kiev:20160825T150000
|
||||
RRULE:FREQ=DAILY;UNTIL=20160828T110000Z
|
||||
DTSTAMP:20160825T061505Z
|
||||
UID:6m2q7kb2l02798oagemrcgm6pk@google.com
|
||||
CREATED:20160823T125221Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20160823T125221Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:repeated
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
@ -1,33 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:ical
|
||||
X-WR-TIMEZONE:Europe/Kiev
|
||||
X-WR-CALDESC:
|
||||
BEGIN:VEVENT
|
||||
UID:98765432-ABCD-DCBB-999A-987765432123
|
||||
DTSTART;TZID=US/Central:20170216T090000
|
||||
DTEND;TZID=US/Central:20170216T190000
|
||||
DTSTAMP:20170727T044436Z
|
||||
EXDATE;TZID=US/Central:20170706T090000,20170717T090000,20170720T090000,20
|
||||
170803T090000
|
||||
LAST-MODIFIED:20170727T044435Z
|
||||
RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20170814T045959Z;INTERVAL=2;BYDAY=MO,TH
|
||||
SEQUENCE:0
|
||||
SUMMARY:Example of comma-separated exdates
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:1234567-ABCD-ABCD-ABCD-123456789012
|
||||
DTSTART:20170814T140000Z
|
||||
DTEND:20170815T000000Z
|
||||
DTSTAMP:20171204T134925Z
|
||||
EXDATE:20171219T060000
|
||||
EXDATE:20171218T060000
|
||||
LAST-MODIFIED:20171024T140004Z
|
||||
RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
|
||||
SEQUENCE:0
|
||||
SUMMARY:Example of exdate with bad times
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
@ -1,83 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
CALSCALE:GREGORIAN
|
||||
X-WR-TIMEZONE;VALUE=TEXT:US/Pacific
|
||||
METHOD:PUBLISH
|
||||
PRODID:-//Apple Computer\, Inc//iCal 1.0//EN
|
||||
X-WR-CALNAME;VALUE=TEXT:Example
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
SEQUENCE:5
|
||||
DTSTART;TZID=US/Pacific:20021028T140000
|
||||
DTSTAMP:20021028T011706Z
|
||||
SUMMARY:Coffee with Jason
|
||||
UID:EC9439B1-FF65-11D6-9973-003065F99D04
|
||||
DTEND;TZID=US/Pacific:20021028T150000
|
||||
END:VEVENT
|
||||
BEGIN:VALARM
|
||||
TRIGGER;VALUE=DURATION:-P1D
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event reminder
|
||||
END:VALARM
|
||||
BEGIN:VEVENT
|
||||
SEQUENCE:1
|
||||
DTSTAMP:20021128T012034Z
|
||||
SUMMARY:Code Review
|
||||
UID:EC944331-FF65-11D6-9973-003065F99D04
|
||||
DTSTART;TZID=US/Pacific:20021127T120000
|
||||
DURATION:PT1H
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SEQUENCE:1
|
||||
DTSTAMP:20021028T012034Z
|
||||
SUMMARY:Dinner with T
|
||||
UID:EC944CFA-FF65-11D6-9973-003065F99D04
|
||||
DTSTART;TZID=US/Pacific:20021216T200000
|
||||
DURATION:PT1H
|
||||
END:VEVENT
|
||||
BEGIN:VTODO
|
||||
DTSTAMP:19980130T134500Z
|
||||
SEQUENCE:2
|
||||
UID:uid4@host1.com
|
||||
ORGANIZER:MAILTO:unclesam@us.gov
|
||||
ATTENDEE;PARTSTAT=ACCEPTED:MAILTO:jqpublic@host.com
|
||||
DUE:19980415T235959
|
||||
STATUS:NEEDS-ACTION
|
||||
SUMMARY:Submit Income Taxes
|
||||
END:VTODO
|
||||
BEGIN:VALARM
|
||||
ACTION:AUDIO
|
||||
TRIGGER:19980403T120000
|
||||
ATTACH;FMTTYPE=audio/basic:http://host.com/pub/audio-
|
||||
files/ssbanner.aud
|
||||
REPEAT:4
|
||||
DURATION:PT1H
|
||||
END:VALARM
|
||||
BEGIN:VJOURNAL
|
||||
DTSTAMP:19970324T120000Z
|
||||
UID:uid5@host1.com
|
||||
ORGANIZER:MAILTO:jsmith@host.com
|
||||
STATUS:DRAFT
|
||||
CLASS:PUBLIC
|
||||
CATEGORY:Project Report, XYZ, Weekly Meeting
|
||||
DESCRIPTION:Project xyz Review Meeting Minutes\n
|
||||
Agenda\n1. Review of project version 1.0 requirements.\n2.
|
||||
Definition
|
||||
of project processes.\n3. Review of project schedule.\n
|
||||
Participants: John Smith, Jane Doe, Jim Dandy\n-It was
|
||||
decided that the requirements need to be signed off by
|
||||
product marketing.\n-Project processes were accepted.\n
|
||||
-Project schedule needs to account for scheduled holidays
|
||||
and employee vacation time. Check with HR for specific
|
||||
dates.\n-New schedule will be distributed by Friday.\n-
|
||||
Next weeks meeting is cancelled. No meeting until 3/23.
|
||||
END:VJOURNAL
|
||||
BEGIN:VFREEBUSY
|
||||
ORGANIZER:MAILTO:jsmith@host.com
|
||||
DTSTART:19980313T141711Z
|
||||
DTEND:19980410T141711Z
|
||||
FREEBUSY:19980314T233000Z/19980315T003000Z
|
||||
FREEBUSY:19980316T153000Z/19980316T163000Z
|
||||
FREEBUSY:19980318T030000Z/19980318T040000Z
|
||||
URL:http://www.host.com/calendar/busytime/jsmith.ifb
|
||||
END:VFREEBUSY
|
||||
END:VCALENDAR
|
@ -1,226 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
CALSCALE:GREGORIAN
|
||||
PRODID:tvcountdown.com
|
||||
X-WR-CALNAME:tvcountdown.com
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-WR-TIMEZONE:US/Eastern
|
||||
X-WR-CALNAME;VALUE=TEXT:tvcountdown.com
|
||||
X-WR-CALDESC:
|
||||
BEGIN:VEVENT
|
||||
UID:20110519T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110519T200000
|
||||
DTEND;VALUE=DATE-TIME:20110519T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E24 - The Roomate Transmogrfication
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110512T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110512T200000
|
||||
DTEND;VALUE=DATE-TIME:20110512T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E23 - The Engagement Reaction
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110505T220000Z-83@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110505T220000
|
||||
DTEND;VALUE=DATE-TIME:20110505T223000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:30 Rock - S05E23 - Respawn
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110505T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110505T200000
|
||||
DTEND;VALUE=DATE-TIME:20110505T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E22 - The Wildebeest Implementation
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110504T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110504T230000
|
||||
DTEND;VALUE=DATE-TIME:20110504T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E59 - David Barton
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110503T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110503T230000
|
||||
DTEND;VALUE=DATE-TIME:20110503T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E58 - Rachel Maddow
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110502T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110502T230000
|
||||
DTEND;VALUE=DATE-TIME:20110502T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E57 - Philip K. Howard
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110428T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110428T230000
|
||||
DTEND;VALUE=DATE-TIME:20110428T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E56 - William Cohan
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110428T220000Z-83@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110428T220000
|
||||
DTEND;VALUE=DATE-TIME:20110428T223000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:30 Rock - S05E22 - Everything Sunny All the Time Always
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110428T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110428T200000
|
||||
DTEND;VALUE=DATE-TIME:20110428T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E21 - The Agreement Dissection
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110427T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110427T230000
|
||||
DTEND;VALUE=DATE-TIME:20110427T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E55 - Sen. Bernie Sanders
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110426T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110426T230000
|
||||
DTEND;VALUE=DATE-TIME:20110426T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E54 - Elizabeth Warren
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110425T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110425T230000
|
||||
DTEND;VALUE=DATE-TIME:20110425T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E53 - Gigi Ibrahim
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110421T220000Z-83@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110421T220000
|
||||
DTEND;VALUE=DATE-TIME:20110421T223000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:30 Rock - S05E21 - 100th Episode Part 2 of 2
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110421T220000Z-83@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110421T220000
|
||||
DTEND;VALUE=DATE-TIME:20110421T223000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:30 Rock - S05E20 - 100th Episode Part 1 of 2
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110414T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110414T230000
|
||||
DTEND;VALUE=DATE-TIME:20110414T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E52 - Ricky Gervais
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110414T220000Z-83@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110414T220000
|
||||
DTEND;VALUE=DATE-TIME:20110414T223000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:30 Rock - S05E19 - I Heart Connecticut
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110413T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110413T230000
|
||||
DTEND;VALUE=DATE-TIME:20110413T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E51 - Tracy Morgan
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110412T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110412T230000
|
||||
DTEND;VALUE=DATE-TIME:20110412T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E50 - Gov. Deval Patrick
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110411T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110411T230000
|
||||
DTEND;VALUE=DATE-TIME:20110411T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E49 - Foo Fighters
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110407T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110407T230000
|
||||
DTEND;VALUE=DATE-TIME:20110407T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E48 - Jamie Oliver
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110407T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110407T200000
|
||||
DTEND;VALUE=DATE-TIME:20110407T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E20 - The Herb Garden Germination
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110406T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110406T230000
|
||||
DTEND;VALUE=DATE-TIME:20110406T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E47 - Mike Huckabee
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110405T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110405T230000
|
||||
DTEND;VALUE=DATE-TIME:20110405T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E46 - Colin Quinn
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110404T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110404T230000
|
||||
DTEND;VALUE=DATE-TIME:20110404T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E45 - Billy Crystal
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110331T230000Z-289@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110331T230000
|
||||
DTEND;VALUE=DATE-TIME:20110331T233000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Daily Show - S16E44 - Norm MacDonald
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20110331T200000Z-79@tvcountdown.com
|
||||
DTSTART;VALUE=DATE-TIME:20110331T200000
|
||||
DTEND;VALUE=DATE-TIME:20110331T203000
|
||||
DTSTAMP:20110430T192946Z
|
||||
URL;VALUE=URI:
|
||||
SUMMARY:The Big Bang Theory - S04E19 - The Zarnecki Incursion
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
@ -1,747 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
X-WR-CALNAME:John Doe (TripIt)
|
||||
X-WR-CALDESC:TripIt Calendar
|
||||
X-PUBLISHED-TTL:PT15M
|
||||
PRODID:-//John Doe/NONSGML Bennu 0.1//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:c32a5eaba2354bb29e012ec18da827db90550a3b@tripit.com
|
||||
DTSTART;VALUE=DATE:20111011
|
||||
DTEND;VALUE=DATE:20111014
|
||||
SUMMARY:South San Francisco\, CA\, October 2011\;
|
||||
LOCATION:South San Francisco\, CA
|
||||
GEO:37.654656;-122.40775
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in South San Francisco\, CA from Oct 11
|
||||
to Oct 13\, 2011\nView and/or edit details in TripIt : http://www.tripit.c
|
||||
om/trip/show/id/23710889\nTripIt - organize your travel at http://www.trip
|
||||
it.com\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-ee275ccffa83f492d9eb63b01953b39f18d4f944@tripit.com
|
||||
DTSTART:20111011T100500
|
||||
DTEND:20111011T110500
|
||||
SUMMARY:Directions from SFO to Embassy Suites San Francisco Airport - Sout
|
||||
h San Francisco
|
||||
LOCATION:250 GATEWAY BLVD\, South San Francisco\, CA\, 94080
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/1234\n \n[Directions] 10/11/2011 10:05am - Directions from S
|
||||
FO to Embassy Suites San Francisco Airport - South San Francisco \nfrom: S
|
||||
FO \nto: 250 GATEWAY BLVD\, South San Francisco\, CA\, 94080 \nView direct
|
||||
ions here: http://maps.google.com/maps?output=mobile&saddr=SFO&daddr=250+G
|
||||
ATEWAY+BLVD%2C+South+San+Francisco%2C+CA%2C+94080 \n \n \n\nTripIt - organ
|
||||
ize your travel at http://www.tripit.com
|
||||
GEO:37.655634;-122.401273
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111011T165500Z
|
||||
SUMMARY:US403 PHX to SFO
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-c576afd397cf1f90578b4ba35e781b61ba8897db@tripit.com
|
||||
DTSTART:20111011T144500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/1234\n \n[Flight] 10/11/2011 US Airways(US) #403 dep PHX 7:4
|
||||
5am MST arr SFO 9:55am PDT\; John Doe\; seat(s) 8B\; conf #DXH9K
|
||||
Z\, BXQ9WH \nBooked on http://www.americanexpress-travel.com/\; Reference
|
||||
#: 4127 8626 9715\; http://www.americanexpress-travel.com/\; US:1-800-297-
|
||||
2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at htt
|
||||
p://www.tripit.com
|
||||
GEO:37.618889;-122.375
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Dollar Rent A Car
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-e99a90ee1c7e4f5b68a4e551009e5bb6c475940c@tripit.com
|
||||
DTSTART:20111011T172500Z
|
||||
DTEND:20111011T182500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/1234\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
|
||||
rnational Airport\; primary driver John Doe\; conf #R9508361 \np
|
||||
ickup 10/11/2011 10:25am\; dropoff 10/13/2011 6:49pm \nEconomy \nBooked on
|
||||
http://www.americanexpress-travel.com/\; Reference #: 4127 8626 9715\; ht
|
||||
tp://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582
|
||||
-2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: Embassy Suites San Francisco Airport - South San Francis
|
||||
co
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-7f3288d418bed063cc82b4512e792fbb5d8ae761@tripit.com
|
||||
DTSTART:20111011T185500Z
|
||||
DTEND:20111011T195500Z
|
||||
LOCATION:250 GATEWAY BLVD\, South San Francisco\, CA\, 94080
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/23710889\n \n[Lodging] Embassy Suites San Francisco Airport - So
|
||||
uth San Francisco\; primary guest John Doe\; conf #R9508361 \n25
|
||||
0 GATEWAY BLVD\, South San Francisco\, CA\, 94080\; tel 1.650.589.3400 \na
|
||||
rrive 10/11/2011\; depart 10/13/2011\; rooms: 1 \nBooked on http://www.ame
|
||||
ricanexpress-travel.com/\; Reference #: 4127 8626 9715\; http://www.americ
|
||||
anexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n \n \n\
|
||||
nTripIt - organize your travel at http://www.tripit.com
|
||||
GEO:37.655634;-122.401273
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: Embassy Suites San Francisco Airport - South San Franci
|
||||
sco
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-5eb4cb5fc25c55b0423921e18336e57f8c34598d@tripit.com
|
||||
DTSTART:20111014T011900Z
|
||||
DTEND:20111014T021900Z
|
||||
LOCATION:250 GATEWAY BLVD\, South San Francisco\, CA\, 94080
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/23710889\n \n[Lodging] Embassy Suites San Francisco Airport - So
|
||||
uth San Francisco\; primary guest John Doe\; conf #R9508361 \n25
|
||||
0 GATEWAY BLVD\, South San Francisco\, CA\, 94080\; tel 1.650.589.3400 \na
|
||||
rrive 10/11/2011\; depart 10/13/2011\; rooms: 1 \nBooked on http://www.ame
|
||||
ricanexpress-travel.com/\; Reference #: 4127 8626 9715\; http://www.americ
|
||||
anexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n \n \n\
|
||||
nTripIt - organize your travel at http://www.tripit.com
|
||||
GEO:37.655634;-122.401273
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Dollar Rent A Car
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-11fdbf5d02e84646025716d9f9c7a4158e1fb025@tripit.com
|
||||
DTSTART:20111014T014900Z
|
||||
DTEND:20111014T024900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/23710889\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
|
||||
rnational Airport\; primary driver John Doe\; conf #R9508361 \np
|
||||
ickup 10/11/2011 10:25am\; dropoff 10/13/2011 6:49pm \nEconomy \nBooked on
|
||||
http://www.americanexpress-travel.com/\; Reference #: 4127 8626 9715\; ht
|
||||
tp://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582
|
||||
-2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111014T051900Z
|
||||
SUMMARY:CO6256 SFO to PHX
|
||||
LOCATION:San Francisco (SFO)
|
||||
UID:item-cb485a571a01972d6bdc74c2b829905d6e3786bf@tripit.com
|
||||
DTSTART:20111014T031900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/23710889\n \n[Flight] 10/13/2011 Continental Airlines(CO) #6256
|
||||
dep SFO 8:19pm PDT arr PHX 10:19pm MST\; John Doe\; conf #DXH9KZ
|
||||
\, BXQ9WH(Operated by United Airlines flight 6256) \nBooked on http://www.
|
||||
americanexpress-travel.com/\; Reference #: 4127 8626 9715\; http://www.ame
|
||||
ricanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n \n
|
||||
\n\nTripIt - organize your travel at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:c7b133db1e7be2713a4a63b75dcbad209690cab5@tripit.com
|
||||
DTSTART;VALUE=DATE:20111023
|
||||
DTEND;VALUE=DATE:20111028
|
||||
SUMMARY:Santa Barbara\, CA\, October 2011
|
||||
LOCATION:Santa Barbara\, CA
|
||||
GEO:34.420831;-119.69819
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in Santa Barbara\, CA from Oct 23 to Oct
|
||||
27\, 2011\nView and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\nTripIt - organize your travel at http://www.tripit.com
|
||||
\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111023T191200Z
|
||||
SUMMARY:US2719 PHX to SBA
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-c4375369e9070fcc04df39ed18c4d93087577591@tripit.com
|
||||
DTSTART:20111023T173500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Flight] 10/23/2011 US Airways(US) #2719 dep PHX 10
|
||||
:35am MST arr SBA 12:12pm PDT\; John Doe Ticket #0378717202638\;
|
||||
conf #A44XS5\, PRX98G\, FYYJZ4 \nBooked on http://www.americanexpress-tra
|
||||
vel.com/\; Reference #: 7128 8086 8504\; http://www.americanexpress-travel
|
||||
.com/\; US:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $699.99 \n
|
||||
\n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
GEO:34.427778;-119.839444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-962e4f045d12149319d1837ec096bf43770abd6e@tripit.com
|
||||
DTSTART:20111025T094000
|
||||
DTEND:20111025T104000
|
||||
SUMMARY:Directions from Hertz to Sofitel San Francisco Bay
|
||||
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Directions] 10/25/2011 9:40am - Directions from He
|
||||
rtz to Sofitel San Francisco Bay \nfrom: 780 McDonnell Road\, San Francisc
|
||||
o\, CA\, 94128 \nto: 223 Twin Dolphin Drive\, Redwood City\, CA\, 94065 \n
|
||||
View directions here: http://maps.google.com/maps?output=mobile&saddr=780+
|
||||
McDonnell+Road%2C+San+Francisco%2C+CA%2C+94128&daddr=223+Twin+Dolphin+Driv
|
||||
e%2C+Redwood+City%2C+CA%2C+94065 \n \n \n\nTripIt - organize your travel a
|
||||
t http://www.tripit.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111025T162600Z
|
||||
SUMMARY:UA5304 SBA to SFO
|
||||
LOCATION:Santa Barbara (SBA)
|
||||
UID:item-ae300a6934c3820974dba2c9c5b8fae843c67693@tripit.com
|
||||
DTSTART:20111025T150900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Flight] 10/25/2011 United Airlines(UA) #5304 dep S
|
||||
BA 8:09am PDT arr SFO 9:26am PDT\; John Doe Ticket #037871720263
|
||||
8\; seat(s) 11B\; conf #A44XS5\, PRX98G\, FYYJZ4 \nBooked on http://www.am
|
||||
ericanexpress-travel.com/\; Reference #: 7128 8086 8504\; http://www.ameri
|
||||
canexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716\; Total
|
||||
Cost: $699.99 \n \n \n\nTripIt - organize your travel at http://www.tripit
|
||||
.com
|
||||
GEO:37.618889;-122.375
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Hertz
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-2a9fd5a57a4cdda4677fc6ce23738e1954fdbe2a@tripit.com
|
||||
DTSTART:20111025T163000Z
|
||||
DTEND:20111025T173000Z
|
||||
LOCATION:780 McDonnell Road\, San Francisco\, CA\, 94128
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Car Rental] Hertz\; San Francisco International Ai
|
||||
rport\; primary driver John Doe\; conf #F2633064194 \n780 McDonn
|
||||
ell Road\, San Francisco\, CA\, 94128 \npickup 10/25/2011 9:30am\; dropoff
|
||||
10/27/2011 7:00pm \nToyota Corolla or similar\; 84.57 USD \nBooked on htt
|
||||
p://www.hertz.com/\; Reference #: F2633064194\; http://www.hertz.com/\; 80
|
||||
0-654-3131\; Booking Rate: 84.57 USD\; Total Cost: 333.76 USD \n \n \n\nTr
|
||||
ipIt - organize your travel at http://www.tripit.com
|
||||
GEO:37.6297569;-122.4000351
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-98dfcb0bcfdcffcce9c58a84947212ed67cadda6@tripit.com
|
||||
DTSTART:20111025T163600Z
|
||||
DTEND:20111025T173600Z
|
||||
SUMMARY:Directions from SFO to Sofitel San Francisco Bay
|
||||
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Directions] 10/25/2011 9:36am - Directions from SF
|
||||
O to Sofitel San Francisco Bay \nfrom: SFO \nto: 223 Twin Dolphin Drive\,
|
||||
Redwood City\, CA\, 94065 \nView directions here: http://maps.google.com/m
|
||||
aps?output=mobile&saddr=SFO&daddr=223+Twin+Dolphin+Drive%2C+Redwood+City%2
|
||||
C+CA%2C+94065 \n \n \n\nTripIt - organize your travel at http://www.tripit
|
||||
.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: Sofitel San Francisco Bay
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-8de3937b336c333faf2d55ad0a41c5ca6cc02393@tripit.com
|
||||
DTSTART:20111025T220000Z
|
||||
DTEND:20111025T230000Z
|
||||
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
|
||||
John Doe\; conf #F80-0GMW \n223 Twin Dolphin Drive\, Redwood Ci
|
||||
ty\, CA\, 94065\; tel (+1)650/598-9000 \narrive 10/25/2011\; depart 10/27/
|
||||
2011\; rooms: 1 \nBooked on http://www.sofitel.com/\; http://www.sofitel.c
|
||||
om/\; Total Cost: 564.00 USD \n \n \n\nTripIt - organize your travel at ht
|
||||
tp://www.tripit.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: Sofitel San Francisco Bay
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-f3ade58646964bde101616a6d26ea7784a1a81e8@tripit.com
|
||||
DTSTART:20111027T190000Z
|
||||
DTEND:20111027T200000Z
|
||||
LOCATION:223 Twin Dolphin Drive\, Redwood City\, CA\, 94065
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
|
||||
John Doe\; conf #F80-0GMW \n223 Twin Dolphin Drive\, Redwood Ci
|
||||
ty\, CA\, 94065\; tel (+1)650/598-9000 \narrive 10/25/2011\; depart 10/27/
|
||||
2011\; rooms: 1 \nBooked on http://www.sofitel.com/\; http://www.sofitel.c
|
||||
om/\; Total Cost: 564.00 USD \n \n \n\nTripIt - organize your travel at ht
|
||||
tp://www.tripit.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Hertz
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-50620273fea0614d37775649034d5e1de92ae361@tripit.com
|
||||
DTSTART:20111028T020000Z
|
||||
DTEND:20111028T030000Z
|
||||
LOCATION:780 McDonnell Road\, San Francisco\, CA\, 94128
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Car Rental] Hertz\; San Francisco International Ai
|
||||
rport\; primary driver John Doe\; conf #F2633064194 \n780 McDonn
|
||||
ell Road\, San Francisco\, CA\, 94128 \npickup 10/25/2011 9:30am\; dropoff
|
||||
10/27/2011 7:00pm \nToyota Corolla or similar\; 84.57 USD \nBooked on htt
|
||||
p://www.hertz.com/\; Reference #: F2633064194\; http://www.hertz.com/\; 80
|
||||
0-654-3131\; Booking Rate: 84.57 USD\; Total Cost: 333.76 USD \n \n \n\nTr
|
||||
ipIt - organize your travel at http://www.tripit.com
|
||||
GEO:37.6297569;-122.4000351
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111028T051900Z
|
||||
SUMMARY:CO6256 SFO to PHX
|
||||
LOCATION:San Francisco (SFO)
|
||||
UID:item-71d327f30d8beeaf7bf50c8fa63ce16005b9b0df@tripit.com
|
||||
DTSTART:20111028T031900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24259445\n \n[Flight] 10/27/2011 Continental Airlines(CO) #6256
|
||||
dep SFO 8:19pm PDT arr PHX 10:19pm MST\; John Doe Ticket #037871
|
||||
7202638\; seat(s) 17D\; conf #A44XS5\, PRX98G\, FYYJZ4(Operated by United
|
||||
Airlines flight 6256) \nBooked on http://www.americanexpress-travel.com/\;
|
||||
Reference #: 7128 8086 8504\; http://www.americanexpress-travel.com/\; US
|
||||
:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $699.99 \n \n \n\nTri
|
||||
pIt - organize your travel at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:2d4b446e63a94ade7dab0f0e9546b2d1965f011c@tripit.com
|
||||
DTSTART;VALUE=DATE:20111108
|
||||
DTEND;VALUE=DATE:20111111
|
||||
SUMMARY:Redwood City\, CA\, November 2011
|
||||
LOCATION:Redwood City\, CA
|
||||
GEO:37.485215;-122.236355
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in Redwood City\, CA from Nov 8 to Nov 1
|
||||
0\, 2011\nView and/or edit details in TripIt : http://www.tripit.com/trip/
|
||||
show/id/24913749\nTripIt - organize your travel at http://www.tripit.com\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111108T175700Z
|
||||
SUMMARY:US403 PHX to SFO
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-7de7d829b2f95991de6d01c3d68f24b84770168c@tripit.com
|
||||
DTSTART:20111108T154500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Flight] 11/8/2011 US Airways(US) #403 dep PHX 8:45
|
||||
am MST arr SFO 9:57am PST\; John Doe\; seat(s) 21C\; conf #FJDX0
|
||||
J\, I2W8HW \nBooked on http://www.americanexpress-travel.com/\; Reference
|
||||
#: 4129 9623 4732\; http://www.americanexpress-travel.com/\; US:1-800-297-
|
||||
2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at htt
|
||||
p://www.tripit.com
|
||||
GEO:37.618889;-122.375
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Dollar Rent A Car
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-1ac6982fefdd79bc5ea849785f415a6291c450b1@tripit.com
|
||||
DTSTART:20111108T182700Z
|
||||
DTEND:20111108T192700Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
|
||||
rnational Airport\; primary driver John Doe\; conf #Q0058133 \np
|
||||
ickup 11/8/2011 10:27am\; dropoff 11/10/2011 6:25pm \nEconomy \nBooked on
|
||||
http://www.americanexpress-travel.com/\; Reference #: 4129 9623 4732\; htt
|
||||
p://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-
|
||||
2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: Sofitel San Francisco Bay
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-126e584ffbefbec32a15ca503f0bdf8d3f9cc2f4@tripit.com
|
||||
DTSTART:20111108T195700Z
|
||||
DTEND:20111108T205700Z
|
||||
LOCATION:223 TWIN DOLPHIN DR\, Redwood City\, CA\, 94065-1514
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
|
||||
John Doe\; conf #Q0058133 \n223 TWIN DOLPHIN DR\, Redwood City\
|
||||
, CA\, 94065-1514\; tel 1.650.598.9000 \narrive 11/8/2011\; depart 11/10/2
|
||||
011\; rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Refere
|
||||
nce #: 4129 9623 4732\; http://www.americanexpress-travel.com/\; US:1-800-
|
||||
297-2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at
|
||||
http://www.tripit.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: Sofitel San Francisco Bay
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-ff48c502022356ccaa862ebb61761a0de08a1ce9@tripit.com
|
||||
DTSTART:20111111T015500Z
|
||||
DTEND:20111111T025500Z
|
||||
LOCATION:223 TWIN DOLPHIN DR\, Redwood City\, CA\, 94065-1514
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Lodging] Sofitel San Francisco Bay\; primary guest
|
||||
John Doe\; conf #Q0058133 \n223 TWIN DOLPHIN DR\, Redwood City\
|
||||
, CA\, 94065-1514\; tel 1.650.598.9000 \narrive 11/8/2011\; depart 11/10/2
|
||||
011\; rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Refere
|
||||
nce #: 4129 9623 4732\; http://www.americanexpress-travel.com/\; US:1-800-
|
||||
297-2977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at
|
||||
http://www.tripit.com
|
||||
GEO:37.5232475;-122.261296
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Dollar Rent A Car
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-c0273c03ddbb68a9b05d5d43a489bc318136ca42@tripit.com
|
||||
DTSTART:20111111T022500Z
|
||||
DTEND:20111111T032500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Car Rental] Dollar Rent A Car\; San Francisco Inte
|
||||
rnational Airport\; primary driver John Doe\; conf #Q0058133 \np
|
||||
ickup 11/8/2011 10:27am\; dropoff 11/10/2011 6:25pm \nEconomy \nBooked on
|
||||
http://www.americanexpress-travel.com/\; Reference #: 4129 9623 4732\; htt
|
||||
p://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-
|
||||
2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111111T055400Z
|
||||
SUMMARY:CO496 SFO to PHX
|
||||
LOCATION:San Francisco (SFO)
|
||||
UID:item-3473cf9275326ac393b37859df3b04306b4849aa@tripit.com
|
||||
DTSTART:20111111T035500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/24913749\n \n[Flight] 11/10/2011 Continental Airlines(CO) #496 d
|
||||
ep SFO 7:55pm PST arr PHX 10:54pm MST\; John Doe\; seat(s) 26B\;
|
||||
conf #FJDX0J\, I2W8HW(Operated by United Airlines flight 496) \nBooked on
|
||||
http://www.americanexpress-travel.com/\; Reference #: 4129 9623 4732\; ht
|
||||
tp://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582
|
||||
-2716 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:4ee5ded058432990e3d8808f48ca851e04923b6d@tripit.com
|
||||
DTSTART;VALUE=DATE:20111129
|
||||
DTEND;VALUE=DATE:20111202
|
||||
SUMMARY:Milpitas\, CA\, November 2011
|
||||
LOCATION:Milpitas\, CA
|
||||
GEO:37.428272;-121.906624
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in Milpitas\, CA from Nov 29 to Dec 1\,
|
||||
2011\nView and/or edit details in TripIt : http://www.tripit.com/trip/show
|
||||
/id/25671681\nTripIt - organize your travel at http://www.tripit.com\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111129T172400Z
|
||||
SUMMARY:US282 PHX to SJC
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-644d5973b50d521d50e475ccf5321605d54bd0d5@tripit.com
|
||||
DTSTART:20111129T152500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Flight] 11/29/2011 US Airways(US) #282 dep PHX 8:2
|
||||
5am MST arr SJC 9:24am PST\; John Doe\; seat(s) 17C\; conf #DQKD
|
||||
GY \nBooked on http://www.americanexpress-travel.com/\; Reference #: 4131
|
||||
3301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2977\, O
|
||||
utside:210-582-2716 \n \n \n\nTripIt - organize your travel at http://www.
|
||||
tripit.com
|
||||
GEO:37.361111;-121.925556
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Alamo
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-10368bbdbc9b6f26f83098500633cc4eb604c751@tripit.com
|
||||
DTSTART:20111129T175400Z
|
||||
DTEND:20111129T185400Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Car Rental] Alamo\; San Jose International Airport
|
||||
\; primary driver John Doe\; conf #372828149COUNT \npickup 11/29
|
||||
/2011 9:54am\; dropoff 12/1/2011 5:45pm \nIntermediate \nBooked on http://
|
||||
www.americanexpress-travel.com/\; Reference #: 4131 3301 9911\; http://www
|
||||
.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n
|
||||
\n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: The Beverly Heritage Hotel
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-98d8638d3f1c011d03cb8f58b3a14a0f1203339b@tripit.com
|
||||
DTSTART:20111129T192400Z
|
||||
DTEND:20111129T202400Z
|
||||
LOCATION:1820 Barber Lane\, Milpitas\, CA\, 95035
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Lodging] The Beverly Heritage Hotel\; primary gues
|
||||
t John Doe\; conf #372828149COUNT \n1820 Barber Lane\, Milpitas\
|
||||
, CA\, 95035\; tel 1.408.943.9080 \narrive 11/29/2011\; depart 12/1/2011\;
|
||||
rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Reference #
|
||||
: 4131 3301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2
|
||||
977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at http
|
||||
://www.tripit.com
|
||||
GEO:37.4010467;-121.9116284
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111201T194400Z
|
||||
SUMMARY:US273 SJC to PHX
|
||||
LOCATION:San Jose (SJC)
|
||||
UID:item-7b9ee9bb4edfe69743e32b33f9be55753956a883@tripit.com
|
||||
DTSTART:20111201T175900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Flight] 12/1/2011 US Airways(US) #273 dep SJC 9:59
|
||||
am PST arr PHX 12:44pm MST\; John Doe Ticket #0378727451156\; co
|
||||
nf #EMF71T \nBooked on http://www.americanexpress-travel.com/\; Reference
|
||||
#: 5133 5264 1627\; http://www.americanexpress-travel.com/\; US:1-800-297-
|
||||
2977\, Outside:210-582-2716\; Total Cost: $316.69 \n \n \n\nTripIt - organ
|
||||
ize your travel at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: The Beverly Heritage Hotel
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-f79f203072002b8f06598dcb2be0e36af17b625b@tripit.com
|
||||
DTSTART:20111202T011500Z
|
||||
DTEND:20111202T021500Z
|
||||
LOCATION:1820 Barber Lane\, Milpitas\, CA\, 95035
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Lodging] The Beverly Heritage Hotel\; primary gues
|
||||
t John Doe\; conf #372828149COUNT \n1820 Barber Lane\, Milpitas\
|
||||
, CA\, 95035\; tel 1.408.943.9080 \narrive 11/29/2011\; depart 12/1/2011\;
|
||||
rooms: 1 \nBooked on http://www.americanexpress-travel.com/\; Reference #
|
||||
: 4131 3301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2
|
||||
977\, Outside:210-582-2716 \n \n \n\nTripIt - organize your travel at http
|
||||
://www.tripit.com
|
||||
GEO:37.4010467;-121.9116284
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Alamo
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-69f526ad49fa8ca0a74486f4fc77cc3f9d23a72f@tripit.com
|
||||
DTSTART:20111202T014500Z
|
||||
DTEND:20111202T024500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Car Rental] Alamo\; San Jose International Airport
|
||||
\; primary driver John Doe\; conf #372828149COUNT \npickup 11/29
|
||||
/2011 9:54am\; dropoff 12/1/2011 5:45pm \nIntermediate \nBooked on http://
|
||||
www.americanexpress-travel.com/\; Reference #: 4131 3301 9911\; http://www
|
||||
.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:210-582-2716 \n
|
||||
\n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111202T045900Z
|
||||
SUMMARY:US288 SJC to PHX
|
||||
LOCATION:San Jose (SJC)
|
||||
UID:item-dab68a87c8dd49064ab0ba1dec5ba75ba46ff1d3@tripit.com
|
||||
DTSTART:20111202T031500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/25671681\n \n[Flight] 12/1/2011 US Airways(US) #288 dep SJC 7:15
|
||||
pm PST arr PHX 9:59pm MST\; John Doe\; seat(s) 13C\; conf #DQKDG
|
||||
Y \nBooked on http://www.americanexpress-travel.com/\; Reference #: 4131 3
|
||||
301 9911\; http://www.americanexpress-travel.com/\; US:1-800-297-2977\, Ou
|
||||
tside:210-582-2716 \n \n \n\nTripIt - organize your travel at http://www.t
|
||||
ripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:67d48ddde166a2e9bbac2cf7d93fe493b0860008@tripit.com
|
||||
DTSTART;VALUE=DATE:20111213
|
||||
DTEND;VALUE=DATE:20111216
|
||||
SUMMARY:San Jose\, CA\, December 2011
|
||||
LOCATION:San Jose\, CA
|
||||
GEO:37.339386;-121.894955
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in San Jose\, CA from Dec 13 to Dec 15\,
|
||||
2011\nView and/or edit details in TripIt : http://www.tripit.com/trip/sho
|
||||
w/id/27037117\nTripIt - organize your travel at http://www.tripit.com\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111213T172400Z
|
||||
SUMMARY:US282 PHX to SJC
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-2b1b9021be548a87dd335f190b60ab78c33b619d@tripit.com
|
||||
DTSTART:20111213T152500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Flight] 12/13/2011 US Airways(US) #282 dep PHX 8:2
|
||||
5am MST arr SJC 9:24am PST\; John Doe Ticket #0378728465928\; se
|
||||
at(s) 15C\; conf #GGNV29 \nBooked on http://www.americanexpress-travel.com
|
||||
/\; Reference #: 3134 0525 5102\; http://www.americanexpress-travel.com/\;
|
||||
US:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $406.39 \n \n \n\n
|
||||
TripIt - organize your travel at http://www.tripit.com
|
||||
GEO:37.361111;-121.925556
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Advantage
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-619d345bb08aaef68e8767b672277243697f5bff@tripit.com
|
||||
DTSTART:20111213T180000Z
|
||||
DTEND:20111213T190000Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Car Rental] Advantage\; San Jose International Air
|
||||
port\; primary driver John Doe\; conf #F31539020E7 \npickup 12/1
|
||||
3/2011 10:00am\; dropoff 12/15/2011 7:00pm \nStandard Convertible \nRefere
|
||||
nce #: 3134 0526 3890 \n \n \n\nTripIt - organize your travel at http://ww
|
||||
w.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: Crestview Hotel:
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-fbe6c08e7523c82fac69b40ad1d0899f3d8d5982@tripit.com
|
||||
DTSTART:20111213T192400Z
|
||||
DTEND:20111213T202400Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Lodging] Crestview Hotel:\; conf #CR31342159 \ntel
|
||||
650-966-8848 \narrive 12/13/2011\; depart 12/15/2011 \nBooking Rate: 153.
|
||||
30 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: Crestview Hotel:
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-7ed8b84628e650a6b37161c7825bac9e72add49f@tripit.com
|
||||
DTSTART:20111216T011500Z
|
||||
DTEND:20111216T021500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Lodging] Crestview Hotel:\; conf #CR31342159 \ntel
|
||||
650-966-8848 \narrive 12/13/2011\; depart 12/15/2011 \nBooking Rate: 153.
|
||||
30 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Advantage
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-623b54ebe07ffd48845f1a120a86940ce79c698b@tripit.com
|
||||
DTSTART:20111216T030000Z
|
||||
DTEND:20111216T040000Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Car Rental] Advantage\; San Jose International Air
|
||||
port\; primary driver John Doe\; conf #F31539020E7 \npickup 12/1
|
||||
3/2011 10:00am\; dropoff 12/15/2011 7:00pm \nStandard Convertible \nRefere
|
||||
nce #: 3134 0526 3890 \n \n \n\nTripIt - organize your travel at http://ww
|
||||
w.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20111216T045900Z
|
||||
SUMMARY:US288 SJC to PHX
|
||||
LOCATION:San Jose (SJC)
|
||||
UID:item-52481e672972d2e88d5eaa5cf49bb801562c6014@tripit.com
|
||||
DTSTART:20111216T031500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27037117\n \n[Flight] 12/15/2011 US Airways(US) #288 dep SJC 7:1
|
||||
5pm PST arr PHX 9:59pm MST\; John Doe Ticket #0378728465928\; se
|
||||
at(s) 7B\; conf #GGNV29 \nBooked on http://www.americanexpress-travel.com/
|
||||
\; Reference #: 3134 0525 5102\; http://www.americanexpress-travel.com/\;
|
||||
US:1-800-297-2977\, Outside:210-582-2716\; Total Cost: $406.39 \n \n \n\nT
|
||||
ripIt - organize your travel at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
UID:7299ff29daed7d5c3e2ed4acc74deec5b7942bd5@tripit.com
|
||||
DTSTART;VALUE=DATE:20120103
|
||||
DTEND;VALUE=DATE:20120106
|
||||
SUMMARY:San Francisco\, CA\, January 2012
|
||||
LOCATION:San Francisco\, CA
|
||||
GEO:37.774929;-122.419415
|
||||
TRANSP:TRANSPARENT
|
||||
DESCRIPTION:John Doe is in San Francisco\, CA from Jan 3 to Jan
|
||||
5\, 2012\nView and/or edit details in TripIt : http://www.tripit.com/trip/
|
||||
show/id/27863159\nTripIt - organize your travel at http://www.tripit.com\n
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20120103T175700Z
|
||||
SUMMARY:US403 PHX to SFO
|
||||
LOCATION:Phoenix (PHX)
|
||||
UID:item-f099e76114bf43ef3b122432579d8b40995412a7@tripit.com
|
||||
DTSTART:20120103T154500Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Flight] 1/3/2012 US Airways(US) #403 dep PHX 8:45a
|
||||
m MST arr SFO 9:57am PST\; John Doe Ticket #0378731791515\; conf
|
||||
#FH9B72\, L4F9M5 \nBooked on http://www.americanexpress-travel.com/\; Ref
|
||||
erence #: 6135 7391 6119\; http://www.americanexpress-travel.com/\; US:1-8
|
||||
00-297-2977\, Outside:210-582-2716\; Total Cost: $668.39 \n \n \n\nTripIt
|
||||
- organize your travel at http://www.tripit.com
|
||||
GEO:37.618889;-122.375
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Pick-up Rental Car: Alamo
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-fae4b4b07b66fc87df125238e0aaf645106cf4f3@tripit.com
|
||||
DTSTART:20120103T180000Z
|
||||
DTEND:20120103T190000Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Car Rental] Alamo\; San Francisco International Ai
|
||||
rport\; primary driver John Doe\; conf #373525981COUNT \npickup
|
||||
1/3/2012 10:00am\; dropoff 1/5/2012 6:00pm \nCompact \nReference #: 6135 7
|
||||
391 6898 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-in: Grand Hotel Sunnyvale
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-d89a856eb9da9dfdcb4da46f42e49af3a838fcbb@tripit.com
|
||||
DTSTART:20120103T195700Z
|
||||
DTEND:20120103T205700Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Lodging] Grand Hotel Sunnyvale\; conf #22084SY0361
|
||||
18 \ntel 1-408-7208500 \narrive 1/3/2012\; depart 1/5/2012 \nBooking Rate:
|
||||
USD 169.00 \nPolicies: Guarantee to valid form of payment is required at
|
||||
time of booking\; Cancel 1 day prior to arrival date to avoid penalty of 1
|
||||
Nights Room Charge. Change fee may apply for early departures and changes
|
||||
made to confirmed reservations.\; \n \n \n\nTripIt - organize your travel
|
||||
at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Check-out: Grand Hotel Sunnyvale
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-6edc82f6411fd0b66f2f7f6baafa41623a8623a9@tripit.com
|
||||
DTSTART:20120106T010900Z
|
||||
DTEND:20120106T020900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Lodging] Grand Hotel Sunnyvale\; conf #22084SY0361
|
||||
18 \ntel 1-408-7208500 \narrive 1/3/2012\; depart 1/5/2012 \nBooking Rate:
|
||||
USD 169.00 \nPolicies: Guarantee to valid form of payment is required at
|
||||
time of booking\; Cancel 1 day prior to arrival date to avoid penalty of 1
|
||||
Nights Room Charge. Change fee may apply for early departures and changes
|
||||
made to confirmed reservations.\; \n \n \n\nTripIt - organize your travel
|
||||
at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
SUMMARY:Drop-off Rental Car: Alamo
|
||||
TRANSP:TRANSPARENT
|
||||
UID:item-58a31b96066ffd09b800af49de59a84f7b7a3a06@tripit.com
|
||||
DTSTART:20120106T020000Z
|
||||
DTEND:20120106T030000Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Car Rental] Alamo\; San Francisco International Ai
|
||||
rport\; primary driver John Doe\; conf #373525981COUNT \npickup
|
||||
1/3/2012 10:00am\; dropoff 1/5/2012 6:00pm \nCompact \nReference #: 6135 7
|
||||
391 6898 \n \n \n\nTripIt - organize your travel at http://www.tripit.com
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20120101T215311Z
|
||||
DTEND:20120106T050500Z
|
||||
SUMMARY:CO496 SFO to PHX
|
||||
LOCATION:San Francisco (SFO)
|
||||
UID:item-7884351ce42d503b90ccc48c33c7c30bd4f44767@tripit.com
|
||||
DTSTART:20120106T030900Z
|
||||
DESCRIPTION:View and/or edit details in TripIt : http://www.tripit.com/tri
|
||||
p/show/id/27863159\n \n[Flight] 1/5/2012 Continental Airlines(CO) #496 dep
|
||||
SFO 7:09pm PST arr PHX 10:05pm MST\; John Doe Ticket #037873179
|
||||
1515\; conf #FH9B72\, L4F9M5(Operated by United Airlines flight 496) \nBoo
|
||||
ked on http://www.americanexpress-travel.com/\; Reference #: 6135 7391 611
|
||||
9\; http://www.americanexpress-travel.com/\; US:1-800-297-2977\, Outside:2
|
||||
10-582-2716\; Total Cost: $668.39 \n \n \n\nTripIt - organize your travel
|
||||
at http://www.tripit.com
|
||||
GEO:33.436111;-112.009444
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
@ -1,41 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Meetup//RemoteApi//EN
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-ORIGINAL-URL:http://www.meetup.com/events/ical/8333638/dfdba2e469216075
|
||||
3404f737feace78d526ff0ce/going
|
||||
X-WR-CALNAME:My Meetups
|
||||
X-MS-OLK-FORCEINSPECTOROPEN:TRUE
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Phoenix
|
||||
TZURL:http://tzurl.org/zoneinfo-outlook/America/Phoenix
|
||||
X-LIC-LOCATION:America/Phoenix
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:-0700
|
||||
TZOFFSETTO:-0700
|
||||
TZNAME:MST
|
||||
DTSTART:19700101T000000
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20111106T155927Z
|
||||
DTSTART;TZID=America/Phoenix:20111109T190000
|
||||
DTEND;TZID=America/Phoenix:20111109T210000
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Phoenix Drupal User Group Monthly Meetup
|
||||
DESCRIPTION:Phoenix Drupal User Group\nWednesday\, November 9 at 7:00 PM\
|
||||
n\nCustomizing node display with template pages in Drupal 6\n\n Jon Shee
|
||||
han and Matthew Berry of the Office of Knowledge Enterprise Development
|
||||
(OKED) Knowledge...\n\nDetails: http://www.meetup.com/Phoenix-Drupal-Use
|
||||
r-Group/events/33627272/
|
||||
CLASS:PUBLIC
|
||||
CREATED:20100630T083023Z
|
||||
GEO:33.56;-111.90
|
||||
LOCATION:Open Source Project Tempe (1415 E University Dr. #103A\, Tempe\,
|
||||
AZ 85281)
|
||||
URL:http://www.meetup.com/Phoenix-Drupal-User-Group/events/33627272/
|
||||
LAST-MODIFIED:20111102T213309Z
|
||||
UID:event_nsmxnyppbfc@meetup.com
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
1170
modules/default/calendar/vendor/ical.js/test/test6.ics
vendored
@ -1,16 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:ownCloud Calendar 0.6.3
|
||||
X-WR-CALNAME:Fête Nationale - Férié
|
||||
BEGIN:VEVENT
|
||||
CREATED:20090502T140513Z
|
||||
DTSTAMP:20111106T124709Z
|
||||
UID:FA9831E7-C238-4FEC-95E5-CD46BD466421
|
||||
SUMMARY:Fête Nationale - Férié
|
||||
RRULE:FREQ=YEARLY
|
||||
DTSTART;VALUE=DATE:20120714
|
||||
DTEND;VALUE=DATE:20120715
|
||||
TRANSP:OPAQUE
|
||||
SEQUENCE:5
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
@ -1,23 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:ownCloud Calendar 0.6.3
|
||||
X-WR-CALNAME:Default calendar
|
||||
BEGIN:VTODO
|
||||
CREATED;VALUE=DATE-TIME:20130714T092804Z
|
||||
UID:0aa462f13c
|
||||
LAST-MODIFIED;VALUE=DATE-TIME:20130714T092804Z
|
||||
DTSTAMP;VALUE=DATE-TIME:20130714T092804Z
|
||||
CATEGORIES:Projets
|
||||
SUMMARY:Migrer le blog
|
||||
PERCENT-COMPLETE:100
|
||||
COMPLETED;VALUE=DATE-TIME;TZID=Europe/Monaco:20130716T105745
|
||||
END:VTODO
|
||||
BEGIN:VTODO
|
||||
CREATED;VALUE=DATE-TIME:20130714T092912Z
|
||||
UID:5e05bbcf34
|
||||
LAST-MODIFIED;VALUE=DATE-TIME:20130714T092912Z
|
||||
DTSTAMP;VALUE=DATE-TIME:20130714T092912Z
|
||||
SUMMARY:Créer test unitaire erreur ical
|
||||
CATEGORIES:Projets
|
||||
END:VTODO
|
||||
END:VCALENDAR
|
@ -1,21 +0,0 @@
|
||||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
UID:eb9e1bd2-ceba-499f-be77-f02773954c72
|
||||
SUMMARY:Event with an alarm
|
||||
DESCRIPTION:This is an event with an alarm.
|
||||
ORGANIZER="mailto:stomlinson@mozilla.com"
|
||||
DTSTART;TZID="America/Los_Angeles":20130418T110000
|
||||
DTEND;TZID="America/Los_Angeles":20130418T120000
|
||||
STATUS:CONFIRMED
|
||||
CLASS:PUBLIC
|
||||
TRANSP:OPAQUE
|
||||
LAST-MODIFIED:20130418T175632Z
|
||||
DTSTAMP:20130418T175632Z
|
||||
SEQUENCE:3
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER;RELATED=START:-PT5M
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
@ -52,10 +52,12 @@ Module.register("clock", {
|
||||
|
||||
//Calculate how many ms should pass until next update depending on if seconds is displayed or not
|
||||
var delayCalculator = function (reducedSeconds) {
|
||||
var EXTRA_DELAY = 50; //Deliberate imperceptable delay to prevent off-by-one timekeeping errors
|
||||
|
||||
if (self.config.displaySeconds) {
|
||||
return 1000 - moment().milliseconds();
|
||||
return 1000 - moment().milliseconds() + EXTRA_DELAY;
|
||||
} else {
|
||||
return (60 - reducedSeconds) * 1000 - moment().milliseconds();
|
||||
return (60 - reducedSeconds) * 1000 - moment().milliseconds() + EXTRA_DELAY;
|
||||
}
|
||||
};
|
||||
|
||||
@ -65,7 +67,7 @@ Module.register("clock", {
|
||||
|
||||
//If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
|
||||
if (self.config.displaySeconds) {
|
||||
self.second = (self.second + 1) % 60;
|
||||
self.second = moment().second();
|
||||
if (self.second !== 0) {
|
||||
self.sendNotification("CLOCK_SECOND", self.second);
|
||||
setTimeout(notificationTimer, delayCalculator(0));
|
||||
@ -74,7 +76,7 @@ Module.register("clock", {
|
||||
}
|
||||
|
||||
//If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
|
||||
self.minute = (self.minute + 1) % 60;
|
||||
self.minute = moment().minute();
|
||||
self.sendNotification("CLOCK_MINUTE", self.minute);
|
||||
setTimeout(notificationTimer, delayCalculator(0));
|
||||
};
|
||||
|
@ -1 +1 @@
|
||||
<svg id="Hour_Markers_-_Singlets" data-name="Hour Markers - Singlets" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;}.cls-2{stroke-width:0.5px;}</style></defs><title>face-001</title><line class="cls-1" x1="125" y1="1.25" x2="125" y2="16.23"/><line class="cls-1" x1="186.87" y1="17.83" x2="179.39" y2="30.8"/><line class="cls-1" x1="232.17" y1="63.12" x2="219.2" y2="70.61"/><line class="cls-1" x1="248.75" y1="125" x2="233.77" y2="125"/><line class="cls-1" x1="232.17" y1="186.87" x2="219.2" y2="179.39"/><line class="cls-1" x1="186.88" y1="232.17" x2="179.39" y2="219.2"/><line class="cls-1" x1="125" y1="248.75" x2="125" y2="233.77"/><line class="cls-1" x1="63.13" y1="232.17" x2="70.61" y2="219.2"/><line class="cls-1" x1="17.83" y1="186.88" x2="30.8" y2="179.39"/><line class="cls-1" x1="1.25" y1="125" x2="16.23" y2="125"/><line class="cls-1" x1="17.83" y1="63.13" x2="30.8" y2="70.61"/><line class="cls-1" x1="63.12" y1="17.83" x2="70.61" y2="30.8"/><line class="cls-2" x1="138.01" y1="1.25" x2="136.96" y2="11.23"/><line class="cls-2" x1="150.87" y1="3.29" x2="148.78" y2="13.11"/><line class="cls-2" x1="163.45" y1="6.66" x2="160.35" y2="16.21"/><line class="cls-2" x1="175.61" y1="11.33" x2="171.53" y2="20.5"/><line class="cls-2" x1="198.14" y1="24.33" x2="192.24" y2="32.45"/><line class="cls-2" x1="208.26" y1="32.53" x2="201.54" y2="39.99"/><line class="cls-2" x1="217.47" y1="41.74" x2="210.01" y2="48.46"/><line class="cls-2" x1="225.67" y1="51.86" x2="217.55" y2="57.76"/><line class="cls-2" x1="238.67" y1="74.39" x2="229.5" y2="78.47"/><line class="cls-2" x1="243.34" y1="86.55" x2="233.79" y2="89.65"/><line class="cls-2" x1="246.71" y1="99.13" x2="236.89" y2="101.22"/><line class="cls-2" x1="248.75" y1="111.99" x2="238.77" y2="113.04"/><line class="cls-2" x1="248.75" y1="138.01" x2="238.77" y2="136.96"/><line class="cls-2" x1="246.71" y1="150.87" x2="236.89" y2="148.78"/><line class="cls-2" x1="243.34" y1="163.45" x2="233.79" y2="160.35"/><line class="cls-2" x1="238.67" y1="175.61" x2="229.5" y2="171.53"/><line class="cls-2" x1="225.67" y1="198.14" x2="217.55" y2="192.24"/><line class="cls-2" x1="217.47" y1="208.26" x2="210.01" y2="201.54"/><line class="cls-2" x1="208.26" y1="217.47" x2="201.54" y2="210.01"/><line class="cls-2" x1="198.14" y1="225.67" x2="192.24" y2="217.55"/><line class="cls-2" x1="175.61" y1="238.67" x2="171.53" y2="229.5"/><line class="cls-2" x1="163.45" y1="243.34" x2="160.35" y2="233.79"/><line class="cls-2" x1="150.87" y1="246.71" x2="148.78" y2="236.89"/><line class="cls-2" x1="138.01" y1="248.75" x2="136.96" y2="238.77"/><line class="cls-2" x1="111.99" y1="248.75" x2="113.04" y2="238.77"/><line class="cls-2" x1="99.13" y1="246.71" x2="101.22" y2="236.89"/><line class="cls-2" x1="86.55" y1="243.34" x2="89.65" y2="233.79"/><line class="cls-2" x1="74.39" y1="238.67" x2="78.47" y2="229.5"/><line class="cls-2" x1="51.86" y1="225.67" x2="57.76" y2="217.55"/><line class="cls-2" x1="41.74" y1="217.47" x2="48.46" y2="210.01"/><line class="cls-2" x1="32.53" y1="208.26" x2="39.99" y2="201.54"/><line class="cls-2" x1="24.33" y1="198.14" x2="32.45" y2="192.24"/><line class="cls-2" x1="11.33" y1="175.61" x2="20.5" y2="171.53"/><line class="cls-2" x1="6.66" y1="163.45" x2="16.21" y2="160.35"/><line class="cls-2" x1="3.29" y1="150.87" x2="13.11" y2="148.78"/><line class="cls-2" x1="1.25" y1="138.01" x2="11.23" y2="136.96"/><line class="cls-2" x1="1.25" y1="111.99" x2="11.23" y2="113.04"/><line class="cls-2" x1="3.29" y1="99.13" x2="13.11" y2="101.22"/><line class="cls-2" x1="6.66" y1="86.55" x2="16.21" y2="89.65"/><line class="cls-2" x1="11.33" y1="74.39" x2="20.5" y2="78.47"/><line class="cls-2" x1="24.33" y1="51.86" x2="32.45" y2="57.76"/><line class="cls-2" x1="32.53" y1="41.74" x2="39.99" y2="48.46"/><line class="cls-2" x1="41.74" y1="32.53" x2="48.46" y2="39.99"/><line class="cls-2" x1="51.86" y1="24.33" x2="57.76" y2="32.45"/><line class="cls-2" x1="74.39" y1="11.33" x2="78.47" y2="20.5"/><line class="cls-2" x1="86.55" y1="6.66" x2="89.65" y2="16.21"/><line class="cls-2" x1="99.13" y1="3.29" x2="101.22" y2="13.11"/><line class="cls-2" x1="111.99" y1="1.25" x2="113.04" y2="11.23"/></svg>
|
||||
<svg id="Hour_Markers_-_Singlets" data-name="Hour Markers - Singlets" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;}.cls-2{stroke-width:0.5px;}</style></defs><title>face-001</title><line class="cls-1" x1="125" y1="1.25" x2="125" y2="16.23"/><line class="cls-1" x1="186.87" y1="17.83" x2="179.39" y2="30.8"/><line class="cls-1" x1="232.17" y1="63.12" x2="219.2" y2="70.61"/><line class="cls-1" x1="248.75" y1="125" x2="233.77" y2="125"/><line class="cls-1" x1="232.17" y1="186.87" x2="219.2" y2="179.39"/><line class="cls-1" x1="186.88" y1="232.17" x2="179.39" y2="219.2"/><line class="cls-1" x1="125" y1="248.75" x2="125" y2="233.77"/><line class="cls-1" x1="63.13" y1="232.17" x2="70.61" y2="219.2"/><line class="cls-1" x1="17.83" y1="186.88" x2="30.8" y2="179.39"/><line class="cls-1" x1="1.25" y1="125" x2="16.23" y2="125"/><line class="cls-1" x1="17.83" y1="63.13" x2="30.8" y2="70.61"/><line class="cls-1" x1="63.12" y1="17.83" x2="70.61" y2="30.8"/><line class="cls-2" x1="138.01" y1="1.25" x2="136.96" y2="11.23"/><line class="cls-2" x1="150.87" y1="3.29" x2="148.78" y2="13.11"/><line class="cls-2" x1="163.45" y1="6.66" x2="160.35" y2="16.21"/><line class="cls-2" x1="175.61" y1="11.33" x2="171.53" y2="20.5"/><line class="cls-2" x1="198.14" y1="24.33" x2="192.24" y2="32.45"/><line class="cls-2" x1="208.26" y1="32.53" x2="201.54" y2="39.99"/><line class="cls-2" x1="217.47" y1="41.74" x2="210.01" y2="48.46"/><line class="cls-2" x1="225.67" y1="51.86" x2="217.55" y2="57.76"/><line class="cls-2" x1="238.67" y1="74.39" x2="229.5" y2="78.47"/><line class="cls-2" x1="243.34" y1="86.55" x2="233.79" y2="89.65"/><line class="cls-2" x1="246.71" y1="99.13" x2="236.89" y2="101.22"/><line class="cls-2" x1="248.75" y1="111.99" x2="238.77" y2="113.04"/><line class="cls-2" x1="248.75" y1="138.01" x2="238.77" y2="136.96"/><line class="cls-2" x1="246.71" y1="150.87" x2="236.89" y2="148.78"/><line class="cls-2" x1="243.34" y1="163.45" x2="233.79" y2="160.35"/><line class="cls-2" x1="238.67" y1="175.61" x2="229.5" y2="171.53"/><line class="cls-2" x1="225.67" y1="198.14" x2="217.55" y2="192.24"/><line class="cls-2" x1="217.47" y1="208.26" x2="210.01" y2="201.54"/><line class="cls-2" x1="208.26" y1="217.47" x2="201.54" y2="210.01"/><line class="cls-2" x1="198.14" y1="225.67" x2="192.24" y2="217.55"/><line class="cls-2" x1="175.61" y1="238.67" x2="171.53" y2="229.5"/><line class="cls-2" x1="163.45" y1="243.34" x2="160.35" y2="233.79"/><line class="cls-2" x1="150.87" y1="246.71" x2="148.78" y2="236.89"/><line class="cls-2" x1="138.01" y1="248.75" x2="136.96" y2="238.77"/><line class="cls-2" x1="111.99" y1="248.75" x2="113.04" y2="238.77"/><line class="cls-2" x1="99.13" y1="246.71" x2="101.22" y2="236.89"/><line class="cls-2" x1="86.55" y1="243.34" x2="89.65" y2="233.79"/><line class="cls-2" x1="74.39" y1="238.67" x2="78.47" y2="229.5"/><line class="cls-2" x1="51.86" y1="225.67" x2="57.76" y2="217.55"/><line class="cls-2" x1="41.74" y1="217.47" x2="48.46" y2="210.01"/><line class="cls-2" x1="32.53" y1="208.26" x2="39.99" y2="201.54"/><line class="cls-2" x1="24.33" y1="198.14" x2="32.45" y2="192.24"/><line class="cls-2" x1="11.33" y1="175.61" x2="20.5" y2="171.53"/><line class="cls-2" x1="6.66" y1="163.45" x2="16.21" y2="160.35"/><line class="cls-2" x1="3.29" y1="150.87" x2="13.11" y2="148.78"/><line class="cls-2" x1="1.25" y1="138.01" x2="11.23" y2="136.96"/><line class="cls-2" x1="1.25" y1="111.99" x2="11.23" y2="113.04"/><line class="cls-2" x1="3.29" y1="99.13" x2="13.11" y2="101.22"/><line class="cls-2" x1="6.66" y1="86.55" x2="16.21" y2="89.65"/><line class="cls-2" x1="11.33" y1="74.39" x2="20.5" y2="78.47"/><line class="cls-2" x1="24.33" y1="51.86" x2="32.45" y2="57.76"/><line class="cls-2" x1="32.53" y1="41.74" x2="39.99" y2="48.46"/><line class="cls-2" x1="41.74" y1="32.53" x2="48.46" y2="39.99"/><line class="cls-2" x1="51.86" y1="24.33" x2="57.76" y2="32.45"/><line class="cls-2" x1="74.39" y1="11.33" x2="78.47" y2="20.5"/><line class="cls-2" x1="86.55" y1="6.66" x2="89.65" y2="16.21"/><line class="cls-2" x1="99.13" y1="3.29" x2="101.22" y2="13.11"/><line class="cls-2" x1="111.99" y1="1.25" x2="113.04" y2="11.23"/></svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@ -1 +1 @@
|
||||
<svg id="Hour_Markers_-_Doubles" data-name="Hour Markers - Doubles" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2.98px;}</style></defs><title>face-002</title><line class="cls-1" x1="122.01" y1="1.75" x2="122.01" y2="16.67"/><line class="cls-1" x1="186.62" y1="18.26" x2="179.17" y2="31.18"/><line class="cls-1" x1="231.74" y1="63.37" x2="218.82" y2="70.83"/><line class="cls-1" x1="248.25" y1="127.99" x2="233.33" y2="127.99"/><line class="cls-1" x1="231.74" y1="186.62" x2="218.82" y2="179.17"/><line class="cls-1" x1="186.63" y1="231.74" x2="179.17" y2="218.82"/><line class="cls-1" x1="127.99" y1="248.25" x2="127.99" y2="233.33"/><line class="cls-1" x1="63.38" y1="231.74" x2="70.83" y2="218.82"/><line class="cls-1" x1="18.26" y1="186.63" x2="31.18" y2="179.17"/><line class="cls-1" x1="1.75" y1="122.01" x2="16.67" y2="122.01"/><line class="cls-1" x1="18.26" y1="63.38" x2="31.18" y2="70.83"/><line class="cls-1" x1="63.37" y1="18.26" x2="70.83" y2="31.18"/><line class="cls-1" x1="127.99" y1="1.75" x2="127.99" y2="16.67"/><line class="cls-1" x1="248.25" y1="122.01" x2="233.33" y2="122.01"/><line class="cls-1" x1="122.01" y1="248.25" x2="122.01" y2="233.33"/><line class="cls-1" x1="1.75" y1="127.99" x2="16.67" y2="127.99"/></svg>
|
||||
<svg id="Hour_Markers_-_Doubles" data-name="Hour Markers - Doubles" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2.98px;}</style></defs><title>face-002</title><line class="cls-1" x1="122.01" y1="1.75" x2="122.01" y2="16.67"/><line class="cls-1" x1="186.62" y1="18.26" x2="179.17" y2="31.18"/><line class="cls-1" x1="231.74" y1="63.37" x2="218.82" y2="70.83"/><line class="cls-1" x1="248.25" y1="127.99" x2="233.33" y2="127.99"/><line class="cls-1" x1="231.74" y1="186.62" x2="218.82" y2="179.17"/><line class="cls-1" x1="186.63" y1="231.74" x2="179.17" y2="218.82"/><line class="cls-1" x1="127.99" y1="248.25" x2="127.99" y2="233.33"/><line class="cls-1" x1="63.38" y1="231.74" x2="70.83" y2="218.82"/><line class="cls-1" x1="18.26" y1="186.63" x2="31.18" y2="179.17"/><line class="cls-1" x1="1.75" y1="122.01" x2="16.67" y2="122.01"/><line class="cls-1" x1="18.26" y1="63.38" x2="31.18" y2="70.83"/><line class="cls-1" x1="63.37" y1="18.26" x2="70.83" y2="31.18"/><line class="cls-1" x1="127.99" y1="1.75" x2="127.99" y2="16.67"/><line class="cls-1" x1="248.25" y1="122.01" x2="233.33" y2="122.01"/><line class="cls-1" x1="122.01" y1="248.25" x2="122.01" y2="233.33"/><line class="cls-1" x1="1.75" y1="127.99" x2="16.67" y2="127.99"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
@ -22,8 +22,7 @@ Module.register("compliments", {
|
||||
afternoonStartTime: 12,
|
||||
afternoonEndTime: 17,
|
||||
random: true,
|
||||
mockDate: null,
|
||||
advice: false
|
||||
mockDate: null
|
||||
},
|
||||
lastIndexUsed: -1,
|
||||
// Set currentweather from module
|
||||
@ -46,18 +45,6 @@ Module.register("compliments", {
|
||||
self.config.compliments = JSON.parse(response);
|
||||
self.updateDom();
|
||||
});
|
||||
} else if (this.config.advice) {
|
||||
var xobj = new XMLHttpRequest();
|
||||
xobj.overrideMimeType("application/json");
|
||||
xobj.open("GET", "https://api.adviceslip.com/advice", true);
|
||||
xobj.onreadystatechange = function () {
|
||||
if (xobj.readyState === 4 && xobj.status === 200) {
|
||||
const adviceResp = JSON.parse(xobj.responseText);
|
||||
self.config.compliments = adviceResp.slip.advice;
|
||||
self.updateDom();
|
||||
}
|
||||
};
|
||||
xobj.send(null);
|
||||
}
|
||||
|
||||
// Schedule update timer.
|
||||
|
@ -23,6 +23,7 @@ Module.register("currentweather", {
|
||||
lang: config.language,
|
||||
decimalSymbol: ".",
|
||||
showHumidity: false,
|
||||
showSun: true,
|
||||
degreeLabel: false,
|
||||
showIndoorTemperature: false,
|
||||
showIndoorHumidity: false,
|
||||
@ -155,13 +156,15 @@ Module.register("currentweather", {
|
||||
small.appendChild(humidityIcon);
|
||||
}
|
||||
|
||||
var sunriseSunsetIcon = document.createElement("span");
|
||||
sunriseSunsetIcon.className = "wi dimmed " + this.sunriseSunsetIcon;
|
||||
small.appendChild(sunriseSunsetIcon);
|
||||
if (this.config.showSun) {
|
||||
var sunriseSunsetIcon = document.createElement("span");
|
||||
sunriseSunsetIcon.className = "wi dimmed " + this.sunriseSunsetIcon;
|
||||
small.appendChild(sunriseSunsetIcon);
|
||||
|
||||
var sunriseSunsetTime = document.createElement("span");
|
||||
sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime;
|
||||
small.appendChild(sunriseSunsetTime);
|
||||
var sunriseSunsetTime = document.createElement("span");
|
||||
sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime;
|
||||
small.appendChild(sunriseSunsetTime);
|
||||
}
|
||||
|
||||
wrapper.appendChild(small);
|
||||
},
|
||||
|
@ -5,9 +5,10 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var FeedMe = require("feedme");
|
||||
var request = require("request");
|
||||
var iconv = require("iconv-lite");
|
||||
const Log = require("../../../js/logger.js");
|
||||
const FeedMe = require("feedme");
|
||||
const request = require("request");
|
||||
const iconv = require("iconv-lite");
|
||||
|
||||
/* Fetcher
|
||||
* Responsible for requesting an update on the set interval and broadcasting the data.
|
||||
@ -34,7 +35,6 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
/* fetchNews()
|
||||
* Request the new items.
|
||||
*/
|
||||
|
||||
var fetchNews = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
@ -59,16 +59,15 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
url: url
|
||||
});
|
||||
} else if (logFeedWarnings) {
|
||||
console.log("Can't parse feed item:");
|
||||
console.log(item);
|
||||
console.log("Title: " + title);
|
||||
console.log("Description: " + description);
|
||||
console.log("Pubdate: " + pubdate);
|
||||
Log.warn("Can't parse feed item:");
|
||||
Log.warn(item);
|
||||
Log.warn("Title: " + title);
|
||||
Log.warn("Description: " + description);
|
||||
Log.warn("Pubdate: " + pubdate);
|
||||
}
|
||||
});
|
||||
|
||||
parser.on("end", function () {
|
||||
//console.log("end parsing - " + url);
|
||||
self.broadcastItems();
|
||||
scheduleTimer();
|
||||
});
|
||||
@ -93,9 +92,7 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
/* scheduleTimer()
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
|
||||
var scheduleTimer = function () {
|
||||
//console.log('Schedule update timer.');
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchNews();
|
||||
@ -127,10 +124,10 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
*/
|
||||
this.broadcastItems = function () {
|
||||
if (items.length <= 0) {
|
||||
//console.log('No items to broadcast yet.');
|
||||
Log.info("Newsfeed-Fetcher: No items to broadcast yet.");
|
||||
return;
|
||||
}
|
||||
//console.log('Broadcasting ' + items.length + ' items.');
|
||||
Log.info("Newsfeed-Fetcher: Broadcasting " + items.length + " items.");
|
||||
itemsReceivedCallback(self);
|
||||
};
|
||||
|
||||
|
@ -5,22 +5,22 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var NodeHelper = require("node_helper");
|
||||
var validUrl = require("valid-url");
|
||||
var Fetcher = require("./fetcher.js");
|
||||
const NodeHelper = require("node_helper");
|
||||
const validUrl = require("valid-url");
|
||||
const Fetcher = require("./fetcher.js");
|
||||
const Log = require("../../../js/logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Subclass start method.
|
||||
// Override start method.
|
||||
start: function () {
|
||||
console.log("Starting module: " + this.name);
|
||||
Log.log("Starting node helper for: " + this.name);
|
||||
this.fetchers = [];
|
||||
},
|
||||
|
||||
// Subclass socketNotificationReceived received.
|
||||
// Override socketNotificationReceived received.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "ADD_FEED") {
|
||||
this.createFetcher(payload.feed, payload.config);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
@ -45,7 +45,7 @@ module.exports = NodeHelper.create({
|
||||
|
||||
var fetcher;
|
||||
if (typeof self.fetchers[url] === "undefined") {
|
||||
console.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval);
|
||||
Log.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval);
|
||||
fetcher = new Fetcher(url, reloadInterval, encoding, config.logFeedWarnings);
|
||||
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
@ -61,7 +61,7 @@ module.exports = NodeHelper.create({
|
||||
|
||||
self.fetchers[url] = fetcher;
|
||||
} else {
|
||||
console.log("Use existing news fetcher for url: " + url);
|
||||
Log.log("Use existing news fetcher for url: " + url);
|
||||
fetcher = self.fetchers[url];
|
||||
fetcher.setReloadInterval(reloadInterval);
|
||||
fetcher.broadcastItems();
|
||||
|
@ -1,9 +1,10 @@
|
||||
var SimpleGit = require("simple-git");
|
||||
var simpleGits = [];
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var defaultModules = require(__dirname + "/../defaultmodules.js");
|
||||
var NodeHelper = require("node_helper");
|
||||
const SimpleGit = require("simple-git");
|
||||
const simpleGits = [];
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const defaultModules = require(__dirname + "/../defaultmodules.js");
|
||||
const Log = require(__dirname + "/../../../js/logger.js");
|
||||
const NodeHelper = require("node_helper");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
config: {},
|
||||
@ -27,7 +28,7 @@ module.exports = NodeHelper.create({
|
||||
var moduleFolder = path.normalize(__dirname + "/../../" + moduleName);
|
||||
|
||||
try {
|
||||
//console.log("checking git for module="+moduleName)
|
||||
Log.info("Checking git for module: " + moduleName);
|
||||
let stat = fs.statSync(path.join(moduleFolder, ".git"));
|
||||
promises.push(this.resolveRemote(moduleName, moduleFolder));
|
||||
} catch (err) {
|
||||
|
303
modules/default/weather/providers/ukmetofficedatahub.js
Normal file
@ -0,0 +1,303 @@
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
* By Malcolm Oakes https://github.com/maloakes
|
||||
* Existing Met Office provider edited for new MetOffice Data Hub by CreepinJesus http://github.com/XBCreepinJesus
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is a provider for UK Met Office Data Hub (the replacement for their Data Point services).
|
||||
* For more information on Data Hub, see https://www.metoffice.gov.uk/services/data/datapoint/notifications/weather-datahub
|
||||
* Data available:
|
||||
* Hourly data for next 2 days ("hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-hourly.pdf
|
||||
* 3-hourly data for the next 7 days ("3hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-3-hourly.pdf
|
||||
* Daily data for the next 7 days ("daily") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-daily.pdf
|
||||
*/
|
||||
|
||||
/* NOTES
|
||||
* This provider requires longitude/latitude coordinates, rather than a location ID (as with the previous Met Office provider)
|
||||
* Provide the following in your config.js file:
|
||||
* weatherProvider: "ukmetofficedatahub",
|
||||
* apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
|
||||
* apiKey: "[YOUR API KEY]",
|
||||
* apiSecret: "[YOUR API SECRET]]",
|
||||
* lat: [LATITUDE (DECIMAL)],
|
||||
* lon: [LONGITUDE (DECIMAL)],
|
||||
* windUnits: "mps" | "kph" | "mph" (default)
|
||||
* tempUnits: "imperial" | "metric" (default)
|
||||
*
|
||||
* At time of writing, free accounts are limited to 360 requests a day per service (hourly, 3hourly, daily); take this in mind when
|
||||
* setting your update intervals. For reference, 360 requests per day is once every 4 minutes.
|
||||
*
|
||||
* Pay attention to the units of the supplied data from the Met Office - it is given in SI/metric units where applicable:
|
||||
* - Temperatures are in degrees Celsius (°C)
|
||||
* - Wind speeds are in metres per second (m/s)
|
||||
* - Wind direction given in degrees (°)
|
||||
* - Pressures are in Pascals (Pa)
|
||||
* - Distances are in metres (m)
|
||||
* - Probabilities and humidity are given as percentages (%)
|
||||
* - Precipitation is measured in millimetres (mm) with rates per hour (mm/h)
|
||||
*
|
||||
* See the PDFs linked above for more information on the data their corresponding units.
|
||||
*/
|
||||
|
||||
WeatherProvider.register("ukmetofficedatahub", {
|
||||
// Set the name of the provider.
|
||||
providerName: "UK Met Office (DataHub)",
|
||||
|
||||
// Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
getUrl(forecastType) {
|
||||
let queryStrings = "?";
|
||||
queryStrings += "latitude=" + this.config.lat;
|
||||
queryStrings += "&longitude=" + this.config.lon;
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
queryStrings += "&includeLocationName=" + true;
|
||||
}
|
||||
|
||||
// Return URL, making sure there is a trailing "/" in the base URL.
|
||||
return this.config.apiBase + (this.config.apiBase.endsWith("/") ? "" : "/") + forecastType + queryStrings;
|
||||
},
|
||||
|
||||
// Build the list of headers for the request
|
||||
// For DataHub requests, the API key/secret are sent in the headers rather than as query strings.
|
||||
// Headers defined according to Data Hub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
getHeaders() {
|
||||
let headers = {
|
||||
accept: "application/json",
|
||||
"x-ibm-client-id": this.config.apiKey,
|
||||
"x-ibm-client-secret": this.config.apiSecret
|
||||
};
|
||||
|
||||
return headers;
|
||||
},
|
||||
|
||||
// Fetch data using supplied URL and request headers
|
||||
async fetchWeather(url, headers) {
|
||||
const response = await fetch(url, { headers: headers });
|
||||
|
||||
// Return JSON data
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// Fetch hourly forecast data (to use for current weather)
|
||||
fetchCurrentWeather() {
|
||||
this.fetchWeather(this.getUrl("hourly"), this.getHeaders())
|
||||
.then((data) => {
|
||||
// Check data is useable
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length == 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
Log.error("Possibly bad current/hourly data?");
|
||||
Log.info(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set location name
|
||||
this.setFetchedLocation(`${data.features[0].properties.location.name}`);
|
||||
|
||||
// Generate current weather data
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
|
||||
// Catch any error(s)
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
|
||||
// Let the module know there're new data available
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Create a WeatherObject using current weather data (data for the current hour)
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
// Extract the actual forecasts
|
||||
let forecastDataHours = currentWeatherData.features[0].properties.timeSeries;
|
||||
|
||||
// Define now
|
||||
let nowUtc = moment.utc();
|
||||
|
||||
// Find hour that contains the current time
|
||||
for (hour in forecastDataHours) {
|
||||
let forecastTime = moment.utc(forecastDataHours[hour].time);
|
||||
if (nowUtc.isSameOrAfter(forecastTime) && nowUtc.isBefore(moment(forecastTime.add(1, "h")))) {
|
||||
currentWeather.date = forecastTime;
|
||||
currentWeather.windSpeed = this.convertWindSpeed(forecastDataHours[hour].windSpeed10m);
|
||||
currentWeather.windDirection = forecastDataHours[hour].windDirectionFrom10m;
|
||||
currentWeather.temperature = this.convertTemp(forecastDataHours[hour].screenTemperature);
|
||||
currentWeather.minTemperature = this.convertTemp(forecastDataHours[hour].minScreenAirTemp);
|
||||
currentWeather.maxTemperature = this.convertTemp(forecastDataHours[hour].maxScreenAirTemp);
|
||||
currentWeather.weatherType = this.convertWeatherType(forecastDataHours[hour].significantWeatherCode);
|
||||
currentWeather.humidity = forecastDataHours[hour].screenRelativeHumidity;
|
||||
currentWeather.rain = forecastDataHours[hour].totalPrecipAmount;
|
||||
currentWeather.snow = forecastDataHours[hour].totalSnowAmount;
|
||||
currentWeather.precipitation = forecastDataHours[hour].probOfPrecipitation;
|
||||
currentWeather.feelsLikeTemp = this.convertTemp(forecastDataHours[hour].feelsLikeTemperature);
|
||||
|
||||
// Pass on full details so they can be used in custom templates
|
||||
// Note the units of the supplied data when using this (see top of file)
|
||||
currentWeather.rawData = forecastDataHours[hour];
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the sunrise/sunset times - (still) not supplied in UK Met Office data
|
||||
// Passes {longitude, latitude, height} to calcAstroData
|
||||
// Could just pass lat/long from this.config, but returned data from MO also contains elevation
|
||||
let times = this.calcAstroData(currentWeatherData.features[0].geometry.coordinates);
|
||||
currentWeather.sunrise = times[0];
|
||||
currentWeather.sunset = times[1];
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
// Fetch daily forecast data
|
||||
fetchWeatherForecast() {
|
||||
this.fetchWeather(this.getUrl("daily"), this.getHeaders())
|
||||
.then((data) => {
|
||||
// Check data is useable
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length == 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
Log.error("Possibly bad forecast data?");
|
||||
Log.info(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set location name
|
||||
this.setFetchedLocation(`${data.features[0].properties.location.name}`);
|
||||
|
||||
// Generate the forecast data
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data);
|
||||
this.setWeatherForecast(forecast);
|
||||
})
|
||||
|
||||
// Catch any error(s)
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
|
||||
// Let the module know there're new data available
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Create a WeatherObject for each day using daily forecast data
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
const dailyForecasts = [];
|
||||
|
||||
// Extract the actual forecasts
|
||||
let forecastDataDays = forecasts.features[0].properties.timeSeries;
|
||||
|
||||
// Define today
|
||||
let today = moment.utc().startOf("date");
|
||||
|
||||
// Go through each day in the forecasts
|
||||
for (day in forecastDataDays) {
|
||||
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
// Get date of forecast
|
||||
let forecastDate = moment.utc(forecastDataDays[day].time);
|
||||
|
||||
// Check if forecast is for today or in the future (i.e., ignore yesterday's forecast)
|
||||
if (forecastDate.isSameOrAfter(today)) {
|
||||
forecastWeather.date = forecastDate;
|
||||
forecastWeather.minTemperature = this.convertTemp(forecastDataDays[day].nightMinScreenTemperature);
|
||||
forecastWeather.maxTemperature = this.convertTemp(forecastDataDays[day].dayMaxScreenTemperature);
|
||||
|
||||
// Using daytime forecast values
|
||||
forecastWeather.windSpeed = this.convertWindSpeed(forecastDataDays[day].midday10MWindSpeed);
|
||||
forecastWeather.windDirection = forecastDataDays[day].midday10MWindDirection;
|
||||
forecastWeather.weatherType = this.convertWeatherType(forecastDataDays[day].daySignificantWeatherCode);
|
||||
forecastWeather.precipitation = forecastDataDays[day].dayProbabilityOfPrecipitation;
|
||||
forecastWeather.temperature = forecastDataDays[day].dayMaxScreenTemperature;
|
||||
forecastWeather.humidity = forecastDataDays[day].middayRelativeHumidity;
|
||||
forecastWeather.rain = forecastDataDays[day].dayProbabilityOfRain;
|
||||
forecastWeather.snow = forecastDataDays[day].dayProbabilityOfSnow;
|
||||
forecastWeather.feelsLikeTemp = this.convertTemp(forecastDataDays[day].dayMaxFeelsLikeTemp);
|
||||
|
||||
// Pass on full details so they can be used in custom templates
|
||||
// Note the units of the supplied data when using this (see top of file)
|
||||
|
||||
forecastWeather.rawData = forecastDataDays[day];
|
||||
|
||||
dailyForecasts.push(forecastWeather);
|
||||
}
|
||||
}
|
||||
|
||||
return dailyForecasts;
|
||||
},
|
||||
|
||||
// Set the fetched location name.
|
||||
setFetchedLocation: function (name) {
|
||||
this.fetchedLocationName = name;
|
||||
},
|
||||
|
||||
// Calculate sunrise/sunset times
|
||||
calcAstroData(location) {
|
||||
const sunTimes = [];
|
||||
|
||||
// Careful to pass values to SunCalc in correct order (latitude, longitude, elevation)
|
||||
let times = SunCalc.getTimes(new Date(), location[1], location[0], location[2]);
|
||||
sunTimes.push(moment(times.sunrise, "X"));
|
||||
sunTimes.push(moment(times.sunset, "X"));
|
||||
|
||||
return sunTimes;
|
||||
},
|
||||
|
||||
// Convert temperatures to Fahrenheit (from degrees C), if required
|
||||
convertTemp(tempInC) {
|
||||
return this.config.tempUnits === "imperial" ? (tempInC * 9) / 5 + 32 : tempInC;
|
||||
},
|
||||
|
||||
// Convert wind speed from metres per second
|
||||
// To keep the supplied metres per second units, use "mps"
|
||||
// To use kilometres per hour, use "kph"
|
||||
// Else assumed imperial and the value is returned in miles per hour (a Met Office user is likely to be UK-based)
|
||||
convertWindSpeed(windInMpS) {
|
||||
if (this.config.windUnits == "mps") {
|
||||
return windInMpS;
|
||||
}
|
||||
|
||||
if (this.config.windUnits == "kph" || this.config.windUnits == "metric") {
|
||||
return windInMpS * 3.6;
|
||||
}
|
||||
|
||||
return windInMpS * 2.23694;
|
||||
},
|
||||
|
||||
// Match the Met Office "significant weather code" to a weathericons.css icon
|
||||
// Use: https://metoffice.apiconnect.ibmcloud.com/metoffice/production/node/264
|
||||
// and: https://erikflowers.github.io/weather-icons/
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
0: "night-clear",
|
||||
1: "day-sunny",
|
||||
2: "night-alt-cloudy",
|
||||
3: "day-cloudy",
|
||||
5: "fog",
|
||||
6: "fog",
|
||||
7: "cloudy",
|
||||
8: "cloud",
|
||||
9: "night-sprinkle",
|
||||
10: "day-sprinkle",
|
||||
11: "raindrops",
|
||||
12: "sprinkle",
|
||||
13: "night-alt-showers",
|
||||
14: "day-showers",
|
||||
15: "rain",
|
||||
16: "night-alt-sleet",
|
||||
17: "day-sleet",
|
||||
18: "sleet",
|
||||
19: "night-alt-hail",
|
||||
20: "day-hail",
|
||||
21: "hail",
|
||||
22: "night-alt-snow",
|
||||
23: "day-snow",
|
||||
24: "snow",
|
||||
25: "night-alt-snow",
|
||||
26: "day-snow",
|
||||
27: "snow",
|
||||
28: "night-alt-thunderstorm",
|
||||
29: "day-thunderstorm",
|
||||
30: "thunderstorm"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
}
|
||||
});
|
@ -15,7 +15,6 @@ Module.register("weather", {
|
||||
|
||||
location: false,
|
||||
locationID: false,
|
||||
appid: "",
|
||||
units: config.units,
|
||||
|
||||
tempUnits: config.units,
|
||||
@ -43,8 +42,10 @@ Module.register("weather", {
|
||||
initialLoadDelay: 0, // 0 seconds delay
|
||||
retryDelay: 2500,
|
||||
|
||||
apiKey: "",
|
||||
apiSecret: "",
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
apiBase: "https://api.openweathermap.org/data/", // TODO: this should not be part of the weather.js file, but should be contained in the openweatherprovider
|
||||
weatherEndpoint: "/weather",
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
@ -208,7 +209,7 @@ Module.register("weather", {
|
||||
if (isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
|
||||
value = "";
|
||||
} else {
|
||||
if (this.config.weatherProvider === "ukmetoffice") {
|
||||
if (this.config.weatherProvider === "ukmetoffice" || this.config.weatherProvider === "ukmetofficedatahub") {
|
||||
value += "%";
|
||||
} else {
|
||||
value = `${value.toFixed(2)} ${this.config.units === "imperial" ? "in" : "mm"}`;
|
||||
|
@ -294,7 +294,15 @@ Module.register("weatherforecast", {
|
||||
return;
|
||||
}
|
||||
|
||||
params += "&cnt=" + (this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 17 ? 7 : this.config.maxNumberOfDays);
|
||||
let numberOfDays;
|
||||
if (this.config.forecastEndpoint === "forecast") {
|
||||
numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 5 ? 5 : this.config.maxNumberOfDays;
|
||||
// don't get forecasts for the 6th day, as it would not represent the whole day
|
||||
numberOfDays = numberOfDays * 8 - (Math.floor(new Date().getHours() / 3) % 8);
|
||||
} else {
|
||||
numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 17 ? 7 : this.config.maxNumberOfDays;
|
||||
}
|
||||
params += "&cnt=" + numberOfDays;
|
||||
|
||||
params += "&units=" + this.config.units;
|
||||
params += "&lang=" + this.config.lang;
|
||||
|
1153
package-lock.json
generated
14
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "magicmirror",
|
||||
"version": "2.12.0-develop",
|
||||
"version": "2.13.0-develop",
|
||||
"description": "The open source modular smart mirror platform.",
|
||||
"main": "js/electron.js",
|
||||
"scripts": {
|
||||
@ -15,6 +15,7 @@
|
||||
"test:prettier": "prettier --check **/*.{js,css,json,md,yml}",
|
||||
"test:js": "eslint *.js js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --quiet",
|
||||
"test:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json",
|
||||
"test:calendar": "node ./modules/default/calendar/debug.js",
|
||||
"config:check": "node js/check_config.js",
|
||||
"lint:prettier": "prettier --write **/*.{js,css,json,md,yml}",
|
||||
"lint:js": "eslint *.js js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --fix",
|
||||
@ -46,7 +47,7 @@
|
||||
"current-week-number": "^1.0.7",
|
||||
"danger": "^3.1.3",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"http-auth": "^3.2.3",
|
||||
"husky": "^4.2.5",
|
||||
"jsdom": "^11.6.2",
|
||||
@ -56,8 +57,8 @@
|
||||
"prettier": "^2.0.5",
|
||||
"pretty-quick": "^2.0.1",
|
||||
"spectron": "^8.0.0",
|
||||
"stylelint": "^13.3.3",
|
||||
"stylelint-config-prettier": "^8.0.1",
|
||||
"stylelint": "^13.6.1",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-prettier": "^1.1.2"
|
||||
},
|
||||
@ -67,16 +68,17 @@
|
||||
"dependencies": {
|
||||
"colors": "^1.1.2",
|
||||
"console-stamp": "^0.2.9",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint": "^7.3.0",
|
||||
"express": "^4.16.2",
|
||||
"express-ipfilter": "^1.0.1",
|
||||
"feedme": "latest",
|
||||
"helmet": "^3.21.2",
|
||||
"ical": "^0.8.0",
|
||||
"iconv-lite": "latest",
|
||||
"lodash": "^4.17.15",
|
||||
"module-alias": "^2.2.2",
|
||||
"moment": "latest",
|
||||
"request": "^2.88.0",
|
||||
"request": "^2.88.2",
|
||||
"rrule": "^2.6.2",
|
||||
"rrule-alt": "^2.2.8",
|
||||
"simple-git": "^1.85.0",
|
||||
|
@ -1,6 +1,8 @@
|
||||
var app = require("../js/app.js");
|
||||
const app = require("../js/app.js");
|
||||
const Log = require("../js/logger.js");
|
||||
|
||||
app.start(function (config) {
|
||||
var bindAddress = config.address ? config.address : "localhost";
|
||||
var httpType = config.useHttps ? "https" : "http";
|
||||
console.log("\nReady to go! Please point your browser to: " + httpType + "://" + bindAddress + ":" + config.port);
|
||||
Log.log("\nReady to go! Please point your browser to: " + httpType + "://" + bindAddress + ":" + config.port);
|
||||
});
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"LOADING": "Зареждане …",
|
||||
"LOADING": "Зареждане на …",
|
||||
|
||||
"TODAY": "Днес",
|
||||
"TOMORROW": "Утре",
|
||||
"DAYAFTERTOMORROW": "Вдругиден",
|
||||
"RUNNING": "Свършва на",
|
||||
"RUNNING": "Свършва след",
|
||||
"EMPTY": "Няма предстоящи събития.",
|
||||
|
||||
"WEEK": "Седмица {weekNumber}",
|
||||
@ -26,8 +26,8 @@
|
||||
"NW": "СЗ",
|
||||
"NNW": "ССЗ",
|
||||
|
||||
"UPDATE_NOTIFICATION": "Налична актуализация за MagicMirror².",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Налична актуализация за {MODULE_NAME} модул.",
|
||||
"UPDATE_INFO_SINGLE": "Текущата инсталация е изостанала с {COMMIT_COUNT} commit къмита на клон {BRANCH_NAME}.",
|
||||
"UPDATE_INFO_MULTIPLE": "Текущата инсталация е изостанала с {COMMIT_COUNT} commits къмита на клон {BRANCH_NAME}."
|
||||
"UPDATE_NOTIFICATION": "Налична е актуализация за MagicMirror².",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Налична е актуализация за модула „{MODULE_NAME}“.",
|
||||
"UPDATE_INFO_SINGLE": "Инсталираната версия е с {COMMIT_COUNT} ревизия назад от клона „{BRANCH_NAME}“.",
|
||||
"UPDATE_INFO_MULTIPLE": "Инсталираната версия е с {COMMIT_COUNT} ревизии назад от клона „{BRANCH_NAME}“."
|
||||
}
|
||||
|