|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const bulk = (function () { |
|
|
'use strict'; |
|
|
|
|
|
const elements = {}; |
|
|
const controls = {}; |
|
|
|
|
|
const delaySubmitKey = "delaySubmitBulk"; |
|
|
const can = { |
|
|
submit: true, |
|
|
}; |
|
|
|
|
|
const apiNextPageMs = 600; |
|
|
const delay15Sec = 15; |
|
|
const delay15SecMs = delay15Sec * 1000; |
|
|
|
|
|
function countdown(key, control, delay, flag) { |
|
|
control.addClass("loading").addClass("disabled"); |
|
|
can[flag] = false; |
|
|
|
|
|
let value = localStorage.getItem(key); |
|
|
if (!moment(value).isValid()) { |
|
|
console.warn('value for %s was not a valid date, resetting to now', key); |
|
|
localStorage.setItem(key, new Date()); |
|
|
value = localStorage.getItem(key); |
|
|
} |
|
|
if (moment(value).isAfter(moment())) { |
|
|
console.warn('value for %s was set in the future, resetting to now', key); |
|
|
localStorage.setItem(key, new Date()); |
|
|
value = localStorage.getItem(key); |
|
|
} |
|
|
let count = (delay - moment().diff(value)) / 1000; |
|
|
control.find(".countdown").text(Math.trunc(count)); |
|
|
|
|
|
function c(control, count) { |
|
|
if (count <= 1) { |
|
|
control.removeClass("loading").removeClass("disabled"); |
|
|
control.find(".countdown").text(""); |
|
|
|
|
|
can[flag] = true; |
|
|
} else { |
|
|
control.find(".countdown").text(Math.trunc(count)); |
|
|
setTimeout(function () { |
|
|
c(control, count - 1) |
|
|
}, 1000); |
|
|
} |
|
|
} |
|
|
|
|
|
setTimeout(function () { |
|
|
c(control, count) |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
function countdownCheck(key, control, delayMs, flag) { |
|
|
const value = localStorage.getItem(key); |
|
|
if (key in localStorage && moment(value).isValid() && moment().diff(value) < delayMs) { |
|
|
countdown(key, control, delayMs, flag); |
|
|
} else { |
|
|
control.removeClass("loading").removeClass("disabled"); |
|
|
control.find(".countdown").text(""); |
|
|
} |
|
|
} |
|
|
|
|
|
function doneProgressMessage() { |
|
|
const about = []; |
|
|
if (rawVideoData.length) { |
|
|
about.push(rawVideoData.length + " video(s)"); |
|
|
} |
|
|
if (Object.keys(unavailableData).length) { |
|
|
about.push(Object.keys(unavailableData).length + " unavailable"); |
|
|
} |
|
|
if (Object.keys(failedData).length) { |
|
|
about.push(Object.keys(failedData).length + " failed"); |
|
|
} |
|
|
if (Object.keys(rawChannelMap).length) { |
|
|
about.push(Object.keys(rawChannelMap).length + " channel(s)"); |
|
|
} |
|
|
if (Object.keys(rawPlaylistMap).length) { |
|
|
about.push(Object.keys(rawPlaylistMap).length + " playlist(s)"); |
|
|
} |
|
|
return about.join(", ") |
|
|
} |
|
|
|
|
|
function unavailableProgressMessage() { |
|
|
const about = []; |
|
|
|
|
|
if (Object.keys(unavailableData).length) { |
|
|
about.push(Object.keys(unavailableData).length + " unavailable"); |
|
|
|
|
|
let noData = 0; |
|
|
let filmot = 0; |
|
|
for (let key in unavailableData) { |
|
|
if (unavailableData[key].hasOwnProperty("filmot")) { |
|
|
filmot = filmot + 1; |
|
|
} else { |
|
|
noData = noData + 1; |
|
|
} |
|
|
} |
|
|
|
|
|
if (filmot > 0) { |
|
|
about.push(filmot + " data"); |
|
|
} |
|
|
if (noData > 0) { |
|
|
about.push(noData + " no-data"); |
|
|
} |
|
|
} |
|
|
|
|
|
return about.join(", "); |
|
|
} |
|
|
|
|
|
function processFromParsed(parsed) { |
|
|
console.log(parsed); |
|
|
|
|
|
const channelUsers = []; |
|
|
const channelHandles = []; |
|
|
const channelCustoms = []; |
|
|
const channelIds = []; |
|
|
const channelIdsCreatedPlaylists = []; |
|
|
const playlistIds = []; |
|
|
const videoIds = []; |
|
|
|
|
|
parsed.forEach(function (p) { |
|
|
if (p.type === 'video_id' && videoIds.indexOf(p.value) === -1) { |
|
|
videoIds.push(p.value); |
|
|
|
|
|
controls.progress.update({ |
|
|
text: videoIds.length |
|
|
}); |
|
|
} else if (p.type === "playlist_id" && playlistIds.indexOf(p.value) === -1) { |
|
|
playlistIds.push(p.value); |
|
|
} else if (p.type === "channel_id" && channelIds.indexOf(p.value) === -1 && shared.isValidChannelId(p.value)) { |
|
|
channelIds.push(p.value); |
|
|
} else if (p.type === "channel_handle" && channelHandles.indexOf(p.value) === -1) { |
|
|
channelHandles.push(p.value); |
|
|
} else if (p.type === "channel_custom" && channelCustoms.indexOf(p.value) === -1) { |
|
|
channelCustoms.push(p.value); |
|
|
} else if (p.type === "channel_user" && channelUsers.indexOf(p.value) === -1) { |
|
|
channelUsers.push(p.value); |
|
|
} |
|
|
}); |
|
|
|
|
|
controls.progress.update({ |
|
|
subtext: 'Grabbing unique video ids' |
|
|
}); |
|
|
|
|
|
Promise.all([ |
|
|
handleChannelCustoms(channelCustoms, channelIds), |
|
|
handleChannelHandles(channelHandles, channelIds), |
|
|
]).then(function () { |
|
|
return Promise.all([ |
|
|
|
|
|
handleChannelUsers(channelUsers, playlistIds, channelIdsCreatedPlaylists), |
|
|
handleChannelIds(channelIds, playlistIds, channelIdsCreatedPlaylists) |
|
|
]); |
|
|
}).then(function () { |
|
|
|
|
|
return handleChannelIdsCreatedPlaylists(channelIdsCreatedPlaylists, playlistIds); |
|
|
}).then(function () { |
|
|
|
|
|
return handlePlaylistNames(playlistIds); |
|
|
}).then(function () { |
|
|
|
|
|
return handlePlaylistIds(playlistIds, videoIds); |
|
|
}).then(function () { |
|
|
controls.progress.update({ |
|
|
subtext: 'Processing video ids' |
|
|
}); |
|
|
|
|
|
|
|
|
return handleVideoIds(videoIds); |
|
|
}).then(function () { |
|
|
return new Promise(function (resolve) { |
|
|
sliceLoad(rows, controls.videosTable, resolve); |
|
|
}); |
|
|
}).then(function () { |
|
|
controls.progress.update({ |
|
|
subtext: 'Processing channel ids' |
|
|
}); |
|
|
|
|
|
|
|
|
const newChannelIds = []; |
|
|
rawVideoData.forEach(function (video) { |
|
|
const channelId = shared.idx(["snippet", "channelId"], video); |
|
|
if (!rawChannelMap.hasOwnProperty(channelId) && newChannelIds.indexOf(channelId) === -1) { |
|
|
newChannelIds.push(channelId); |
|
|
} |
|
|
}); |
|
|
|
|
|
return handleChannelIds(newChannelIds, [], []); |
|
|
}).then(function () { |
|
|
console.log(videoIds); |
|
|
|
|
|
const resultIds = []; |
|
|
rawVideoData.forEach(function (video) { |
|
|
resultIds.push(video.id); |
|
|
}); |
|
|
videoIds.forEach(function (videoId) { |
|
|
if (!unavailableData.hasOwnProperty(videoId) && !failedData.hasOwnProperty(videoId) && resultIds.indexOf(videoId) === -1) { |
|
|
unavailableData[videoId] = { |
|
|
title: "Did not come back in API", |
|
|
source: "" |
|
|
}; |
|
|
} |
|
|
}); |
|
|
|
|
|
controls.videosTable.columns.adjust().draw(false); |
|
|
}).then(function () { |
|
|
controls.progress.update({ |
|
|
text: doneProgressMessage(), |
|
|
subtext: 'Done' + (Object.keys(failedData).length ? ' (with errors). Check browser console.' : '') |
|
|
}); |
|
|
|
|
|
console.log(failedData) |
|
|
|
|
|
controls.unavailableTable.columns.adjust().draw(false); |
|
|
|
|
|
if (Object.keys(unavailableData).length > 0) { |
|
|
controls.checkUnavailable.removeClass("disabled"); |
|
|
} |
|
|
|
|
|
setTimeout(loadAggregateTables, 200); |
|
|
}).catch(function (err) { |
|
|
console.error(err); |
|
|
}); |
|
|
} |
|
|
|
|
|
function handleChannelUsers(channelUsers, playlistIds, channelIdsCreatedPlaylists) { |
|
|
return new Promise(function (resolve) { |
|
|
if (channelUsers.length === 0) { |
|
|
console.log("no channelUsers") |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
|
|
|
function get(index) { |
|
|
if (index >= channelUsers.length) { |
|
|
console.log("finished channelUsers"); |
|
|
setTimeout(resolve, apiNextPageMs); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("handleChannelUsers.get(" + index + ")") |
|
|
console.log(channelUsers[index]) |
|
|
|
|
|
youtube.ajax("channels", { |
|
|
part: "snippet,statistics,brandingSettings,contentDetails,localizations,status,topicDetails", |
|
|
forUsername: channelUsers[index] |
|
|
}).done(function (res) { |
|
|
console.log(res); |
|
|
|
|
|
const channel = shared.idx(["items", 0], res); |
|
|
if (!channel) { |
|
|
get(index + 1); |
|
|
return; |
|
|
} |
|
|
|
|
|
const channelId = shared.idx(["id"], channel); |
|
|
rawChannelMap[channelId] = channel; |
|
|
|
|
|
if (channelIdsCreatedPlaylists.indexOf(channelId) === -1) { |
|
|
channelIdsCreatedPlaylists.push(channelId); |
|
|
} |
|
|
|
|
|
const uploadsPlaylistId = shared.idx(["items", 0, "contentDetails", "relatedPlaylists", "uploads"], res); |
|
|
console.log(uploadsPlaylistId); |
|
|
|
|
|
if (playlistIds.indexOf(uploadsPlaylistId) === -1) { |
|
|
playlistIds.push(uploadsPlaylistId); |
|
|
} |
|
|
|
|
|
get(index + 1); |
|
|
}).fail(function (err) { |
|
|
console.error(err); |
|
|
get(index + 1); |
|
|
}); |
|
|
} |
|
|
|
|
|
get(0); |
|
|
}); |
|
|
} |
|
|
|
|
|
function handleChannelCustoms(channelCustoms, channelIds) { |
|
|
return new Promise(function (resolve) { |
|
|
if (channelCustoms.length === 0) { |
|
|
console.log("no channelCustoms") |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
|
|
|
function get(index) { |
|
|
if (index >= channelCustoms.length) { |
|
|
console.log("finished channelCustoms"); |
|
|
setTimeout(resolve, apiNextPageMs); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("handleChannelCustoms.get(" + index + ")") |
|
|
|
|
|
$.ajax({ |
|
|
url: "https://cors-proxy-mw324.herokuapp.com/https://www.youtube.com/" + channelCustoms[index], |
|
|
dataType: 'html' |
|
|
}).then(function (res) { |
|
|
const pageHtml = $("<div>").html(res); |
|
|
const ogUrl = pageHtml.find("meta[property='og:url']").attr('content'); |
|
|
console.log('Retrieved og:url ' + ogUrl); |
|
|
|
|
|
const newParsed = shared.determineInput(ogUrl); |
|
|
if (newParsed.type === "channel_id") { |
|
|
channelIds.push(newParsed.value); |
|
|
setTimeout(function () { |
|
|
get(index + 1); |
|
|
}, apiNextPageMs); |
|
|
} else { |
|
|
console.log('Could not resolve custom url'); |
|
|
console.warn(newParsed); |
|
|
|
|
|
setTimeout(function () { |
|
|
get(index + 1); |
|
|
}, apiNextPageMs); |
|
|
} |
|
|
}).fail(function (err) { |
|
|
console.warn(err); |
|
|
|
|
|
setTimeout(function () { |
|
|
get(index + 1); |
|
|
}, apiNextPageMs); |
|
|
}); |
|
|
} |
|
|
|
|
|
get(0); |
|
|
}); |
|
|
} |
|
|
|
|
|
function handleChannelHandles(channelHandles, channelIds) { |
|
|
return new Promise(function (resolve) { |
|
|
if (channelHandles.length === 0) { |
|
|
console.log("no channelHandles") |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
|
|
|
function get(index) { |
|
|
if (index >= channelHandles.length) { |
|
|
console.log("finished channelHandles"); |
|
|
setTimeout(resolve, apiNextPageMs); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("handleChannelHandles.get(" + index + ")") |
|
|
|
|
|
$.ajax({ |
|
|
url: "https://cors-proxy-mw324.herokuapp.com/https://www.youtube.com/@" + channelHandles[index], |
|
|
dataType: 'html' |
|
|
}).then(function (res) { |
|
|
const pageHtml = $("<div>").html(res); |
|
|
const channelId = pageHtml.find("meta[itemprop='channelId']").attr('content'); |
|
|
const ogUrl = pageHtml.find("meta[property='og:url']").attr('content'); |
|
|
const canonical = pageHtml.find("link[rel='canonical']").attr('href'); |
|
|
|
|
|
console.log('Retrieved [channelId=%s, ogUrl=%s, canonical=%s]', channelId, ogUrl, canonical); |
|
|
|
|
|
const newParsed = shared.determineInput(channelId || ogUrl || canonical); |
|
|
if (newParsed.type === "channel_id") { |
|
|
channelIds.push(newParsed.value); |
|
|
setTimeout(function () { |
|
|
get(index + 1); |
|
|
}, apiNextPageMs); |
|
|
} else { |
|
|
console.log('Could not resolve handle'); |
|
|
console.warn(newParsed); |
|
|
|
|
|
setTimeout(function () { |
|
|
get(index + 1); |
|
|
}, apiNextPageMs); |
|
|
} |
|
|
}).fail(function (err) { |
|
|
console.warn(err); |
|
|
|
|
|
setTimeout(function () { |
|
|
get(index + 1); |
|
|
}, apiNextPageMs); |
|
|
}); |
|
|
} |
|
|
|
|
|
get(0); |
|
|
}); |
|
|
} |
|
|
|
|
|
function handleChannelIds(channelIds, playlistIds, channelIdsCreatedPlaylists) { |
|
|
let processed = 0; |
|
|
channelIds.forEach(function (channelId) { |
|
|
if (channelIdsCreatedPlaylists.indexOf(channelId) === -1) { |
|
|
channelIdsCreatedPlaylists.push(channelId); |
|
|
} |
|
|
}); |
|
|
return new Promise(function (resolve) { |
|
|
if (channelIds.length === 0) { |
|
|
console.log("no channelIds") |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
|
|
|
controls.progress.update({ |
|
|
value: 0, |
|
|
max: channelIds.length, |
|
|
text: "0 / " + channelIds.length |
|
|
}); |
|
|
|
|
|
function get(index, slice) { |
|
|
if (index >= channelIds.length) { |
|
|
console.log("finished channelIds"); |
|
|
setTimeout(resolve, apiNextPageMs); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("handleChannelIds.get(" + index + ", " + slice + ")") |
|
|
|
|
|
const ids = channelIds.slice(index, index + slice); |
|
|
|
|
|
youtube.ajax("channels", { |
|
|
part: "snippet,statistics,brandingSettings,contentDetails,localizations,status,topicDetails", |
|
|
id: ids.join(","), |
|
|
maxResults: 50 |
|
|
}).done(function (res) { |
|
|
console.log(res); |
|
|
|
|
|
(res.items || []).forEach(function (channel) { |
|
|
const channelId = shared.idx(["id"], channel); |
|
|
rawChannelMap[channelId] = channel; |
|
|
|
|
|
const uploadsPlaylistId = shared.idx(["contentDetails", "relatedPlaylists", "uploads"], channel); |
|
|
console.log(uploadsPlaylistId); |
|
|
|
|
|
if (playlistIds.indexOf(uploadsPlaylistId) === -1) { |
|
|
playlistIds.push(uploadsPlaylistId); |
|
|
} |
|
|
}); |
|
|
|
|
|
processed = processed + ids.length; |
|
|
|
|
|
controls.progress.update({ |
|
|
value: processed, |
|
|
text: processed + " / " + channelIds.length |
|
|
}); |
|
|
|
|
|
get(index + slice, slice); |
|
|
}).fail(function (err) { |
|
|
console.error(err); |
|
|
get(index + slice, slice); |
|
|
}); |
|
|
} |
|
|
|
|
|
get(0, 50); |
|
|
}); |
|
|
} |
|
|
|
|
|
function handleChannelIdsCreatedPlaylists(channelIds, playlistIds) { |
|
|
return new Promise(function (resolve) { |
|
|
if (channelIds.length === 0) { |
|
|
console.log("no handleChannelIdsCreatedPlaylists") |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (controls.createdPlaylists.is(":checked") === false) { |
|
|
console.log("createdPlaylists not checked, skipping") |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
function get(index) { |
|
|
if (index >= channelIds.length) { |
|
|
console.log("finished handleChannelIdsCreatedPlaylists"); |
|
|
setTimeout(resolve, apiNextPageMs); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("handleChannelIdsCreatedPlaylists.get(" + index + ")") |
|
|
|
|
|
const id = channelIds[index]; |
|
|
console.log(id); |
|
|
|
|
|
function paginate(pageToken) { |
|
|
youtube.ajax("playlists", { |
|
|
part: "snippet,status,localizations,contentDetails", |
|
|
channelId: id, |
|
|
maxResults: 50, |
|
|
pageToken: pageToken |
|
|
}).done(function (res) { |
|
|
console.log(res); |
|
|
|
|
|
(res.items || []).forEach(function (playlist) { |
|
|
const createdPlaylistId = shared.idx(["id"], playlist); |
|
|
console.log(createdPlaylistId); |
|
|
|
|
|
rawPlaylistMap[createdPlaylistId] = playlist; |
|
|
|
|
|
playlistMap[createdPlaylistId] = shared.idx(["snippet", "title"], playlist); |
|
|
|
|
|
if (playlistIds.indexOf(createdPlaylistId) === -1) { |
|
|
playlistIds.push(createdPlaylistId); |
|
|
} |
|
|
}); |
|
|
|
|
|
if (res.hasOwnProperty("nextPageToken")) { |
|
|
paginate(res.nextPageToken); |
|
|
} else { |
|
|
get(index + 1); |
|
|
} |
|
|
}).fail(function (err) { |
|
|
console.error(err); |
|
|
get(index + 1); |
|
|
}); |
|
|
} |
|
|
|
|
|
paginate(""); |
|
|
} |
|
|
|
|
|
get(0); |
|
|
}); |
|
|
} |
|
|
|
|
|
function handlePlaylistNames(playlistIds) { |
|
|
return new Promise(function (resolve) { |
|
|
if (playlistIds.length === 0) { |
|
|
console.log("no playlistIds") |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
|
|
|
const notYetRetrieved = []; |
|
|
playlistIds.forEach(function (id) { |
|
|
if (!playlistMap.hasOwnProperty(id)) { |
|
|
notYetRetrieved.push(id); |
|
|
} |
|
|
}); |
|
|
console.log(notYetRetrieved); |
|
|
|
|
|
function get(index) { |
|
|
if (index >= notYetRetrieved.length) { |
|
|
console.log("finished notYetRetrieved"); |
|
|
setTimeout(resolve, apiNextPageMs); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("handlePlaylistNames.get(" + index + ")") |
|
|
|
|
|
function paginate(pageToken) { |
|
|
console.log(pageToken); |
|
|
youtube.ajax("playlists", { |
|
|
part: "snippet,status,localizations,contentDetails", |
|
|
maxResults: 50, |
|
|
id: notYetRetrieved[index], |
|
|
pageToken: pageToken |
|
|
}).done(function (res) { |
|
|
console.log(res); |
|
|
|
|
|
(res.items || []).forEach(function (playlist) { |
|
|
const playlistId = shared.idx(["id"], playlist); |
|
|
console.log(playlistId); |
|
|
|
|
|
rawPlaylistMap[playlistId] = playlist; |
|
|
playlistMap[playlistId] = shared.idx(["snippet", "title"], playlist); |
|
|
}); |
|
|
|
|
|
if (res.hasOwnProperty("nextPageToken")) { |
|
|
paginate(res.nextPageToken); |
|
|
} else { |
|
|
setTimeout(function () { |
|
|
get(index + 1); |
|
|
}, apiNextPageMs); |
|
|
} |
|
|
}).fail(function (err) { |
|
|
console.error(err); |
|
|
setTimeout(function () { |
|
|
get(index + 1); |
|
|
}, apiNextPageMs); |
|
|
}); |
|
|
} |
|
|
|
|
|
paginate(""); |
|
|
} |
|
|
|
|
|
get(0); |
|
|
}); |
|
|
} |
|
|
|
|
|
function handlePlaylistIds(playlistIds, videoIds) { |
|
|
return new Promise(function (resolve) { |
|
|
if (playlistIds.length === 0) { |
|
|
console.log("no playlistIds") |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
|
|
|
function get(index) { |
|
|
if (index >= playlistIds.length) { |
|
|
console.log("finished playlistIds"); |
|
|
setTimeout(resolve, apiNextPageMs); |
|
|
return; |
|
|
} |
|
|
|
|
|
function paginate(pageToken) { |
|
|
console.log("handlePlaylistIds.get(" + index + ")") |
|
|
|
|
|
youtube.ajax("playlistItems", { |
|
|
part: "snippet", |
|
|
maxResults: 50, |
|
|
playlistId: playlistIds[index], |
|
|
pageToken: pageToken |
|
|
}).done(function (res) { |
|
|
console.log(res); |
|
|
|
|
|
(res.items || []).forEach(function (video) { |
|
|
const videoId = shared.idx(["snippet", "resourceId", "videoId"], video); |
|
|
const videoOwnerChannelId = shared.idx(["snippet", "videoOwnerChannelId"], video); |
|
|
const dateFormat = "YYYY-MM-DD"; |
|
|
const dateAdded = shared.idx(["snippet", "publishedAt"], video); |
|
|
|
|
|
if (videoIds.indexOf(videoId) === -1) { |
|
|
videoIds.push(videoId); |
|
|
|
|
|
availableData[videoId] = { |
|
|
source: "Playlist: " + |
|
|
"<a target='_blank' href='https://www.youtube.com/playlist?list=" + playlistIds[index] + "'>" + |
|
|
playlistMap[playlistIds[index]] + |
|
|
"</a> (added " + moment(dateAdded).format(dateFormat) + ")" |
|
|
} |
|
|
} |
|
|
if (!videoOwnerChannelId) { |
|
|
unavailableData[videoId] = { |
|
|
title: shared.idx(["snippet", "title"], video), |
|
|
source: "Playlist: " + |
|
|
"<a target='_blank' href='https://www.youtube.com/playlist?list=" + playlistIds[index] + "'>" + |
|
|
playlistMap[playlistIds[index]] + |
|
|
"</a> (added " + moment(dateAdded).format(dateFormat) + ")" |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
controls.progress.update({ |
|
|
text: videoIds.length |
|
|
}) |
|
|
|
|
|
if (res.hasOwnProperty("nextPageToken")) { |
|
|
setTimeout(function () { |
|
|
paginate(res.nextPageToken); |
|
|
}, apiNextPageMs); |
|
|
} else { |
|
|
setTimeout(function () { |
|
|
get(index + 1); |
|
|
}, apiNextPageMs); |
|
|
} |
|
|
}).fail(function (err) { |
|
|
console.error(err); |
|
|
setTimeout(function () { |
|
|
get(index + 1); |
|
|
}, 150); |
|
|
}); |
|
|
} |
|
|
|
|
|
paginate(""); |
|
|
} |
|
|
|
|
|
get(0); |
|
|
}); |
|
|
} |
|
|
|
|
|
function handleUnavailableVideos() { |
|
|
const videoIds = []; |
|
|
for (let videoId in unavailableData) { |
|
|
if (shared.isValidVideoId(videoId)) { |
|
|
videoIds.push(videoId); |
|
|
} else { |
|
|
unavailableData[videoId].title = "Invalid ID"; |
|
|
} |
|
|
} |
|
|
|
|
|
let processed = 0; |
|
|
return new Promise(function (resolve) { |
|
|
const hostname = window.location.hostname; |
|
|
if (hostname !== "localhost" && hostname !== "mattw.io") { |
|
|
console.log("do not call filmot from other instances") |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (videoIds.length === 0) { |
|
|
console.log("no videoIds") |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("checking " + videoIds.length + " videoIds"); |
|
|
|
|
|
function get(index, slice) { |
|
|
if (index >= videoIds.length) { |
|
|
console.log("finished videoIds"); |
|
|
setTimeout(resolve, apiNextPageMs); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("handleUnavailableVideos.get(" + index + ", " + (index + slice) + ")") |
|
|
|
|
|
const ids = videoIds.slice(index, index + slice); |
|
|
|
|
|
|
|
|
|
|
|
$.ajax({ |
|
|
cache: false, |
|
|
data: { |
|
|
key: "md5paNgdbaeudounjp39", |
|
|
id: ids.join(","), |
|
|
flags: 1 |
|
|
}, |
|
|
dataType: "json", |
|
|
type: "GET", |
|
|
timeout: 5000, |
|
|
url: "https://filmot.com/api/getvideos", |
|
|
}).done(function (res) { |
|
|
console.log(res); |
|
|
|
|
|
res.forEach(function (video) { |
|
|
unavailableData[video.id]["filmot"] = video; |
|
|
}); |
|
|
|
|
|
processed = processed + ids.length; |
|
|
|
|
|
controls.unavailableProgress.update({ |
|
|
value: processed, |
|
|
max: videoIds.length, |
|
|
text: processed + " / " + videoIds.length |
|
|
}) |
|
|
|
|
|
setTimeout(function () { |
|
|
get(index + slice, slice); |
|
|
}, 1500); |
|
|
}).fail(function (err) { |
|
|
console.error(err); |
|
|
setTimeout(function () { |
|
|
get(index + slice, slice); |
|
|
}, 1500); |
|
|
}); |
|
|
} |
|
|
|
|
|
get(0, 100); |
|
|
}); |
|
|
} |
|
|
|
|
|
function handleVideoIds(videoIds) { |
|
|
let processed = 0; |
|
|
|
|
|
return new Promise(function (resolve) { |
|
|
if (videoIds.length === 0) { |
|
|
console.log("no videoIds") |
|
|
resolve(); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("checking " + videoIds.length + " videoIds"); |
|
|
|
|
|
function get(index, slice) { |
|
|
if (index >= videoIds.length) { |
|
|
console.log("finished videoIds"); |
|
|
setTimeout(resolve, apiNextPageMs); |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log("handleVideoIds.get(" + index + ", " + (index + slice) + ")") |
|
|
|
|
|
const ids = videoIds.slice(index, index + slice); |
|
|
|
|
|
console.log(ids.length); |
|
|
console.log(ids); |
|
|
|
|
|
try { |
|
|
youtube.ajax("videos", { |
|
|
part: "snippet,statistics,recordingDetails," + |
|
|
"status,liveStreamingDetails,localizations," + |
|
|
"contentDetails,topicDetails", |
|
|
maxResults: 50, |
|
|
id: ids.join(",") |
|
|
}).done(function (res) { |
|
|
try { |
|
|
console.log(res); |
|
|
|
|
|
(res.items || []).forEach(function (video) { |
|
|
loadVideo(video, true); |
|
|
}); |
|
|
|
|
|
processed = processed + ids.length; |
|
|
|
|
|
controls.progress.update({ |
|
|
value: processed, |
|
|
max: videoIds.length, |
|
|
text: processed + " / " + videoIds.length |
|
|
}); |
|
|
|
|
|
setTimeout(function () { |
|
|
get(index + slice, slice); |
|
|
}, apiNextPageMs); |
|
|
} catch (error) { |
|
|
controls.progress.addClass('error'); |
|
|
console.error(error); |
|
|
const reason = JSON.stringify(error, null, 0); |
|
|
for (let i = 0; i < ids.length; i++) { |
|
|
failedData[ids[i]] = {reason: reason} |
|
|
} |
|
|
setTimeout(function () { |
|
|
get(index + slice, slice); |
|
|
}, apiNextPageMs); |
|
|
} |
|
|
}).fail(function (err) { |
|
|
controls.progress.addClass('error'); |
|
|
console.warn(err) |
|
|
const reason = shared.idx(["responseJSON", "error", "errors", 0, "reason"], err) || |
|
|
JSON.stringify(err, null, 0); |
|
|
for (let i = 0; i < ids.length; i++) { |
|
|
failedData[ids[i]] = {reason: reason} |
|
|
} |
|
|
setTimeout(function () { |
|
|
get(index + slice, slice); |
|
|
}, apiNextPageMs); |
|
|
}); |
|
|
} catch (error) { |
|
|
controls.progress.addClass('error'); |
|
|
console.error(error); |
|
|
const reason = JSON.stringify(error, null, 0); |
|
|
for (let i = 0; i < ids.length; i++) { |
|
|
failedData[ids[i]] = {reason: reason} |
|
|
} |
|
|
get(index + slice, slice); |
|
|
} |
|
|
} |
|
|
|
|
|
get(0, 50); |
|
|
}); |
|
|
} |
|
|
|
|
|
function loadVideo(video, skipAdd) { |
|
|
const dataRow = []; |
|
|
const csvDataRow = []; |
|
|
const publishedAt = moment(shared.idx(["snippet", "publishedAt"], video)); |
|
|
publishedAt.utc(); |
|
|
|
|
|
const tags = shared.idx(["snippet", "tags"], video); |
|
|
(tags || []).forEach(function (tag) { |
|
|
if (!tagsData[tag]) { |
|
|
tagsData[tag] = {} |
|
|
} |
|
|
|
|
|
tagsData[tag].count = ++tagsData[tag].count || 1; |
|
|
|
|
|
if (tagsData[tag].firstUsed) { |
|
|
if (publishedAt.isBefore(tagsData[tag].firstUsed)) { |
|
|
tagsData[tag].firstUsed = publishedAt; |
|
|
tagsData[tag].firstVideo = video.id; |
|
|
} |
|
|
} else { |
|
|
tagsData[tag].firstUsed = publishedAt; |
|
|
tagsData[tag].firstVideo = video.id; |
|
|
} |
|
|
|
|
|
if (tagsData[tag].lastUsed) { |
|
|
if (publishedAt.isAfter(tagsData[tag].lastUsed)) { |
|
|
tagsData[tag].lastUsed = publishedAt; |
|
|
tagsData[tag].lastVideo = video.id; |
|
|
} |
|
|
} else { |
|
|
tagsData[tag].lastUsed = publishedAt; |
|
|
tagsData[tag].lastVideo = video.id; |
|
|
} |
|
|
}); |
|
|
|
|
|
const geotag = shared.idx(["recordingDetails"], video); |
|
|
if (geotag.location) { |
|
|
const latLng = geotag.location.latitude + "," + geotag.location.longitude; |
|
|
const name = geotag.locationDescription; |
|
|
if (geotagsData.hasOwnProperty(latLng)) { |
|
|
const data = geotagsData[latLng]; |
|
|
data.count = data.count + 1; |
|
|
|
|
|
if (data.names.indexOf(name) === -1) { |
|
|
data.names.push(name); |
|
|
} |
|
|
} else { |
|
|
geotagsData[latLng] = { |
|
|
names: [name], |
|
|
count: 1 |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const description = shared.idx(["snippet", "description"], video); |
|
|
if (description) { |
|
|
|
|
|
|
|
|
|
|
|
const URL_XREGEX = XRegExp("(\\p{L}[\\p{L}\\d\\-+.]*)?:\\/\\/[-\\p{L}0-9@:%._+~#=]{1,256}\\.[\\p{L}0-9()]{1,6}\\b([-\\p{L}0-9@:%_!+.~#?&\\/=]*)", "gi"); |
|
|
const matches = XRegExp.match(description, URL_XREGEX); |
|
|
const END_CHARS = /[.()]*$/gi; |
|
|
(matches || []).forEach(function (match) { |
|
|
const link = match.replace(END_CHARS, ""); |
|
|
if (!linksData[link]) { |
|
|
linksData[link] = {} |
|
|
} |
|
|
linksData[link].count = ++linksData[link].count || 1; |
|
|
|
|
|
if (linksData[link].firstUsed) { |
|
|
if (publishedAt.isBefore(linksData[link].firstUsed)) { |
|
|
linksData[link].firstUsed = publishedAt; |
|
|
linksData[link].firstVideo = video.id; |
|
|
} |
|
|
} else { |
|
|
linksData[link].firstUsed = publishedAt; |
|
|
linksData[link].firstVideo = video.id; |
|
|
} |
|
|
|
|
|
if (linksData[link].lastUsed) { |
|
|
if (publishedAt.isAfter(linksData[link].lastUsed)) { |
|
|
linksData[link].lastUsed = publishedAt; |
|
|
linksData[link].lastVideo = video.id; |
|
|
} |
|
|
} else { |
|
|
linksData[link].lastUsed = publishedAt; |
|
|
linksData[link].lastVideo = video.id; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
for (let key in otherData) { |
|
|
otherData[key].check(video); |
|
|
} |
|
|
|
|
|
columns.forEach(function (column) { |
|
|
const value = column._idx ? shared.idx(column._idx, video) : undefined; |
|
|
const displayValue = column.hasOwnProperty("valueMod") ? column.valueMod(value, video) : value; |
|
|
|
|
|
dataRow.push(displayValue); |
|
|
|
|
|
const button = document.querySelector("button[title='" + column.title + "']"); |
|
|
const input = document.querySelector("button[title='" + column.title + "'] input"); |
|
|
if (!$(button).hasClass("active") && input.indeterminate === true && column._visibleIf && column._visibleIf(value)) { |
|
|
button.click(); |
|
|
} |
|
|
|
|
|
if (column.csvSkip) { |
|
|
return; |
|
|
} |
|
|
|
|
|
let csvValue = displayValue && displayValue.hasOwnProperty("display") ? |
|
|
displayValue.display : displayValue; |
|
|
|
|
|
if (csvValue) { |
|
|
csvValue = String(csvValue).replace(/([\r]?\n)/g, "<br>"); |
|
|
} |
|
|
|
|
|
csvDataRow.push(csvValue); |
|
|
}); |
|
|
|
|
|
tableRows.push(csvDataRow.join("\t")); |
|
|
rows.push(dataRow); |
|
|
rawVideoData.push(video); |
|
|
|
|
|
if (skipAdd) { |
|
|
return dataRow; |
|
|
} else { |
|
|
controls.videosTable.row.add(dataRow).draw(false); |
|
|
} |
|
|
} |
|
|
|
|
|
function loadAggregateTables(callback) { |
|
|
const dateFormat = "YYYY-MM-DD"; |
|
|
|
|
|
const tagRows = []; |
|
|
for (let tag in tagsData) { |
|
|
const tagData = tagsData[tag]; |
|
|
tagRows.push([ |
|
|
tag, |
|
|
"<a data-value='" + tagData.firstUsed + "' href='https://youtu.be/" + tagData.firstVideo + "' target='_blank'>" + tagData.firstUsed.format(dateFormat) + "</a>", |
|
|
"<a data-value='" + tagData.lastUsed + "' href='https://youtu.be/" + tagData.lastVideo + "' target='_blank'>" + tagData.lastUsed.format(dateFormat) + "</a>", |
|
|
tagData.count |
|
|
]); |
|
|
} |
|
|
sliceLoad(tagRows, controls.tagsTable); |
|
|
|
|
|
const geotagRows = []; |
|
|
for (let geotag in geotagsData) { |
|
|
geotagRows.push([ |
|
|
"<a href='https://maps.google.com/maps?q=loc:" + geotag + "' target='_blank'>" + geotag + "</a>", |
|
|
"<a href='https://maps.google.com/maps/search/" + encodeURI(geotagsData[geotag].names[0]).replace(/'/g, "%27") + "/@" + geotag + ",14z' target='_blank'>" + geotagsData[geotag].names.join(", ") + "</a>", |
|
|
geotagsData[geotag].count |
|
|
]); |
|
|
} |
|
|
sliceLoad(geotagRows, controls.geotagsTable); |
|
|
|
|
|
const linksRows = []; |
|
|
for (let link in linksData) { |
|
|
const linkData = linksData[link]; |
|
|
linksRows.push([ |
|
|
link, |
|
|
link.startsWith('http') ? "<a href='" + link + "' target='_blank'>" + link + "</a>" : link, |
|
|
"<a data-value='" + linkData.firstUsed + "' href='https://youtu.be/" + linkData.firstVideo + "' target='_blank'>" + linkData.firstUsed.format(dateFormat) + "</a>", |
|
|
"<a data-value='" + linkData.lastUsed + "' href='https://youtu.be/" + linkData.lastVideo + "' target='_blank'>" + linkData.lastUsed.format(dateFormat) + "</a>", |
|
|
linkData.count |
|
|
]); |
|
|
} |
|
|
sliceLoad(linksRows, controls.linksTable); |
|
|
|
|
|
const timezoneOffset = controls.offset.val(); |
|
|
const years = []; |
|
|
const yearCount = {}; |
|
|
rawVideoData.forEach(function (video) { |
|
|
const timestamp = moment(shared.idx(["snippet", "publishedAt"], video)).utcOffset(String(timezoneOffset)); |
|
|
const year = timestamp.format('yyyy'); |
|
|
if (years.indexOf(year) === -1) { |
|
|
years.push(year); |
|
|
yearCount[year] = 1; |
|
|
} else { |
|
|
yearCount[year] = yearCount[year] + 1; |
|
|
} |
|
|
}); |
|
|
years.sort(); |
|
|
years.reverse(); |
|
|
years.forEach(function (year) { |
|
|
controls.year.append("<option value='" + year + "'>" + year + " (" + yearCount[year] + " videos)</option>"); |
|
|
}); |
|
|
loadChartData(timezoneOffset); |
|
|
|
|
|
console.log(unavailableData); |
|
|
const unavailableRows = []; |
|
|
for (let videoId in unavailableData) { |
|
|
const video = unavailableData[videoId]; |
|
|
const filmotTitle = shared.idx(["filmot", "title"], video) || "<span style='color:gray'>No data</span>"; |
|
|
const filmotDesc = shared.idx(["filmot", "description"], video) || ""; |
|
|
const filmotAuthorName = shared.idx(["filmot", "channelname"], video); |
|
|
const filmotAuthor = filmotAuthorName ? |
|
|
"<a target='_blank' href='https://www.youtube.com/channel/" + shared.idx(["filmot", "channelid"], video) + "'>" + shared.idx(["filmot", "channelname"], video) + "</a>" : ""; |
|
|
const filmotUploadDate = shared.idx(["filmot", "uploaddate"], video) || ""; |
|
|
const duration = shared.idx(["filmot", "duration"], video) || -1; |
|
|
const filmotDuration = duration === -1 ? "" : shared.formatDuration(moment.duration({seconds: duration})); |
|
|
unavailableRows.push([ |
|
|
"<a target='_blank' href='./?submit=true&url=https://youtu.be/" + videoId + "'>" + |
|
|
"<img src='./img/metadata.png' style='margin-left:4px;width:24px;' alt='youtube metadata icon' >" + |
|
|
"</a>", |
|
|
"<a target='_blank' href='https://youtu.be/" + videoId + "'>" + videoId + "</a>", |
|
|
String(video.title), |
|
|
"<a target='_blank' href='https://filmot.com/video/" + videoId + "'>Filmot</a> · " + |
|
|
"<a target='_blank' href='https://web.archive.org/web/*/https://www.youtube.com/watch?v=" + videoId + "'>Archive Web</a> · " + |
|
|
"<a target='_blank' href='https://archive.org/details/youtube-" + videoId + "'>Archive Details</a> · " + |
|
|
"<a target='_blank' href='https://web.archive.org/web/2oe_/http://wayback-fakeurl.archive.org/yt/" + videoId + "'>Archive Video</a> · " + |
|
|
"<a target='_blank' href='https://ghostarchive.org/varchive/" + videoId + "'>GhostArchive</a> · " + |
|
|
"<a target='_blank' href='https://www.google.com/search?q=\"" + videoId + "\"'>Google</a>", |
|
|
video.source, |
|
|
filmotTitle, |
|
|
filmotAuthor, |
|
|
filmotUploadDate, |
|
|
{"display": filmotDuration, "num": duration}, |
|
|
filmotDesc |
|
|
]); |
|
|
} |
|
|
sliceLoad(unavailableRows, controls.unavailableTable); |
|
|
|
|
|
unavailableColumns.forEach(function (column) { |
|
|
const button = document.querySelector("button[title='" + column.title + "']"); |
|
|
const input = document.querySelector("button[title='" + column.title + "'] input"); |
|
|
if (!$(button).hasClass("active") && input.indeterminate === true && column._visibleIf && column._visibleIf(value)) { |
|
|
button.click(); |
|
|
} |
|
|
}); |
|
|
|
|
|
controls.unavailableTable.columns.adjust(); |
|
|
|
|
|
console.log(otherData) |
|
|
for (let key in otherData) { |
|
|
const row = otherData[key]; |
|
|
const value = row.value; |
|
|
const displayValue = Number(value) === value ? Number(value).toLocaleString() : value; |
|
|
|
|
|
controls.otherTable.row.add([row.text, displayValue]).draw(false); |
|
|
} |
|
|
|
|
|
if (callback) { |
|
|
callback(); |
|
|
} |
|
|
} |
|
|
|
|
|
function loadChartData(timezoneOffset, yearFilter) { |
|
|
if (rawVideoData.length === 0) { |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log('Loading chart data [offset=' + timezoneOffset + ", yearFilter=" + yearFilter + "]") |
|
|
|
|
|
const days = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday'] |
|
|
const rawChartData = {}; |
|
|
days.forEach(function (dayName) { |
|
|
rawChartData[dayName] = { |
|
|
name: dayName, |
|
|
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
|
|
} |
|
|
}); |
|
|
|
|
|
rawVideoData.forEach(function (video) { |
|
|
const timestamp = moment(shared.idx(["snippet", "publishedAt"], video)).utcOffset(String(timezoneOffset)); |
|
|
const dayName = timestamp.format('dddd'); |
|
|
const hour24 = timestamp.format('H'); |
|
|
const year = timestamp.format('yyyy'); |
|
|
|
|
|
if (!yearFilter || yearFilter === "" || year === yearFilter) { |
|
|
rawChartData[dayName].data[hour24] = rawChartData[dayName].data[hour24] + 1; |
|
|
} |
|
|
}); |
|
|
|
|
|
console.log(rawChartData); |
|
|
|
|
|
const newChartData = []; |
|
|
days.forEach(function (dayName) { |
|
|
for (let weekday in rawChartData) { |
|
|
if (weekday === dayName) { |
|
|
newChartData.push(rawChartData[weekday]); |
|
|
chartData[rawChartData[weekday].name] = rawChartData[weekday].data; |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
controls.uploadFrequency.updateSeries(newChartData); |
|
|
} |
|
|
|
|
|
function sliceLoad(data, table, callback) { |
|
|
function slice(index, size) { |
|
|
const toAdd = data.slice(index, index + size); |
|
|
if (toAdd.length === 0) { |
|
|
if (callback) { |
|
|
callback(); |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
table.rows.add(toAdd).draw(false); |
|
|
table.columns.adjust().draw(false); |
|
|
|
|
|
setTimeout(function () { |
|
|
slice(index + size, size) |
|
|
}, 200); |
|
|
} |
|
|
|
|
|
slice(0, 1000); |
|
|
} |
|
|
|
|
|
const columns = [ |
|
|
{ |
|
|
title: " ", |
|
|
type: "html", |
|
|
visible: true, |
|
|
valueMod: function (value, video) { |
|
|
return "<a target='_blank' href='./?submit=true&url=https://youtu.be/" + video.id + "'>" + |
|
|
"<img src='./img/metadata.png' style='margin-left:4px;width:24px;' alt='youtube metadata icon' >" + |
|
|
"</a>" |
|
|
}, |
|
|
csvSkip: true |
|
|
}, |
|
|
{ |
|
|
title: "Video ID", |
|
|
type: "html", |
|
|
visible: false, |
|
|
_idx: ["id"], |
|
|
valueMod: function (value, video) { |
|
|
return "<a target='_blank' href='https://youtu.be/" + value + "'>" + value + "</a>" |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Title", |
|
|
type: "html", |
|
|
visible: true, |
|
|
_idx: ["snippet", "title"], |
|
|
valueMod: function (value, video) { |
|
|
return "<a target='_blank' href='https://youtu.be/" + video.id + "'>" + value + "</a>" |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Channel ID", |
|
|
type: "html", |
|
|
visible: false, |
|
|
_idx: ["snippet", "channelId"], |
|
|
valueMod: function (value) { |
|
|
return "<a target='_blank' href='https://www.youtube.com/channel/" + value + "'>" + value + "</a>" |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Author", |
|
|
type: "html", |
|
|
visible: true, |
|
|
_idx: ["snippet", "channelTitle"], |
|
|
valueMod: function (value, video) { |
|
|
const channelId = shared.idx(["snippet", "channelId"], video); |
|
|
return "<a target='_blank' href='https://www.youtube.com/channel/" + channelId + "'>" + value + "</a>" |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Description", |
|
|
visible: false, |
|
|
_idx: ["snippet", "description"] |
|
|
}, |
|
|
{ |
|
|
title: "Length", |
|
|
type: "num", |
|
|
visible: true, |
|
|
_idx: ["contentDetails", "duration"], |
|
|
valueMod: function (value) { |
|
|
const duration = moment.duration(value); |
|
|
const length = duration.asMilliseconds(); |
|
|
|
|
|
return length === 0 ? { |
|
|
display: 'livestream', |
|
|
num: length |
|
|
} : { |
|
|
display: shared.formatDuration(duration, false), |
|
|
num: length |
|
|
} |
|
|
}, |
|
|
render: { |
|
|
_: 'display', |
|
|
sort: 'num' |
|
|
}, |
|
|
className: "dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Length (Seconds)", |
|
|
type: "num", |
|
|
visible: false, |
|
|
_idx: ["contentDetails", "duration"], |
|
|
valueMod: function (value) { |
|
|
const duration = moment.duration(value); |
|
|
const length = duration.asMilliseconds(); |
|
|
|
|
|
return length === 0 ? { |
|
|
display: 'livestream', |
|
|
num: length |
|
|
} : { |
|
|
display: length / 1000, |
|
|
num: length |
|
|
} |
|
|
}, |
|
|
render: { |
|
|
_: 'display', |
|
|
sort: 'num' |
|
|
}, |
|
|
className: "dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Published", |
|
|
type: "date", |
|
|
visible: true, |
|
|
_idx: ["snippet", "publishedAt"], |
|
|
className: "dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Language", |
|
|
visible: false, |
|
|
_idx: ["snippet", "defaultLanguage"] |
|
|
}, |
|
|
{ |
|
|
title: "Audio Language", |
|
|
visible: false, |
|
|
_idx: ["snippet", "defaultAudioLanguage"] |
|
|
}, |
|
|
{ |
|
|
title: "Views", |
|
|
type: "num", |
|
|
visible: true, |
|
|
_idx: ["statistics", "viewCount"], |
|
|
valueMod: function (value) { |
|
|
return value ? { |
|
|
display: Number(value).toLocaleString(), |
|
|
num: value |
|
|
} : { |
|
|
display: "disabled", |
|
|
num: -1 |
|
|
}; |
|
|
}, |
|
|
render: { |
|
|
_: 'display', |
|
|
sort: 'num' |
|
|
}, |
|
|
className: "text-right dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Likes", |
|
|
type: "num", |
|
|
visible: true, |
|
|
_idx: ["statistics", "likeCount"], |
|
|
valueMod: function (value) { |
|
|
return value ? { |
|
|
display: Number(value).toLocaleString(), |
|
|
num: value |
|
|
} : { |
|
|
display: "disabled", |
|
|
num: -1 |
|
|
}; |
|
|
}, |
|
|
render: { |
|
|
_: 'display', |
|
|
sort: 'num' |
|
|
}, |
|
|
className: "text-right dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Dislikes", |
|
|
type: "num", |
|
|
visible: false, |
|
|
_visibleIf: function (value) { |
|
|
return value; |
|
|
}, |
|
|
_idx: ["statistics", "dislikeCount"], |
|
|
valueMod: function (value) { |
|
|
return value ? { |
|
|
display: Number(value).toLocaleString(), |
|
|
num: value |
|
|
} : { |
|
|
display: "", |
|
|
num: -1 |
|
|
}; |
|
|
}, |
|
|
render: { |
|
|
_: 'display', |
|
|
sort: 'num' |
|
|
}, |
|
|
className: "text-right dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Like Ratio", |
|
|
type: "num", |
|
|
visible: false, |
|
|
_idx: ["statistics"], |
|
|
valueMod: function (value) { |
|
|
const likes = value.likeCount; |
|
|
const dislikes = value.dislikeCount; |
|
|
|
|
|
if (likes && dislikes) { |
|
|
const ratio = dislikes === 0 ? likes : likes / dislikes; |
|
|
return { |
|
|
display: Number(ratio).toLocaleString(), |
|
|
num: ratio |
|
|
} |
|
|
} else { |
|
|
return { |
|
|
display: "", |
|
|
num: -1 |
|
|
} |
|
|
} |
|
|
}, |
|
|
render: { |
|
|
_: 'display', |
|
|
sort: 'num' |
|
|
}, |
|
|
className: "text-right dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Comments", |
|
|
type: "num", |
|
|
visible: true, |
|
|
_idx: ["statistics", "commentCount"], |
|
|
valueMod: function (value) { |
|
|
return value ? { |
|
|
display: Number(value).toLocaleString(), |
|
|
num: value |
|
|
} : { |
|
|
display: "disabled", |
|
|
num: "-1" |
|
|
}; |
|
|
}, |
|
|
render: { |
|
|
_: 'display', |
|
|
sort: 'num' |
|
|
}, |
|
|
className: "text-right dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Source", |
|
|
type: "html", |
|
|
visible: false, |
|
|
valueMod: function (value, video) { |
|
|
if (availableData.hasOwnProperty(video.id)) { |
|
|
return availableData[video.id].source; |
|
|
} else { |
|
|
return "" |
|
|
} |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Kids", |
|
|
type: "boolean", |
|
|
visible: false, |
|
|
_visibleIf: function (value) { |
|
|
return value; |
|
|
}, |
|
|
_idx: ["status", "madeForKids"], |
|
|
className: "text-center" |
|
|
}, |
|
|
{ |
|
|
title: "Embeddable", |
|
|
type: "boolean", |
|
|
visible: false, |
|
|
_idx: ["status", "embeddable"], |
|
|
className: "text-center" |
|
|
}, |
|
|
{ |
|
|
title: "Licensed Content", |
|
|
type: "boolean", |
|
|
visible: false, |
|
|
_idx: ["contentDetails", "licensedContent"], |
|
|
className: "text-center" |
|
|
}, |
|
|
{ |
|
|
title: "Dimension", |
|
|
visible: false, |
|
|
_idx: ["contentDetails", "dimension"] |
|
|
}, |
|
|
{ |
|
|
title: "Definition", |
|
|
visible: false, |
|
|
_idx: ["contentDetails", "definition"] |
|
|
}, |
|
|
{ |
|
|
title: "Caption", |
|
|
visible: false, |
|
|
_idx: ["contentDetails", "caption"] |
|
|
}, |
|
|
{ |
|
|
title: "Projection", |
|
|
visible: false, |
|
|
_idx: ["contentDetails", "projection"] |
|
|
}, |
|
|
{ |
|
|
title: "License", |
|
|
visible: false, |
|
|
_idx: ["status", "license"] |
|
|
}, |
|
|
{ |
|
|
title: "Privacy Status", |
|
|
visible: false, |
|
|
_visibleIf: function (value) { |
|
|
return value && value !== "public"; |
|
|
}, |
|
|
_idx: ["status", "privacyStatus"] |
|
|
}, |
|
|
{ |
|
|
title: "Content Rating", |
|
|
visible: false, |
|
|
_idx: ["contentDetails", "contentRating"], |
|
|
valueMod: function (value) { |
|
|
if (!$.isEmptyObject(value)) { |
|
|
console.log(value); |
|
|
|
|
|
const pairs = []; |
|
|
Object.keys(value).forEach(function (key) { |
|
|
pairs.push(key + "/" + value[key]); |
|
|
}); |
|
|
|
|
|
return pairs.join(", "); |
|
|
} |
|
|
|
|
|
return ""; |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Region Restriction Count", |
|
|
type: "num", |
|
|
visible: false, |
|
|
indeterminate: true, |
|
|
_visibleIf: function (value) { |
|
|
return !$.isEmptyObject(value) && (value.allowed || value.blocked).length > 0; |
|
|
}, |
|
|
_idx: ["contentDetails", "regionRestriction"], |
|
|
valueMod: function (value) { |
|
|
if (!$.isEmptyObject(value)) { |
|
|
if (value.hasOwnProperty('allowed')) { |
|
|
return { |
|
|
display: value.allowed.length + " (allowed)", |
|
|
num: value.allowed.length |
|
|
}; |
|
|
} else if (value.hasOwnProperty('blocked')) { |
|
|
return { |
|
|
display: value.blocked.length + " (blocked)", |
|
|
num: value.blocked.length |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
return { |
|
|
display: "", |
|
|
num: 0 |
|
|
}; |
|
|
}, |
|
|
render: { |
|
|
_: 'display', |
|
|
sort: 'num' |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Region Restriction", |
|
|
visible: false, |
|
|
_idx: ["contentDetails", "regionRestriction"], |
|
|
valueMod: function (value) { |
|
|
if (!$.isEmptyObject(value)) { |
|
|
console.log(value); |
|
|
|
|
|
if (value.hasOwnProperty('allowed')) { |
|
|
value.allowed.sort(); |
|
|
|
|
|
return "Allowed [" + value.allowed.join(", ") + "]"; |
|
|
} else if (value.hasOwnProperty('blocked')) { |
|
|
value.blocked.sort(); |
|
|
|
|
|
return "Blocked [" + value.blocked.join(", ") + "]"; |
|
|
} |
|
|
} |
|
|
|
|
|
return ""; |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Livestream", |
|
|
type: "num", |
|
|
visible: false, |
|
|
_idx: ["liveStreamingDetails"], |
|
|
valueMod: function (value) { |
|
|
const is = value !== null; |
|
|
|
|
|
if (is) { |
|
|
if (value.hasOwnProperty("actualEndTime")) { |
|
|
return { |
|
|
display: "true (ended)", |
|
|
num: 1 |
|
|
} |
|
|
} else if (value.hasOwnProperty("actualStartTime")) { |
|
|
return { |
|
|
display: "true (on-going)", |
|
|
num: 2 |
|
|
} |
|
|
} |
|
|
|
|
|
return { |
|
|
display: "true (scheduled)", |
|
|
num: 3 |
|
|
} |
|
|
} else { |
|
|
return { |
|
|
display: "", |
|
|
num: 0 |
|
|
} |
|
|
} |
|
|
}, |
|
|
render: { |
|
|
_: 'display', |
|
|
sort: 'num' |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Location Name", |
|
|
type: "html", |
|
|
visible: false, |
|
|
_visibleIf: function (value) { |
|
|
return !$.isEmptyObject(value) && value.locationDescription; |
|
|
}, |
|
|
_idx: ["recordingDetails"], |
|
|
valueMod: function (value) { |
|
|
if ($.isEmptyObject(value) || !value.locationDescription) { |
|
|
return ""; |
|
|
} |
|
|
|
|
|
if (!value.location) { |
|
|
return value.locationDescription |
|
|
} |
|
|
|
|
|
const latlng = value.location.latitude + "," + value.location.longitude; |
|
|
|
|
|
return "<a href='https://maps.google.com/maps/search/" + encodeURI(value.locationDescription).replace(/'/g, "%27") + "/@" + latlng + ",14z' target='_blank'>" + value.locationDescription + "</a>"; |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Location", |
|
|
type: "html", |
|
|
visible: false, |
|
|
_visibleIf: function (value) { |
|
|
|
|
|
|
|
|
}, |
|
|
_idx: ["recordingDetails"], |
|
|
valueMod: function (value) { |
|
|
if ($.isEmptyObject(value) || !value.location) { |
|
|
return ""; |
|
|
} |
|
|
|
|
|
const latlng = value.location.latitude + "," + value.location.longitude; |
|
|
|
|
|
return "<a href='https://maps.google.com/maps?q=loc:" + latlng + "' target='_blank'>" + latlng + "</a>"; |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Recording Date", |
|
|
type: "date", |
|
|
visible: false, |
|
|
_idx: ["recordingDetails", "recordingDate"], |
|
|
className: "dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Tag Count", |
|
|
type: "num", |
|
|
visible: true, |
|
|
_idx: ["snippet", "tags"], |
|
|
valueMod: function (value) { |
|
|
return value ? value.length : 0; |
|
|
}, |
|
|
className: "text-right dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Tags", |
|
|
visible: false, |
|
|
_idx: ["snippet", "tags"], |
|
|
valueMod: function (value) { |
|
|
return value ? value.join(", ") : ""; |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Localization Count", |
|
|
type: "num", |
|
|
visible: false, |
|
|
_idx: ["localizations"], |
|
|
valueMod: function (value) { |
|
|
return value ? Object.keys(value).length : 0; |
|
|
}, |
|
|
className: "text-right dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Localizations", |
|
|
visible: false, |
|
|
_idx: ["localizations"], |
|
|
valueMod: function (value) { |
|
|
return value ? Object.keys(value).sort().join(", ") : ""; |
|
|
} |
|
|
} |
|
|
]; |
|
|
|
|
|
const unavailableColumns = [ |
|
|
{ |
|
|
title: " ", |
|
|
type: "html", |
|
|
visible: true |
|
|
}, |
|
|
{ |
|
|
title: "Video ID", |
|
|
type: "html", |
|
|
visible: true |
|
|
}, |
|
|
{ |
|
|
title: "Status", |
|
|
type: "html", |
|
|
visible: true |
|
|
}, |
|
|
{ |
|
|
title: "Research", |
|
|
type: "html", |
|
|
visible: true |
|
|
}, |
|
|
{ |
|
|
title: "Source", |
|
|
type: "html", |
|
|
visible: true |
|
|
}, |
|
|
{ |
|
|
title: "Title (Filmot)", |
|
|
type: "html", |
|
|
visible: false, |
|
|
indeterminate: true, |
|
|
_idx: ["filmot", "title"], |
|
|
_visibleIf: function (value) { |
|
|
return !$.isEmptyObject(value); |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Author (Filmot)", |
|
|
type: "html", |
|
|
visible: false, |
|
|
indeterminate: true, |
|
|
_idx: ["filmot", "channelname"], |
|
|
_visibleIf: function (value) { |
|
|
return !$.isEmptyObject(value); |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Published (Filmot)", |
|
|
type: "html", |
|
|
visible: false, |
|
|
indeterminate: true, |
|
|
_idx: ["filmot", "uploaddate"], |
|
|
_visibleIf: function (value) { |
|
|
return !$.isEmptyObject(value); |
|
|
} |
|
|
}, |
|
|
{ |
|
|
title: "Length (Filmot)", |
|
|
type: "num", |
|
|
visible: false, |
|
|
indeterminate: true, |
|
|
_idx: ["filmot", "duration"], |
|
|
_visibleIf: function (value) { |
|
|
return !$.isEmptyObject(value); |
|
|
}, |
|
|
render: { |
|
|
_: 'display', |
|
|
sort: 'num' |
|
|
}, |
|
|
className: "dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Description (Filmot)", |
|
|
type: "html", |
|
|
visible: false |
|
|
} |
|
|
]; |
|
|
|
|
|
const csvHeaderRow = []; |
|
|
for (let j = 0; j < columns.length; j++) { |
|
|
const column = columns[j]; |
|
|
if (column.csvSkip) { |
|
|
continue; |
|
|
} |
|
|
csvHeaderRow.push(column.title); |
|
|
} |
|
|
|
|
|
let tableRows = [csvHeaderRow.join("\t")]; |
|
|
let rows = []; |
|
|
let rawVideoData = []; |
|
|
let availableData = {}; |
|
|
let rawChannelMap = {}; |
|
|
let rawPlaylistMap = {}; |
|
|
let playlistMap = {}; |
|
|
let unavailableData = {}; |
|
|
let failedData = {}; |
|
|
let tagsData = {}; |
|
|
let geotagsData = {}; |
|
|
let linksData = {}; |
|
|
let chartData = {}; |
|
|
let otherData = { |
|
|
totalVideos: { |
|
|
text: "Total videos", value: 0, check: function (video) { |
|
|
if (video) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
totalViews: { |
|
|
text: "Total views", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const views = shared.idx(["statistics", "viewCount"], video); |
|
|
this.value = this.value + (views ? Number(views) : 0); |
|
|
} |
|
|
}, |
|
|
totalLikes: { |
|
|
text: "Total likes", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const likes = shared.idx(["statistics", "likeCount"], video); |
|
|
this.value = this.value + (likes ? Number(likes) : 0); |
|
|
} |
|
|
}, |
|
|
totalDislikes: { |
|
|
text: "Total dislikes", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const dislikes = shared.idx(["statistics", "dislikeCount"], video); |
|
|
this.value = this.value + (dislikes ? Number(dislikes) : 0); |
|
|
} |
|
|
}, |
|
|
totalComments: { |
|
|
text: "Total comments", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const comments = shared.idx(["statistics", "commentCount"], video); |
|
|
this.value = this.value + (comments ? Number(comments) : 0); |
|
|
} |
|
|
}, |
|
|
totalLength: { |
|
|
text: "Total video length", |
|
|
value: '', |
|
|
reset: function () { |
|
|
this.totalSeconds = 0; |
|
|
}, |
|
|
totalSeconds: 0, |
|
|
check: function (video) { |
|
|
const duration = moment.duration(shared.idx(["contentDetails", "duration"], video)); |
|
|
this.totalSeconds = this.totalSeconds + duration.asSeconds(); |
|
|
this.value = shared.formatDuration(moment.duration({seconds: this.totalSeconds}), false); |
|
|
} |
|
|
}, |
|
|
averageLength: { |
|
|
text: "Average video length", |
|
|
value: '', |
|
|
check: function (video) { |
|
|
const seconds = otherData.totalLength.totalSeconds / otherData.totalVideos.value; |
|
|
this.value = shared.formatDuration(moment.duration({seconds: seconds}), false); |
|
|
} |
|
|
}, |
|
|
withGeolocation: { |
|
|
text: "Videos with geolocation", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["recordingDetails", "location", "latitude"], video); |
|
|
if (stat) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
withRecordingDate: { |
|
|
text: "Videos with recordingDate", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["recordingDetails", "recordingDate"], video); |
|
|
if (stat) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
withTags: { |
|
|
text: "Videos with tags", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["snippet", "tags"], video); |
|
|
if (stat) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
withLanguage: { |
|
|
text: "Videos with language", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["snippet", "defaultLanguage"], video); |
|
|
if (stat) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
withAudioLanguage: { |
|
|
text: "Videos with audio language", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["snippet", "defaultAudioLanguage"], video); |
|
|
if (stat) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
withLocalizations: { |
|
|
text: "Videos with localizations", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["localizations"], video); |
|
|
if (stat) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
withContentRatings: { |
|
|
text: "Videos with content rating(s)", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["contentDetails", "contentRating"], video); |
|
|
if (!$.isEmptyObject(stat)) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
withRegionRestrictions: { |
|
|
text: "Videos with region restriction(s)", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["contentDetails", "regionRestriction"], video); |
|
|
if (!$.isEmptyObject(stat)) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
withCaptions: { |
|
|
text: "Videos with captions", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["contentDetails", "caption"], video); |
|
|
if (stat === "true") { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
withCommentsDisabled: { |
|
|
text: "Videos with comments disabled", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["statistics", "commentCount"], video); |
|
|
if (!stat) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
withLikesDisabled: { |
|
|
text: "Videos with likes disabled", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["statistics", "likeCount"], video); |
|
|
if (!stat) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
withViewsDisabled: { |
|
|
text: "Videos with views disabled", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["statistics", "viewCount"], video); |
|
|
if (!stat) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
consideredLivestreams: { |
|
|
text: "Videos considered livestreams", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["liveStreamingDetails"], video); |
|
|
if (stat !== null) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
isCreativeCommons: { |
|
|
text: "Videos with license=creativeCommon", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["status", "license"], video); |
|
|
if (stat === "creativeCommon") { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
isUnlisted: { |
|
|
text: "Videos with privacyStatus=unlisted", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["status", "privacyStatus"], video); |
|
|
if (stat === "unlisted") { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
isMadeForKids: { |
|
|
text: "Videos with madeForKids=true", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["status", "madeForKids"], video); |
|
|
if (stat === true) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
isNotEmbeddable: { |
|
|
text: "Videos with embeddable=false", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["status", "embeddable"], video); |
|
|
if (stat === false) { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
is3d: { |
|
|
text: "Videos with dimension=3d", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["contentDetails", "dimension"], video); |
|
|
if (stat === "3d") { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
}, |
|
|
is360: { |
|
|
text: "Videos with projection=360", |
|
|
value: 0, |
|
|
check: function (video) { |
|
|
const stat = shared.idx(["contentDetails", "projection"], video); |
|
|
if (stat === "360") { |
|
|
this.value = this.value + 1; |
|
|
} |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
const columnOptionsHtml = []; |
|
|
for (let i = 0; i < columns.length; i++) { |
|
|
const column = columns[i]; |
|
|
|
|
|
columnOptionsHtml.push("<option value='" + i + "'" + (column.visible ? " selected" : "") + " title='" + column.title + "'>" + |
|
|
column.title + |
|
|
"</option>") |
|
|
} |
|
|
|
|
|
const internal = { |
|
|
init: function () { |
|
|
controls.darkMode = $("#darkMode"); |
|
|
controls.inputValue = $("#value"); |
|
|
controls.inputValue.val(shared.randomFromList(EXAMPLE_BULK)); |
|
|
controls.btnSubmit = $("#submit"); |
|
|
controls.shareLink = $("#shareLink"); |
|
|
controls.videosTable = $('#videosTable').DataTable({ |
|
|
columns: columns, |
|
|
columnDefs: [{ |
|
|
"defaultContent": "", |
|
|
"targets": "_all" |
|
|
}], |
|
|
order: [[8, 'desc']], |
|
|
lengthMenu: [[10, 25, 50, 100, 250, -1], [10, 25, 50, 100, 250, "All"]], |
|
|
deferRender: true, |
|
|
bDeferRender: true, |
|
|
|
|
|
}); |
|
|
controls.columnOptions = $("#column-options"); |
|
|
controls.columnOptions.html(columnOptionsHtml.join("")); |
|
|
controls.columnOptions.multiselect({ |
|
|
buttonClass: 'form-select', |
|
|
templates: { |
|
|
button: '<button type="button" class="multiselect dropdown-toggle" data-bs-toggle="dropdown"><span class="multiselect-selected-text"></span></button>', |
|
|
}, |
|
|
onChange: function (option, checked, select) { |
|
|
external.toggleResultsColumn($(option).val()) |
|
|
} |
|
|
}); |
|
|
columns.forEach(function (column) { |
|
|
if (column.hasOwnProperty("_visibleIf")) { |
|
|
document.querySelector("button[title='" + column.title + "'] input").indeterminate = true; |
|
|
} |
|
|
}) |
|
|
controls.progress = $("#progressBar"); |
|
|
elements.progressText = $("#progressText") |
|
|
controls.progress.progressData = { |
|
|
min: 0, |
|
|
value: 0, |
|
|
max: 100 |
|
|
} |
|
|
controls.progress.update = function (options) { |
|
|
console.log(options) |
|
|
if (String(options["reset"]).toLowerCase() === "true") { |
|
|
console.log('reset') |
|
|
this.update({ |
|
|
min: 0, |
|
|
value: 0, |
|
|
max: 100, |
|
|
text: "", |
|
|
subtext: 'Idle' |
|
|
}); |
|
|
return; |
|
|
} |
|
|
if (options.hasOwnProperty("subtext")) { |
|
|
elements.progressText.text(options.subtext); |
|
|
} |
|
|
if (options.hasOwnProperty("text")) { |
|
|
this.find('.label').text(options.text); |
|
|
} |
|
|
if (options.hasOwnProperty("min")) { |
|
|
this.progressData.min = options.min; |
|
|
} |
|
|
if (options.hasOwnProperty("value")) { |
|
|
this.progressData.value = options.value; |
|
|
} |
|
|
if (options.hasOwnProperty("max")) { |
|
|
this.progressData.max = options.max; |
|
|
} |
|
|
|
|
|
const data = this.progressData; |
|
|
const percent = 100 * ((data.value - data.min) / (data.max - data.min)); |
|
|
this.css('width', percent + "%"); |
|
|
} |
|
|
controls.unavailableProgress = $("#unavailableProgressBar"); |
|
|
elements.unavailableProgressText = $("#unavailableProgressText") |
|
|
controls.unavailableProgress.progressData = { |
|
|
min: 0, |
|
|
value: 0, |
|
|
max: 100 |
|
|
} |
|
|
controls.unavailableProgress.update = function (options) { |
|
|
console.log(options) |
|
|
if (String(options["reset"]).toLowerCase() === "true") { |
|
|
console.log('reset') |
|
|
this.update({ |
|
|
min: 0, |
|
|
value: 0, |
|
|
max: 100, |
|
|
text: "", |
|
|
subtext: 'Idle' |
|
|
}); |
|
|
return; |
|
|
} |
|
|
if (options.hasOwnProperty("subtext")) { |
|
|
elements.unavailableProgressText.text(options.subtext); |
|
|
} |
|
|
if (options.hasOwnProperty("text")) { |
|
|
this.find('.label').text(options.text); |
|
|
} |
|
|
if (options.hasOwnProperty("min")) { |
|
|
this.progressData.min = options.min; |
|
|
} |
|
|
if (options.hasOwnProperty("value")) { |
|
|
this.progressData.value = options.value; |
|
|
} |
|
|
if (options.hasOwnProperty("max")) { |
|
|
this.progressData.max = options.max; |
|
|
} |
|
|
|
|
|
const data = this.progressData; |
|
|
const percent = 100 * ((data.value - data.min) / (data.max - data.min)); |
|
|
this.css('width', percent + "%"); |
|
|
} |
|
|
controls.createdPlaylists = $("#createdPlaylists"); |
|
|
controls.includeThumbs = $("#includeThumbs"); |
|
|
elements.thumbProgress = $("#thumbProgress"); |
|
|
|
|
|
controls.tagsTable = $("#tagsTable").DataTable({ |
|
|
columns: [ |
|
|
{title: "Tag"}, |
|
|
{ |
|
|
title: "First used", |
|
|
type: 'datetime', |
|
|
className: "dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Last used", |
|
|
type: 'datetime', |
|
|
className: "dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Count", |
|
|
type: "num", |
|
|
className: "text-right dt-nowrap" |
|
|
} |
|
|
], |
|
|
columnDefs: [{ |
|
|
"defaultContent": "", |
|
|
"targets": "_all" |
|
|
}, { |
|
|
"width": "100%", |
|
|
"targets": 0 |
|
|
}], |
|
|
order: [[3, 'desc'], [0, 'asc']], |
|
|
lengthMenu: [[10, 25, 50, 100, 250, -1], [10, 25, 50, 100, 250, "All"]], |
|
|
deferRender: true, |
|
|
bDeferRender: true |
|
|
}); |
|
|
controls.geotagsTable = $("#geotagsTable").DataTable({ |
|
|
columns: [ |
|
|
{title: "Coords"}, |
|
|
{title: "Name(s)"}, |
|
|
{ |
|
|
title: "Count", |
|
|
type: "num", |
|
|
className: "text-right dt-nowrap" |
|
|
} |
|
|
], |
|
|
columnDefs: [{ |
|
|
"defaultContent": "", |
|
|
"targets": "_all" |
|
|
}], |
|
|
order: [[2, 'desc'], [0, 'asc']], |
|
|
lengthMenu: [[10, 25, 50, 100, 250, -1], [10, 25, 50, 100, 250, "All"]], |
|
|
deferRender: true, |
|
|
bDeferRender: true |
|
|
}); |
|
|
controls.linksTable = $("#linksTable").DataTable({ |
|
|
columns: [ |
|
|
{ |
|
|
title: "Link", |
|
|
visible: true |
|
|
}, |
|
|
{ |
|
|
title: "Link (hyper)", |
|
|
visible: false, |
|
|
}, |
|
|
{ |
|
|
title: "First used", |
|
|
type: 'datetime', |
|
|
className: "dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Last used", |
|
|
type: 'datetime', |
|
|
className: "dt-nowrap" |
|
|
}, |
|
|
{ |
|
|
title: "Count", |
|
|
type: "num", |
|
|
className: "text-right dt-nowrap" |
|
|
} |
|
|
], |
|
|
columnDefs: [{ |
|
|
"defaultContent": "", |
|
|
"targets": "_all" |
|
|
}, { |
|
|
"width": "100%", |
|
|
"className": "wrap", |
|
|
"targets": [0, 1] |
|
|
}], |
|
|
order: [[4, 'desc'], [0, 'asc']], |
|
|
lengthMenu: [[10, 25, 50, 100, 250, -1], [10, 25, 50, 100, 250, "All"]], |
|
|
deferRender: true, |
|
|
bDeferRender: true |
|
|
}); |
|
|
controls.offset = $("#offset"); |
|
|
controls.offset.on('change', function () { |
|
|
loadChartData(controls.offset.val(), controls.year.val()); |
|
|
}); |
|
|
controls.year = $("#year"); |
|
|
controls.year.on('change', function () { |
|
|
loadChartData(controls.offset.val(), controls.year.val()); |
|
|
}); |
|
|
const options = { |
|
|
series: [ |
|
|
{ |
|
|
name: 'Saturday', |
|
|
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
|
|
}, |
|
|
{ |
|
|
name: 'Friday', |
|
|
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
|
|
}, |
|
|
{ |
|
|
name: 'Thursday', |
|
|
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
|
|
}, |
|
|
{ |
|
|
name: 'Wednesday', |
|
|
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
|
|
}, |
|
|
{ |
|
|
name: 'Tuesday', |
|
|
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
|
|
}, |
|
|
{ |
|
|
name: 'Monday', |
|
|
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
|
|
}, |
|
|
{ |
|
|
name: 'Sunday', |
|
|
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
|
|
} |
|
|
], |
|
|
xaxis: { |
|
|
categories: [ |
|
|
"12AM", "1AM", "2AM", "3AM", "4AM", "5AM", "6AM", "7AM", "8AM", "9AM", "10AM", "11AM", |
|
|
"12PM", "1PM", "2PM", "3PM", "4PM", "5PM", "6PM", "7PM", "8PM", "9PM", "10PM", "11PM" |
|
|
] |
|
|
}, |
|
|
chart: { |
|
|
height: 350, |
|
|
type: 'heatmap', |
|
|
}, |
|
|
dataLabels: { |
|
|
enabled: false |
|
|
}, |
|
|
colors: ["#008FFB"], |
|
|
title: { |
|
|
text: 'Day and Time Frequency' |
|
|
}, |
|
|
stroke: { |
|
|
show: false |
|
|
}, |
|
|
grid: { |
|
|
show: false |
|
|
} |
|
|
}; |
|
|
controls.uploadFrequency = new ApexCharts(document.querySelector("#uploadFrequency"), options); |
|
|
controls.uploadFrequency.render(); |
|
|
controls.checkUnavailable = $("#checkUnavailable"); |
|
|
|
|
|
const unavailableColumnsHtml = []; |
|
|
for (let i = 0; i < unavailableColumns.length; i++) { |
|
|
const column = unavailableColumns[i]; |
|
|
|
|
|
unavailableColumnsHtml.push("<option value='" + i + "'" + (column.visible ? " selected" : "") + ">" + |
|
|
column.title + |
|
|
"</option>") |
|
|
} |
|
|
controls.unavailableTable = $("#unavailableTable").DataTable({ |
|
|
columns: unavailableColumns, |
|
|
columnDefs: [{ |
|
|
"defaultContent": "", |
|
|
"targets": "_all" |
|
|
}], |
|
|
order: [[7, 'desc'], [4, 'asc']], |
|
|
lengthMenu: [[10, 25, 50, 100, 250, -1], [10, 25, 50, 100, 250, "All"]], |
|
|
deferRender: true, |
|
|
bDeferRender: true |
|
|
}); |
|
|
controls.unavailableColumns = $("#unavailable-columns"); |
|
|
controls.unavailableColumns.html(unavailableColumnsHtml.join("")); |
|
|
controls.unavailableColumns.multiselect({ |
|
|
buttonClass: 'form-select', |
|
|
templates: { |
|
|
button: '<button type="button" class="multiselect dropdown-toggle" data-bs-toggle="dropdown"><span class="multiselect-selected-text"></span></button>', |
|
|
}, |
|
|
onChange: function (option, checked, select) { |
|
|
external.toggleUnavailableColumn($(option).val()) |
|
|
} |
|
|
}); |
|
|
unavailableColumns.forEach(function (column) { |
|
|
if (column.hasOwnProperty("_visibleIf")) { |
|
|
document.querySelector("button[title='" + column.title + "'] input").indeterminate = true; |
|
|
} |
|
|
}) |
|
|
|
|
|
controls.otherTable = $("#otherTable").DataTable({ |
|
|
columns: [ |
|
|
{title: "Statistic"}, |
|
|
{ |
|
|
title: "Value", |
|
|
className: "text-right dt-nowrap" |
|
|
} |
|
|
], |
|
|
columnDefs: [{ |
|
|
"defaultContent": "", |
|
|
"targets": "_all" |
|
|
}], |
|
|
order: [], |
|
|
lengthMenu: [[10, 25, 50, 100, 250, -1], [10, 25, 50, 100, 250, "All"]], |
|
|
deferRender: true, |
|
|
bDeferRender: true, |
|
|
pageLength: -1 |
|
|
}); |
|
|
|
|
|
controls.btnExport = $("#export"); |
|
|
controls.btnImport = $("#import"); |
|
|
controls.importFileChooser = $("#importFileChooser"); |
|
|
|
|
|
new ClipboardJS(".clipboard"); |
|
|
|
|
|
internal.buildPage(true); |
|
|
}, |
|
|
buildPage: function (doSetup) { |
|
|
if (doSetup) { |
|
|
internal.setupControls(); |
|
|
} |
|
|
}, |
|
|
setupControls: function () { |
|
|
function checkTheme() { |
|
|
if (DarkMode.getColorScheme() === "dark") { |
|
|
controls.uploadFrequency.updateOptions({ |
|
|
theme: { |
|
|
mode: 'dark' |
|
|
} |
|
|
}); |
|
|
} else { |
|
|
controls.uploadFrequency.updateOptions({ |
|
|
theme: { |
|
|
mode: 'light' |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
controls.darkMode.change(function () { |
|
|
checkTheme(); |
|
|
}); |
|
|
checkTheme(); |
|
|
|
|
|
controls.progress.update({reset: true}); |
|
|
|
|
|
controls.inputValue.on('keypress', function (e) { |
|
|
if (e.originalEvent.code === "Enter") { |
|
|
controls.btnSubmit.click(); |
|
|
} |
|
|
}); |
|
|
|
|
|
countdownCheck(delaySubmitKey, controls.btnSubmit, delay15SecMs, "submit"); |
|
|
|
|
|
controls.btnSubmit.on('click', function () { |
|
|
if (!can.submit) { |
|
|
return; |
|
|
} |
|
|
localStorage.setItem(delaySubmitKey, new Date()); |
|
|
countdownCheck(delaySubmitKey, controls.btnSubmit, delay15SecMs, "submit"); |
|
|
|
|
|
internal.reset(); |
|
|
|
|
|
const value = controls.inputValue.val(); |
|
|
|
|
|
const parsed = []; |
|
|
value.split(/[,\s]+/g).forEach(function (valuePart) { |
|
|
parsed.push(shared.determineInput(valuePart.trim())); |
|
|
}); |
|
|
if (parsed.length === 0) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const checkCreatedPlaylists = controls.createdPlaylists.is(":checked"); |
|
|
const optionalCreatedPlaylists = checkCreatedPlaylists ? "&createdPlaylists=true" : ""; |
|
|
const minifiedInput = []; |
|
|
parsed.forEach(function (input) { |
|
|
if (input.type === "video_id" || input.type === "playlist_id" || input.type === "channel_id") { |
|
|
minifiedInput.push(input.original); |
|
|
} else { |
|
|
minifiedInput.push(input.original); |
|
|
} |
|
|
}); |
|
|
controls.shareLink.val(location.origin + location.pathname + "?url=" + encodeURIComponent(minifiedInput.join(",")) + optionalCreatedPlaylists + "&submit=true"); |
|
|
controls.shareLink.attr("disabled", false); |
|
|
|
|
|
controls.progress.update({ |
|
|
text: 'Indeterminate', |
|
|
value: 100, |
|
|
max: 100 |
|
|
}); |
|
|
|
|
|
processFromParsed(parsed); |
|
|
}); |
|
|
|
|
|
controls.checkUnavailable.on('click', function () { |
|
|
controls.checkUnavailable.addClass("disabled"); |
|
|
|
|
|
if (Object.keys(unavailableData).length <= 0) { |
|
|
return; |
|
|
} |
|
|
|
|
|
controls.unavailableProgress.update({ |
|
|
text: '0 / ' + Object.keys(unavailableData).length, |
|
|
subtext: 'Processing unavailable ids', |
|
|
value: 0, |
|
|
max: Object.keys(unavailableData).length |
|
|
}); |
|
|
|
|
|
handleUnavailableVideos().then(function () { |
|
|
controls.unavailableTable.rows().every(function (rowIdx, tableLoop, rowLoop) { |
|
|
const data = this.data(); |
|
|
|
|
|
const videoId = $(data[1]).text(); |
|
|
const video = unavailableData[videoId]; |
|
|
|
|
|
const filmotTitle = shared.idx(["filmot", "title"], video) || "<span style='color:gray'>No data</span>"; |
|
|
const filmotDesc = shared.idx(["filmot", "description"], video) || ""; |
|
|
const filmotAuthorName = shared.idx(["filmot", "channelname"], video); |
|
|
const filmotAuthor = filmotAuthorName ? |
|
|
"<a target='_blank' href='https://www.youtube.com/channel/" + shared.idx(["filmot", "channelid"], video) + "'>" + shared.idx(["filmot", "channelname"], video) + "</a>" : ""; |
|
|
const filmotUploadDate = shared.idx(["filmot", "uploaddate"], video) || ""; |
|
|
const duration = shared.idx(["filmot", "duration"], video) || -1; |
|
|
const filmotDuration = duration === -1 ? "" : shared.formatDuration(moment.duration({seconds: duration})); |
|
|
data[5] = filmotTitle; |
|
|
data[6] = filmotAuthor; |
|
|
data[7] = filmotUploadDate; |
|
|
data[8] = {"display": filmotDuration, "num": duration}; |
|
|
data[9] = filmotDesc; |
|
|
|
|
|
this.data(data).draw(); |
|
|
}); |
|
|
|
|
|
controls.unavailableTable.columns.adjust(); |
|
|
|
|
|
controls.unavailableProgress.update({ |
|
|
text: unavailableProgressMessage(), |
|
|
subtext: 'Done' |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
function getImageBinaryCorsProxy(fileName, imageUrl, zip, delay, imageStatuses) { |
|
|
return new Promise(function (resolve) { |
|
|
setTimeout(function () { |
|
|
|
|
|
|
|
|
console.log('Attempting to download image over CORS proxy (' + delay + ' ms start delay): ' + imageUrl); |
|
|
const start = new Date(); |
|
|
JSZipUtils.getBinaryContent("https://cors-proxy-mw324.herokuapp.com/" + imageUrl, function (err, data) { |
|
|
const ms = new Date() - start; |
|
|
|
|
|
if (err) { |
|
|
console.log('Failed ' + fileName + " (" + ms + "ms)"); |
|
|
console.warn("Could not get image: " + imageUrl) |
|
|
console.warn(err); |
|
|
imageStatuses[false] = imageStatuses[false] + 1 || 1; |
|
|
} else { |
|
|
console.log('Retrieved ' + fileName + " (" + ms + "ms)"); |
|
|
console.log("Creating " + fileName + "..."); |
|
|
zip.folder('thumbs').file(fileName, data, {binary: true}); |
|
|
imageStatuses[true] = imageStatuses[true] + 1 || 1; |
|
|
} |
|
|
|
|
|
elements.thumbProgress.text("(" + imageStatuses[true] + " downloaded / " + imageStatuses[false] + " failed)"); |
|
|
|
|
|
resolve(); |
|
|
}); |
|
|
}, delay); |
|
|
}); |
|
|
} |
|
|
|
|
|
controls.btnExport.on('click', function () { |
|
|
const includeThumbs = controls.includeThumbs.is(":checked"); |
|
|
const dateFormat = "YYYY-MM-DD"; |
|
|
controls.btnExport.addClass("loading").addClass("disabled"); |
|
|
|
|
|
const zip = new JSZip(); |
|
|
console.log("Creating about.txt...") |
|
|
zip.file("about.txt", |
|
|
"Downloaded by YouTube Metadata " + new Date().toLocaleString() + "\n\n" + |
|
|
"URL: " + window.location + "\n\n" + |
|
|
"Input: " + controls.inputValue.val() + "\n\n" + |
|
|
"Created Playlists: " + controls.createdPlaylists.is(":checked") |
|
|
); |
|
|
|
|
|
console.log("Creating videos.json...") |
|
|
zip.file("videos.json", JSON.stringify(rawVideoData)); |
|
|
|
|
|
console.log("Creating available.json...") |
|
|
zip.file("available.json", JSON.stringify(availableData)); |
|
|
|
|
|
console.log("Creating channels.json...") |
|
|
const rawChannelData = []; |
|
|
for (let id in rawChannelMap) { |
|
|
rawChannelData.push(rawChannelMap[id]); |
|
|
} |
|
|
zip.file("channels.json", JSON.stringify(rawChannelData)); |
|
|
|
|
|
console.log("Creating playlists.json...") |
|
|
const rawPlaylistData = []; |
|
|
for (let id in rawPlaylistMap) { |
|
|
rawPlaylistData.push(rawPlaylistMap[id]); |
|
|
} |
|
|
zip.file("playlists.json", JSON.stringify(rawPlaylistData)); |
|
|
|
|
|
console.log("Creating unavailable.json...") |
|
|
zip.file("unavailable.json", JSON.stringify(unavailableData)); |
|
|
|
|
|
console.log("Creating videos.csv...") |
|
|
zip.file("videos.csv", tableRows.join("\r\n")); |
|
|
|
|
|
console.log("Creating tags.csv...") |
|
|
const tagCsvRows = ["Tag\tCount\tFirst used\tFirst video\tLast used\tLast video"]; |
|
|
for (let tag in tagsData) { |
|
|
const tagData = tagsData[tag]; |
|
|
tagCsvRows.push(tag + "\t" + tagData.count + "\t" + tagData.firstUsed.format(dateFormat) + "\t" + tagData.firstVideo + "\t" + tagData.lastUsed.format(dateFormat) + "\t" + tagData.lastVideo); |
|
|
} |
|
|
zip.file("tags.csv", tagCsvRows.join("\r\n")); |
|
|
|
|
|
console.log("Creating geotags.csv...") |
|
|
const geotagCsvRows = ["Coords\tName(s)\tCount"]; |
|
|
for (let geotag in geotagsData) { |
|
|
const tag = geotagsData[geotag]; |
|
|
geotagCsvRows.push(geotag + "\t" + tag.names.join(", ") + "\t" + tag.count); |
|
|
} |
|
|
zip.file("geotags.csv", geotagCsvRows.join("\r\n")); |
|
|
|
|
|
console.log("Creating links.csv...") |
|
|
const linkCsvRows = ["Link\tCount\tFirst used\tFirst video\tLast used\tLast video"]; |
|
|
for (let link in linksData) { |
|
|
const linkData = linksData[link]; |
|
|
linkCsvRows.push(link + "\t" + linkData.count + "\t" + linkData.firstUsed.format(dateFormat) + "\t" + linkData.firstVideo + "\t" + linkData.lastUsed.format(dateFormat) + "\t" + linkData.lastVideo); |
|
|
} |
|
|
zip.file("links.csv", linkCsvRows.join("\r\n")); |
|
|
|
|
|
console.log("Creating frequency.csv...") |
|
|
const frequencyCsvRows = ["Weekday\t12AM\t1AM\t2AM\t3AM\t4AM\t5AM\t6AM\t7AM\t8AM\t9AM\t10AM\t11AM\t12PM\t1PM\t2PM\t3PM\t4PM\t5PM\t6PM\t7PM\t8PM\t9PM\t10PM\t11PM"]; |
|
|
for (let row in chartData) { |
|
|
const data = chartData[row]; |
|
|
frequencyCsvRows.push(row + "\t" + data.join("\t")); |
|
|
} |
|
|
zip.file("frequency.csv", frequencyCsvRows.join("\r\n")); |
|
|
|
|
|
console.log("Creating other.csv...") |
|
|
const otherCsvRows = ["Statistic\tValue"]; |
|
|
for (let key in otherData) { |
|
|
const row = otherData[key]; |
|
|
const statistic = row.text; |
|
|
const rowValue = row.value; |
|
|
const displayValue = Number(rowValue) === rowValue ? Number(rowValue).toLocaleString() : rowValue; |
|
|
otherCsvRows.push(statistic + "\t" + displayValue); |
|
|
} |
|
|
zip.file("other.csv", otherCsvRows.join("\r\n")); |
|
|
|
|
|
const optionalImages = []; |
|
|
const imageStatuses = {true: 0, false: 0}; |
|
|
if (includeThumbs) { |
|
|
let delay = 0; |
|
|
for (let i = 0; i < rawVideoData.length; i++) { |
|
|
const video = rawVideoData[i]; |
|
|
const fileName = video.id + ".png"; |
|
|
const thumbs = shared.idx(["snippet", "thumbnails"], video) || {}; |
|
|
const thumbUrl = (thumbs.maxres || thumbs.high || thumbs.medium || thumbs.default || {url: null}).url; |
|
|
if (thumbUrl) { |
|
|
optionalImages.push(getImageBinaryCorsProxy(fileName, thumbUrl, zip, delay * 100, imageStatuses)); |
|
|
} |
|
|
delay++; |
|
|
} |
|
|
for (let id in rawPlaylistMap) { |
|
|
console.log(id); |
|
|
const playlist = rawPlaylistMap[id]; |
|
|
const fileName = id + ".png"; |
|
|
const thumbs = shared.idx(["snippet", "thumbnails"], playlist) || {}; |
|
|
const thumbUrl = (thumbs.maxres || thumbs.high || thumbs.medium || thumbs.default || {url: null}).url; |
|
|
if (thumbUrl) { |
|
|
optionalImages.push(getImageBinaryCorsProxy(fileName, thumbUrl, zip, delay * 100, imageStatuses)); |
|
|
} |
|
|
delay++; |
|
|
} |
|
|
for (let id in rawChannelMap) { |
|
|
console.log(id); |
|
|
const channel = rawChannelMap[id]; |
|
|
const fileName = id + ".png"; |
|
|
const thumbs = shared.idx(["snippet", "thumbnails"], channel) || {}; |
|
|
const thumbUrl = (thumbs.maxres || thumbs.high || thumbs.medium || thumbs.default || {url: null}).url; |
|
|
if (thumbUrl) { |
|
|
optionalImages.push(getImageBinaryCorsProxy(fileName, thumbUrl, zip, delay * 100, imageStatuses)); |
|
|
} |
|
|
delay++; |
|
|
const bannerFileName = id + "-banner.png"; |
|
|
const bannerUrl = shared.idx(["brandingSettings", "image", "bannerExternalUrl"], channel); |
|
|
if (bannerUrl) { |
|
|
optionalImages.push(getImageBinaryCorsProxy(bannerFileName, bannerUrl, zip, delay * 100, imageStatuses)); |
|
|
} |
|
|
delay++; |
|
|
} |
|
|
} |
|
|
|
|
|
Promise.all(optionalImages).then(function () { |
|
|
const channelTitles = []; |
|
|
rawVideoData.forEach(function (video) { |
|
|
const channelTitle = shared.idx(["snippet", "channelTitle"], video) |
|
|
if (channelTitles.indexOf(channelTitle) === -1) { |
|
|
channelTitles.push(channelTitle); |
|
|
} |
|
|
}); |
|
|
|
|
|
console.log(channelTitles) |
|
|
let hint = ''; |
|
|
if (channelTitles.length === 0) { |
|
|
hint = ' (none)' |
|
|
} else if (channelTitles.length === 1) { |
|
|
hint = ' (' + channelTitles[0].substr(0, 15) + ")" |
|
|
} else { |
|
|
hint = ' (' + channelTitles[0].substr(0, 15) + " and " + (channelTitles.length - 1) + " others)" |
|
|
} |
|
|
|
|
|
const fileName = shared.safeFileName("bulk_metadata" + hint + ".zip"); |
|
|
|
|
|
console.log("Saving as " + fileName); |
|
|
zip.generateAsync({ |
|
|
type: "blob", |
|
|
compression: "DEFLATE", |
|
|
compressionOptions: { |
|
|
level: 9 |
|
|
} |
|
|
}).then(function (content) { |
|
|
saveAs(content, fileName); |
|
|
|
|
|
controls.btnExport.removeClass("loading").removeClass("disabled"); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener("dragover", function (event) { |
|
|
event.preventDefault(); |
|
|
}); |
|
|
document.documentElement.addEventListener('drop', async function (e) { |
|
|
e.stopPropagation(); |
|
|
e.preventDefault(); |
|
|
|
|
|
let file = e.dataTransfer.files[0]; |
|
|
if (file) { |
|
|
controls.inputValue.val(file.name); |
|
|
} else { |
|
|
return; |
|
|
} |
|
|
|
|
|
importFile(file); |
|
|
}); |
|
|
|
|
|
controls.importFileChooser.on('change', function (event) { |
|
|
console.log(event); |
|
|
|
|
|
let file = event.target.files[0]; |
|
|
if (file) { |
|
|
controls.inputValue.val(file.name); |
|
|
} else { |
|
|
return; |
|
|
} |
|
|
|
|
|
importFile(file); |
|
|
}); |
|
|
|
|
|
function importFile(file) { |
|
|
console.log("Importing from file " + file.name); |
|
|
|
|
|
controls.btnImport.addClass("loading").addClass("disabled"); |
|
|
|
|
|
internal.reset(); |
|
|
controls.progress.update({ |
|
|
text: '', |
|
|
subtext: 'Importing file', |
|
|
value: 2, |
|
|
max: 5 |
|
|
}); |
|
|
|
|
|
function loadZipFile(fileName, process, onfail) { |
|
|
return new Promise(function (resolve, reject) { |
|
|
console.log('loading ' + fileName); |
|
|
|
|
|
JSZip.loadAsync(file).then(function (content) { |
|
|
|
|
|
return content.file(fileName).async("string"); |
|
|
}).then(function (text) { |
|
|
process(text); |
|
|
|
|
|
resolve(); |
|
|
}).catch(function (err) { |
|
|
console.warn(err); |
|
|
console.warn(fileName + ' not in imported file'); |
|
|
if (onfail) { |
|
|
onfail() |
|
|
reject() |
|
|
} else { |
|
|
resolve() |
|
|
} |
|
|
}); |
|
|
}) |
|
|
} |
|
|
|
|
|
loadZipFile('available.json', function (text) { |
|
|
availableData = JSON.parse(text); |
|
|
}).then(function () { |
|
|
return Promise.all([ |
|
|
loadZipFile('unavailable.json', function (text) { |
|
|
unavailableData = JSON.parse(text); |
|
|
}), |
|
|
loadZipFile('playlists.json', function (text) { |
|
|
rawPlaylistMap = JSON.parse(text); |
|
|
}), |
|
|
loadZipFile('channels.json', function (text) { |
|
|
const channels = JSON.parse(text); |
|
|
if (Array.isArray(channels)) { |
|
|
channels.forEach(function (channel) { |
|
|
rawChannelMap[channel.id] = channel; |
|
|
}) |
|
|
} else { |
|
|
rawChannelMap = channels; |
|
|
} |
|
|
}), |
|
|
loadZipFile('videos.json', function (text) { |
|
|
const rows = []; |
|
|
(JSON.parse(text) || []).forEach(function (video) { |
|
|
rows.push(loadVideo(video, true)); |
|
|
}); |
|
|
|
|
|
sliceLoad(rows, controls.videosTable); |
|
|
}, function () { |
|
|
controls.progress.update({ |
|
|
value: 0, |
|
|
max: 5, |
|
|
subtext: 'Import failed (no videos.json)' |
|
|
}); |
|
|
}) |
|
|
]); |
|
|
}).then(function () { |
|
|
loadAggregateTables(function () { |
|
|
controls.btnImport.removeClass("loading").removeClass("disabled"); |
|
|
controls.progress.update({ |
|
|
value: 5, |
|
|
max: 5, |
|
|
text: rows.length + " / " + rows.length |
|
|
}); |
|
|
}); |
|
|
|
|
|
controls.progress.update({ |
|
|
value: 5, |
|
|
max: 5, |
|
|
text: doneProgressMessage(), |
|
|
subtext: 'Import done' |
|
|
}); |
|
|
|
|
|
controls.unavailableProgress.update({ |
|
|
value: 5, |
|
|
max: 5, |
|
|
text: unavailableProgressMessage(), |
|
|
subtext: 'Import done' |
|
|
}); |
|
|
|
|
|
controls.btnImport.removeClass("loading").removeClass("disabled"); |
|
|
}).catch(function () { |
|
|
controls.btnImport.removeClass("loading").removeClass("disabled"); |
|
|
}) |
|
|
} |
|
|
|
|
|
const query = shared.parseQuery(window.location.search); |
|
|
console.log(query); |
|
|
if (String(query["searchMode"]).toLowerCase() === "true") { |
|
|
elements.regularInput.attr("hidden", true); |
|
|
elements.searchInput.attr("hidden", false); |
|
|
$("#formatShare").hide(); |
|
|
} |
|
|
const input = query.url || query.id; |
|
|
if (input) { |
|
|
controls.inputValue.val(decodeURIComponent(input)); |
|
|
} |
|
|
if (String(query["createdPlaylists"]).toLowerCase() === "true") { |
|
|
controls.createdPlaylists.prop("checked", true); |
|
|
} else { |
|
|
controls.createdPlaylists.prop("checked", false); |
|
|
} |
|
|
if (String(query["submit"]).toLowerCase() === "true") { |
|
|
setTimeout(function () { |
|
|
controls.btnSubmit.click(); |
|
|
}, 500); |
|
|
} |
|
|
}, |
|
|
reset: function () { |
|
|
controls.progress.update({reset: true}); |
|
|
controls.progress.removeClass('error'); |
|
|
controls.checkUnavailable.addClass("disabled"); |
|
|
|
|
|
rows = []; |
|
|
tableRows = [csvHeaderRow.join("\t")]; |
|
|
rawVideoData = []; |
|
|
availableData = {}; |
|
|
rawChannelMap = {}; |
|
|
rawPlaylistMap = {}; |
|
|
failedData = {}; |
|
|
controls.videosTable.clear(); |
|
|
controls.videosTable.draw(false); |
|
|
|
|
|
columns.forEach(function (column) { |
|
|
const button = document.querySelector("button[title='" + column.title + "']"); |
|
|
const input = document.querySelector("button[title='" + column.title + "'] input"); |
|
|
|
|
|
|
|
|
if (input.indeterminate === true && button.className.indexOf('active') !== -1) { |
|
|
button.click(); |
|
|
} |
|
|
}); |
|
|
|
|
|
tagsData = {}; |
|
|
controls.tagsTable.clear(); |
|
|
controls.tagsTable.draw(false); |
|
|
|
|
|
geotagsData = {}; |
|
|
controls.geotagsTable.clear(); |
|
|
controls.geotagsTable.draw(false); |
|
|
|
|
|
linksData = {}; |
|
|
controls.linksTable.clear(); |
|
|
controls.linksTable.draw(false); |
|
|
|
|
|
unavailableData = {}; |
|
|
playlistMap = {}; |
|
|
controls.unavailableTable.clear(); |
|
|
controls.unavailableTable.draw(false); |
|
|
controls.unavailableProgress.update({reset: true}); |
|
|
|
|
|
unavailableColumns.forEach(function (column) { |
|
|
const button = document.querySelector("button[title='" + column.title + "']"); |
|
|
const input = document.querySelector("button[title='" + column.title + "'] input"); |
|
|
|
|
|
|
|
|
if (input.indeterminate === true && button.className.indexOf('active') !== -1) { |
|
|
button.click(); |
|
|
} |
|
|
}); |
|
|
|
|
|
controls.year.html("<option value='' selected>All years</option>") |
|
|
|
|
|
for (let key in otherData) { |
|
|
const row = otherData[key]; |
|
|
if (row.hasOwnProperty('reset')) { |
|
|
row.reset(); |
|
|
} else { |
|
|
row.value = 0; |
|
|
} |
|
|
} |
|
|
controls.otherTable.clear(); |
|
|
controls.otherTable.draw(false); |
|
|
} |
|
|
} |
|
|
|
|
|
$(document).ready(internal.init); |
|
|
|
|
|
const external = { |
|
|
toggleResultsColumn(index) { |
|
|
const column = controls.videosTable.column(index); |
|
|
|
|
|
column.visible(!column.visible()); |
|
|
}, |
|
|
toggleLinksColumn(index) { |
|
|
console.log('toggle links ' + index) |
|
|
const column = controls.linksTable.column(index); |
|
|
|
|
|
column.visible(!column.visible()); |
|
|
}, |
|
|
toggleUnavailableColumn(index) { |
|
|
const column = controls.unavailableTable.column(index); |
|
|
|
|
|
column.visible(!column.visible()); |
|
|
} |
|
|
} |
|
|
return external; |
|
|
}()); |
|
|
|