Features: Video Duration, Double click to full screen and more.

Loading Subscriptions is MUCH faster.
This commit is contained in:
Preston 2018-03-15 10:59:50 -04:00
parent 5729aa2c8b
commit 1ac6ce7dce
15 changed files with 425 additions and 180 deletions

View File

@ -65,7 +65,6 @@
"jquery": "^3.3.1",
"mustache": "^2.3.0",
"nedb": "^1.8.0",
"node-async-loop": "^1.2.2",
"opml-to-json": "0.0.3",
"youtube-dl": "^1.12.2"
}

View File

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -80,8 +80,12 @@ function goToChannel(channelId) {
order: 'date',
}, function (data) {
// Display recent uploads to #main
data['items'].forEach((video) => {
displayVideo(video);
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
videoList.items.forEach((video) => {
displayVideo(video, 'history');
});
});
});
});

View File

@ -199,6 +199,10 @@ let videoShortcutHandler = function(event) {
}
};
let fullscreenVideo = function(event){
$('.videoPlayer').get(0).webkitRequestFullscreen()
}
/**
* ---------------------------
* Bind click events
@ -208,6 +212,8 @@ $(document).on('click', '#showComments', showComments);
$(document).on('click', '.videoPlayer', playPauseVideo);
$(document).on('dblclick', '.videoPlayer', fullscreenVideo);
$(document).on('keydown', videoShortcutHandler);
$(document).on('click', '#confirmNo', hideConfirmFunction);

View File

@ -81,9 +81,13 @@ function showHistory(){
maxResults: 50,
}, function (data) {
createVideoListContainer('Watch History:');
data['items'].forEach((video) => {
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
videoList.items.forEach((video) => {
displayVideo(video, 'history');
});
});
stopLoadingAnimation()
});
});

View File

@ -69,17 +69,6 @@ function playVideo(videoId) {
return;
}
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.
@ -94,7 +83,7 @@ function playVideo(videoId) {
});
/*
* FreeTube calls youtube-dl server to grab the direct video URL.
* FreeTube calls youtube-dl to grab the direct video URL.
*/
youtubedlGetInfo(videoId, (info) => {
console.log(info);
@ -109,7 +98,6 @@ function playVideo(videoId) {
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.
@ -127,7 +115,6 @@ function playVideo(videoId) {
// Grab all subtitles
Object.keys(videoSubtitles).forEach((subtitle) => {
console.log(subtitle);
subtitleLabel = subtitle.toUpperCase();
subtitleUrl = videoSubtitles[subtitle]['url'];
@ -142,7 +129,6 @@ function playVideo(videoId) {
// 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_id']) {
case '18':
video480p = videoUrls[key]['url'];
@ -173,11 +159,18 @@ function playVideo(videoId) {
if (!useEmbedPlayer) {
videoHtml = '<video class="videoPlayer" onmousemove="hideMouseTimeout()" onmouseleave="removeMouseTimeout()" controls="" src="' + defaultUrl + '" poster="' + videoThumbnail + '" autoplay>' + subtitleHtml + '</video>';
console.log(videoHtml);
}
console.log(defaultUrl);
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';
}
});
// API Request
youtubeAPI('channels', {

View File

@ -143,9 +143,12 @@ function showSavedVideos(){
}, (data) => {
// Render the videos to the screen
createVideoListContainer('Saved Videos:');
data.items.forEach((video) => {
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
videoList.items.forEach((video) => {
displayVideo(video, 'saved');
});
});
stopLoadingAnimation();
});
});

View File

@ -89,8 +89,82 @@ function loadSubscriptions() {
// Welcome to callback hell, we hope you enjoy your stay.
subscriptions.then((results) => {
let channelId = '';
let videoList = [];
if (results.length > 0) {
let counter = 0;
for (let i = 0; i < results.length; i++) {
channelId = results[i]['channelId'];
youtubeAPI('search', {
part: 'snippet',
channelId: channelId,
type: 'video',
maxResults: 15,
order: 'date',
}, (data) => {
console.log(data);
videoList = videoList.concat(data.items);
counter++;
if (counter === results.length) {
videoList.sort((a, b) => {
const date1 = Date.parse(a.snippet.publishedAt);
const date2 = Date.parse(b.snippet.publishedAt);
return date2.valueOf() - date1.valueOf();
});
// Render the videos to the application.
createVideoListContainer('Latest Subscriptions:');
// The YouTube website limits the subscriptions to 100 before grabbing more so we only show 100
// to keep the app running at a good speed.
if (videoList.length < 50) {
let grabDuration = getDuration(videoList.slice(0, 49));
grabDuration.then((list) => {
list.items.forEach((video) => {
displayVideo(video);
});
});
} else {
console.log(videoList);
let finishedList = [];
let firstBatchDuration = getDuration(videoList.slice(0, 49));
firstBatchDuration.then((list1) => {
finishedList = finishedList.concat(list1.items);
let secondBatchDuration = getDuration(videoList.slice(50, 99));
secondBatchDuration.then((list2) => {
finishedList = finishedList.concat(list2.items);
finishedList.forEach((video) => {
displayVideo(video);
});
stopLoadingAnimation();
});
});
}
}
}
);
}
} else {
// User has no subscriptions. Display message.
const container = document.getElementById('main');
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>`;
}
// Yes, This function is the thing that needs to most improvment
/*
if (results.length > 0) {
showToast('Getting Subscriptions. This may take a while...');
@ -101,6 +175,7 @@ function loadSubscriptions() {
* while then sorting them afterwards, this was my best solution at the time. I'm sure someone more
* experienced in Node can help out with this.
*/
/*
asyncLoop(results, (sub, next) => {
const channelId = sub['channelId'];
@ -108,15 +183,16 @@ function loadSubscriptions() {
* Grab the channels 15 most recent uploads. Typically this should be enough.
* This number can be changed if we feel necessary.
*/
/*
youtubeAPI('search', {
part: 'snippet', // Try getting content details for video duration in the near future.
part: 'snippet',
channelId: channelId,
type: 'video',
maxResults: 15,
order: 'date',
}, function (data){
videoList = videoList.concat(data['items']);
// Iterate through the next object in the loop.
}, (data) => {
videoList = videoList.concat(data.items);
next();
});
}, (err) => {
@ -133,17 +209,30 @@ function loadSubscriptions() {
// The YouTube website limits the subscriptions to 100 before grabbing more so we only show 100
// to keep the app running at a good speed.
if(videoList.length < 100){
videoList.forEach((video) => {
console.log('Getting all videos');
if(videoList.length < 50){
let grabDuration = getDuration(videoList.slice(0,49));
grabDuration.then((list) => {
list.items.forEach((video) => {
displayVideo(video);
});
});
}
else{
console.log('Getting top 100 videos');
for(let i = 0; i < 100; i++){
displayVideo(videoList[i]);
}
let finishedList = []
let firstBatchDuration = getDuration(videoList.slice(0, 49));
firstBatchDuration.then((list1) => {
finishedList = finishedList.concat(list1.items);
let secondBatchDuration = getDuration(videoList.slice(50, 99));
secondBatchDuration.then((list2) => {
finishedList = finishedList.concat(list2.items);
finishedList.forEach((video) => {
displayVideo(video);
});
});
});
}
stopLoadingAnimation()
});
@ -155,6 +244,7 @@ function loadSubscriptions() {
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>`;
}
});*/
});
}
@ -241,7 +331,9 @@ function toggleSubscription(channelId) {
*/
function isSubscribed(channelId) {
return new Promise((resolve, reject) => {
subDb.find({channelId: channelId}, (err, docs) => {
subDb.find({
channelId: channelId
}, (err, docs) => {
if (jQuery.isEmptyObject(docs)) {
resolve(false);
} else {

View File

@ -41,20 +41,64 @@ function search(nextPageToken = '') {
youtubeAPI('search', {
q: query,
part: 'id, snippet',
part: 'id',
type: 'video',
pageToken: nextPageToken,
maxResults: 25,
}, function (data){
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
videoList.items.forEach(displayVideo);
});
if (nextPageToken === '') {
createVideoListContainer('Search results:');
stopLoadingAnimation();
}
data.items.forEach(displayVideo);
addNextPage(data.nextPageToken);
})
}
/**
* Grab the duration of the videos
*
* @param {array} data - An array of videos to get the duration from
*
* @return {promise} - The list of videos with the duration included.
*/
function getDuration(data){
return new Promise((resolve, reject) => {
let videoIdList = '';
for(let i = 0; i < data.length; i++){
if (videoIdList === ''){
if(typeof(data[i]['id']) === 'string'){
videoIdList = data[i]['id'];
}
else{
videoIdList = data[i]['id']['videoId'];
}
}
else{
if(typeof(data[i]['id']) === 'string'){
videoIdList = videoIdList + ', ' + data[i]['id'];
}
else{
videoIdList = videoIdList + ', ' + data[i]['id']['videoId'];
}
}
}
youtubeAPI('videos', {
part: 'snippet, contentDetails',
id: videoIdList
}, (data) => {
resolve(data);
});
});
}
/**
* Display a video on the page. Function is typically contained in a loop.
*
@ -66,9 +110,14 @@ function search(nextPageToken = '') {
*/
function displayVideo(video, listType = '') {
const videoSnippet = video.snippet;
const videoDuration = parseVideoDuration(video.contentDetails.duration);
//const videoDuration = '00:00';
// Grab the published date for the video and convert to a user readable state.
const dateString = new Date(videoSnippet.publishedAt);
const publishedDate = dateFormat(dateString, "mmm dS, yyyy");
const searchMenu = $('#videoListContainer').html();
const videoId = video.id;
@ -94,6 +143,7 @@ function displayVideo(video, listType = '') {
channelName: videoSnippet.channelTitle,
videoDescription: videoSnippet.description,
channelId: videoSnippet.channelId,
videoDuration: videoDuration,
publishedDate: publishedDate,
liveText: liveText,
deleteHtml: deleteHtml,
@ -143,28 +193,32 @@ function addNextPage(nextPageToken) {
*/
function showVideoRecommendations(videoId) {
youtubeAPI('search', {
part: 'snippet',
part: 'id',
type: 'video',
relatedToVideoId: videoId,
maxResults: 15,
}, function (data){
const recommendations = data.items;
recommendations.forEach((data) => {
const snippet = data.snippet;
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
videoList.items.forEach((video) => {
const snippet = video.snippet;
const videoDuration = parseVideoDuration(video.contentDetails.duration);
const recommTemplate = require('./templates/recommendations.html')
mustache.parse(recommTemplate);
const rendered = mustache.render(recommTemplate, {
videoId: data.id.videoId,
videoId: video.id,
videoTitle: snippet.title,
channelName: snippet.channelTitle,
videoThumbnail: snippet.thumbnails.medium.url,
videoDuration: videoDuration,
publishedDate: dateFormat(snippet.publishedAt, "mmm dS, yyyy")
});
const recommendationHtml = $('#recommendations').html();
$('#recommendations').html(recommendationHtml + rendered);
});
});
});
}
/**
@ -195,6 +249,57 @@ function parseVideoLink() {
}
}
/**
* Convert duration into a more readable format
*
* @param {string} durationString - The string containing the video duration. Formated as 'PT12H34M56S'
*
* @return {string} - The formated string. Ex: 12:34:56
*/
function parseVideoDuration(durationString){
let match = durationString.match(/PT(\d+H)?(\d+M)?(\d+S)?/);
let duration = '';
match = match.slice(1).map(function(x) {
if (x != null) {
return x.replace(/\D/, '');
}
});
let hours = (parseInt(match[0]) || 0);
let minutes = (parseInt(match[1]) || 0);
let seconds = (parseInt(match[2]) || 0);
if (hours != 0){
duration = hours + ':';
}
else{
duration = minutes + ':';
}
if (hours != 0 && minutes < 10){
duration = duration + '0' + minutes + ':';
}
else if (hours != 0 && minutes > 10){
duration = duration + minutes + ':';
}
else if (hours != 0 && minutes == 0){
duration = duration + '00:';
}
if (seconds == 0){
duration = duration + '00';
}
else if (seconds < 10){
duration = duration + '0' + seconds;
}
else{
duration = duration + seconds;
}
return duration;
}
/**
* Grab the most popular videos over the last couple of days and display them.
*
@ -213,15 +318,21 @@ function showMostPopular() {
// Applications grab these. Videos in the 'Trending' tab on YouTube will be different.
// And there is no way to grab those videos.
youtubeAPI('search', {
part: 'snippet',
part: 'id',
order: 'viewCount',
type: 'video',
publishedAfter: d.toISOString(),
maxResults: 50,
}, function (data){
createVideoListContainer('Most Popular:');
console.log(data);
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
console.log(videoList);
videoList.items.forEach(displayVideo);
});
stopLoadingAnimation();
data.items.forEach(displayVideo);
});
}

View File

@ -18,10 +18,15 @@ iframe{
.videoThumbnail {
width: 275px;
height: 160px;
float: left;
cursor: pointer;
}
.videoThumbnail img{
width: 100%;
}
.videoTitle {
font-weight: bold;
margin-left: 285px;
@ -39,9 +44,26 @@ iframe{
margin-left: 285px;
font-size: 13px;
cursor: pointer;
height: 45px;
overflow: hidden;
}
.videoPlayer{width: 100%;}
.videoDuration {
display: inline-block;
float: right;
position: relative;
bottom: 36px;
color: white;
background-color: black;
opacity: 0.7;
padding: 2px;
font-size: 13px;
text-align: right;
}
.videoPlayer {
width: 100%;
}
.statistics {
padding: 20px;
@ -231,9 +253,14 @@ iframe{
.recommendThumbnail {
width: 250px;
height: 145px;
float: left;
}
.recommendThumbnail img{
width: 100%;
}
.recommendTitle {
font-size: 16px;
font-weight: bold;

View File

@ -1,5 +1,8 @@
<div class='recommendVideo' onclick='playVideo("{{videoId}}")'>
<image class='recommendThumbnail' src='{{videoThumbnail}}'></image>
<div class='recommendThumbnail'>
<img src='{{videoThumbnail}}'></img>
<p onclick='playVideo("{{videoId}}")' class='videoDuration'>{{videoDuration}}</p>
</div>
<p class='recommendTitle'>{{videoTitle}}</p>
<p class='recommendChannel'>{{channelName}}</p>
<p class='recommendDate'>{{publishedDate}}</p>

View File

@ -1,6 +1,9 @@
<div class='video'>
{{{deleteHtml}}}
<img onclick='playVideo("{{videoId}}")' src={{videoThumbnail}} class='videoThumbnail' />
<div class='videoThumbnail'>
<img onclick='playVideo("{{videoId}}")' src={{videoThumbnail}} />
<p onclick='playVideo("{{videoId}}")' class='videoDuration'>{{videoDuration}}</p>
</div>
<p onclick='playVideo("{{videoId}}")' class='videoTitle'>{{videoTitle}}</p>
<p onclick='goToChannel("{{channelId}}")' class='channelName'>{{channelName}} - {{publishedDate}}</p>
<p onclick='playVideo("{{videoId}}")' class='videoDescription'>{{videoDescription}}</p>