diff --git a/tubearchivist/www/public/js/cast-videos.js b/tubearchivist/www/public/js/cast-videos.js
new file mode 100644
index 0000000..867093e
--- /dev/null
+++ b/tubearchivist/www/public/js/cast-videos.js
@@ -0,0 +1,148 @@
+function initializeCastApi() {
+ cast.framework.CastContext.getInstance().setOptions({
+ receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, // Use built in reciver app on cast device, see https://developers.google.com/cast/docs/styled_receiver if you want to be able to add a theme, splash screen or watermark. Has a $5 one time fee.
+ autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
+ });
+
+ var player = new cast.framework.RemotePlayer();
+ var playerController = new cast.framework.RemotePlayerController(player);
+
+ // Add event listerner to check if a connection to a cast device is initiated
+ playerController.addEventListener(
+ cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED, function() {
+ castConnectionChange(player)
+ }
+ );
+ playerController.addEventListener(
+ cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED, function() {
+ castVideoProgress(player)
+ }
+ );
+ playerController.addEventListener(
+ cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED, function() {
+ castVideoPaused(player)
+ }
+ );
+}
+
+
+function castConnectionChange(player) {
+ // If cast connection is initialized start cast
+ if (player.isConnected) {
+ // console.log("Cast Connected.");
+ castStart();
+ } else if (!player.isConnected) {
+ // console.log("Cast Disconnected.");
+ }
+}
+
+function castVideoProgress(player) {
+ var videoId = getVideoPlayerVideoId();
+ if (player.mediaInfo.contentId.includes(videoId)) {
+ var currentTime = player.currentTime;
+ var duration = player.duration;
+ if ((currentTime % 10) <= 1.0 && currentTime != 0 && duration != 0) { // Check progress every 10 seconds or else progress is checked a few times a second
+ postVideoProgress(videoId, currentTime);
+ setProgressBar(videoId, currentTime, duration);
+ if (!getVideoPlayerWatchStatus()) { // Check if video is already marked as watched
+ if (watchedThreshold(currentTime, duration)) {
+ isWatched(videoId);
+ }
+ }
+ }
+ }
+}
+
+function castVideoPaused(player) {
+ var videoId = getVideoPlayerVideoId();
+ var currentTime = player.currentTime;
+ var duration = player.duration;
+ if (player.mediaInfo != null) {
+ if (player.mediaInfo.contentId.includes(videoId)) {
+ if (currentTime != 0 && duration != 0) {
+ postVideoProgress(videoId, currentTime);
+ }
+ }
+ }
+}
+
+function castStart() {
+ var castSession = cast.framework.CastContext.getInstance().getCurrentSession();
+ // Check if there is already media playing on the cast target to prevent recasting on page reload or switching to another video page
+ if (!castSession.getMediaSession()) {
+ var videoId = getVideoPlayerVideoId();
+ var videoData = getVideoData(videoId);
+ var contentId = getURL() + videoData.data.media_url;
+ var contentTitle = videoData.data.title;
+ var contentImage = getURL() + videoData.data.vid_thumb_url;
+
+ contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
+ contentCurrentTime = getVideoPlayerCurrentTime(); // Get video's current position
+ contentActiveSubtitle = [];
+ // Check if a subtitle is turned on.
+ for (var i = 0; i < getVideoPlayer().textTracks.length; i++) {
+ if (getVideoPlayer().textTracks[i].mode == "showing") {
+ contentActiveSubtitle =[i + 1];
+ }
+ }
+ contentSubtitles = [];
+ var videoSubtitles = videoData.data.subtitles; // Array of subtitles
+ if (typeof(videoSubtitles) != 'undefined' && videoData.config.downloads.subtitle) {
+ for (var i = 0; i < videoSubtitles.length; i++) {
+ subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
+ subtitle.trackContentId = videoSubtitles[i].media_url;
+ subtitle.trackContentType = 'text/vtt';
+ subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
+ subtitle.name = videoSubtitles[i].name;
+ subtitle.language = videoSubtitles[i].lang;
+ subtitle.customData = null;
+ contentSubtitles.push(subtitle);
+ }
+ }
+
+ mediaInfo = new chrome.cast.media.MediaInfo(contentId, contentType); // Create MediaInfo var that contains url and content type
+ // mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED; // Set type of stream, BUFFERED, LIVE, OTHER
+ mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); // Create metadata var and add it to MediaInfo
+ mediaInfo.metadata.title = contentTitle.replace("&", "&"); // Set the video title
+ mediaInfo.metadata.images = [new chrome.cast.Image(contentImage)]; // Set the video thumbnail
+ // mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle();
+ mediaInfo.tracks = contentSubtitles;
+
+ var request = new chrome.cast.media.LoadRequest(mediaInfo); // Create request with the previously set MediaInfo.
+ // request.queueData = new chrome.cast.media.QueueData(); // See https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueData for playlist support.
+ request.currentTime = shiftCurrentTime(contentCurrentTime); // Set video start position based on the browser video position
+ request.activeTrackIds = contentActiveSubtitle; // Set active subtitle based on video player
+ // request.autoplay = false; // Set content to auto play, true by default
+ castSession.loadMedia(request).then(
+ function() {
+ castSuccessful();
+ },
+ function() {
+ castFailed(errorCode);
+ }
+ ); // Send request to cast device
+ }
+}
+
+function shiftCurrentTime(contentCurrentTime) { // Shift media back 3 seconds to prevent missing some of the content
+ if (contentCurrentTime > 5) {
+ return(contentCurrentTime - 3);
+ } else {
+ return(0);
+ }
+}
+
+function castSuccessful() {
+ // console.log('Cast Successful.');
+ getVideoPlayer().pause(); // Pause browser video on successful cast
+}
+
+function castFailed(errorCode) {
+ console.log('Error code: ' + errorCode);
+}
+
+window['__onGCastApiAvailable'] = function(isAvailable) {
+ if (isAvailable) {
+ initializeCastApi();
+ }
+}
diff --git a/tubearchivist/www/public/js/progress.js b/tubearchivist/www/public/js/progress.js
new file mode 100644
index 0000000..34a586e
--- /dev/null
+++ b/tubearchivist/www/public/js/progress.js
@@ -0,0 +1,106 @@
+/**
+ * Handle multi channel notifications
+ *
+ */
+
+checkMessages()
+
+// page map to notification status
+const messageTypes = {
+ "download": ["message:download", "message:add", "message:rescan", "message:playlistscan"],
+ "channel": ["message:subchannel"],
+ "channel_id": ["message:playlistscan"],
+ "playlist": ["message:subplaylist"],
+ "setting": ["message:setting"]
+}
+
+// start to look for messages
+function checkMessages() {
+ var notifications = document.getElementById("notifications");
+ if (notifications) {
+ var dataOrigin = notifications.getAttribute("data");
+ getMessages(dataOrigin);
+ }
+}
+
+// get messages for page on timer
+function getMessages(dataOrigin) {
+ fetch('/progress/').then(response => {
+ return response.json();
+ }).then(responseData => {
+ var messages = buildMessage(responseData, dataOrigin);
+ if (messages.length > 0) {
+ // restart itself
+ setTimeout(function() {
+ getMessages(dataOrigin);
+ }, 3000);
+ };
+ });
+}
+
+// make div for all messages, return relevant
+function buildMessage(responseData, dataOrigin) {
+ // filter relevan messages
+ var allMessages = responseData["messages"];
+ var messages = allMessages.filter(function(value) {
+ return messageTypes[dataOrigin].includes(value["status"])
+ }, dataOrigin);
+ // build divs
+ var notificationDiv = document.getElementById("notifications");
+ var nots = notificationDiv.childElementCount;
+ notificationDiv.innerHTML = "";
+ for (let i = 0; i < messages.length; i++) {
+ var messageData = messages[i];
+ var messageStatus = messageData["status"];
+ var messageBox = document.createElement("div");
+ var title = document.createElement("h3");
+ title.innerHTML = messageData["title"];
+ var message = document.createElement("p");
+ message.innerHTML = messageData["message"];
+ messageBox.appendChild(title);
+ messageBox.appendChild(message);
+ messageBox.classList.add(messageData["level"], "notification");
+ notificationDiv.appendChild(messageBox);
+ if (messageStatus === "message:download") {
+ checkDownloadIcons();
+ };
+ };
+ // reload page when no more notifications
+ if (nots > 0 && messages.length === 0) {
+ location.reload();
+ };
+ return messages
+}
+
+// check if download icons are needed
+function checkDownloadIcons() {
+ var iconBox = document.getElementById("downloadControl");
+ if (iconBox.childElementCount === 0) {
+ var downloadIcons = buildDownloadIcons();
+ iconBox.appendChild(downloadIcons);
+ };
+}
+
+// add dl control icons
+function buildDownloadIcons() {
+ var downloadIcons = document.createElement('div');
+ downloadIcons.classList = 'dl-control-icons';
+ // stop icon
+ var stopIcon = document.createElement('img');
+ stopIcon.setAttribute('id', "stop-icon");
+ stopIcon.setAttribute('title', "Stop Download Queue");
+ stopIcon.setAttribute('src', "/static/img/icon-stop.svg");
+ stopIcon.setAttribute('alt', "stop icon");
+ stopIcon.setAttribute('onclick', 'stopQueue()');
+ // kill icon
+ var killIcon = document.createElement('img');
+ killIcon.setAttribute('id', "kill-icon");
+ killIcon.setAttribute('title', "Kill Download Queue");
+ killIcon.setAttribute('src', "/static/img/icon-close.svg");
+ killIcon.setAttribute('alt', "kill icon");
+ killIcon.setAttribute('onclick', 'killQueue()');
+ // stich together
+ downloadIcons.appendChild(stopIcon);
+ downloadIcons.appendChild(killIcon);
+ return downloadIcons
+}
diff --git a/tubearchivist/www/public/js/script.js b/tubearchivist/www/public/js/script.js
new file mode 100644
index 0000000..bc9696d
--- /dev/null
+++ b/tubearchivist/www/public/js/script.js
@@ -0,0 +1,902 @@
+
+function sortChange(sortValue) {
+ var payload = JSON.stringify({'sort_order': sortValue});
+ sendPost(payload);
+ setTimeout(function(){
+ location.reload();
+ return false;
+ }, 500);
+}
+
+// Updates video watch status when passed a video id and it's current state (ex if the video was unwatched but you want to mark it as watched you will pass "unwatched")
+function updateVideoWatchStatus(input1, videoCurrentWatchStatus) {
+ if (videoCurrentWatchStatus) {
+ videoId = input1;
+ } else if (input1.getAttribute("data-id")) {
+ videoId = input1.getAttribute("data-id");
+ videoCurrentWatchStatus = input1.getAttribute("data-status");
+ }
+
+ postVideoProgress(videoId, 0); // Reset video progress on watched/unwatched;
+ removeProgressBar(videoId);
+
+ if (videoCurrentWatchStatus == "watched") {
+ var watchStatusIndicator = createWatchStatusIndicator(videoId, "unwatched");
+ var payload = JSON.stringify({'un_watched': videoId});
+ sendPost(payload);
+ } else if (videoCurrentWatchStatus == "unwatched") {
+ var watchStatusIndicator = createWatchStatusIndicator(videoId, "watched");
+ var payload = JSON.stringify({'watched': videoId});
+ sendPost(payload);
+ }
+
+ var watchButtons = document.getElementsByClassName("watch-button");
+ for (let i = 0; i < watchButtons.length; i++) {
+ if (watchButtons[i].getAttribute("data-id") == videoId) {
+ watchButtons[i].outerHTML = watchStatusIndicator;
+ }
+ }
+}
+
+// Creates a watch status indicator when passed a video id and the videos watch status
+function createWatchStatusIndicator(videoId, videoWatchStatus) {
+ if (videoWatchStatus == "watched") {
+ var seen = "seen";
+ var title = "Mark as unwatched";
+ } else if (videoWatchStatus == "unwatched") {
+ var seen = "unseen";
+ var title = "Mark as watched";
+ }
+ var watchStatusIndicator = ``;
+ return watchStatusIndicator;
+}
+
+// function isWatched(youtube_id) {
+// var payload = JSON.stringify({'watched': youtube_id});
+// sendPost(payload);
+// var seenIcon = document.createElement('img');
+// seenIcon.setAttribute('src', "/static/img/icon-seen.svg");
+// seenIcon.setAttribute('alt', 'seen-icon');
+// seenIcon.setAttribute('id', youtube_id);
+// seenIcon.setAttribute('title', "Mark as unwatched");
+// seenIcon.setAttribute('onclick', "isUnwatched(this.id)");
+// seenIcon.classList = 'seen-icon';
+// document.getElementById(youtube_id).replaceWith(seenIcon);
+// }
+
+// Removes the progress bar when passed a video id
+function removeProgressBar(videoId) {
+ setProgressBar(videoId, 0, 1);
+}
+
+function isWatchedButton(button) {
+ youtube_id = button.getAttribute("data-id");
+ var payload = JSON.stringify({'watched': youtube_id});
+ button.remove();
+ sendPost(payload);
+ setTimeout(function(){
+ location.reload();
+ return false;
+ }, 1000);
+}
+
+// function isUnwatched(youtube_id) {
+// postVideoProgress(youtube_id, 0); // Reset video progress on unwatched;
+// var payload = JSON.stringify({'un_watched': youtube_id});
+// sendPost(payload);
+// var unseenIcon = document.createElement('img');
+// unseenIcon.setAttribute('src', "/static/img/icon-unseen.svg");
+// unseenIcon.setAttribute('alt', 'unseen-icon');
+// unseenIcon.setAttribute('id', youtube_id);
+// unseenIcon.setAttribute('title', "Mark as watched");
+// unseenIcon.setAttribute('onclick', "isWatched(this.id)");
+// unseenIcon.classList = 'unseen-icon';
+// document.getElementById(youtube_id).replaceWith(unseenIcon);
+// }
+
+function unsubscribe(id_unsub) {
+ var payload = JSON.stringify({'unsubscribe': id_unsub});
+ sendPost(payload);
+ var message = document.createElement('span');
+ message.innerText = "You are unsubscribed.";
+ document.getElementById(id_unsub).replaceWith(message);
+}
+
+function subscribe(id_sub) {
+ var payload = JSON.stringify({'subscribe': id_sub});
+ sendPost(payload);
+ var message = document.createElement('span');
+ message.innerText = "You are subscribed.";
+ document.getElementById(id_sub).replaceWith(message);
+}
+
+function changeView(image) {
+ var sourcePage = image.getAttribute("data-origin");
+ var newView = image.getAttribute("data-value");
+ var payload = JSON.stringify({'change_view': sourcePage + ":" + newView});
+ sendPost(payload);
+ setTimeout(function(){
+ location.reload();
+ return false;
+ }, 500);
+}
+
+function toggleCheckbox(checkbox) {
+ // pass checkbox id as key and checkbox.checked as value
+ var toggleId = checkbox.id;
+ var toggleVal = checkbox.checked;
+ var payloadDict = {};
+ payloadDict[toggleId] = toggleVal;
+ var payload = JSON.stringify(payloadDict);
+ sendPost(payload);
+ setTimeout(function(){
+ var currPage = window.location.pathname;
+ window.location.replace(currPage);
+ return false;
+ }, 500);
+}
+
+// download page buttons
+function rescanPending() {
+ var payload = JSON.stringify({'rescan_pending': true});
+ animate('rescan-icon', 'rotate-img');
+ sendPost(payload);
+ setTimeout(function(){
+ checkMessages();
+ }, 500);
+}
+
+function dlPending() {
+ var payload = JSON.stringify({'dl_pending': true});
+ animate('download-icon', 'bounce-img');
+ sendPost(payload);
+ setTimeout(function(){
+ checkMessages();
+ }, 500);
+}
+
+function toIgnore(button) {
+ var youtube_id = button.getAttribute('data-id');
+ var payload = JSON.stringify({'ignore': youtube_id});
+ sendPost(payload);
+ document.getElementById('dl-' + youtube_id).remove();
+}
+
+function downloadNow(button) {
+ var youtube_id = button.getAttribute('data-id');
+ var payload = JSON.stringify({'dlnow': youtube_id});
+ sendPost(payload);
+ document.getElementById(youtube_id).remove();
+ setTimeout(function(){
+ checkMessages();
+ }, 500);
+}
+
+function forgetIgnore(button) {
+ var youtube_id = button.getAttribute('data-id');
+ var payload = JSON.stringify({'forgetIgnore': youtube_id});
+ sendPost(payload);
+ document.getElementById("dl-" + youtube_id).remove();
+}
+
+function addSingle(button) {
+ var youtube_id = button.getAttribute('data-id');
+ var payload = JSON.stringify({'addSingle': youtube_id});
+ sendPost(payload);
+ document.getElementById("dl-" + youtube_id).remove();
+ setTimeout(function(){
+ checkMessages();
+ }, 500);
+}
+
+function deleteQueue(button) {
+ var to_delete = button.getAttribute('data-id');
+ var payload = JSON.stringify({'deleteQueue': to_delete});
+ sendPost(payload);
+ setTimeout(function(){
+ location.reload();
+ return false;
+ }, 1000);
+}
+
+function stopQueue() {
+ var payload = JSON.stringify({'queue': 'stop'});
+ sendPost(payload);
+ document.getElementById('stop-icon').remove();
+}
+
+function killQueue() {
+ var payload = JSON.stringify({'queue': 'kill'});
+ sendPost(payload);
+ document.getElementById('kill-icon').remove();
+}
+
+// settings page buttons
+function manualImport() {
+ var payload = JSON.stringify({'manual-import': true});
+ sendPost(payload);
+ // clear button
+ var message = document.createElement('p');
+ message.innerText = 'processing import';
+ var toReplace = document.getElementById('manual-import');
+ toReplace.innerHTML = '';
+ toReplace.appendChild(message);
+}
+
+function reEmbed() {
+ var payload = JSON.stringify({'re-embed': true});
+ sendPost(payload);
+ // clear button
+ var message = document.createElement('p');
+ message.innerText = 'processing thumbnails';
+ var toReplace = document.getElementById('re-embed');
+ toReplace.innerHTML = '';
+ toReplace.appendChild(message);
+}
+
+function dbBackup() {
+ var payload = JSON.stringify({'db-backup': true});
+ sendPost(payload);
+ // clear button
+ var message = document.createElement('p');
+ message.innerText = 'backing up archive';
+ var toReplace = document.getElementById('db-backup');
+ toReplace.innerHTML = '';
+ toReplace.appendChild(message);
+}
+
+function dbRestore(button) {
+ var fileName = button.getAttribute("data-id");
+ var payload = JSON.stringify({'db-restore': fileName});
+ sendPost(payload);
+ // clear backup row
+ var message = document.createElement('p');
+ message.innerText = 'restoring from backup';
+ var toReplace = document.getElementById(fileName);
+ toReplace.innerHTML = '';
+ toReplace.appendChild(message);
+}
+
+function fsRescan() {
+ var payload = JSON.stringify({'fs-rescan': true});
+ sendPost(payload);
+ // clear button
+ var message = document.createElement('p');
+ message.innerText = 'File system scan in progress';
+ var toReplace = document.getElementById('fs-rescan');
+ toReplace.innerHTML = '';
+ toReplace.appendChild(message);
+}
+
+function resetToken() {
+ var payload = JSON.stringify({'reset-token': true});
+ sendPost(payload);
+ var message = document.createElement("p");
+ message.innerText = "Token revoked";
+ document.getElementById("text-reveal").replaceWith(message);
+}
+
+// delete from file system
+function deleteConfirm() {
+ to_show = document.getElementById("delete-button");
+ document.getElementById("delete-item").style.display = 'none';
+ to_show.style.display = "block";
+}
+
+function deleteVideo(button) {
+ var to_delete = button.getAttribute("data-id");
+ var to_redirect = button.getAttribute("data-redirect");
+ var payload = JSON.stringify({"delete-video": to_delete});
+ sendPost(payload);
+ setTimeout(function(){
+ var redirect = "/channel/" + to_redirect;
+ window.location.replace(redirect);
+ return false;
+ }, 1000);
+}
+
+function deleteChannel(button) {
+ var to_delete = button.getAttribute("data-id");
+ var payload = JSON.stringify({"delete-channel": to_delete});
+ sendPost(payload);
+ setTimeout(function(){
+ window.location.replace("/channel/");
+ return false;
+ }, 1000);
+}
+
+function deletePlaylist(button) {
+ var playlist_id = button.getAttribute("data-id");
+ var playlist_action = button.getAttribute("data-action");
+ var payload = JSON.stringify({
+ "delete-playlist": {
+ "playlist-id": playlist_id,
+ "playlist-action": playlist_action
+ }
+ });
+ sendPost(payload);
+ setTimeout(function(){
+ window.location.replace("/playlist/");
+ return false;
+ }, 1000);
+}
+
+function cancelDelete() {
+ document.getElementById("delete-button").style.display = 'none';
+ document.getElementById("delete-item").style.display = 'block';
+}
+
+// player
+function createPlayer(button) {
+ var videoId = button.getAttribute('data-id');
+ var videoData = getVideoData(videoId);
+ var videoProgress = getVideoProgress(videoId).position;
+ var videoName = videoData.data.title;
+
+ var videoTag = createVideoTag(videoData, videoProgress);
+
+ var playlist = '';
+ var videoPlaylists = videoData.data.playlist; // Array of playlists the video is in
+ if (typeof(videoPlaylists) != 'undefined') {
+ var subbedPlaylists = getSubbedPlaylists(videoPlaylists); // Array of playlist the video is in that are subscribed
+ if (subbedPlaylists.length != 0) {
+ var playlistData = getPlaylistData(subbedPlaylists[0]); // Playlist data for first subscribed playlist
+ var playlistId = playlistData.playlist_id;
+ var playlistName = playlistData.playlist_name;
+ var playlist = `