Skip to content

Commit

Permalink
Merge branch 'development'
Browse files Browse the repository at this point in the history
  • Loading branch information
PrestonN committed Mar 8, 2018
2 parents 37007e1 + dbb3a6a commit 3ebcde1
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 75 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

<a href='https://github.com/FreeTubeApp/FreeTube/releases' >Download</a>

Expand All @@ -13,7 +13,7 @@ Please note that FreeTube is currently in Beta and using the proprietary and obf
<img src="https://freetubeapp.github.io/images/FreeTube5.png" width=200 >

# 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
Expand Down
2 changes: 1 addition & 1 deletion src/js/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
16 changes: 10 additions & 6 deletions src/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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)];
Expand Down Expand Up @@ -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;
Expand All @@ -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);
});
Expand All @@ -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 {
Expand Down Expand Up @@ -169,7 +173,7 @@ function updateSettings() {
settingsDb.update({
_id: 'apiKey'
}, {
value: 'AIzaSyDjszXMCw44W_k-pdNoOxUHFyKGtU_ejwE'
value: apiKey
}, {});
}

Expand Down
2 changes: 1 addition & 1 deletion src/js/subscriptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
144 changes: 79 additions & 65 deletions src/js/videos.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,22 @@ function playVideo(videoId) {
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 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.');
Expand All @@ -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(/\&quot\;/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 = '<video class="videoPlayer" onmousemove="hideMouseTimeout()" onmouseleave="removeMouseTimeout()" controls="" src="' + video480p + '" poster="' + videoThumbnail + '" autoplay></video>';
defaultQuality = '480p';
} else {
// Default to the 720p video.
videoHtml = '<video class="videoPlayer" onmousemove="hideMouseTimeout()" onmouseleave="removeMouseTimeout()" controls="" src="' + video720p + '" poster="' + videoThumbnail + '" autoplay></video>';
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);

Expand All @@ -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(/\&quot\;/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 = '<video class="videoPlayer" onmousemove="hideMouseTimeout()" onmouseleave="removeMouseTimeout()" controls="" src="' + video480p + '" poster="' + videoThumbnail + '" autoplay></video>';
defaultQuality = '480p';
} else {
// Default to the 720p video.
videoHtml = '<video class="videoPlayer" onmousemove="hideMouseTimeout()" onmouseleave="removeMouseTimeout()" controls="" src="' + video720p + '" poster="' + videoThumbnail + '" autoplay></video>';
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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
});

Expand All @@ -518,7 +517,11 @@ function getEmbedPlayer(videoId) {
embedHtml = embedHtml.replace('width="480px"', '');
embedHtml = embedHtml.replace('height="270px"', '');
embedHtml = embedHtml.replace(/\"/g, '&quot;');
resolve(embedHtml);
data[0] = embedHtml;
data[1] = response['items'][0]['snippet']['channelId'];


resolve(data);
});
});

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/style/darkTheme.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;}
Expand Down
1 change: 1 addition & 0 deletions src/style/lightTheme.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;}
Expand Down
34 changes: 34 additions & 0 deletions src/style/player.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 3ebcde1

Please sign in to comment.