diff --git a/README.md b/README.md index 9696416b33937..509e69a33ad95 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 [HookTube](https://hooktube.com/) HTTP API. +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. Download @@ -13,7 +13,7 @@ Please note that FreeTube is currently in Beta and using the proprietary and obf # How Does It Work? -FreeTube uses the YouTube API to search for videos. It then uses the HookTube API to grab the raw video files and play them in a basic HTML5 video player, preventing YouTube from tracking you using cookies or JavaScript. Subscriptions, history, and saved videos are stored locally on the user's computer and is never sent out to Google or anyone else. You own your data. +FreeTube uses the YouTube API to search for videos. It then uses the youtube-dl API to grab the raw video files and play them in a basic HTML5 video player, preventing YouTube from tracking you using cookies or JavaScript. Subscriptions, history, and saved videos are stored locally on the user's computer and is never sent out to Google or anyone else. You own your data. ## Features * Watch videos free of ads diff --git a/src/js/init.js b/src/js/init.js index 99a7a7696f555..3e35f6218bebd 100644 --- a/src/js/init.js +++ b/src/js/init.js @@ -34,7 +34,7 @@ if(require('electron-squirrel-startup')) app.quit(); */ let init = function() { const Menu = require('electron').Menu; - win = new BrowserWindow({width: 1200, height: 800}); + win = new BrowserWindow({width: 1200, height: 800, autoHideMenuBar: true}); win.loadURL(url.format({ pathname: path.join(__dirname, '../index.html'), diff --git a/src/js/settings.js b/src/js/settings.js index b818ac9e835a0..da89966b2611b 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -34,7 +34,7 @@ function showSettings() { let key = ''; // To any third party devs that fork the project, please be ethical and change the API keys. - const apiKeyBank = ['AIzaSyDjszXMCw44W_k-pdNoOxUHFyKGtU_ejwE', 'AIzaSyA0CkT2lS1q9HHaFYGNGM4Ycjl1kmRy22s', 'AIzaSyAiKgR75e3XAznCcb1cj4NUJ5rR_y3uB8E', 'AIzaSyDPy5jq2l1Bgv3-MbpGdZd3W3ik1BMZeDc', 'AIzaSyBeQ-Jd0lyMmul-K1QMZ2S4GSlnGFdCd3M']; + 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 @@ -44,7 +44,7 @@ function showSettings() { docs.forEach((setting) => { switch (setting['_id']) { case 'apiKey': - if (apiKeyBank.indexOf(setting['value']) < -1) { + if (apiKeyBank.indexOf(setting['value']) == -1) { key = setting['value']; } break; @@ -83,7 +83,7 @@ function showSettings() { */ function checkDefaultSettings() { // To any third party devs that fork the project, please be ethical and change the API keys. - const apiKeyBank = ['AIzaSyDjszXMCw44W_k-pdNoOxUHFyKGtU_ejwE', 'AIzaSyA0CkT2lS1q9HHaFYGNGM4Ycjl1kmRy22s', 'AIzaSyAiKgR75e3XAznCcb1cj4NUJ5rR_y3uB8E', 'AIzaSyDPy5jq2l1Bgv3-MbpGdZd3W3ik1BMZeDc', 'AIzaSyBeQ-Jd0lyMmul-K1QMZ2S4GSlnGFdCd3M']; + 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)]; @@ -117,7 +117,7 @@ function checkDefaultSettings() { setTheme(setting['value']); break; case 'apiKey': - if (apiKeyBank.indexOf(setting['value']) < -1) { + if (apiKeyBank.indexOf(setting['value']) == -1) { apiKey = setting['value']; } break; @@ -127,7 +127,6 @@ function checkDefaultSettings() { }); } - console.log("Using API key: " + apiKey); // Loads the JavaScript client library and invokes `start` afterwards. gapi.load('client', start); }); @@ -142,6 +141,11 @@ function updateSettings() { var themeSwitch = document.getElementById('themeSwitch').checked; var key = document.getElementById('api-key').value; + // 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']; + + apiKey = apiKeyBank[Math.floor(Math.random() * apiKeyBank.length)]; + if (themeSwitch == true) { var theme = 'dark'; } else { @@ -169,7 +173,7 @@ function updateSettings() { settingsDb.update({ _id: 'apiKey' }, { - value: 'AIzaSyDjszXMCw44W_k-pdNoOxUHFyKGtU_ejwE' + value: apiKey }, {}); } diff --git a/src/js/subscriptions.js b/src/js/subscriptions.js index cfcc589b23fee..a164c4fc0f745 100644 --- a/src/js/subscriptions.js +++ b/src/js/subscriptions.js @@ -119,7 +119,7 @@ function loadSubscriptions() { */ try { let request = gapi.client.youtube.search.list({ - part: 'snippet', + part: 'snippet', // Try getting content details for video duration in the near future. channelId: channelId, type: 'video', maxResults: 15, diff --git a/src/js/videos.js b/src/js/videos.js index 9ac84561ea41e..aaf10b6a61c2c 100644 --- a/src/js/videos.js +++ b/src/js/videos.js @@ -172,6 +172,7 @@ function playVideo(videoId) { let video720p; let defaultUrl; let defaultQuality; + let channelId; let videoHtml; let videoThumbnail; let videoType = 'video'; @@ -179,11 +180,14 @@ function playVideo(videoId) { let validUrl; // Grab the embeded player. Used as fallback if the video URL cannot be found. + // Also grab the channel ID. try { - let getEmbedFunction = getEmbedPlayer(videoId); + let getInfoFunction = getChannelAndPlayer(videoId); - getEmbedFunction.then((url) => { - embedPlayer = url; + getInfoFunction.then((data) => { + console.log(data); + embedPlayer = data[0]; + channelId = data[1]; }); } catch (ex) { showToast('Video not found. ID may be invalid.'); @@ -192,64 +196,36 @@ function playVideo(videoId) { } /* - * FreeTube calls the HookTube API so that it can get the direct video URL instead of the embeded player. - * If anyone knows how to grab these files without relying on their API it would be very welcome. - * It helps that HookTube returns mostly the same information as a YouTube API call so performance - * shouldn't be hindered by this. + * 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://hooktube.com/api?mode=video&id=' + videoId; + 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 videoSummary = response['json_1']; - const videoSnippet = response['json_2']['items'][0]['snippet']; - const videoStatistics = response['json_2']['items'][0]['statistics']; + const info = response['info']; - // Sometimes the max resolution URL isn't found. Grab the default one as a fallback. - try { - videoThumbnail = videoSnippet['thumbnails']['maxres']['url']; - } catch (e) { - videoThumbnail = videoSnippet['thumbnails']['default']['url']; - } - - // Search through the returned object to get the 480p and 720p video URLs (If available) - Object.keys(videoSummary['link']).forEach((key) => { - console.log(key); - switch (videoSummary['link'][key][2]) { - case 'medium': - video480p = videoSummary['link'][key][0]; - break; - case 'hd720': - video720p = videoSummary['link'][key][0]; - 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; - } + videoThumbnail = info['thumbnail']; + let videoUrls = info['formats']; // Add commas to the video view count. - const videoViews = videoSummary['view_count'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + const videoViews = info['view_count'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); // Format the date to a more readable format. - const dateString = videoSnippet['publishedAt']; + 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"); - const channelId = videoSnippet['channelId']; - let description = videoSnippet['description']; + + // 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); @@ -275,19 +251,40 @@ function playVideo(videoId) { } }); - // Figure out the width for the like/dislike bar. - const videoLikes = parseInt(videoStatistics['likeCount']); - const videoDislikes = parseInt(videoStatistics['dislikeCount']); - const totalLikes = videoLikes + videoDislikes; - const likePercentage = parseInt((videoLikes / totalLikes) * 100); + // 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; + } + }); - // Adds clickable links to the description. - description = autolinker.link(description); + // 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,contentDetails,statistics' + 'part': 'snippet' }); // Execute request @@ -300,10 +297,10 @@ function playVideo(videoId) { const rendered = mustache.render(template, { videoHtml: videoHtml, videoQuality: defaultQuality, - videoTitle: videoSummary['title'], + videoTitle: info['title'], videoViews: videoViews, videoThumbnail: videoThumbnail, - channelName: videoSummary['author'], + channelName: info['uploader'], videoLikes: videoLikes, videoDislikes: videoDislikes, likePercentage: likePercentage, @@ -497,17 +494,19 @@ function copyLink(website, videoId) { } /** -* Get the YouTube embeded player of a video. +* Get the YouTube embeded player of a video as well as channel information.. * * @param {string} videoId - The video ID of the video to get. * * @return {promise} - The HTML of the embeded player */ -function getEmbedPlayer(videoId) { +function getChannelAndPlayer(videoId) { console.log(videoId); return new Promise((resolve, reject) => { + let data = []; + let request = gapi.client.youtube.videos.list({ - part: 'player', + part: 'snippet, player', id: videoId, }); @@ -518,7 +517,11 @@ function getEmbedPlayer(videoId) { embedHtml = embedHtml.replace('width="480px"', ''); embedHtml = embedHtml.replace('height="270px"', ''); embedHtml = embedHtml.replace(/\"/g, '"'); - resolve(embedHtml); + data[0] = embedHtml; + data[1] = response['items'][0]['snippet']['channelId']; + + + resolve(data); }); }); @@ -583,6 +586,17 @@ function changeQuality(videoHtml, qualityType, isEmbed = false) { } } +/** +* Change the playpack speed of the video. +* +* @param {double} speed - The playback speed of the video. +* +* @return {Void} +*/ +function changeVideoSpeed(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/style/darkTheme.css b/src/style/darkTheme.css index 57a9100f909cb..63261ce2eb68b 100644 --- a/src/style/darkTheme.css +++ b/src/style/darkTheme.css @@ -41,6 +41,7 @@ input[type=text] {color: #EEEEEE;} .recommendDate{color: #E0E0E0;} .settingsButton {color: #BDBDBD; background-color: #424242;} .qualityTypes{color: #E0E0E0; background-color: #757575;} +.speedTypes{color: #E0E0E0; background-color: #757575;} .unsaved{color: #E0E0E0;} #main{color: #EEEEEE;} diff --git a/src/style/lightTheme.css b/src/style/lightTheme.css index 230377e868b19..add8d57f9d2f7 100644 --- a/src/style/lightTheme.css +++ b/src/style/lightTheme.css @@ -37,6 +37,7 @@ body{background-color: #e0e0e0;} .recommendDate{color: #616161;} .settingsButton {color: #424242; background-color: #BDBDBD;} .qualityTypes{background-color: #eeeeee;} +.speedTypes{background-color: #eeeeee;} .unsaved{color: #616161;} #subscriptions img{border: 0px solid #000000;} diff --git a/src/style/player.css b/src/style/player.css index d0ed363ef960c..219860a69880f 100644 --- a/src/style/player.css +++ b/src/style/player.css @@ -178,6 +178,40 @@ iframe{ right: 24px; } +.qualityTypes ul li{ + width: 72px; + position: relative; + right: 15px; +} + +.videoSpeed{ + width: 42px; +} + +.videoSpeed:hover .speedTypes{ + visibility: visible; +} + +.speedTypes{ + visibility: hidden; + width: 72px; + position: relative; + bottom: 10px; + right: 15px; +} + +.speedTypes ul{ + list-style-type: none; + position: relative; + right: 24px; +} + +.speedTypes ul li{ + width: 72px; + position: relative; + right: 15px; +} + #showComments{ text-align: center; height: 40px; diff --git a/src/templates/player.html b/src/templates/player.html index b42b2a0c0fb8e..58a395c1f1fd1 100644 --- a/src/templates/player.html +++ b/src/templates/player.html @@ -13,6 +13,21 @@ +
+ 1x +
+ +
+
{{savedText}}