/* 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 . */ let popularTimer; let checkPopular = true; let trendingTimer; let checkTrending = true; const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; /** * Perform a search using the YouTube API. The search query is grabbed from the #search element. * * @param {string} page - Optional: The page token to be inlcuded in the search. * * @return {Void} */ function search(page = 1) { const query = document.getElementById('search').value; const searchSortby = document.getElementById('searchSortby').value; const searchType = document.getElementById('searchType').value; const searchDate = document.getElementById('searchDate').value; const searchDuration = document.getElementById('searchDuration').value; searchFilter.seen = false; if (query === '') { return; } if (page === 1) { hideViews(); headerView.seen = true; headerView.title = 'Search Results'; searchView.videoList = []; searchView.seen = true; } else { console.log(page); showToast('Fetching results. Please wait...'); } invidiousAPI('search', '', { q: query, page: page, sort_by: searchSortby, date: searchDate, duration: searchDuration, type: searchType, }, function (data) { console.log(data); data.forEach((video) => { switch (video.type) { case 'video': displayVideo(video, 'search'); break; case 'channel': displayChannel(video); break; case 'playlist': if (video.videoCount > 0) { displayPlaylist(video); } break; default: } }); searchView.page = searchView.page + 1; loadingView.seen = false; }) } /** * Display a video on the page. Function is typically contained in a loop. * * @param {video} video - The video ID of the video to be displayed. * @param {string} listType - Optional: Specifies the list type of the video * Used for displaying the remove icon for history and saved videos. * * @return {Void} */ function displayVideo(videoData, listType = '') { if (videoData.paid) { return; } let video = {}; video.id = videoData.videoId; if (videoData.type == 'playlist') { video.isPlaylist = true; } historyDb.find({ videoId: video.id }, (err, docs) => { if (jQuery.isEmptyObject(docs)) { // Do nothing } else { video.watched = true; } video.views = videoData.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); let time = videoData.lengthSeconds; if (videoData.liveNow === true || time < 0){ video.liveText = 'LIVE NOW'; video.duration = ''; video.publishedDate = ''; video.viewText = 'watching'; } else{ let now = Date.now(); video.liveText = ''; if (video.views <= 1) { video.viewText = 'view'; } else{ video.viewText = 'views'; } let published = new Date(videoData.published * 1000); let hours = 0; if (now < published.getTime()) { video.publishedDate = 'Premieres on ' + published.toLocaleString(); } else { if (time >= 3600) { hours = Math.floor(time / 3600); time = time - hours * 3600; } let minutes = Math.floor(time / 60); let seconds = time - minutes * 60; if (seconds < 10) { seconds = '0' + seconds; } if (minutes < 10 && hours > 0) { minutes = '0' + minutes; } if (hours > 0) { video.duration = hours + ":" + minutes + ":" + seconds; } else { video.duration = minutes + ":" + seconds; } video.publishedDate = videoData.publishedText; } } //const searchMenu = $('#videoListContainer').html(); // Include a remove icon in the list if the application is displaying the history list or saved videos. video.deleteHtml = () => { switch (listType) { case 'saved': return `
  • Remove Saved Video
  • `; case 'history': return `
  • Remove From History
  • `; } }; video.youtubeUrl = 'https://youtube.com/watch?v=' + video.id; video.invidiousUrl = 'https://invidio.us/watch?v=' + video.id; video.thumbnail = videoData.videoThumbnails[4].url; video.title = videoData.title; video.channelName = videoData.author; video.channelId = videoData.authorId; video.description = videoData.description; video.isVideo = true; switch (listType) { case 'subscriptions': subscriptionView.videoList = subscriptionView.videoList.concat(video); video.removeFromSave = true; break; case 'search': searchView.videoList = searchView.videoList.concat(video); video.removeFromSave = false; break; case 'popular': popularView.videoList = popularView.videoList.concat(video); video.removeFromSave = false; break; case 'trending': trendingView.videoList = trendingView.videoList.concat(video); video.removeFromSave = false break; case 'saved': savedView.videoList.splice(videoData.position, 0, video); video.removeFromSave = false; break; case 'history': historyView.videoList.splice(videoData.position, 0, video); video.removeFromSave = false; break; case 'channel': channelVideosView.videoList = channelVideosView.videoList.concat(video); video.removeFromSave = false; break; } }); } function displayChannel(channel) { let channelData = {}; channelData.channelId = channel.authorId; channelData.thumbnail = "https:" + channel.authorThumbnails[4].url; channelData.channelName = channel.author; channelData.description = channel.description; channelData.subscriberCount = channel.subCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); channelData.videoCount = channel.videoCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); channelData.isVideo = false; searchView.videoList = searchView.videoList.concat(channelData); } function displayPlaylist(playlist) { let playListData = {}; playListData.isPlaylist = true; playListData.isVideo = false; playListData.thumbnail = playlist.videos[0].videoThumbnails[4].url; playListData.channelName = playlist.author; playListData.channelId = playlist.authorId; playListData.id = playlist.playlistId; playListData.description = playlist.videos[0].title + "\r\n" + playlist.videos[1].title; playListData.title = playlist.title; playListData.videoCount = playlist.videoCount; if (playListData.channelName == 'YouTube' && playListData.title.includes('Mix')){ // Hide Mix playlists. return; } searchView.videoList = searchView.videoList.concat(playListData); } /** * Check if a link is a valid YouTube/HookTube link and play that video. Gets input * from the #jumpToInput element. * * @return {Void} */ function parseSearchText(url = '') { let input; if (url === '') { input = document.getElementById('search').value; } else { input = url.replace(/freetube\:\/\//, ''); } if (input === '') { return; } // The regex to get the video id from a YouTube link. Thanks StackOverflow. let rx = /^.*(?:(?:(you|hook)tu\.?be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/; let match = input.match(rx); ft.log('Video ID: ', match); let urlSplit = input.split('/'); if (match) { ft.log('Video found'); loadingView.seen = true; playVideo(match[2]); } else if (urlSplit[3] == 'channel') { ft.log('channel found'); loadingView.seen = true; goToChannel(urlSplit[4]); } else if (urlSplit[3] == 'user') { ft.log('user found'); loadingView.seen = true; goToChannel(urlSplit[4]); } else { ft.log('Video not found'); document.getElementById('search').value = decodeURIComponent(input); loadingView.seen = true; search(); } } /** * Grab the most popular videos over the last couple of days and display them. * * @return {Void} */ function showMostPopular() { if (checkPopular === false && popularView.videoList.length > 0) { console.log('Will not load popular. Timer still on.'); loadingView.seen = false; return; } else { checkPopular = false; } invidiousAPI('top', '', {}, function (data) { console.log(data); popularView.videoList = []; data.forEach((video) => { loadingView.seen = false; console.log(video); displayVideo(video, 'popular'); }); }); popularTimer = window.setTimeout(() => { checkPopular = true; }, 60000); } /** * Grab trending videos over the last couple of days and display them. * * @return {Void} */ function showTrending() { if (checkTrending === false && trendingView.videoList.length > 0) { console.log('Will not load trending. Timer still on.'); loadingView.seen = false; return; } else { checkTrending = false; } invidiousAPI('trending', '', {}, function (data) { console.log(data); popularView.videoList = []; data.forEach((video) => { loadingView.seen = false; console.log(video); displayVideo(video, 'trending'); }); }); trendingTimer = window.setTimeout(() => { checkTrending = true; }, 60000); } /** * Create a link of the video to Invidious or YouTube and copy it to the user's clipboard. * * @param {string} website - The website to watch the video on. * @param {string} videoId - The video ID of the video to add to the URL * * @return {Void} */ function copyLink(website, videoId) { // Create the URL and copy to the clipboard. if (website == "youtube") { const url = 'https://' + website + '.com/watch?v=' + videoId; clipboard.writeText(url); showToast('URL has been copied to the clipboard'); } if (website == "invidious") { website = "invidio"; const url = "https://" + website + ".us/watch?v=" + videoId; clipboard.writeText(url); showToast('URL has been copied to the clipboard'); } } /** * 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 * is why this check is needed. The video URL will typically be resolved over time. * * @param {string} video480p - The URL to the 480p video. * @param {string} video720p - The URL to the 720p video. */ function checkVideoUrls(video480p, video720p, videoAudio) { const currentQuality = $('#currentQuality').html(); let buttonEmbed = document.getElementById('qualityEmbed'); let valid480 = false; if (typeof (videoAudio) !== 'undefined') { let getAudioUrl = fetch(videoAudio); getAudioUrl.then((status) => { switch (status.status) { case 404: playerView.validAudio = false; break; case 403: showToast('This video is unavailable in your country.'); playerView.validAudio = false; return; break; default: ft.log('Audio is valid'); break; } }); } else{ playerView.validAudio = false; } if (typeof (video480p) !== 'undefined') { let get480pUrl = fetch(video480p); get480pUrl.then((status) => { switch (status.status) { case 404: showToast('Found valid URL for 480p, but returned a 404. Video type might be available in the future.'); playerView.valid480p = false; buttonEmbed.click(); return; break; case 403: showToast('This video is unavailable in your country.'); playerView.valid480p = false; return; break; default: ft.log('480p is valid'); if (currentQuality === '720p' && typeof (video720p) === 'undefined') { playerView.currentQuality = '480p'; } break; } }); } else{ playerView.valid480p = false; } if (typeof (video720p) !== 'undefined') { let get720pUrl = fetch(video720p); get720pUrl.then((status) => { switch (status.status) { case 404: showToast('Found valid URL for 720p, but returned a 404. Video type might be available in the future.'); playerView.valid720p = false; if (typeof (valid480) !== 'undefined') { playerView.currentQuality = '480p'; } break; case 403: showToast('This video is unavailable in your country.'); playerView.valid720p = false; return; break; default: ft.log('720p is valid'); break; } }); } else{ playerView.valid720p = false; } }