Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Allow using YouTube links in addition to video IDs #76

Merged
merged 8 commits into from
Oct 23, 2020
9 changes: 8 additions & 1 deletion docs/devlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Devlog template: 1.1.0
- [jQuery](#jquery)
- [CORS](#cors)
- [Browser History API](#browser-history-api)
- [URL API](#url-api)
- [Similar Projects](#similar-projects)
- [Websockets](#websockets)
- [Libraries/modules](#librariesmodules)
Expand Down Expand Up @@ -65,7 +66,7 @@ Devlog template: 1.1.0

### General

* [Current sprint board](https://github.com/Phixyn/no-bs-looper/projects/1)
* [Current sprint board](https://github.com/Phixyn/no-bs-looper/projects/2)
* [Convert HH:MM:SS to seconds - Online tools](https://www.tools4noobs.com/online_tools/hh_mm_ss_to_seconds/)

### YouTube Player API
Expand Down Expand Up @@ -136,6 +137,12 @@ Devlog template: 1.1.0

* [Working with the History API - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API)

### URL API

* https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
* https://developer.mozilla.org/en-US/docs/Web/API/URL
* https://url.spec.whatwg.org/#dom-url-href

### Similar Projects

* [Developing a Progressive Fetch YouTube Downloader | by Param Singh | Medium](https://medium.com/@paramsingh_66174/developing-a-progressive-fetch-youtube-downloader-75a709bff1ef)
Expand Down
2 changes: 1 addition & 1 deletion static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
<form class="grid-x grid-padding-x grid-padding-y">
<!-- Video ID -->
<div class="cell small-12 medium-8 large-8 medium-offset-2 large-offset-2">
<label>Video ID
<label>Video URL or ID
<div class="input-group">
<input type="text" id="video-id" class="input-group-field" name="video-id" placeholder="Video ID" value="KcUnXunuDTs">
<div class="input-group-button">
Expand Down
116 changes: 111 additions & 5 deletions static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ $(document).foundation();

// Add your websocket server IP address here
const websocket = new WebSocket("ws://<server IP address here>:14670");
const VIDEO_ID_LENGTH = 11;

var player;
var state;
Expand Down Expand Up @@ -256,9 +257,10 @@ function onPlayerStateChange(event) {
/**
* Updates the YouTube player with a new video. Called when the user clicks
* the "Update" button. The video ID is taken from the text input element
* in the HTML form. This function also requests video info from our backend
* server via websocket (see why this is necessary below), which hopefully
* causes the websocket client's "onmessage" handler to get called.
* in the HTML form. If the user enters a URL, the ID is extracted from it.
* This function also requests video info from our backend server via websocket
* (see why this is necessary below), which hopefully causes the websocket
* client's "onmessage" handler to get called.
*
* Normally, we'd update the slider and input attributes with new max values
* based on the video duration here. However, we can't update them here
Expand All @@ -280,8 +282,31 @@ function onPlayerStateChange(event) {
*/
function updatePlayer() {
console.debug("[DEBUG] Updating player (state.v, state.start).");
state.v = videoIdInput.val();
console.log("[INFO] Loading new video in player...");
let videoIdInputVal = videoIdInput.val();

if (isValidHttpUrl(videoIdInputVal)) {
let videoId = extractVideoId(videoIdInputVal);

if (videoId === null) {
// TODO #75: Show error toast to the user.
console.error(
`[ERROR] Invalid video URL or ID in input: '${videoIdInputVal}'.`
);
return;
}

state.v = videoId;
} else if (videoIdInputVal.length === VIDEO_ID_LENGTH) {
state.v = videoIdInputVal;
} else {
// TODO #75: Show error toast to the user.
console.error(
`[ERROR] Invalid video URL or ID in input: '${videoIdInputVal}'.`
);
return;
}

console.log(`[INFO] Loading new video in player with ID '${state.v}'.`);
/* loadPlaylist() and setLoop() are required to make infinite loops of full
* videos (i.e. not portions of a video). It's for this same reason that we
* set 'playlist' and 'loop' in the 'playerVars' (see
Expand Down Expand Up @@ -460,3 +485,84 @@ function updateSliderAndInputAttributes(newStartTime, newEndTime) {
*/
endTimeInput.val(state.end.toString()).change();
}

/**
* Checks if the given string is a valid HTTP or HTTPS URL.
*
* @param {string} urlString The string to validate.
* @return {boolean} A boolean indicating if the string is a valid HTTP or
* HTTPS URL.
*/
function isValidHttpUrl(urlString) {
console.debug(`[DEBUG] Checking if '${urlString}' is a valid URL.`);
let urlObj;

try {
urlObj = new URL(urlString);
} catch (err) {
console.error(`[ERROR] ${err.name}: ${err.message}`);
return false;
}

return urlObj.protocol === "http:" || urlObj.protocol === "https:";
}

/**
* Extracts a YouTube video ID from a URL string. The URL can be either a known
* YouTube domain (such as youtube.com or youtu.be) or any other URL that
* contains a 'v=' in its querystring.
*
* For 'youtu.be' or 'youtube.com/embed' links, the last part of the URL's
* pathname will be extracted as a potential ID. Note that the extracted ID is
* validated, and only returned if deemed to be valid. Otherwise, null is
* returned.
*
* @param {string} youtubeUrl A YouTube video URL string.
* @return {string} A YouTube video ID, if a valid one is found. Otherwise,
* returns null.
*/
function extractVideoId(youtubeUrl) {
console.log(`[INFO] Attempting to extract video ID from '${youtubeUrl}'.`);
let videoId;
let urlObj;

try {
urlObj = new URL(youtubeUrl);
} catch (err) {
// TODO #75: Show error toast to the user.
console.error(`[ERROR] ${err.name}: ${err.message}`);
return null;
}

// Check if there is a querystring in the URL and parse it
if (urlObj.search !== "") {
console.log("[INFO] Found querystring in URL, parsing it.");

let qsParse = Qs.parse(urlObj.search, { ignoreQueryPrefix: true });
if (!qsParse.hasOwnProperty("v") || qsParse.v === "") {
// TODO #75: Show error toast to the user.
console.error("[ERROR] Could not get video ID from YouTube URL.");
return null;
}

videoId = qsParse.v;
console.debug(`[DEBUG] Got video ID from querystring: '${videoId}'.`);
} else if (urlObj.pathname !== "") {
console.log("[INFO] Extracting potential video ID from URL pathname.");
// Handle 'youtu.be/id' and 'youtube.com/embed/id'
let pathArray = urlObj.pathname.split("/");
videoId = pathArray[pathArray.length - 1];
}

console.log("[INFO] Validating video ID.");
// Validate video ID by checking the length
if (videoId.length !== VIDEO_ID_LENGTH) {
// TODO #75: Show error toast to the user.
console.error(`[ERROR] Invalid video ID in URL: '${youtubeUrl}'.`);
console.debug(`[DEBUG] Got unexpected length in ID: '${videoId}'.`);
return null;
}

console.debug(`[DEBUG] Got a valid video ID from URL: '${videoId}'.`);
return videoId;
}