/** * YouTube Metadata * * Grab everything publicly available from the YouTube API. * * @requires jquery * @author mattwright324 */ (function () { 'use strict'; const elements = {}; const controls = {}; let exportData = {}; const delaySubmitKey = "delaySubmitNormal"; const can = { submit: true, }; const delay5Sec = 5; const delay5SecMs = delay5Sec * 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 getSuggestedHtml(parsedInput, fullJson, jsonType, filmot) { const suggested = getSuggestedLinks(parsedInput, fullJson, jsonType, filmot); suggested.sort((a, b) => (a.text > b.text) ? 1 : -1) const html = []; for (let i = 0; i < suggested.length; i++) { const link = suggested[i]; html.push("
Localizations for..." + "
" + partJson.title + "
"; partDiv.append(titleHtml); const authorHtml = "Published by " + "" + partJson.channelTitle + "" + "
"; partDiv.append(authorHtml); const published = new Date(partJson.publishedAt); const dateHtml = "Published on " + "" + published.toUTCString() + "" + " (" + moment(published).utc().fromNow() + ") " + getConvertTime(published) + "
"; partDiv.append(dateHtml); if (partJson.tags) { const tagsHtml = "Tag(s): " + "" + partJson.tags.join(", ") + "" + "
"; partDiv.append(tagsHtml); } else { partDiv.append("There were no tags.
") } if (partJson.categoryId) { const html = "Category id is " + "" + partJson.categoryId + " which means " + "" + ytCategory.lookup(partJson.categoryId) + "" + "
"; partDiv.append(html); } if (partJson.defaultLanguage) { const code = partJson.defaultLanguage.toUpperCase(); const translated = bcp47.lookup(code); partDiv.append("Default language is " + code + " which means " + formatBCP47(translated) + "
") } if (partJson.defaultAudioLanguage) { const code = partJson.defaultAudioLanguage.toUpperCase(); const translated = bcp47.lookup(code); partDiv.append("Audio language is " + code + " which means " + formatBCP47(translated) + "
") } partDiv.append("The video id is " + fullJson.id + "
"); partDiv.append("" +
"
" +
"Inspect the metadata for the rest of this channel's videos" +
"
" + "Normalized like ratio: " + "" + Math.trunc(normalized.a) + " like(s) per " + "" + Math.trunc(normalized.b) + " dislike(s)" + "
"; partDiv.append(html); } else if (!partJson.hasOwnProperty("likeCount")) { partDiv.append("This video has likes disabled.
"); } if (!partJson.hasOwnProperty("viewCount")) { partDiv.append("This video has view counts disabled.
") } if (!partJson.hasOwnProperty("commentCount")) { partDiv.append("This video has comments disabled.
") } if (!partJson.hasOwnProperty("dislikeCount")) { partDiv.append( "YouTube no longer provides the dislikeCount since 2021-12-13 " + "(see more here). " + "
"); partDiv.append("Want dislikes back? Check out the " + "return-youtube-dislike project!" + "
"); // Potentially output RYD dislikes from their API. // $.ajax({ // type: "GET", // url: "https://returnyoutubedislikeapi.com/votes?videoId=" + fullJson.id // }).then(function (res) { // const dislikes = shared.idx(["dislikes"], res); // if (dislikes) { // partDiv.append("RYD Estimated Dislikes: " + dislikes + "
") // } // }); } } }, recordingDetails: { title: "Geolocation", postProcess: function (partJson, fullJson) { const partDiv = $("#video-section #recordingDetails"); const location = partJson.location; if (location && location.latitude && location.longitude) { const latlng = location.latitude + "," + location.longitude; const staticMap = "https://maps.googleapis.com/maps/api/staticmap?center=" + latlng + "&zoom=14&size=1000x300&key=AIzaSyAa-o55aIMt4YC0mhPyp8WfGql5DVg_fp4&markers=color:red|" + latlng; const link = partJson.hasOwnProperty("locationDescription") ? "https://maps.google.com/maps/search/" + encodeURI(partJson.locationDescription).replace(/'/g, "%27") + "/@" + latlng + ",14z" : "https://maps.google.com/maps?q=loc:" + latlng; const html = "" +
" Click to open in Google Maps" +
"
Recorded on " + "" + recordDate.format("ddd, DD MMM YYYY") + "" + " (" + recordDate.fromNow() + "). YouTube Studio only allows creators to pick the date so there is no time on this timestamp." + "
"; partDiv.append(dateHtml); const published = moment(fullJson.snippet.publishedAt).utc(); const format = shared.formatDuration(getDuration(recordDate, published), false, true); if (format === "0s") { partDiv.append("The video was recorded same day as the publish date.
") } else if (published.isAfter(recordDate)) { partDiv.append("The video was recorded " + format + " before the publish date.
") } else { partDiv.append("The video was recorded " + format + " after the publish date.
"); } } } }, status: { title: "Status", postProcess: function (partJson) { const partDiv = $("#video-section #status"); if (partJson.hasOwnProperty("privacyStatus")) { if (partJson.privacyStatus !== "public") { partDiv.append("This video has its privacy set to " + partJson.privacyStatus + "
") } } if (partJson.hasOwnProperty("license")) { if (partJson.license !== "youtube") { partDiv.append("This video has its license set to " + partJson.license + "
") } } if (partJson.hasOwnProperty("embeddable")) { if (partJson.embeddable) { partDiv.append("This video may be embedded on other websites
") } else { partDiv.append("This video may not be embedded on other websites
") } } if (partJson.hasOwnProperty("madeForKids")) { if (partJson.madeForKids) { partDiv.append("This video is designated as child-directed
") } else { partDiv.append("This video is not child-directed
") } } if (partJson.hasOwnProperty("selfDeclaredMadeForKids")) { if (partJson.selfDeclaredMadeForKids) { partDiv.append("The video owner designated this video as child-directed
") } else { partDiv.append("The video owner designated this video as not child-directed.
") } } } }, liveStreamingDetails: { title: "Livestream Details", postProcess: function (partJson) { const partDiv = $("#video-section #liveStreamingDetails"); if (partJson.hasOwnProperty("actualStartTime")) { const start = new Date(partJson.actualStartTime); const dateHtml = "The stream started on " + "" + start.toUTCString() + "" + " (" + moment(start).utc().fromNow() + ") " + getConvertTime(start) + "
"; partDiv.append(dateHtml); } if (partJson.hasOwnProperty("actualEndTime")) { const end = new Date(partJson.actualEndTime); const dateHtml = "The stream ended on " + "" + end.toUTCString() + "" + " (" + moment(end).utc().fromNow() + ") " + getConvertTime(end) + "
"; partDiv.append(dateHtml); } const now = moment(new Date()).utc(); if (partJson.hasOwnProperty("scheduledStartTime") && partJson.hasOwnProperty("scheduledEndTime")) { const start = moment(partJson.scheduledStartTime).utc(); const end = moment(partJson.scheduledEndTime).utc(); const format = shared.formatDuration(getDuration(start, end)); partDiv.append("The stream was scheduled to run for " + format + "
"); } if (partJson.hasOwnProperty("scheduledStartTime") && !partJson.hasOwnProperty("actualStartTime")) { // Stream hasn't started const start = moment(partJson.scheduledStartTime).utc(); const format = shared.formatDuration(getDuration(start, now)); if (start.isAfter(now)) { partDiv.append("The stream hasn't started yet. It will start in " + format + "
"); } else { partDiv.append("The stream hasn't started yet. It was supposed to start " + format + " ago
"); } } if (partJson.hasOwnProperty("actualStartTime") && partJson.hasOwnProperty("scheduledStartTime")) { // Stream started. Time between schedule date and actual start? const start = moment(partJson.actualStartTime).utc(); const scheduled = moment(partJson.scheduledStartTime).utc(); const format = shared.formatDuration(getDuration(start, scheduled)); if (start.isAfter(scheduled)) { partDiv.append("The stream was " + format + " late to start
") } else { partDiv.append("The stream was " + format + " early to start
"); } } if (partJson.hasOwnProperty("actualStartTime") && !partJson.hasOwnProperty("actualEndTime")) { // Stream started but still going. Time between start and now? const start = moment(partJson.actualStartTime).utc(); const format = shared.formatDuration(getDuration(start, now)); partDiv.append("The stream is still going. It has been running for " + format + "
"); } if (partJson.hasOwnProperty("actualStartTime") && partJson.hasOwnProperty("actualEndTime")) { // Stream done. Time between start and end? const start = moment(partJson.actualStartTime).utc(); const end = moment(partJson.actualEndTime).utc(); const format = shared.formatDuration(getDuration(start, end)); partDiv.append("The stream is over. It's length was " + format + "
"); } } }, localizations: { title: "Localizations", postProcess: function (partJson) { const partDiv = $("#video-section #localizations"); processLocalizations(partDiv, partJson); } }, contentDetails: { title: "Content Details", postProcess: function (partJson) { const partDiv = $("#video-section #contentDetails"); const duration = moment.duration(partJson.duration); const format = shared.formatDuration(duration); if (format === "0s") { partDiv.append("A video can't be 0 seconds. This must be a livestream.
"); } else { partDiv.append("The video length was " + format + "
"); } if (partJson.hasOwnProperty('regionRestriction')) { const restriction = partJson.regionRestriction; const totalIsoCodes = iso3166.codes.length; // Should have only one or the other, never both let message; if (restriction.hasOwnProperty('allowed')) { partDiv.append("This video is region-restriction allowed."); message = "These " + restriction.allowed.length + " / " + totalIsoCodes + " region(s) are allowed to watch the video.
"; restriction.allowed.sort(); } else if (restriction.hasOwnProperty('blocked')) { partDiv.append("This video is region-restriction blocked."); message = "These " + restriction.blocked.length + " / " + totalIsoCodes + " region(s) are not allowed to watch the video.
"; restriction.blocked.sort(); } const inList = restriction.allowed || restriction.blocked; const translations = []; inList.forEach(function (code) { const result = iso3166.lookup(code); const name = (result ? result.name : 'ISO-3166 Could not translate') translations.push("This video has a content rating of " + pairs.join(", ") + "
") } } }, topicDetails: { title: "Topic Details", postProcess: function (partJson) { const partDiv = $("#video-section #topicDetails"); const topics = []; (partJson.topicCategories || []).forEach(function (categoryUrl) { const text = categoryUrl.substr(categoryUrl.lastIndexOf('/') + 1).replace(/_/g, " "); topics.push("" + partJson.title + "
"); const published = new Date(partJson.publishedAt); const dateHtml = "Channel created on " + "" + published.toUTCString() + "" + " (" + moment(published).utc().fromNow() + ")" + getConvertTime(published) + "
"; partDiv.append(dateHtml); if (partJson.hasOwnProperty("country")) { const countryCode = partJson.country; const country = bcp47.lookup(countryCode).country; const translated = country ? " which is " + country.name + "" : ""; partDiv.append("The channel is associated with country code " + countryCode + "" + translated + "
"); } else { partDiv.append("The channel doesn't have an associated country.
"); } if (partJson.hasOwnProperty("customUrl")) { const urlPart = partJson.customUrl.startsWith('@') ? '' : 'c/'; const customUrl = "https://www.youtube.com/" + urlPart + partJson.customUrl; partDiv.append("The channel has a custom url of value '" + partJson.customUrl + "'
"); } partDiv.append("The channel id is " + fullJson.id + "
"); } }, statistics: { title: "Statistics", postProcess: function (partJson, fullJson) { const partDiv = $("#channel-section #statistics"); const subLevels = { graphite: { min: 1, max: 999, qualCount: "1-1k", learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/graphite/" }, opal: { min: 1000, max: 9999, qualCount: "1k-10k", learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/opal/" }, bronze: { min: 10000, max: 99999, qualCount: "10k-100k", learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/bronze/" }, silver: { min: 100000, max: 999999, qualCount: "100k-1m", learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/silver/" }, gold: { min: 1000000, max: 9999999, qualCount: "1m-10m", learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/silver/" }, diamond: { min: 10000000, max: Number.MAX_SAFE_INTEGER, qualCount: "10m+", learnMore: "https://www.youtube.com/intl/en-GB/creators/benefits/silver/" } } const subs = partJson.subscriberCount; const learnMore = "Click here to learn more."; for (let levelName in subLevels) { const level = subLevels[levelName]; if (subs >= level.min && subs <= level.max) { partDiv.append("This channel's subscriber count qualifies for benefit level " + levelName + " (" + level.qualCount + "). " + learnMore + "
") } } if (partJson.hiddenSubscriberCount) { partDiv.append("This channel has their subscriber count hidden.
"); } else if (subs <= 0) { partDiv.append("This channel has no subscribers and does not qualify for any benefit level. " + learnMore + "
"); } else { partDiv.append("Check out this channel on SocialBlade.
"); } if (partJson.videoCount > 0) { partDiv.append("" +
"
" +
"Inspect the metadata for all of this channel's videos" +
"
This channel has no public videos.
"); } } }, brandingSettings: { title: "Branding Settings", postProcess: function (partJson) { const partDiv = $("#channel-section #brandingSettings"); const bannerImage = shared.idx(["image", "bannerExternalUrl"], partJson); if (bannerImage) { partDiv.append("This channel is tracking and measuring traffic with Google Analytics " + partJson.channel.trackingAnalyticsAccountId + "
") } if (partJson.channel.hasOwnProperty("moderateComments")) { if (partJson.channel.moderateComments) { partDiv.append("Comments on the channel page are moderated and require approval by the owner.
") } else { partDiv.append("Comments on the channel page are not moderated.
") } } if (partJson.channel.keywords) { const keywords = partJson.channel.keywords; const parsed = []; // Custom parser because regex for all languages is funky // and why doesn't browser JS regex support \p{L} !?!?! // Also, why didn't google make this an array like video tags? let word = ""; let inQuotes = false; for (let i = 0; i < keywords.length; i++) { const char = keywords.charAt(i); if (char === '"' && inQuotes === false) { inQuotes = true; } else if (char === '"' && inQuotes === true) { inQuotes = false; } if (char !== '"') { if (char === " " && inQuotes === false) { parsed.push(word); word = ""; } else if (char !== " " || inQuotes === true) { word += char; } } } if (parsed.indexOf(word) === -1) { parsed.push(word); } const keywordsHtml = "Channel Keyword(s): " + (parsed && parsed.length ? "" + parsed.join(", ") + "" : "") + "
"; partDiv.append(keywordsHtml); } else { partDiv.append("There were no keywords.
") } } }, contentDetails: { title: "Content Details", postProcess: function (partJson) { const partDiv = $("#channel-section #contentDetails"); const related = partJson.relatedPlaylists; if (related) { if (related.hasOwnProperty("uploads") && related.uploads) { partDiv.append("") } if (related.hasOwnProperty("favorites") && related.favorites) { partDiv.append("") } if (related.hasOwnProperty("likes") && related.likes) { partDiv.append("") } } } }, localizations: { title: "Localizations", postProcess: function (partJson) { const partDiv = $("#channel-section #localizations"); processLocalizations(partDiv, partJson); } }, status: { title: "Status", postProcess: function (partJson) { const partDiv = $("#channel-section #status"); if (partJson.hasOwnProperty("madeForKids")) { if (partJson.madeForKids) { partDiv.append("This channel is designated as child-directed
") } else { partDiv.append("This channel is not child-directed
") } } if (partJson.hasOwnProperty("selfDeclaredMadeForKids")) { if (partJson.selfDeclaredMadeForKids) { partDiv.append("The channel owner designated this channel as child-directed
") } else { partDiv.append("The channel owner designated this channel as not child-directed.
") } } } }, topicDetails: { title: "Topic Details", postProcess: function (partJson) { const partDiv = $("#channel-section #topicDetails"); const topics = []; (partJson.topicCategories || []).forEach(function (categoryUrl) { const text = categoryUrl.substr(categoryUrl.lastIndexOf('/') + 1).replace(/_/g, " "); topics.push("" + partJson.title + "
"); const authorHtml = "Published by " + "" + partJson.channelTitle + "" + "
"; partDiv.append(authorHtml); const published = new Date(partJson.publishedAt); const dateHtml = "Playlist created on " + "" + published.toUTCString() + "" + " (" + moment(published).utc().fromNow() + ")" + getConvertTime(published) + "
"; partDiv.append(dateHtml); partDiv.append("The playlist id is " + fullJson.id + "
"); partDiv.append("" +
"
" +
"Inspect the metadata for all of this playlist's videos" +
"
");
const json = reasonAppend.find("code");
json.text(JSON.stringify(errorJson, null, 4));
hljs.highlightElement(json[0]);
}
}
function attemptLoadFilmot(parsedInput) {
$("#filmot").hide();
if (parsedInput.type === "video_id") {
// Note: Filmot has limited resources. Do not misuse, please contact about usage first.
// https://filmot.com/contactus
const hostname = window.location.hostname;
if (hostname !== "localhost" && hostname !== "mattw.io") {
console.log("do not call filmot from other instances")
return;
}
if (!shared.isValidVideoId(parsedInput.value)) {
console.log("do not call filmot for invalid ids")
$("#reason-append").append("However, this is an invalid video id and must follow pattern: [A-Za-z0-9_-]{10}[AEIMQUYcgkosw048].
"); return; } $.ajax({ cache: false, data: { key: "md5paNgdbaeudounjp39", id: parsedInput.value, flags: 1 // Get channel and description too, }, dataType: "json", type: "GET", timeout: 5000, url: "https://filmot.com/api/getvideos", }).done(function (res) { const filmotAppend = $("#filmot-append"); filmotAppend.empty(); const video = shared.idx([0], res); if (!video) { filmotAppend.append("No archive about this video id.
"); $("#filmot").show(); return; } exportData["filmot"] = video; filmotAppend.append("");
const json = filmotAppend.find("code");
json.text(JSON.stringify(video, null, 4));
hljs.highlightElement(json[0]);
$("#filmot").show();
const titleHtml = "" + video.title + "
"; filmotAppend.append(titleHtml); const authorHtml = "Published by " + "" + video.channelname + "" + "
"; filmotAppend.append(authorHtml); const published = new Date(video.uploaddate); const dateHtml = "Published on " + "" + moment(published).format("ddd, DD MMM YYYY") + "" + " (" + moment(published).utc().fromNow() + ")" + "
"; filmotAppend.append(dateHtml); const duration = moment.duration({"seconds": video.duration}); const format = shared.formatDuration(duration); if (format === "0s") { filmotAppend.append("A video can't be 0 seconds. This must be a livestream.
"); } else { filmotAppend.append("The video length was " + format + "
"); } filmotAppend.append(getSuggestedHtml(null, null, null, video)); }).fail(function (err) { console.warn(err); }) } else if (parsedInput.type === "channel_id") { if (!shared.isValidChannelId(parsedInput.value)) { $("#reason-append").append("However, this is an invalid channel id and must follow pattern: [A-Za-z0-9_-]{21}[AQgw].
"); } } } function attemptWaybackCDX(parsedInput) { if (parsedInput.type === "video_id") { $("#wayback").show(); $("#wayback-show-older,#wayback-copy").hide(); $("#wayback-append").html("Checking... ") const results = [] const promises = [] const cdxUrls = [] // Video thumbs cdxUrls.push("https://web.archive.org/cdx/search/cdx?url=i.ytimg.com/vi/" + parsedInput.value + "*&collapse=digest&filter=statuscode:200&mimetype:image/jpeg&output=json") cdxUrls.push("https://web.archive.org/cdx/search/cdx?url=s.ytimg.com/vi/" + parsedInput.value + "*&collapse=digest&filter=statuscode:200&mimetype:image/jpeg&output=json") cdxUrls.push("https://web.archive.org/cdx/search/cdx?url=img.youtube.com/vi/" + parsedInput.value + "*&collapse=digest&filter=statuscode:200&mimetype:image/jpeg&output=json") // Storyboard thumbs const sbSub = ["i", "i9", "i1", "i2", "i3", "i4", "i5", "i6", "i7", "i8"] for (let i in sbSub) { const subdomain = sbSub[i]; const cdxUrl = "https://web.archive.org/cdx/search/cdx?url=" + subdomain + ".ytimg.com/sb/" + parsedInput.value + "*&collapse=digest&filter=statuscode:200&mimetype:image/jpeg&output=json" cdxUrls.push(cdxUrl) } let progress = 0 let failed = 0 $("#wayback-progress").html(`${progress+failed}/${cdxUrls.length} done; ${failed} failed`) promises.push(new Promise(function (resolve) { function cdxCall(i) { if (i >= cdxUrls.length) { console.log("promise done") resolve() return } console.log("calling " + i) const url = cdxUrls[i] if (!url) { resolve() return } const promise = new Promise(function (resolve2) { $.ajax({ url: "https://cors-proxy-mw324.herokuapp.com/" + url, }).done(function (res) { console.log(url) if (res) { for (let i = 0; i < res.length; i++) { results.push(res[i]) } } progress += 1 $("#wayback-progress").html(`${progress+failed}/${cdxUrls.length} done; ${failed} failed`) resolve2() }).fail(function (err) { console.error(url) failed += 1 $("#wayback-progress").html(`${progress+failed}/${cdxUrls.length} done; ${failed} failed`) resolve2() }) }) promises.push(promise) promise.then(function () {cdxCall(i + 1)}) } cdxCall(0) })) Promise.all(promises).then(function () { console.log("Done!") console.log(results) const links = [] for (let i in results) { const result = results[i]; if (result[0] === "urlkey") { continue } const url = new URL(result[2]) const base = url.hostname + url.pathname links.push([base, result[2], result[1], `https://web.archive.org/web/${result[1]}/${result[2]}`]) } if (links.length) { links.sort(function (a,b) { // url base if (a[0] < b[0]) return -1 if (a[0] > b[0]) return 1 // timestamp return b[2] - a[2] }) const hideOlder = $("#checkHideOlder").is(":checked") function formatTime(waybackTime) { return waybackTime.replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, "$1-$2-$3 $4:$5:$6") } let currentBase = null; const linkHtml = [] for (let i in links) { const link = links[i]; if (link[0] === currentBase) { linkHtml.push(`");
const json = section.find("code");
json.text(JSON.stringify(item[part], null, 4));
hljs.highlightElement(json[0]);
partMap[partMapType][part].postProcess(item[part], item);
}
if (!item.hasOwnProperty(part) || $.isEmptyObject(item[part])) {
section.append("The " + partMapType + " does not have " + part + ".
"); } } } else { errorState("Your input looked like a " + partMapType + " but nothing came back. " + "It may have been deleted or made private.", function (append) { append.append("" + "You may find more details by trying..." + getSuggestedHtml(parsedInput) + "
"); }); attemptLoadFilmot(parsedInput); $("#wayback,#wayback-check").show() $("#wayback-append").html("") } } async function parseVideo(res, input, skipGetChannel) { const channelId = shared.idx(["items", "0", "snippet", "channelId"], res) if (!skipGetChannel && channelId) { submit({ type: 'channel_id', value: channelId, mayHideOthers: false }); } parseType("video", "video-section", res, input); const id = input.value; const thumbsDiv = $("#thumbnails"); thumbsDiv.empty(); const names = ["0", "hq1", "hq2", "hq3"] for (let i in names) { const name = names[i] const thumbUrl = "https://img.youtube.com/vi/" + id + "/" + name + ".jpg"; const html = ""; thumbsDiv.append(html); } const videoMore = $("#video-more"); videoMore.empty(); videoMore.append(getSuggestedHtml(null, shared.idx(["items", 0], res), "video")); } async function parsePlaylist(res, input, skipGetChannel) { const channelId = shared.idx(["items", "0", "snippet", "channelId"], res) if (!skipGetChannel && channelId) { submit({ type: 'channel_id', value: channelId, mayHideOthers: false }); } parseType("playlist", "playlist-section", res, input); const playlistMore = $("#playlist-more"); playlistMore.empty(); playlistMore.append(getSuggestedHtml(null, shared.idx(["items", 0], res), "playlist")); } async function parseChannel(res, input) { parseType("channel", "channel-section", res, input); const channelMore = $("#channel-more"); channelMore.empty(); channelMore.append(getSuggestedHtml(null, shared.idx(["items", 0], res), "channel")); } /** * Attempt to resolve the channel handle URL via CORS workaround. Grab webpage content and extract url pattern. */ async function resolveChannelHandleCORS(parsedInput, callbackResubmit) { console.log('Attempting to resolve custom channel via CORS') $.ajax({ url: "https://cors-proxy-mw324.herokuapp.com/https://www.youtube.com/@" + parsedInput.value, dataType: 'html' }).then(function (res) { const pageHtml = $("" + "Custom channel URLs have no direct API method, an indirect resolving method was unable to find it. " + "
"); append.append("" + "Verify that the custom URL actually exists, if it does than you may try manually resolving it. " + "
"); append.append("" + "More detail about the issue and what you can do can be found here at " + "#1 - Channel custom url unsupported." + "
"); }) } }).fail(function (err) { errorState("Could not resolve Custom Channel URL", function (append) { append.append("" + "Custom channel URLs have no direct API method, an indirect resolving method was unable to find it. " + "
"); append.append("" + "Verify that the custom URL actually exists, if it does than you may try manually resolving it. " + "
"); append.append("" + "More detail about the issue and what you can do can be found here at " + "#1 - Channel custom url unsupported." + "
"); }) }); } async function submit(parsedInput) { console.log(parsedInput); if (parsedInput.original) { controls.inputValue.val(parsedInput.original); const baseUrl = location.origin + location.pathname; if (parsedInput.type === "video_id" || parsedInput.type === "playlist_id" || parsedInput.type === "channel_id") { controls.shareLink.val(baseUrl + "?url=" + encodeURIComponent(parsedInput.original) + "&submit=true"); } else { controls.shareLink.val(baseUrl + "?url=" + encodeURIComponent(parsedInput.original) + "&submit=true"); } controls.shareLink.attr("disabled", false); } if (parsedInput.type === 'unknown') { errorState("Your link did not follow an accepted format."); } else if (parsedInput.type === 'channel_handle' || parsedInput.type === 'channel_custom') { resolveChannelHandleCORS(parsedInput, submit); } else if (parsedInput.type === 'video_id') { console.log('grabbing video'); if (parsedInput.mayHideOthers) { $("#playlist").hide(); } youtube.ajax('videos', { part: Object.keys(partMap.video).join(','), id: parsedInput.value }).done(function (res) { console.log(res); parseVideo(res, parsedInput); }).fail(function (err) { console.log(err); errorState("There was a problem querying for the video.", null, err); }); } else if (parsedInput.type === 'channel_id') { console.log('grabbing channel id'); if (parsedInput.mayHideOthers) { $("#video,#playlist").hide(); } youtube.ajax('channels', { part: "id," + Object.keys(partMap.channel).join(','), id: parsedInput.value }).done(function (res) { console.log(res); parseChannel(res, parsedInput); }).fail(function (err) { console.error(err); errorState("There was a problem querying for the channel.", null, err); }); } else if (parsedInput.type === 'channel_user') { console.log('grabbing channel user'); if (parsedInput.mayHideOthers) { $("#video,#playlist").hide(); } youtube.ajax('channels', { part: Object.keys(partMap.channel).join(','), forUsername: parsedInput.value }).done(function (res) { console.log(res); parseChannel(res, parsedInput); }).fail(function (err) { console.error(err); errorState("There was a problem querying for the channel.", null, err); }); } else if (parsedInput.type === 'playlist_id') { console.log('grabbing playlist'); if (parsedInput.mayHideOthers) { $("#video").hide(); } youtube.ajax('playlists', { part: Object.keys(partMap.playlist).join(','), id: parsedInput.value }).done(function (res) { console.log(res); parsePlaylist(res, parsedInput); }).fail(function (err) { console.error(err); errorState("There was a problem querying for the playlist.", null, err); }); } } const internal = { init: function () { elements.hljsTheme = $("#highlightjs-theme"); controls.darkMode = $("#darkMode"); controls.inputValue = $("#value"); controls.btnSubmit = $("#submit"); controls.shareLink = $("#shareLink"); new ClipboardJS(".clipboard"); controls.btnExport = $("#export"); controls.btnImport = $("#import"); controls.importFileChooser = $("#importFileChooser"); elements.videoSection = $("#video-section"); elements.channelSection = $("#channel-section"); elements.playlistSection = $("#playlist-section"); const randomVideoId = shared.randomFromList(EXAMPLE_VIDEOS); const exampleLink = "https://youtu.be/" + randomVideoId; controls.inputValue.val(exampleLink); internal.buildPage(true); }, buildPage: function (doSetup) { $(".part-section").remove(); $("#thumbnails").empty(); for (let part in partMap.video) { const partData = partMap.video[part]; const html = "