Format/slightly modernize the JS (#345)

* add basic JS tooling

* fix accidental uses of global variables

* auto-format

* add and fix a couple more standard lint rules

* remove useless return false from settimeout callbacks

* document JS contributing

* fix whitespace in package.json

* add JS stuff to codespell skiplist

* codespell take two

* update github action and add comments about duplicated logic
This commit is contained in:
Kevin Gibbons 2022-10-25 19:43:58 -07:00 committed by GitHub
parent 700a8cb54a
commit 39902cb1c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 2879 additions and 1029 deletions

17
.eslintrc.js Normal file
View File

@ -0,0 +1,17 @@
'use strict';
module.exports = {
extends: ['eslint:recommended', 'eslint-config-prettier'],
parserOptions: {
ecmaVersion: 2020,
},
env: {
browser: true,
},
rules: {
strict: ['error', 'global'],
'no-unused-vars': ['error', { vars: 'local' }],
eqeqeq: ['error', 'always', { null: 'ignore' }],
curly: ['error', 'multi-line'],
'no-var': 'error',
},
};

View File

@ -6,11 +6,13 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
# note: this logic is duplicated in the `validate` function in ./deploy.sh
# if you update this file, you should update that as well
- run: pip install --upgrade pip wheel - run: pip install --upgrade pip wheel
- run: pip install bandit black codespell flake8 flake8-bugbear - run: pip install bandit black codespell flake8 flake8-bugbear
flake8-comprehensions isort flake8-comprehensions isort
- run: black --check --diff --line-length 79 . - run: black --check --diff --line-length 79 .
- run: codespell - run: codespell --skip="./.git,./package.json,./package-lock.json,./node_modules"
- run: flake8 . --count --max-complexity=10 --max-line-length=79 - run: flake8 . --count --max-complexity=10 --max-line-length=79
--show-source --statistics --show-source --statistics
- run: isort --check-only --line-length 79 --profile black . - run: isort --check-only --line-length 79 --profile black .

5
.gitignore vendored
View File

@ -5,4 +5,7 @@ __pycache__
db.sqlite3 db.sqlite3
# vscode custom conf # vscode custom conf
.vscode .vscode
# JavaScript stuff
node_modules

View File

@ -76,6 +76,10 @@ Do you see anything on the roadmap that you would like to take a closer look at
To fix a bug or implement a feature, fork the repository and make all changes to the testing branch. When ready, create a pull request. To fix a bug or implement a feature, fork the repository and make all changes to the testing branch. When ready, create a pull request.
## Making changes to the JavaScript
The JavaScript does not require any build step; you just edit the files directly. However, there is config for eslint and prettier (a linter and formatter respectively); their use is recommended but not required. To use them, install `node`, run `npm i` from the root directory of this repository to install dependencies, then run `npm run lint` and `npm run format` to run eslint and prettier respectively.
## Releases ## Releases
There are three different docker tags: There are three different docker tags:

View File

@ -82,10 +82,12 @@ function validate {
echo "run validate on $check_path" echo "run validate on $check_path"
# note: this logic is duplicated in the `./github/workflows/lint_python.yml` config
# if you update this file, you should update that as well
echo "running black" echo "running black"
black --diff --color --check -l 79 "$check_path" black --diff --color --check -l 79 "$check_path"
echo "running codespell" echo "running codespell"
codespell --skip="./.git" "$check_path" codespell --skip="./.git,./package.json,./package-lock.json,./node_modules" "$check_path"
echo "running flake8" echo "running flake8"
flake8 "$check_path" --count --max-complexity=10 --max-line-length=79 \ flake8 "$check_path" --count --max-complexity=10 --max-line-length=79 \
--show-source --statistics --show-source --statistics

1761
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View File

@ -0,0 +1,17 @@
{
"private": true,
"scripts": {
"lint": "eslint 'tubearchivist/static/**/*.js'",
"format": "prettier --write 'tubearchivist/static/**/*.js'"
},
"devDependencies": {
"eslint": "^8.26.0",
"prettier": "^2.7.1",
"eslint-config-prettier": "^8.5.0"
},
"prettier": {
"singleQuote": true,
"arrowParens": "avoid",
"printWidth": 100
}
}

View File

@ -1,148 +1,157 @@
'use strict';
/* global cast chrome getVideoPlayerVideoId postVideoProgress setProgressBar getVideoPlayer getVideoPlayerWatchStatus watchedThreshold isWatched getVideoData getURL getVideoPlayerCurrentTime */
function initializeCastApi() { function initializeCastApi() {
cast.framework.CastContext.getInstance().setOptions({ cast.framework.CastContext.getInstance().setOptions({
receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, // Use built in receiver 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. receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, // Use built in receiver 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 autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
}); });
var player = new cast.framework.RemotePlayer(); let player = new cast.framework.RemotePlayer();
var playerController = new cast.framework.RemotePlayerController(player); let playerController = new cast.framework.RemotePlayerController(player);
// Add event listerner to check if a connection to a cast device is initiated // Add event listerner to check if a connection to a cast device is initiated
playerController.addEventListener( playerController.addEventListener(
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED, function() { cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
castConnectionChange(player) function () {
} castConnectionChange(player);
); }
playerController.addEventListener( );
cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED, function() { playerController.addEventListener(
castVideoProgress(player) cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED,
} function () {
); castVideoProgress(player);
playerController.addEventListener( }
cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED, function() { );
castVideoPaused(player) playerController.addEventListener(
} cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED,
); function () {
castVideoPaused(player);
}
);
} }
function castConnectionChange(player) { function castConnectionChange(player) {
// If cast connection is initialized start cast // If cast connection is initialized start cast
if (player.isConnected) { if (player.isConnected) {
// console.log("Cast Connected."); // console.log("Cast Connected.");
castStart(); castStart();
} else if (!player.isConnected) { } else if (!player.isConnected) {
// console.log("Cast Disconnected."); // console.log("Cast Disconnected.");
} }
} }
function castVideoProgress(player) { function castVideoProgress(player) {
var videoId = getVideoPlayerVideoId(); let videoId = getVideoPlayerVideoId();
if (player.mediaInfo.contentId.includes(videoId)) { if (player.mediaInfo.contentId.includes(videoId)) {
var currentTime = player.currentTime; let currentTime = player.currentTime;
var duration = player.duration; let 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 if (currentTime % 10 <= 1.0 && currentTime !== 0 && duration !== 0) {
postVideoProgress(videoId, currentTime); // Check progress every 10 seconds or else progress is checked a few times a second
setProgressBar(videoId, currentTime, duration); postVideoProgress(videoId, currentTime);
if (!getVideoPlayerWatchStatus()) { // Check if video is already marked as watched setProgressBar(videoId, currentTime, duration);
if (watchedThreshold(currentTime, duration)) { if (!getVideoPlayerWatchStatus()) {
isWatched(videoId); // Check if video is already marked as watched
} if (watchedThreshold(currentTime, duration)) {
} isWatched(videoId);
} }
}
} }
}
} }
function castVideoPaused(player) { function castVideoPaused(player) {
var videoId = getVideoPlayerVideoId(); let videoId = getVideoPlayerVideoId();
var currentTime = player.currentTime; let currentTime = player.currentTime;
var duration = player.duration; let duration = player.duration;
if (player.mediaInfo != null) { if (player.mediaInfo != null) {
if (player.mediaInfo.contentId.includes(videoId)) { if (player.mediaInfo.contentId.includes(videoId)) {
if (currentTime != 0 && duration != 0) { if (currentTime !== 0 && duration !== 0) {
postVideoProgress(videoId, currentTime); postVideoProgress(videoId, currentTime);
} }
}
} }
}
} }
function castStart() { function castStart() {
var castSession = cast.framework.CastContext.getInstance().getCurrentSession(); let 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 // 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()) { if (!castSession.getMediaSession()) {
var videoId = getVideoPlayerVideoId(); let videoId = getVideoPlayerVideoId();
var videoData = getVideoData(videoId); let videoData = getVideoData(videoId);
var contentId = getURL() + videoData.data.media_url; let contentId = getURL() + videoData.data.media_url;
var contentTitle = videoData.data.title; let contentTitle = videoData.data.title;
var contentImage = getURL() + videoData.data.vid_thumb_url; let contentImage = getURL() + videoData.data.vid_thumb_url;
contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded let contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
contentCurrentTime = getVideoPlayerCurrentTime(); // Get video's current position let contentCurrentTime = getVideoPlayerCurrentTime(); // Get video's current position
contentActiveSubtitle = []; let contentActiveSubtitle = [];
// Check if a subtitle is turned on. // Check if a subtitle is turned on.
for (var i = 0; i < getVideoPlayer().textTracks.length; i++) { for (let i = 0; i < getVideoPlayer().textTracks.length; i++) {
if (getVideoPlayer().textTracks[i].mode == "showing") { if (getVideoPlayer().textTracks[i].mode === 'showing') {
contentActiveSubtitle =[i + 1]; contentActiveSubtitle = [i + 1];
} }
} }
contentSubtitles = []; let contentSubtitles = [];
var videoSubtitles = videoData.data.subtitles; // Array of subtitles let videoSubtitles = videoData.data.subtitles; // Array of subtitles
if (typeof(videoSubtitles) != 'undefined' && videoData.config.downloads.subtitle) { if (typeof videoSubtitles !== 'undefined' && videoData.config.downloads.subtitle) {
for (var i = 0; i < videoSubtitles.length; i++) { for (let i = 0; i < videoSubtitles.length; i++) {
subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT); let subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
subtitle.trackContentId = videoSubtitles[i].media_url; subtitle.trackContentId = videoSubtitles[i].media_url;
subtitle.trackContentType = 'text/vtt'; subtitle.trackContentType = 'text/vtt';
subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES; subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
subtitle.name = videoSubtitles[i].name; subtitle.name = videoSubtitles[i].name;
subtitle.language = videoSubtitles[i].lang; subtitle.language = videoSubtitles[i].lang;
subtitle.customData = null; subtitle.customData = null;
contentSubtitles.push(subtitle); contentSubtitles.push(subtitle);
} }
} }
mediaInfo = new chrome.cast.media.MediaInfo(contentId, contentType); // Create MediaInfo var that contains url and content type let 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.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 = new chrome.cast.media.GenericMediaMetadata(); // Create metadata var and add it to MediaInfo
mediaInfo.metadata.title = contentTitle.replace("&amp;", "&"); // Set the video title mediaInfo.metadata.title = contentTitle.replace('&amp;', '&'); // Set the video title
mediaInfo.metadata.images = [new chrome.cast.Image(contentImage)]; // Set the video thumbnail mediaInfo.metadata.images = [new chrome.cast.Image(contentImage)]; // Set the video thumbnail
// mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle(); // mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle();
mediaInfo.tracks = contentSubtitles; mediaInfo.tracks = contentSubtitles;
var request = new chrome.cast.media.LoadRequest(mediaInfo); // Create request with the previously set MediaInfo. let 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.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.currentTime = shiftCurrentTime(contentCurrentTime); // Set video start position based on the browser video position
request.activeTrackIds = contentActiveSubtitle; // Set active subtitle based on video player request.activeTrackIds = contentActiveSubtitle; // Set active subtitle based on video player
// request.autoplay = false; // Set content to auto play, true by default // request.autoplay = false; // Set content to auto play, true by default
castSession.loadMedia(request).then( castSession.loadMedia(request).then(
function() { function () {
castSuccessful(); castSuccessful();
}, },
function() { function (error) {
castFailed(errorCode); castFailed(error.code);
} }
); // Send request to cast device ); // Send request to cast device
} }
} }
function shiftCurrentTime(contentCurrentTime) { // Shift media back 3 seconds to prevent missing some of the content function shiftCurrentTime(contentCurrentTime) {
if (contentCurrentTime > 5) { // Shift media back 3 seconds to prevent missing some of the content
return(contentCurrentTime - 3); if (contentCurrentTime > 5) {
} else { return contentCurrentTime - 3;
return(0); } else {
} return 0;
}
} }
function castSuccessful() { function castSuccessful() {
// console.log('Cast Successful.'); // console.log('Cast Successful.');
getVideoPlayer().pause(); // Pause browser video on successful cast getVideoPlayer().pause(); // Pause browser video on successful cast
} }
function castFailed(errorCode) { function castFailed(errorCode) {
console.log('Error code: ' + errorCode); console.log('Error code: ' + errorCode);
} }
window['__onGCastApiAvailable'] = function(isAvailable) { window['__onGCastApiAvailable'] = function (isAvailable) {
if (isAvailable) { if (isAvailable) {
initializeCastApi(); initializeCastApi();
} }
} };

View File

@ -1,106 +1,110 @@
/** /**
* Handle multi channel notifications * Handle multi channel notifications
* *
*/ */
checkMessages() 'use strict';
checkMessages();
// page map to notification status // page map to notification status
const messageTypes = { const messageTypes = {
"download": ["message:download", "message:add", "message:rescan", "message:playlistscan"], download: ['message:download', 'message:add', 'message:rescan', 'message:playlistscan'],
"channel": ["message:subchannel"], channel: ['message:subchannel'],
"channel_id": ["message:playlistscan"], channel_id: ['message:playlistscan'],
"playlist": ["message:subplaylist"], playlist: ['message:subplaylist'],
"setting": ["message:setting"] setting: ['message:setting'],
} };
// start to look for messages // start to look for messages
function checkMessages() { function checkMessages() {
var notifications = document.getElementById("notifications"); let notifications = document.getElementById('notifications');
if (notifications) { if (notifications) {
var dataOrigin = notifications.getAttribute("data"); let dataOrigin = notifications.getAttribute('data');
getMessages(dataOrigin); getMessages(dataOrigin);
} }
} }
// get messages for page on timer // get messages for page on timer
function getMessages(dataOrigin) { function getMessages(dataOrigin) {
fetch('/progress/').then(response => { fetch('/progress/')
return response.json(); .then(response => {
}).then(responseData => { return response.json();
var messages = buildMessage(responseData, dataOrigin); })
if (messages.length > 0) { .then(responseData => {
// restart itself let messages = buildMessage(responseData, dataOrigin);
setTimeout(function() { if (messages.length > 0) {
getMessages(dataOrigin); // restart itself
}, 3000); setTimeout(function () {
}; getMessages(dataOrigin);
}, 3000);
}
}); });
} }
// make div for all messages, return relevant // make div for all messages, return relevant
function buildMessage(responseData, dataOrigin) { function buildMessage(responseData, dataOrigin) {
// filter relevan messages // filter relevan messages
var allMessages = responseData["messages"]; let allMessages = responseData['messages'];
var messages = allMessages.filter(function(value) { let messages = allMessages.filter(function (value) {
return messageTypes[dataOrigin].includes(value["status"]) return messageTypes[dataOrigin].includes(value['status']);
}, dataOrigin); }, dataOrigin);
// build divs // build divs
var notificationDiv = document.getElementById("notifications"); let notificationDiv = document.getElementById('notifications');
var nots = notificationDiv.childElementCount; let nots = notificationDiv.childElementCount;
notificationDiv.innerHTML = ""; notificationDiv.innerHTML = '';
for (let i = 0; i < messages.length; i++) { for (let i = 0; i < messages.length; i++) {
var messageData = messages[i]; let messageData = messages[i];
var messageStatus = messageData["status"]; let messageStatus = messageData['status'];
var messageBox = document.createElement("div"); let messageBox = document.createElement('div');
var title = document.createElement("h3"); let title = document.createElement('h3');
title.innerHTML = messageData["title"]; title.innerHTML = messageData['title'];
var message = document.createElement("p"); let message = document.createElement('p');
message.innerHTML = messageData["message"]; message.innerHTML = messageData['message'];
messageBox.appendChild(title); messageBox.appendChild(title);
messageBox.appendChild(message); messageBox.appendChild(message);
messageBox.classList.add(messageData["level"], "notification"); messageBox.classList.add(messageData['level'], 'notification');
notificationDiv.appendChild(messageBox); notificationDiv.appendChild(messageBox);
if (messageStatus === "message:download") { if (messageStatus === 'message:download') {
checkDownloadIcons(); checkDownloadIcons();
}; }
}; }
// reload page when no more notifications // reload page when no more notifications
if (nots > 0 && messages.length === 0) { if (nots > 0 && messages.length === 0) {
location.reload(); location.reload();
}; }
return messages return messages;
} }
// check if download icons are needed // check if download icons are needed
function checkDownloadIcons() { function checkDownloadIcons() {
var iconBox = document.getElementById("downloadControl"); let iconBox = document.getElementById('downloadControl');
if (iconBox.childElementCount === 0) { if (iconBox.childElementCount === 0) {
var downloadIcons = buildDownloadIcons(); let downloadIcons = buildDownloadIcons();
iconBox.appendChild(downloadIcons); iconBox.appendChild(downloadIcons);
}; }
} }
// add dl control icons // add dl control icons
function buildDownloadIcons() { function buildDownloadIcons() {
var downloadIcons = document.createElement('div'); let downloadIcons = document.createElement('div');
downloadIcons.classList = 'dl-control-icons'; downloadIcons.classList = 'dl-control-icons';
// stop icon // stop icon
var stopIcon = document.createElement('img'); let stopIcon = document.createElement('img');
stopIcon.setAttribute('id', "stop-icon"); stopIcon.setAttribute('id', 'stop-icon');
stopIcon.setAttribute('title', "Stop Download Queue"); stopIcon.setAttribute('title', 'Stop Download Queue');
stopIcon.setAttribute('src', "/static/img/icon-stop.svg"); stopIcon.setAttribute('src', '/static/img/icon-stop.svg');
stopIcon.setAttribute('alt', "stop icon"); stopIcon.setAttribute('alt', 'stop icon');
stopIcon.setAttribute('onclick', 'stopQueue()'); stopIcon.setAttribute('onclick', 'stopQueue()');
// kill icon // kill icon
var killIcon = document.createElement('img'); let killIcon = document.createElement('img');
killIcon.setAttribute('id', "kill-icon"); killIcon.setAttribute('id', 'kill-icon');
killIcon.setAttribute('title', "Kill Download Queue"); killIcon.setAttribute('title', 'Kill Download Queue');
killIcon.setAttribute('src', "/static/img/icon-close.svg"); killIcon.setAttribute('src', '/static/img/icon-close.svg');
killIcon.setAttribute('alt', "kill icon"); killIcon.setAttribute('alt', 'kill icon');
killIcon.setAttribute('onclick', 'killQueue()'); killIcon.setAttribute('onclick', 'killQueue()');
// stich together // stich together
downloadIcons.appendChild(stopIcon); downloadIcons.appendChild(stopIcon);
downloadIcons.appendChild(killIcon); downloadIcons.appendChild(killIcon);
return downloadIcons return downloadIcons;
} }

File diff suppressed because it is too large Load Diff