mirror of
https://github.com/webrecorder/pywb.git
synced 2025-03-25 23:47:47 +01:00
492 lines
16 KiB
JavaScript
492 lines
16 KiB
JavaScript
const PywbMonthLabels = {1:"Jan", 2:"Feb",3:"Mar",4:"Apr",5:"May",6:"Jun",7:"Jul",8:"Aug",9:"Sep",10:"Oct",11:"Nov",12:"Dec"};
|
|
const PywbPeriodIdDelimiter = '-';
|
|
export function PywbData(rawSnaps) {
|
|
const allTimePeriod = new PywbPeriod({type: PywbPeriod.Type.all, id: "all"});
|
|
const snapshots = [];
|
|
let lastSingle = null;
|
|
let lastYear, lastMonth, lastDay, lastHour;
|
|
rawSnaps.forEach((rawSnap, i) => {
|
|
const snap = new PywbSnapshot(rawSnap, i);
|
|
let year, month, day, hour, single;
|
|
if (!(year = allTimePeriod.getChildById(snap.year))) {
|
|
if (lastYear) lastYear.checkIfSingleSnapshotOnly();
|
|
lastYear = year = new PywbPeriod({type: PywbPeriod.Type.year, id: snap.year});
|
|
allTimePeriod.addChild(year);
|
|
}
|
|
if (!(month = year.getChildById(snap.month))) {
|
|
if (lastMonth) lastMonth.checkIfSingleSnapshotOnly();
|
|
lastMonth = month = new PywbPeriod({type: PywbPeriod.Type.month, id: snap.month});
|
|
year.addChild(month);
|
|
}
|
|
if (!(day = month.getChildById(snap.day))) {
|
|
if (lastDay) lastDay.checkIfSingleSnapshotOnly();
|
|
lastDay = day = new PywbPeriod({type: PywbPeriod.Type.day, id: snap.day});
|
|
month.addChild(day);
|
|
}
|
|
const hourValue = Math.ceil((snap.hour + .0001) / (24/8)); // divide day in 4 six-hour periods (aka quarters)
|
|
if (!(hour = day.getChildById(hourValue))) {
|
|
if (lastHour) lastHour.checkIfSingleSnapshotOnly();
|
|
lastHour = hour = new PywbPeriod({type: PywbPeriod.Type.hour, id: hourValue});
|
|
day.addChild(hour);
|
|
}
|
|
if (!(single = hour.getChildById(snap.id))) {
|
|
single = new PywbPeriod({type: PywbPeriod.Type.snapshot, id: snap.id});
|
|
hour.addChild(single);
|
|
}
|
|
single.setSnapshot(snap);
|
|
if (lastSingle) {
|
|
lastSingle.setNextSnapshotPeriod(single);
|
|
single.setPreviousSnapshotPeriod(lastSingle);
|
|
}
|
|
lastSingle = single;
|
|
|
|
snapshots.push(snap);
|
|
});
|
|
|
|
this.timeline = allTimePeriod;
|
|
this.snapshots = snapshots;
|
|
this.getSnapshot = function(index) {
|
|
if (index < 0 || index >= this.snapshots.length) {
|
|
return null;
|
|
}
|
|
return this.snapshots[index];
|
|
};
|
|
this.getPreviousSnapshot = function(snapshot) {
|
|
const index = snapshot.index;
|
|
return this.getSnapshot(index-1);
|
|
};
|
|
this.getNextSnapshot = function(snapshot) {
|
|
const index = snapshot.index;
|
|
return this.getSnapshot(index+1);
|
|
};
|
|
}
|
|
/* ---------------- SNAP SHOT object ----------------- */
|
|
export class PywbSnapshot {
|
|
constructor(init, index) {
|
|
this.index = index;
|
|
this.year = parseInt(init.timestamp.substr(0, 4));
|
|
this.month = parseInt(init.timestamp.substr(4, 2));
|
|
this.day = parseInt(init.timestamp.substr(6, 2));
|
|
this.hour = parseInt(init.timestamp.substr(8, 2));
|
|
this.minute = parseInt(init.timestamp.substr(10, 2));
|
|
this.second = parseInt(init.timestamp.substr(12, 2));
|
|
this.id = parseInt(init.timestamp);
|
|
|
|
this.urlkey = init.urlkey;
|
|
this.url = init.url;
|
|
this.mime = init.mime;
|
|
this.status = init.status;
|
|
this.digest = init.digest;
|
|
this.redirect = init.redirect;
|
|
this.robotflags = init.robotflags;
|
|
this.length = init.length;
|
|
this.offset = init.offset;
|
|
this.filename = init.filename;
|
|
this.load_url = init.load_url;
|
|
this["source-col"] = init["source-col"];
|
|
this.access = init.access;
|
|
}
|
|
|
|
getTimeDateFormatted() {
|
|
return `${PywbMonthLabels[this.month]} ${this.day}, ${this.year} at ${this.getTimeFormatted()}`;
|
|
}
|
|
|
|
getDateFormatted() {
|
|
return `${this.year}-${PywbMonthLabels[this.month]}-${this.day}`;
|
|
}
|
|
|
|
getTimeFormatted() {
|
|
return (this.hour < 13 ? this.hour : (this.hour % 12)) + ":" + ((this.minute < 10 ? "0":"")+this.minute) + ":" + ((this.second < 10 ? "0":"")+this.second) + " " + (this.hour < 12 ? "am":"pm");
|
|
}
|
|
|
|
getParentIds() {
|
|
return [this.year, this.month, this.day, Math.ceil((this.hour + .0001) / (24/8))];
|
|
}
|
|
|
|
getFullId() {
|
|
return [this.year, this.month, this.day, Math.ceil((this.hour + .0001) / (24/8)), this.id].join(PywbPeriodIdDelimiter);
|
|
}
|
|
}
|
|
|
|
/* ---------------- PERIOD object ----------------- */
|
|
export function PywbPeriod(init) {
|
|
this.type = init.type;
|
|
this.id = init.id;
|
|
this.fullId = ''; // full-id property that include string id of parents and self with a delimitor
|
|
|
|
this.childrenIds = {}; // allow for query by ID
|
|
this.children = []; // allow for sequentiality / order
|
|
|
|
this.maxGrandchildSnapshotCount = 0;
|
|
this.snapshotCount = 0;
|
|
}
|
|
PywbPeriod.Type = {all: 0,year: 1,month: 2,day: 3,hour: 4,snapshot:5};
|
|
PywbPeriod.TypeLabel = ["timeline","year","month","day","hour","snapshot"];
|
|
|
|
PywbPeriod.prototype.getTypeLabel = function() {
|
|
return PywbPeriod.TypeLabel[this.type];
|
|
};
|
|
PywbPeriod.GetTypeLabel = function(type) {
|
|
return PywbPeriod.TypeLabel[type] ? PywbPeriod.TypeLabel[type] : "";
|
|
};
|
|
|
|
PywbPeriod.prototype.getChildById = function(id) {
|
|
return this.children[this.childrenIds[id]];
|
|
};
|
|
|
|
// previous period (ONLY SET at the period level/type: snapshot)
|
|
PywbPeriod.prototype.getPreviousSnapshotPeriod = () => {};
|
|
PywbPeriod.prototype.setPreviousSnapshotPeriod = function(period) {
|
|
this.getPreviousSnapshotPeriod = () => period;
|
|
};
|
|
// next period (ONLY SET at the period level/type: snapshot)
|
|
PywbPeriod.prototype.getNextSnapshotPeriod = () => {};
|
|
PywbPeriod.prototype.setNextSnapshotPeriod = function(period) {
|
|
this.getNextSnapshotPeriod = () => period;
|
|
};
|
|
|
|
PywbPeriod.prototype.getFirstSnapshotPeriod = function() {
|
|
return this.getFirstLastSnapshotPeriod_("first");
|
|
};
|
|
PywbPeriod.prototype.getLastSnapshotPeriod = function() {
|
|
return this.getFirstLastSnapshotPeriod_("last");
|
|
};
|
|
PywbPeriod.prototype.getFirstLastSnapshotPeriod_ = function(direction) {
|
|
let period = this;
|
|
let iFailSafe = 100; // in case a parser has a bug and the snapshotCount is not correct; avoid infinite-loop
|
|
while (period.snapshotCount && period.type !== PywbPeriod.Type.snapshot) {
|
|
let i = 0;
|
|
for(i=0; i < period.children.length; i++) {
|
|
const ii = direction === "first" ? i : (period.children.length - 1 - i);
|
|
if (period.children[ii].snapshotCount) {
|
|
period = period.children[ii];
|
|
break;
|
|
}
|
|
}
|
|
if (iFailSafe-- < 0) {
|
|
break;
|
|
}
|
|
}
|
|
if (period.type === PywbPeriod.Type.snapshot && period.snapshot) {
|
|
return period;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
PywbPeriod.prototype.getPrevious = function() {
|
|
const firstSnapshotPeriod = this.getFirstSnapshotPeriod();
|
|
if (!firstSnapshotPeriod) {
|
|
return null;
|
|
}
|
|
const previousSnapshotPeriod = firstSnapshotPeriod.getPreviousSnapshotPeriod();
|
|
if (!previousSnapshotPeriod) {
|
|
return null;
|
|
}
|
|
if (this.type === PywbPeriod.Type.snapshot) {
|
|
return previousSnapshotPeriod;
|
|
}
|
|
let parent = previousSnapshotPeriod.parent;
|
|
while(parent) {
|
|
if (parent.type === this.type) {
|
|
break;
|
|
}
|
|
parent = parent.parent;
|
|
}
|
|
return parent;
|
|
};
|
|
PywbPeriod.prototype.getNext = function() {
|
|
const lastSnapshotPeriod = this.getLastSnapshotPeriod();
|
|
if (!lastSnapshotPeriod) {
|
|
return null;
|
|
}
|
|
const nextSnapshotPeriod = lastSnapshotPeriod.getNextSnapshotPeriod();
|
|
if (!nextSnapshotPeriod) {
|
|
return null;
|
|
}
|
|
if (this.type === PywbPeriod.Type.snapshot) {
|
|
return nextSnapshotPeriod;
|
|
}
|
|
let parent = nextSnapshotPeriod.parent;
|
|
while(parent) {
|
|
if (parent.type === this.type) {
|
|
break;
|
|
}
|
|
parent = parent.parent;
|
|
}
|
|
return parent;
|
|
};
|
|
|
|
PywbPeriod.prototype.parent = null;
|
|
PywbPeriod.prototype.addChild = function(period) {
|
|
if (this.getChildById(period.id)) {
|
|
return false;
|
|
}
|
|
period.parent = this;
|
|
this.childrenIds[period.id] = this.children.length;
|
|
this.children.push(period);
|
|
period.initFullId();
|
|
return true;
|
|
};
|
|
|
|
PywbPeriod.prototype.getChildrenRange = function() {
|
|
switch (this.type) {
|
|
case PywbPeriod.Type.all:
|
|
// year range: first to last year available
|
|
return [this.children[0].id, this.children[this.children.length-1].id];
|
|
case PywbPeriod.Type.year:
|
|
// month is simple: 1 to 12
|
|
return [1,12];
|
|
case PywbPeriod.Type.month: {
|
|
// days in month: 1 to last day in month
|
|
const y = this.parent.id; const m = this.id;
|
|
const lastDateInMonth = (new Date((new Date(y, m, 1)).getTime() - 1000)).getDate(); // 1 sec earlier
|
|
return [1, lastDateInMonth];
|
|
}
|
|
case PywbPeriod.Type.day:
|
|
// hours: 0 to 23
|
|
// return [1,4];
|
|
return [1,8];
|
|
}
|
|
return null;
|
|
};
|
|
PywbPeriod.prototype.fillEmptyGrancChildPeriods = function() {
|
|
if (this.hasFilledEmptyGrandchildPeriods) {
|
|
return;
|
|
}
|
|
this.children.forEach(c => {
|
|
c.fillEmptyChildPeriods();
|
|
});
|
|
this.hasFilledEmptyGrandchildPeriods = true;
|
|
};
|
|
|
|
PywbPeriod.prototype.fillEmptyChildPeriods = function(isFillEmptyGrandChildrenPeriods=false) {
|
|
if (this.snapshotCount === 0 || this.type > PywbPeriod.Type.day) {
|
|
return;
|
|
}
|
|
|
|
if (isFillEmptyGrandChildrenPeriods) {
|
|
this.fillEmptyGrancChildPeriods();
|
|
}
|
|
|
|
if (this.hasFilledEmptyChildPeriods) {
|
|
return;
|
|
}
|
|
this.hasFilledEmptyChildPeriods = true;
|
|
|
|
const idRange = this.getChildrenRange();
|
|
if (!idRange) {
|
|
return;
|
|
}
|
|
|
|
let i = 0;
|
|
for (let newId = idRange[0]; newId <= idRange[1]; newId++) {
|
|
if (i < this.children.length) {
|
|
// if existing and new id match, skip, item already in place
|
|
// else
|
|
if (this.children[i].id !== newId) {
|
|
const empty = new PywbPeriod({type: this.type + 1, id: newId});
|
|
if (newId < this.children[i].id) {
|
|
// insert new before existing
|
|
this.children.splice(i, 0, empty);
|
|
} else {
|
|
// insert new after existing
|
|
this.children.splice(i+1, 0, empty);
|
|
}
|
|
// manually push children (no need to reverse link parent
|
|
//empty.parent = this;
|
|
}
|
|
i++;
|
|
} else {
|
|
const empty = new PywbPeriod({type: this.type + 1, id: newId});
|
|
this.children.push(empty);
|
|
// manually push children (no need to reverse link parent
|
|
//empty.parent = this;
|
|
}
|
|
}
|
|
|
|
// re-calculate indexes
|
|
for(let i=0;i<this.children.length;i++) {
|
|
this.childrenIds[this.children[i].id] = i;
|
|
}
|
|
|
|
return idRange;
|
|
};
|
|
|
|
PywbPeriod.prototype.getParents = function(skipAllTime=false) {
|
|
let parents = [];
|
|
let parent = this.parent;
|
|
while(parent) {
|
|
parents.push(parent);
|
|
parent = parent.parent;
|
|
}
|
|
parents = parents.reverse();
|
|
if (skipAllTime) {
|
|
parents.shift(); // skip first "all-time"
|
|
}
|
|
return parents;
|
|
};
|
|
|
|
PywbPeriod.prototype.contains = function(periodOrSnapshot) {
|
|
if (this.type === 0) {
|
|
return true; // all-time contains everything
|
|
}
|
|
if (periodOrSnapshot instanceof PywbPeriod) {
|
|
return periodOrSnapshot.getParents(true).slice(0,this.type).map(p => p.id).join(PywbPeriodIdDelimiter) === this.fullId;
|
|
}
|
|
if (periodOrSnapshot instanceof PywbSnapshot) {
|
|
if (this.type === PywbPeriod.Type.snapshot) {
|
|
return periodOrSnapshot.getFullId() === this.fullId;
|
|
} else {
|
|
return periodOrSnapshot.getParentIds(true).slice(0,this.type).join(PywbPeriodIdDelimiter) === this.fullId;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
PywbPeriod.prototype.snapshot = null;
|
|
PywbPeriod.prototype.snapshotPeriod = null;
|
|
|
|
PywbPeriod.prototype.checkIfSingleSnapshotOnly = function() {
|
|
if (this.snapshotCount === 1) {
|
|
let snapshotPeriod = this;
|
|
let failSafe = PywbPeriod.Type.snapshot;
|
|
while(!snapshotPeriod.snapshot) {
|
|
if (--failSafe <=0) break;
|
|
snapshotPeriod = snapshotPeriod.children[0];
|
|
}
|
|
this.snapshot = snapshotPeriod.snapshot;
|
|
this.snapshotPeriod = snapshotPeriod;
|
|
}
|
|
};
|
|
|
|
PywbPeriod.prototype.setSnapshot = function(snap) {
|
|
this.snapshot = snap;
|
|
this.snapshotCount++;
|
|
let parent = this.parent;
|
|
let child = this;
|
|
while (parent) {
|
|
parent.snapshotCount++;
|
|
|
|
let grandParent = parent.parent;
|
|
if (grandParent) { // grandparent
|
|
grandParent.maxGrandchildSnapshotCount = Math.max(grandParent.maxGrandchildSnapshotCount, child.snapshotCount);
|
|
}
|
|
child = parent;
|
|
parent = parent.parent;
|
|
}
|
|
};
|
|
|
|
|
|
PywbPeriod.prototype.getSnapshotPeriodsFlat = function(flatArray=false) {
|
|
if (!flatArray) {
|
|
flatArray = [];
|
|
}
|
|
if (!this.snapshotCount) {
|
|
return flatArray;
|
|
}
|
|
|
|
if (this.snapshotCount === 1) {
|
|
flatArray.push(this.snapshotPeriod || this);
|
|
return flatArray;
|
|
}
|
|
|
|
this.children.forEach(child => {
|
|
child.getSnapshotPeriodsFlat(flatArray);
|
|
});
|
|
return flatArray;
|
|
};
|
|
|
|
/**
|
|
* Return the "full" id, which includes all parents ID and self ID, delimited by a ${PywbPeriodIdDelimiter}
|
|
* @returns {string}
|
|
*/
|
|
PywbPeriod.prototype.initFullId = function() {
|
|
const ids = this.getParents(true).map(p => p.id);
|
|
ids.push(this.id);
|
|
this.fullId = ids.join(PywbPeriodIdDelimiter);
|
|
};
|
|
|
|
/**
|
|
* Find a period by its full ID (of all ancestors and self, delimited by a hyphen). Start by locating the great-grand-parent (aka timeline), then looping on all IDs and finding the period in loop
|
|
* @param {string} fullId
|
|
* @returns {boolean}
|
|
*/
|
|
PywbPeriod.prototype.findByFullId = function(fullId) {
|
|
let parent = this;
|
|
if (this.type !== PywbPeriod.Type.all) {
|
|
parent = this.getParents()[0];
|
|
}
|
|
const ids = fullId.split(PywbPeriodIdDelimiter);
|
|
|
|
let found = false;
|
|
for(let i=0; i<ids.length; i++) {
|
|
parent = parent.getChildById(ids[i]);
|
|
if (parent) {
|
|
// if last chunk of ID in loop, the period is found
|
|
if (i === ids.length - 1) {
|
|
found = parent;
|
|
}
|
|
} else {
|
|
// if no parent is found with ID chunk, abort "mission"
|
|
break;
|
|
}
|
|
}
|
|
return found;
|
|
};
|
|
PywbPeriod.prototype.getFullReadableId = function(hasDayCardinalSuffix) {
|
|
// remove "all-time" from parents (getParents(true) when printing readable id (of all parents and currrent
|
|
switch (this.type) {
|
|
case PywbPeriod.Type.all:
|
|
return "";
|
|
case PywbPeriod.Type.year:
|
|
return this.id;
|
|
case PywbPeriod.Type.month:
|
|
return this.getReadableId(hasDayCardinalSuffix) + ' ' + this.parent.id;
|
|
case PywbPeriod.Type.day: {
|
|
return this.parent.getReadableId(hasDayCardinalSuffix) + ' ' + this.getReadableId(hasDayCardinalSuffix) + ', ' + this.parent.parent.id;
|
|
}
|
|
case PywbPeriod.Type.hour:
|
|
return this.parent.parent.getReadableId(hasDayCardinalSuffix) + ' ' + this.parent.getReadableId(hasDayCardinalSuffix) + ', ' + this.parent.parent.parent.id + ' at ' + this.getReadableId(hasDayCardinalSuffix);
|
|
case PywbPeriod.Type.snapshot:
|
|
return this.parent.parent.getReadableId(hasDayCardinalSuffix) + ' ' + this.parent.getReadableId(hasDayCardinalSuffix) + ', ' + this.parent.parent.parent.id + ' at ' + this.snapshot.getTimeFormatted();
|
|
}
|
|
};
|
|
PywbPeriod.prototype.getReadableId = function(hasDayCardinalSuffix) {
|
|
switch (this.type) {
|
|
case PywbPeriod.Type.all:
|
|
return "All-time";
|
|
case PywbPeriod.Type.year:
|
|
return this.id;
|
|
case PywbPeriod.Type.month:
|
|
return PywbMonthLabels[this.id];
|
|
case PywbPeriod.Type.day: {
|
|
let suffix = "";
|
|
if (hasDayCardinalSuffix) {
|
|
const singleDigit = this.id % 10;
|
|
const isTens = Math.floor(this.id / 10) === 1;
|
|
const suffixes = {1:"st", 2:"nd",3:"rd"};
|
|
suffix = (isTens || !suffixes[singleDigit]) ? "th" : suffixes[singleDigit];
|
|
}
|
|
return this.id + suffix;
|
|
}
|
|
case PywbPeriod.Type.hour:
|
|
return ({1:"12 am", 2: "3 am", 3: "6 am", 4: "9 am", 5: "noon", 6: "3 pm", 7: "6 pm", 8: "9 pm"})[this.id];
|
|
//return ({1:'midnight', 2: '6 am', 3: 'noon', 4: '6 pm'})[this.id];
|
|
//return (this.id < 13 ? this.id : this.id % 12) + ' ' + (this.id < 12 ? 'am':'pm');
|
|
case PywbPeriod.Type.snapshot:
|
|
return this.snapshot.getTimeFormatted();
|
|
}
|
|
};
|
|
|
|
PywbPeriod.prototype.getYear = function() { this.get(PywbPeriod.Type.year); };
|
|
PywbPeriod.prototype.getMonth = function() { this.get(PywbPeriod.Type.month); };
|
|
PywbPeriod.prototype.getDay = function() { this.get(PywbPeriod.Type.day); };
|
|
PywbPeriod.prototype.getHour = function() { this.get(PywbPeriod.Type.hour); };
|
|
PywbPeriod.prototype.get = function(type) {
|
|
if (this.type === type) {
|
|
return this;
|
|
} else if (this.type > type) {
|
|
return this.getParents()[type];
|
|
}
|
|
};
|