Skip to content

Commit

Permalink
Merge pull request #76 from Phixyn/feat/allow-using-urls
Browse files Browse the repository at this point in the history
Feat: Allow using YouTube links in addition to video IDs
  • Loading branch information
Phixyn authored Oct 23, 2020
2 parents 4405ea1 + 3699c7c commit 274eb8c
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 7 deletions.
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;
}

0 comments on commit 274eb8c

Please sign in to comment.