Merge branch 'master' into development

This commit is contained in:
Preston 2018-03-10 16:06:58 -05:00
commit 84ae24ea26
16 changed files with 686 additions and 376 deletions

57
CONTRIBUTING.md Normal file
View File

@ -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.

13
PULL_REQUEST_TEMPLATE.md Normal file
View File

@ -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**

View File

@ -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).
<a href='https://github.com/FreeTubeApp/FreeTube/releases' >Download</a>
@ -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)

View File

@ -11,10 +11,11 @@
<link rel="stylesheet" href="style/loading.css">
<link rel="stylesheet" href="style/fa-solid.min.css">
<link rel="stylesheet" href="style/fontawesome-all.min.css">
<script src="js/googleApi.js"></script>
<script src="js/youtubeApi.js"></script>
<script src="js/settings.js"></script>
<script src="js/layout.js"></script>
<script src="js/videos.js"></script>
<script src="js/player.js"></script>
<script src="js/subscriptions.js"></script>
<script src="js/channels.js"></script>
<script src="js/savedVideos.js"></script>

View File

@ -24,14 +24,11 @@ along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
/*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 <http://www.gnu.org/licenses/>.
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);
});
});

View File

@ -18,70 +18,190 @@ along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
/*
* 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);

File diff suppressed because one or more lines are too long

View File

@ -55,7 +55,7 @@ function removeFromHistory(videoId){
*/
function showHistory(){
clearMainContainer();
toggleLoading();
startLoadingAnimation();
console.log('checking history');
let videoList = '';
@ -75,18 +75,16 @@ function showHistory(){
});
}
let request = gapi.client.youtube.videos.list({
youtubeAPI('videos', {
part: 'snippet',
id: videoList,
maxResults: 50,
});
request.execute((response) => {
}, function (data) {
createVideoListContainer('Watch History:');
response['items'].forEach((video) => {
data['items'].forEach((video) => {
displayVideos(video, 'history');
});
toggleLoading();
stopLoadingAnimation()
});
});
}

View File

@ -20,7 +20,7 @@ along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
/*
* 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;

View File

@ -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();
});
}

339
src/js/player.js Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/*
* 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(/\&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'
});
// 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(/\&quot\;/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;
}

View File

@ -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();
});
});
}

View File

@ -21,6 +21,9 @@ along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
* 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 <http://www.gnu.org/licenses/>.
*/
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);
});
}

View File

@ -31,25 +31,22 @@ along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
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 = `<h2 class="message">Your Subscription list is currently empty. Start adding subscriptions
to see them here.<br /><br /><i class="far fa-frown" style="font-size: 200px"></i></h2>`;

View File

@ -19,7 +19,6 @@ along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
/*
* 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, '&quot;');
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(/\&quot\;/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

22
src/js/youtubeApi.js Normal file
View File

@ -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();
});
}