diff --git "a/js/youtube-metadata-bulk.js" "b/js/youtube-metadata-bulk.js" new file mode 100644--- /dev/null +++ "b/js/youtube-metadata-bulk.js" @@ -0,0 +1,2916 @@ +/** + * YouTube Metadata + * + * Grab everything publicly available from the YouTube API. + * + * @requires jquery + * @author mattwright324 + */ +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([ + // Channels condense to uploads playlist ids and channel ids + handleChannelUsers(channelUsers, playlistIds, channelIdsCreatedPlaylists), + handleChannelIds(channelIds, playlistIds, channelIdsCreatedPlaylists) + ]); + }).then(function () { + // Created playlists condense to playlist ids (when option checked) + return handleChannelIdsCreatedPlaylists(channelIdsCreatedPlaylists, playlistIds); + }).then(function () { + // Grab playlist names + return handlePlaylistNames(playlistIds); + }).then(function () { + // Playlists condense to video ids + return handlePlaylistIds(playlistIds, videoIds); + }).then(function () { + controls.progress.update({ + subtext: 'Processing video ids' + }); + + // Videos are results to be displayed + return handleVideoIds(videoIds); + }).then(function () { + return new Promise(function (resolve) { + sliceLoad(rows, controls.videosTable, resolve); + }); + }).then(function () { + controls.progress.update({ + subtext: 'Processing channel ids' + }); + + // Ids for channels not in the original request, likely from playlists + 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 = $("
" +
+ "",
+ "" + videoId + "",
+ String(video.title),
+ "Filmot · " +
+ "Archive Web · " +
+ "Archive Details · " +
+ "Archive Video · " +
+ "GhostArchive · " +
+ "Google",
+ 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 "" +
+ "
" +
+ ""
+ },
+ csvSkip: true
+ },
+ {
+ title: "Video ID",
+ type: "html",
+ visible: false,
+ _idx: ["id"],
+ valueMod: function (value, video) {
+ return "" + value + ""
+ }
+ },
+ {
+ title: "Title",
+ type: "html",
+ visible: true,
+ _idx: ["snippet", "title"],
+ valueMod: function (value, video) {
+ return "" + value + ""
+ }
+ },
+ {
+ title: "Channel ID",
+ type: "html",
+ visible: false,
+ _idx: ["snippet", "channelId"],
+ valueMod: function (value) {
+ return "" + value + ""
+ }
+ },
+ {
+ title: "Author",
+ type: "html",
+ visible: true,
+ _idx: ["snippet", "channelTitle"],
+ valueMod: function (value, video) {
+ const channelId = shared.idx(["snippet", "channelId"], video);
+ return "" + value + ""
+ }
+ },
+ {
+ 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 "" + value.locationDescription + "";
+ }
+ },
+ {
+ title: "Location",
+ type: "html",
+ visible: false,
+ _visibleIf: function (value) {
+ // Display when there are coordinates but no location name, this sometimes happens and not sure how.
+ // return !$.isEmptyObject(value) && value.location && !value.locationDescription;
+ },
+ _idx: ["recordingDetails"],
+ valueMod: function (value) {
+ if ($.isEmptyObject(value) || !value.location) {
+ return "";
+ }
+
+ const latlng = value.location.latitude + "," + value.location.longitude;
+
+ return "" + latlng + "";
+ }
+ },
+ {
+ 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 = {}; // google requests failed, don't send to filmot
+ 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("")
+ }
+
+ 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: '',
+ },
+ 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("")
+ }
+ 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: '',
+ },
+ 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) || "No data";
+ const filmotDesc = shared.idx(["filmot", "description"], video) || "";
+ const filmotAuthorName = shared.idx(["filmot", "channelname"], video);
+ const filmotAuthor = filmotAuthorName ?
+ "" + shared.idx(["filmot", "channelname"], video) + "" : "";
+ 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 () {
+ // CORS proxy workaround for downloading YouTube thumbnails in client-side app
+ // https://github.com/Rob--W/cors-anywhere/issues/301#issuecomment-962623118
+ 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");
+ });
+ });
+ });
+
+ // Drag & Drop listener
+ 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) {
+ // if you return a promise in a "then", you will chain the two promises
+ 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");
+
+ // Reset still-indeterminate columns
+ 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");
+
+ // Reset still-indeterminate columns
+ if (input.indeterminate === true && button.className.indexOf('active') !== -1) {
+ button.click();
+ }
+ });
+
+ controls.year.html("")
+
+ 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;
+}());