diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..0e69c35c9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# API Keys + +When you are testing and working on FreeTube, PLEASE use your own API Key. The keys included in the project are in use by the userbase and testing can cause these keys to max out. Please do not risk degrading the experience for other users and use your own key if at all possible. Thank you for your cooperation. + +# Code Contributions + +Please follow these guidlines before sending your pull request and making contributions. + +* When you submit a pull request, you agree that your code is published under the [GNU General Public License](https://www.gnu.org/licenses/gpl.html) +* Do not include non-free software or modules with your code. +* Make sure your pull request is setup to merge your branch to FreeTube's development branch. +* Make sure your branch is up to date with the development branch before submitting your pull request. +* Stick to a similar style of code already in the project. Please look at current code to get an idea on how to do this. +* Follow [ES6](http://es6-features.org/) standards in your code. Ex: Use `let` and `const` instead of `var`. Do not use `function(response){//code}` for callbacks, use `(response) => {//code}`. +* Comment your code when necessary. Follow the [JavaScript Documentation and Comments Standard](https://www.drupal.org/docs/develop/standards/javascript/javascript-api-documentation-and-comment-standards) for functions. +* Please test your code. Make sure new features work as well as core features such as watching videos or loading subscriptions. +* Please limit the amount of Node Modules that you introduce into the project. Only include them when absolutely necessary for your code to work (Ex: Using nedb for databases) or if a module provides similar functionality to what you are trying to achieve (Ex: Using autolinker to create links to outside URLs instead of writing the functionality myself). +* If using a new Node Module, please include the `require` statement in `layout.js` to keep them together. +* Please try to stay involved with the community and maintain your code. I am only one person and I work on FreeTube only in my spare time. I do not have time to work on everything and it would be nice if you can maintain your code when necessary. + +# Setting up Your Environment + +Here's how to get your environment setup. You will need Git and NPM installed on your system. + +Clone down the repositoy: +``` +git clone https://github.com/FreeTubeApp/FreeTube.git +``` + +Install Dependencies: +``` +npm install +``` + +Run the application: +``` +npm start +``` + +Make / Package application: + +Windows (Requires Wine on Linux): +``` +npm run make:win32 +``` + +Mac: +``` +npm run make:darwin +``` + +Linux (Requires deb and rpm to be installed): +``` +npm run make:linux +``` + +I will update this document when necessary. Anyone who has questions or suggestions on this document are welcome to create an issue or pull request. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..a2fd36065 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +- [ ] I have read and agree to the [Contribution Guidelines](https://github.com/FreeTubeApp/FreeTube/blob/master/CONTRIBUTING.md) +- [ ] I can maintain / support my code if issues arise. + +**Does your change relate to a current issue? Please list it here if applicable.** + + +**Please list out the changes you've made** + + +**Does your change include any new Node Modules? If yes, what modules and what are they licensed under?** + + +**Other Comments** diff --git a/README.md b/README.md index 509e69a33..f75f357fa 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # FreeTube FreeTube is an Open Source Desktop YouTube player built with privacy in mind. Watch your favorite YouTube videos ad free as well as prevent Google from tracking what you watch. Available for Windows / Mac / Linux -Please note that FreeTube is currently in Beta and using the proprietary and obfuscated [Google API script](https://apis.google.com/js/api.js) (bundled as `src/js/googleApi.js`), which is planned to be ditched in the future. Video URLs are resolved using the [youtube-dl](https://github.com/jaimeMF/youtube-dl-api-server) HTTP API. +Please note that FreeTube is currently in Beta and uses the proprietary [YouTube HTTP API](https://developers.google.com/youtube/v3/). Video URLs are resolved using a [youtube-dl-api-server](https://github.com/jaimeMF/youtube-dl-api-server). Download @@ -30,38 +30,7 @@ While I believe that FreeTube should work well for most users, there will probab # I'd like to help! If you have an idea or if you found a bug, please create an issue so that we can track it. Please check the current issues and make sure that someone else hasn't mentioned it already before submitting an issue. -If you like to get your hands dirty and want to contribute, we would love to have your help. Send a pull request and someone will review your code. Proper contribution guidelines will be included soon, but in the meantime here's how to get start: - -After you pull down the code: - -Install Dependencies: -``` -npm install -``` - -Run the application: -``` -npm start -``` - -Make / Package application: - -Windows (Requires Wine on Linux): -``` -npm run make:win32 -``` - -Mac: -``` -npm run make:darwin -``` - -Linux (Requires deb and rpm): -``` -npm run make:linux -``` - -The bundled application will then be located in the "/out" folder in your project directory. +If you like to get your hands dirty and want to contribute, we would love to have your help. Send a pull request and someone will review your code. Please follow the [Contribution Guidelines](https://github.com/FreeTubeApp/FreeTube/blob/master/CONTRIBUTING.md) before sending your pull request. # License [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) diff --git a/src/index.html b/src/index.html index 15852a2bf..8d4165900 100644 --- a/src/index.html +++ b/src/index.html @@ -11,10 +11,11 @@ - + + diff --git a/src/js/channels.js b/src/js/channels.js index dd7e44048..c2bfd0bd3 100644 --- a/src/js/channels.js +++ b/src/js/channels.js @@ -24,14 +24,11 @@ along with FreeTube. If not, see . /*function getChannelThumbnail(channelId, callback) { let url = ''; - let request = gapi.client.youtube.channels.list({ + youtubeAPI('channels', { 'id': channelId, 'part': 'snippet', - }); - - request.execute((response) => { - url = response['items'][0]['snippet']['thumbnails']['high']['url']; - callback(url); + }, function (data){ + callback(data.items[0].snippet.thumbnails.high.url); }); }*/ @@ -45,7 +42,7 @@ along with FreeTube. If not, see . function goToChannel(channelId) { event.stopPropagation(); clearMainContainer(); - toggleLoading(); + startLoadingAnimation(); // Check if the user is subscribed to the channel. Display different text based on the information @@ -62,17 +59,14 @@ function goToChannel(channelId) { }); // Call YouTube API to grab channel information - let request = gapi.client.youtube.channels.list({ + youtubeAPI('channels', { part: 'snippet, brandingSettings, statistics', id: channelId, - }); - - // Perform API Execution - request.execute((response) => { + }, function (data){ // Set variables of extracted information - const brandingSettings = response['items'][0]['brandingSettings']; - const statistics = response['items'][0]['statistics']; - const snippet = response['items'][0]['snippet']; + const brandingSettings = data['items'][0]['brandingSettings']; + const statistics = data['items'][0]['statistics']; + const snippet = data['items'][0]['snippet']; const channelName = brandingSettings['channel']['title']; const channelBanner = brandingSettings['image']['bannerImageUrl']; const channelImage = snippet['thumbnails']['high']['url']; @@ -97,22 +91,19 @@ function goToChannel(channelId) { }); // Render the template on to #main $('#main').html(rendered); - toggleLoading(); + stopLoadingAnimation(); }); // Grab the channel's latest upload. API forces a max of 50. - let request = gapi.client.youtube.search.list({ + youtubeAPI('search', { part: 'snippet', channelId: channelId, type: 'video', maxResults: 50, order: 'date', - }); - - // Execute API request - request.execute((response) => { + }, function (data) { // Display recent uploads to #main - response['items'].forEach((video) => { + data['items'].forEach((video) => { displayVideos(video); }); }); diff --git a/src/js/events.js b/src/js/events.js index b96b178c6..cd838f9f5 100644 --- a/src/js/events.js +++ b/src/js/events.js @@ -18,70 +18,190 @@ along with FreeTube. If not, see . /* -* File for events within application. Work needs to be done throughout the application -* to use this style more. Please use this style going forward if possible. -*/ + * File for events within application. Work needs to be done throughout the application + * to use this style more. Please use this style going forward if possible. + */ - /** - * Event when user clicks comment box, - * and wants to show/display comments for the user. - */ - let showComments = function(event) { - let comments = $('#comments'); +/** + * Event when user clicks comment box, + * and wants to show/display comments for the user. + */ +let showComments = function(event) { + let comments = $('#comments'); - if (comments.css('display') === 'none') { - comments.attr('loaded', 'true'); + if (comments.css('display') === 'none') { + comments.attr('loaded', 'true'); - let commentsTemplate = $.get('templates/comments.html'); + let commentsTemplate = $.get('templates/comments.html'); commentsTemplate.done((template) => { - let request = gapi.client.youtube.commentThreads.list({ + youtubeAPI('commentThreads', { 'videoId': $('#comments').attr('data-video-id'), 'part': 'snippet,replies', 'maxResults': 100, - }); - - request.execute((data) => { + }, function (data){ let comments = []; let items = data.items; - items.forEach((object) => { - let snippet = object['snippet']['topLevelComment']['snippet']; - let dateString = new Date(snippet.publishedAt); - let publishedDate = dateFormat(dateString, "mmm dS, yyyy"); + items.forEach((object) => { + let snippet = object['snippet']['topLevelComment']['snippet']; + let dateString = new Date(snippet.publishedAt); + let publishedDate = dateFormat(dateString, "mmm dS, yyyy"); - snippet.publishedAt = publishedDate; + snippet.publishedAt = publishedDate; - comments.push(snippet); - }) - const html = mustache.render(template, { - comments: comments, - }); - $('#comments').html(html); + comments.push(snippet); + }) + const html = mustache.render(template, { + comments: comments, }); + $('#comments').html(html); }); + }); - comments.show(); - } else { - comments.hide(); - } - }; - - /** - * Play / Pause the video player upon click. - */ - let playPauseVideo = function(event) { - let el = event.currentTarget; - el.paused ? el.play() : el.pause(); + comments.show(); + } else { + comments.hide(); } +}; - /** - * --------------------------- - * Bind click events - * -------------------------- - */ - $(document).on('click', '#showComments', showComments); +/** + * Play / Pause the video player upon click. + */ +let playPauseVideo = function(event) { + let el = event.currentTarget; + el.paused ? el.play() : el.pause(); +}; - $(document).on('click', '.videoPlayer', playPauseVideo); +$('.videoPlayer').keypress((event) => { + console.log(event.which); +}); - $(document).on('click', '#confirmNo', hideConfirmFunction); +let videoShortcutHandler = function(event) { + console.log(event.which); + let videoPlayer = $('.videoPlayer').get(0); + if (typeof(videoPlayer) !== 'undefined'){ + switch (event.which) { + case 32: + // Space Bar + event.preventDefault(); + videoPlayer.paused ? videoPlayer.play() : videoPlayer.pause(); + break; + case 74: + // J Key + event.preventDefault(); + changeDurationBySeconds(-10); + break; + case 75: + // K Key + event.preventDefault(); + videoPlayer.paused ? videoPlayer.play() : videoPlayer.pause(); + break; + case 76: + // L Key + event.preventDefault(); + changeDurationBySeconds(10); + break; + case 70: + // F Key + event.preventDefault(); + videoPlayer.webkitRequestFullscreen(); + break; + case 77: + // M Key + event.preventDefault(); + let volume = videoPlayer.volume; + console.log(volume); + if (volume > 0){ + changeVolume(-1); + } + else{ + changeVolume(1); + } + break; + case 38: + // Up Arrow Key + event.preventDefault(); + changeVolume(0.05); + break; + case 40: + // Down Arrow Key + event.preventDefault(); + changeVolume(-0.05); + break; + case 37: + // Left Arrow Key + event.preventDefault(); + changeDurationBySeconds(-5); + break; + case 39: + // Right Arrow Key + event.preventDefault(); + changeDurationBySeconds(5); + break; + case 49: + // 1 Key + event.preventDefault(); + changeDurationByPercentage(0.1); + break; + case 50: + // 2 Key + event.preventDefault(); + changeDurationByPercentage(0.2); + break; + case 51: + // 3 Key + event.preventDefault(); + changeDurationByPercentage(0.3); + break; + case 52: + // 4 Key + event.preventDefault(); + changeDurationByPercentage(0.4); + break; + case 53: + // 5 Key + event.preventDefault(); + changeDurationByPercentage(0.5); + break; + case 54: + // 6 Key + event.preventDefault(); + changeDurationByPercentage(0.6); + break; + case 55: + // 7 Key + event.preventDefault(); + changeDurationByPercentage(0.7); + break; + case 56: + // 8 Key + event.preventDefault(); + changeDurationByPercentage(0.8); + break; + case 57: + // 9 Key + event.preventDefault(); + changeDurationByPercentage(0.9); + break; + case 48: + // 0 Key + event.preventDefault(); + changeDurationByPercentage(0); + break; + } + } +}; + +/** + * --------------------------- + * Bind click events + * -------------------------- + */ +$(document).on('click', '#showComments', showComments); + +$(document).on('click', '.videoPlayer', playPauseVideo); + +$(document).on('keydown', videoShortcutHandler); + +$(document).on('click', '#confirmNo', hideConfirmFunction); diff --git a/src/js/googleApi.js b/src/js/googleApi.js deleted file mode 100644 index 7d177d869..000000000 --- a/src/js/googleApi.js +++ /dev/null @@ -1,18 +0,0 @@ -// File was downloaded to prevent dependency on website. -// You can find this file at https://apis.google.com/js/api.js -// -// This file is not covered by GPL v3. If the owners of this file would like to have -// this file removed from FreeTube, please contact me at FreeTubeApp@protonmail.com - -var gapi=window.gapi=window.gapi||{};gapi._bs=new Date().getTime();(function(){/* - gapi.loader.OBJECT_CREATE_TEST_OVERRIDE &&*/ -var g=window,h=document,m=g.location,n=function(){},q=/\[native code\]/,u=function(a,b,c){return a[b]=a[b]||c},aa=function(a){a=a.sort();for(var b=[],c=void 0,d=0;df}f&&c.push(e)}return c},U=function(){var a=A.nonce;if(void 0!==a)return a&&a===String(a)&&a.match(S)?a:A.nonce=null;var b=u(A,"us",[]);if(!b||!b.length)return A.nonce=null;for(var c=h.getElementsByTagName(R),d=0,e=c.length;d")}},V=function(a){var b=h.createElement(R);b.setAttribute("src",a);a=U();null!==a&&b.setAttribute("nonce",a);b.async="true";(a=h.getElementsByTagName(R)[0])?a.parentNode.insertBefore(b,a):(h.head||h.body||h.documentElement).appendChild(b)},qa=function(a, -b){var c=b&&b._c;if(c)for(var d=0;d { + }, function (data) { createVideoListContainer('Watch History:'); - response['items'].forEach((video) => { + data['items'].forEach((video) => { displayVideos(video, 'history'); }); - toggleLoading(); + stopLoadingAnimation() }); }); } diff --git a/src/js/init.js b/src/js/init.js index 3e35f6218..c549a9a05 100644 --- a/src/js/init.js +++ b/src/js/init.js @@ -20,7 +20,7 @@ along with FreeTube. If not, see . /* * File used to initializing the application */ -const {app, BrowserWindow} = require('electron'); +const {app, BrowserWindow, dialog} = require('electron'); const path = require('path'); const url = require('url'); let win; diff --git a/src/js/layout.js b/src/js/layout.js index 7aa3058f5..cf9ed7eac 100644 --- a/src/js/layout.js +++ b/src/js/layout.js @@ -115,23 +115,6 @@ $(document).ready(() => { loadSubscriptions(); }); -/** -* Start the YouTube API. -* -* @return {Void} -*/ -function start() { - // Initializes the client with the API key and the Translate API. - gapi.client.init({ - 'apiKey': apiKey, - }) - - gapi.client.load('youtube', 'v3', () => { - let isLoad = true; - }); -} - - /** * Toggle the ability to view the side navigation bar. * @@ -161,28 +144,27 @@ function clearMainContainer() { hideConfirmFunction(); } -/** -* Show the loading animation before / after a function runs. Also disables / enables input -* -* @return {Void} -*/ -function toggleLoading() { +function startLoadingAnimation() { const loading = document.getElementById('loading'); const sideNavDisabled = document.getElementById('sideNavDisabled'); const searchBar = document.getElementById('search'); const goToVideoInput = document.getElementById('jumpToInput'); - if (loading.style.display === 'none' || loading.style.display === '') { loading.style.display = 'inherit'; sideNavDisabled.style.display = 'inherit'; searchBar.disabled = true; goToVideoInput.disabled = true; - } else { +} +function stopLoadingAnimation() { + const loading = document.getElementById('loading'); + const sideNavDisabled = document.getElementById('sideNavDisabled'); + const searchBar = document.getElementById('search'); + const goToVideoInput = document.getElementById('jumpToInput'); + loading.style.display = 'none'; sideNavDisabled.style.display = 'none'; searchBar.disabled = false; goToVideoInput.disabled = false; - } } /** @@ -214,7 +196,7 @@ function createVideoListContainer(headerLabel = '') { function showAbout(){ // Remove current information and display loading animation clearMainContainer(); - toggleLoading(); + startLoadingAnimation(); // Grab about.html to be used as a template $.get('templates/about.html', (template) => { @@ -224,7 +206,7 @@ function showAbout(){ }); // Render to #main and remove loading animation $('#main').html(rendered); - toggleLoading(); + stopLoadingAnimation(); }); } diff --git a/src/js/player.js b/src/js/player.js new file mode 100644 index 000000000..311d43905 --- /dev/null +++ b/src/js/player.js @@ -0,0 +1,339 @@ +/* +This file is part of FreeTube. + +FreeTube is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +FreeTube is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with FreeTube. If not, see . +*/ + + + +/* +* File for functions related to videos. +*/ + +/** +* Display the video player and play a video +* +* @param {string} videoId - The video ID of the video to be played. +* +* @return {Void} +*/ +function playVideo(videoId) { + clearMainContainer(); + toggleLoading(); + + let subscribeText = ''; + let savedText = ''; + let savedIconClass = ''; + let savedIconColor = ''; + let video480p; + let video720p; + let defaultUrl; + let defaultQuality; + let channelId; + let videoHtml; + let videoThumbnail; + let videoType = 'video'; + let embedPlayer; + let validUrl; + + // Grab the embeded player. Used as fallback if the video URL cannot be found. + // Also grab the channel ID. + try { + let getInfoFunction = getChannelAndPlayer(videoId); + + getInfoFunction.then((data) => { + console.log(data); + embedPlayer = data[0]; + channelId = data[1]; + }); + } catch (ex) { + showToast('Video not found. ID may be invalid.'); + toggleLoading(); + return; + } + + /* + * FreeTube calls an instance of a youtube-dl server to grab the direct video URL. Please do not use this API in third party projects. + */ + const url = 'https://stormy-inlet-41826.herokuapp.com/api/info?url=https://www.youtube.com/watch?v=' + videoId + 'flatten=True'; + $.getJSON(url, (response) => { + console.log(response); + + const info = response['info']; + + videoThumbnail = info['thumbnail']; + let videoUrls = info['formats']; + + // Add commas to the video view count. + const videoViews = info['view_count'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + + // Format the date to a more readable format. + let dateString = info['upload_date']; + dateString = [dateString.slice(0, 4), '-', dateString.slice(4)].join(''); + dateString = [dateString.slice(0, 7), '-', dateString.slice(7)].join(''); + console.log(dateString); + const publishedDate = dateFormat(dateString, "mmm dS, yyyy"); + + // Figure out the width for the like/dislike bar. + const videoLikes = info['like_count']; + const videoDislikes = info['dislike_count']; + const totalLikes = videoLikes + videoDislikes; + const likePercentage = parseInt((videoLikes / totalLikes) * 100); + + let description = info['description']; + // Adds clickable links to the description. + description = autolinker.link(description); + + const checkSubscription = isSubscribed(channelId); + + // Change the subscribe button text depending on if the user has subscribed to the channel or not. + checkSubscription.then((results) => { + if (results === false) { + subscribeText = 'SUBSCRIBE'; + } else { + subscribeText = 'UNSUBSCRIBE'; + } + }); + + const checkSavedVideo = videoIsSaved(videoId); + + // Change the save button icon and text depending on if the user has saved the video or not. + checkSavedVideo.then((results) => { + if (results === false) { + savedText = 'SAVE'; + savedIconClass = 'far unsaved'; + } else { + savedText = 'SAVED'; + savedIconClass = 'fas saved'; + } + }); + + // Search through the returned object to get the 480p and 720p video URLs (If available) + Object.keys(videoUrls).forEach((key) => { + console.log(key); + switch (videoUrls[key]['format_note']) { + case 'medium': + video480p = videoUrls[key]['url']; + break; + case 'hd720': + video720p = videoUrls[key]['url']; + break; + } + }); + + // Default to the embeded player if the URLs cannot be found. + if (typeof(video720p) === 'undefined' && typeof(video480p) === 'undefined') { + defaultQuality = 'EMBED'; + videoHtml = embedPlayer.replace(/\"\;/g, '"'); + showToast('Unable to get video file. Reverting to embeded player.'); + } else if (typeof(video720p) === 'undefined' && typeof(video480p) !== 'undefined') { + // Default to the 480p video if the 720p URL cannot be found. + videoHtml = ''; + defaultQuality = '480p'; + } else { + // Default to the 720p video. + videoHtml = ''; + defaultQuality = '720p'; + // Force the embeded player if needed. + //videoHtml = embedPlayer; + } + + // API Request + let request = gapi.client.youtube.channels.list({ + 'id': channelId, + 'part': 'snippet' + }); + + // Execute request + request.execute((response) => { + console.log(response); + const channelThumbnail = response['items'][0]['snippet']['thumbnails']['high']['url']; + + $.get('templates/player.html', (template) => { + mustache.parse(template); + const rendered = mustache.render(template, { + videoHtml: videoHtml, + videoQuality: defaultQuality, + videoTitle: info['title'], + videoViews: videoViews, + videoThumbnail: videoThumbnail, + channelName: info['uploader'], + videoLikes: videoLikes, + videoDislikes: videoDislikes, + likePercentage: likePercentage, + videoId: videoId, + channelId: channelId, + channelIcon: channelThumbnail, + publishedDate: publishedDate, + description: description, + isSubscribed: subscribeText, + savedText: savedText, + savedIconClass: savedIconClass, + savedIconColor: savedIconColor, + video480p: video480p, + video720p: video720p, + embedPlayer: embedPlayer, + }); + $('#main').html(rendered); + toggleLoading(); + showVideoRecommendations(videoId); + console.log('done'); + }); + }); + // Sometimes a video URL is found, but the video will not play. I believe the issue is + // that the video has yet to render for that quality, as the video will be available at a later time. + // This will check the URLs and switch video sources if there is an error. + checkVideoUrls(video480p, video720p); + // Add the video to the user's history + addToHistory(videoId); + }); +} + +/** +* Open up the mini player to watch the video outside of the main application. +* +* @param {string} videoThumbnail - The URL of the video thumbnail. Used to prevent another API call. +* +* @return {Void} +*/ +function openMiniPlayer(videoThumbnail) { + let lastTime; + let videoHtml; + + // Grabs whatever the HTML is for the current video player. Done this way to grab + // the HTML5 player (with varying qualities) as well as the YouTube embeded player. + if ($('.videoPlayer').length > 0) { + $('.videoPlayer').get(0).pause(); + lastTime = $('.videoPlayer').get(0).currentTime; + videoHtml = $('.videoPlayer').get(0).outerHTML; + } else { + videoHtml = $('iframe').get(0).outerHTML; + } + + // Create a new browser window. + const BrowserWindow = electron.remote.BrowserWindow; + + let miniPlayer = new BrowserWindow({ + width: 1200, + height: 700 + }); + + // Use the miniPlayer.html template. + $.get('templates/miniPlayer.html', (template) => { + mustache.parse(template); + const rendered = mustache.render(template, { + videoHtml: videoHtml, + videoThumbnail: videoThumbnail, + startTime: lastTime, + }); + // Render the template to the new browser window. + miniPlayer.loadURL("data:text/html;charset=utf-8," + encodeURI(rendered)); + }); +} + +/** +* Change the quality of the current video. +* +* @param {string} videoHtml - The HTML of the video player to be set. +* @param {string} qualityType - The Quality Type of the video. Ex: 720p, 480p +* @param {boolean} isEmbed - Optional: Value on if the videoHtml is the embeded player. +* +* @return {Void} +*/ +function changeQuality(videoHtml, qualityType, isEmbed = false) { + if (videoHtml == '') { + showToast('Video quality type is not available. Unable to change quality.') + return; + } + + videoHtml = videoHtml.replace(/\"\;/g, '"'); + + console.log(videoHtml); + console.log(isEmbed); + + // The YouTube API creates 2 more iFrames. This is why a boolean value is sent + // with the function. + const embedPlayer = document.getElementsByTagName('IFRAME')[0]; + + const html5Player = document.getElementsByClassName('videoPlayer'); + + console.log(embedPlayer); + console.log(html5Player); + + if (isEmbed && html5Player.length == 0) { + // The embeded player is already playing. Return. + showToast('You are already using the embeded player.') + return; + } else if (isEmbed) { + // Switch from HTML 5 player to embeded Player + html5Player[0].remove(); + const mainHtml = $('#main').html(); + $('#main').html(videoHtml + mainHtml); + $('#currentQuality').html(qualityType); + } else if (html5Player.length == 0) { + // Switch from embeded player to HTML 5 player + embedPlayer.remove(); + let videoPlayer = document.createElement('video'); + videoPlayer.className = 'videoPlayer'; + videoPlayer.src = videoHtml; + videoPlayer.controls = true; + videoPlayer.autoplay = true; + $('#main').prepend(videoPlayer); + $('#currentQuality').html(qualityType); + } else { + // Switch src on HTML 5 player + const currentPlayBackTime = $('.videoPlayer').get(0).currentTime; + html5Player[0].src = videoHtml; + html5Player[0].load(); + $('.videoPlayer').get(0).currentTime = currentPlayBackTime; + $('#currentQuality').html(qualityType); + } +} + +/** +* Change the playpack speed of the video. +* +* @param {double} speed - The playback speed of the video. +* +* @return {Void} +*/ +function changeVideoSpeed(speed){ + $('#currentSpeed').html(speed); + $('.videoPlayer').get(0).playbackRate = speed; +} + +function changeVolume(amount){ + const videoPlayer = $('.videoPlayer').get(0); + let volume = videoPlayer.volume; + volume = volume + amount; + if (volume > 1){ + videoPlayer.volume = 1; + } + else if (volume < 0){ + videoPlayer.volume = 0; + } + else{ + videoPlayer.volume = volume; + } +} + +function changeDurationBySeconds(seconds){ + const videoPlayer = $('.videoPlayer').get(0); + videoPlayer.currentTime = videoPlayer.currentTime + seconds; +} + +function changeDurationByPercentage(percentage){ + const videoPlayer = $('.videoPlayer').get(0); + videoPlayer.currentTime = videoPlayer.duration * percentage; +} diff --git a/src/js/savedVideos.js b/src/js/savedVideos.js index 0b491a403..d99c5c496 100644 --- a/src/js/savedVideos.js +++ b/src/js/savedVideos.js @@ -113,7 +113,7 @@ function videoIsSaved(videoId) { */ function showSavedVideos(){ clearMainContainer(); - toggleLoading(); + startLoadingAnimation(); console.log('checking saved videos'); let videoList = ''; @@ -136,20 +136,17 @@ function showSavedVideos(){ } // Call the YouTube API - let request = gapi.client.youtube.videos.list({ + youtubeAPI('videos', { part: 'snippet', id: videoList, maxResults: 50, - }); - - // Execute the API request - request.execute((response) => { + }, function (data) { // Render the videos to the screen createVideoListContainer('Saved Videos:'); - response['items'].forEach((video) => { + data['items'].forEach((video) => { displayVideos(video, 'history'); }); - toggleLoading(); + stopLoadingAnimation(); }); }); } diff --git a/src/js/settings.js b/src/js/settings.js index da89966b2..17dbaa098 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -21,6 +21,9 @@ along with FreeTube. If not, see . * A file for functions used for settings. */ +// To any third party devs that fork the project, please be ethical and change the API keys. +const apiKeyBank = ['AIzaSyC9E579nh_qqxg6BH4xIce3k_7a9mT4uQc', 'AIzaSyCKplYT6hZIlm2O9FbWTi1G7rkpsLNTq78', 'AIzaSyAE5xzh5GcA_tEDhXmMFd1pEzrL-W7z51E', 'AIzaSyDoFzqwuO9l386eF6BmNkVapjiTJ93CBy4', 'AIzaSyBljfZFPioB0TRJAj-0LS4tlIKl2iucyY4']; + /** * Display the settings screen to the user. * @@ -28,14 +31,11 @@ along with FreeTube. If not, see . */ function showSettings() { clearMainContainer(); - toggleLoading(); + startLoadingAnimation(); let isChecked = ''; let key = ''; - // To any third party devs that fork the project, please be ethical and change the API keys. - const apiKeyBank = ['AIzaSyC9E579nh_qqxg6BH4xIce3k_7a9mT4uQc', 'AIzaSyCKplYT6hZIlm2O9FbWTi1G7rkpsLNTq78', 'AIzaSyAE5xzh5GcA_tEDhXmMFd1pEzrL-W7z51E', 'AIzaSyDoFzqwuO9l386eF6BmNkVapjiTJ93CBy4', 'AIzaSyBljfZFPioB0TRJAj-0LS4tlIKl2iucyY4']; - /* * Check the settings database for the user's current settings. This is so the * settings page has the correct toggles related when it is rendered. @@ -64,7 +64,7 @@ function showSettings() { }); // Render template to application $('#main').html(rendered); - toggleLoading(); + stopLoadingAnimation(); // Check / uncheck the switch depending on the user's settings. if (currentTheme === 'light') { @@ -82,8 +82,6 @@ function showSettings() { * @return {Void} */ function checkDefaultSettings() { - // To any third party devs that fork the project, please be ethical and change the API keys. - const apiKeyBank = ['AIzaSyC9E579nh_qqxg6BH4xIce3k_7a9mT4uQc', 'AIzaSyCKplYT6hZIlm2O9FbWTi1G7rkpsLNTq78', 'AIzaSyAE5xzh5GcA_tEDhXmMFd1pEzrL-W7z51E', 'AIzaSyDoFzqwuO9l386eF6BmNkVapjiTJ93CBy4', 'AIzaSyBljfZFPioB0TRJAj-0LS4tlIKl2iucyY4']; // Grab a random API Key. apiKey = apiKeyBank[Math.floor(Math.random() * apiKeyBank.length)]; @@ -126,9 +124,6 @@ function checkDefaultSettings() { } }); } - - // Loads the JavaScript client library and invokes `start` afterwards. - gapi.load('client', start); }); } diff --git a/src/js/subscriptions.js b/src/js/subscriptions.js index a164c4fc0..30ccc51f3 100644 --- a/src/js/subscriptions.js +++ b/src/js/subscriptions.js @@ -31,25 +31,22 @@ along with FreeTube. If not, see . function addSubscription(channelId, useToast = true) { console.log(channelId); // Request YouTube API - let request = gapi.client.youtube.channels.list({ + youtubeAPI('channels', { part: 'snippet', id: channelId, - }); - - // Execute API request - request.execute((response) => { - const channelInfo = response['items'][0]['snippet']; + }, function (data){ + const channelInfo = data['items'][0]['snippet']; const channelName = channelInfo['title']; const thumbnail = channelInfo['thumbnails']['high']['url']; - const data = { + const channel = { channelId: channelId, channelName: channelName, channelThumbnail: thumbnail, }; // Refresh the list of subscriptions on the side navigation bar. - subDb.insert(data, (err, newDoc) => { + subDb.insert(channel, (err, newDoc) => { if (useToast){ showToast('Added ' + channelName + ' to subscriptions.'); displaySubs(); @@ -84,13 +81,7 @@ function loadSubscriptions() { clearMainContainer(); const loading = document.getElementById('loading'); - /* - * It is possible for the function to be called several times. This prevents the loading - * from being turned off when the situation occurs. - */ - if (loading.style.display !== 'inherit'){ - toggleLoading(); - } + startLoadingAnimation() let videoList = []; @@ -117,28 +108,17 @@ function loadSubscriptions() { * Grab the channels 15 most recent uploads. Typically this should be enough. * This number can be changed if we feel necessary. */ - try { - let request = gapi.client.youtube.search.list({ - part: 'snippet', // Try getting content details for video duration in the near future. - channelId: channelId, - type: 'video', - maxResults: 15, - order: 'date', - }); - - request.execute((response) => { - videoList = videoList.concat(response['items']); - // Iterate through the next object in the loop. - next(); - }); - } catch (err) { - /* - * The above API requests sometimes forces an error for some reason. Restart - * the function to prevent this. This should be changed if possible. - */ - loadSubscriptions(); - return; - } + youtubeAPI('search', { + part: 'snippet', // Try getting content details for video duration in the near future. + channelId: channelId, + type: 'video', + maxResults: 15, + order: 'date', + }, function (data){ + videoList = videoList.concat(data['items']); + // Iterate through the next object in the loop. + next(); + }); }, (err) => { // Sort the videos by date videoList.sort((a, b) => { @@ -165,12 +145,12 @@ function loadSubscriptions() { displayVideos(videoList[i]); } } - toggleLoading(); + stopLoadingAnimation() }); } else { // User has no subscriptions. Display message. const container = document.getElementById('main'); - toggleLoading(); + stopLoadingAnimation() container.innerHTML = `

Your Subscription list is currently empty. Start adding subscriptions to see them here.

`; diff --git a/src/js/videos.js b/src/js/videos.js index 042ef98b5..540d6a999 100644 --- a/src/js/videos.js +++ b/src/js/videos.js @@ -19,7 +19,6 @@ along with FreeTube. If not, see . /* * File for functions related to videos. -* TODO: Split some of these functions into their own file. */ /** @@ -39,31 +38,27 @@ function search(nextPageToken = '') { if (nextPageToken === '') { clearMainContainer(); - toggleLoading(); + startLoadingAnimation(); } else { console.log(nextPageToken); showToast('Fetching results. Please wait...'); } // Start API request - let request = gapi.client.youtube.search.list({ + youtubeAPI('search', { q: query, part: 'id, snippet', type: 'video', pageToken: nextPageToken, maxResults: 25, - }); - - // Execute API Request - request.execute((response) => { - console.log(response); + }, function (data){ if (nextPageToken === '') { createVideoListContainer('Search Results:'); - toggleLoading(); + stopLoadingAnimation(); } - response['items'].forEach(displayVideos); - addNextPage(response['result']['nextPageToken']); - }); + data.items.forEach(displayVideos); + addNextPage(data.result.nextPageToken); + }) } /** @@ -162,7 +157,7 @@ function addNextPage(nextPageToken) { */ function playVideo(videoId) { clearMainContainer(); - toggleLoading(); + startLoadingAnimation(); let subscribeText = ''; let savedText = ''; @@ -191,7 +186,7 @@ function playVideo(videoId) { }); } catch (ex) { showToast('Video not found. ID may be invalid.'); - toggleLoading(); + stopLoadingAnimation(); return; } @@ -282,15 +277,11 @@ function playVideo(videoId) { } // API Request - let request = gapi.client.youtube.channels.list({ + youtubeAPI('channels', { 'id': channelId, 'part': 'snippet' - }); - - // Execute request - request.execute((response) => { - console.log(response); - const channelThumbnail = response['items'][0]['snippet']['thumbnails']['high']['url']; + }, function (data){ + const channelThumbnail = data['items'][0]['snippet']['thumbnails']['high']['url']; $.get('templates/player.html', (template) => { mustache.parse(template); @@ -318,7 +309,7 @@ function playVideo(videoId) { embedPlayer: embedPlayer, }); $('#main').html(rendered); - toggleLoading(); + stopLoadingAnimation(); showVideoRecommendations(videoId); console.log('done'); }); @@ -340,15 +331,13 @@ function playVideo(videoId) { * @param {string} videoId - The video ID of the video to get recommendations from. */ function showVideoRecommendations(videoId) { - let request = gapi.client.youtube.search.list({ + youtubeAPI('search', { part: 'snippet', type: 'video', relatedToVideoId: videoId, maxResults: 15, - }); - - request.execute((response) => { - const recommendations = response['items']; + }, function (data){ + const recommendations = data.items; recommendations.forEach((data) => { const snippet = data['snippet']; const videoId = data['id']['videoId']; @@ -374,48 +363,6 @@ function showVideoRecommendations(videoId) { }); } -/** -* Open up the mini player to watch the video outside of the main application. -* -* @param {string} videoThumbnail - The URL of the video thumbnail. Used to prevent another API call. -* -* @return {Void} -*/ -function openMiniPlayer(videoThumbnail) { - let lastTime; - let videoHtml; - - // Grabs whatever the HTML is for the current video player. Done this way to grab - // the HTML5 player (with varying qualities) as well as the YouTube embeded player. - if ($('.videoPlayer').length > 0) { - $('.videoPlayer').get(0).pause(); - lastTime = $('.videoPlayer').get(0).currentTime; - videoHtml = $('.videoPlayer').get(0).outerHTML; - } else { - videoHtml = $('iframe').get(0).outerHTML; - } - - // Create a new browser window. - const BrowserWindow = electron.remote.BrowserWindow; - - let miniPlayer = new BrowserWindow({ - width: 1200, - height: 700 - }); - - // Use the miniPlayer.html template. - $.get('templates/miniPlayer.html', (template) => { - mustache.parse(template); - const rendered = mustache.render(template, { - videoHtml: videoHtml, - videoThumbnail: videoThumbnail, - startTime: lastTime, - }); - // Render the template to the new browser window. - miniPlayer.loadURL("data:text/html;charset=utf-8," + encodeURI(rendered)); - }); -} - /** * Check if a link is a valid YouTube/HookTube link and play that video. Gets input * from the #jumpToInput element. @@ -451,7 +398,7 @@ function parseVideoLink() { */ function showMostPopular() { clearMainContainer(); - toggleLoading(); + startLoadingAnimation(); // Get the date of 2 days ago. var d = new Date(); @@ -462,19 +409,16 @@ function showMostPopular() { // These are the videos that are considered as 'most popular' and is how similar // Applications grab these. Videos in the 'Trending' tab on YouTube will be different. // And there is no way to grab those videos. - let request = gapi.client.youtube.search.list({ + youtubeAPI('search', { part: 'snippet', order: 'viewCount', type: 'video', publishedAfter: d.toISOString(), maxResults: 50, - }); - - request.execute((response) => { - console.log(response); + }, function (data){ createVideoListContainer('Most Popular:'); - toggleLoading(); - response['items'].forEach(displayVideos); + stopLoadingAnimation(); + data['items'].forEach(displayVideos); }); } @@ -503,101 +447,21 @@ function copyLink(website, videoId) { function getChannelAndPlayer(videoId) { console.log(videoId); return new Promise((resolve, reject) => { - let data = []; - - let request = gapi.client.youtube.videos.list({ - part: 'snippet, player', + youtubeAPI('videos', { + part: 'snippet,player', id: videoId, - }); - - request.execute((response) => { - console.log(response); - let embedHtml = response['items'][0]['player']['embedHtml']; + }, function (data){ + let embedHtml = data.items[0].player.embedHtml; embedHtml = embedHtml.replace('src="', 'src="https:'); embedHtml = embedHtml.replace('width="480px"', ''); embedHtml = embedHtml.replace('height="270px"', ''); embedHtml = embedHtml.replace(/\"/g, '"'); - data[0] = embedHtml; - data[1] = response['items'][0]['snippet']['channelId']; - - - resolve(data); + resolve([embedHtml, data.items[0].snippet.channelId]); }); }); } -/** -* Change the quality of the current video. -* -* @param {string} videoHtml - The HTML of the video player to be set. -* @param {string} qualityType - The Quality Type of the video. Ex: 720p, 480p -* @param {boolean} isEmbed - Optional: Value on if the videoHtml is the embeded player. -* -* @return {Void} -*/ -function changeQuality(videoHtml, qualityType, isEmbed = false) { - if (videoHtml == '') { - showToast('Video quality type is not available. Unable to change quality.') - return; - } - - videoHtml = videoHtml.replace(/\"\;/g, '"'); - - console.log(videoHtml); - console.log(isEmbed); - - // The YouTube API creates 2 more iFrames. This is why a boolean value is sent - // with the function. - const embedPlayer = document.getElementsByTagName('IFRAME')[0]; - - const html5Player = document.getElementsByClassName('videoPlayer'); - - console.log(embedPlayer); - console.log(html5Player); - - if (isEmbed && html5Player.length == 0) { - // The embeded player is already playing. Return. - showToast('You are already using the embeded player.') - return; - } else if (isEmbed) { - // Switch from HTML 5 player to embeded Player - html5Player[0].remove(); - const mainHtml = $('#main').html(); - $('#main').html(videoHtml + mainHtml); - $('#currentQuality').html(qualityType); - } else if (html5Player.length == 0) { - // Switch from embeded player to HTML 5 player - embedPlayer.remove(); - let videoPlayer = document.createElement('video'); - videoPlayer.className = 'videoPlayer'; - videoPlayer.src = videoHtml; - videoPlayer.controls = true; - videoPlayer.autoplay = true; - $('#main').prepend(videoPlayer); - $('#currentQuality').html(qualityType); - } else { - // Switch src on HTML 5 player - const currentPlayBackTime = $('.videoPlayer').get(0).currentTime; - html5Player[0].src = videoHtml; - html5Player[0].load(); - $('.videoPlayer').get(0).currentTime = currentPlayBackTime; - $('#currentQuality').html(qualityType); - } -} - -/** -* Change the playpack speed of the video. -* -* @param {double} speed - The playback speed of the video. -* -* @return {Void} -*/ -function changeVideoSpeed(speed){ - $('#currentSpeed').html(speed); - $('.videoPlayer').get(0).playbackRate = speed; -} - /** * Check to see if the video URLs are valid. Change the video quality if one is not. * The API will grab video URLs, but they will sometimes return a 404. This diff --git a/src/js/youtubeApi.js b/src/js/youtubeApi.js new file mode 100644 index 000000000..96c845c01 --- /dev/null +++ b/src/js/youtubeApi.js @@ -0,0 +1,22 @@ +/** +* List a YouTube HTTP API resource. +* +* @param {string} resource - The path of the resource. +* @param {object} params - The API parameters. +* @param {function} success - The function to be called on success. +* +* @return {Void} +*/ +function youtubeAPI(resource, params, success) { + params.key = apiKey; + console.log(resource, params, success) + $.getJSON( + 'https://www.googleapis.com/youtube/v3/' + resource, + params, + success + ).fail((xhr, textStatus, error) => { + showToast('There was an error calling the YouTube API.'); + console.log(error); + stopLoadingAnimation(); + }); +}