diff --git a/configuration.default.ini b/configuration.default.ini
index fc5c39e..f26a266 100644
--- a/configuration.default.ini
+++ b/configuration.default.ini
@@ -28,7 +28,7 @@ certificate =
[bot]
username = botamusique
-comment = Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!
+comment = "Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!"
# default volume from 0 to 1.
volume = 0.1
stereo = True
@@ -126,10 +126,10 @@ jazz = http://jazz-wr04.ice.infomaniak.ch/jazz-wr04-128.mp3 "Jazz Yeah !"
-# ========================================================
+# =========================================================
# WARNING: WE DO NOT SUGGEST YOU MODIFY THE FOLLOWING
-# PARTS, EXCEPT YOU KNOW WHAT YOU ARE DOING.
-# ========================================================
+# PARTS, EXCEPT IF YOU KNOW WHAT YOU ARE DOING.
+# =========================================================
[commands]
# This is a list of characters the bot recognizes as command prefix.
command_symbol = !:!
diff --git a/configuration.example.ini b/configuration.example.ini
index 27b68af..2b5ee62 100644
--- a/configuration.example.ini
+++ b/configuration.example.ini
@@ -22,7 +22,7 @@ port = 64738
# 'username' is the user name of the bot.
# 'comment' is the comment displayed by the bot.
#username = botamusique
-#comment = Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!
+#comment = "Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!"
# 'language': Available languages can be found inside lang/ folder.
#language=en_US
diff --git a/web/.editorconfig b/web/.editorconfig
index 29a9bb0..db4f9c8 100644
--- a/web/.editorconfig
+++ b/web/.editorconfig
@@ -1,5 +1,3 @@
-root = true
-
[*]
charset = utf-8
insert_final_newline = true
diff --git a/web/.eslintrc.json b/web/.eslintrc.json
index 2fd2b19..2d298ca 100644
--- a/web/.eslintrc.json
+++ b/web/.eslintrc.json
@@ -1,38 +1,40 @@
{
+ "parser": "@babel/eslint-parser",
"env": {
"browser": true,
"es6": true,
"es2017": true,
"es2020": true,
+ "es2021": true,
"jquery": true
},
"plugins": [
- "node",
+ "@babel",
"import",
- "jsdoc"
+ "jsdoc",
+ "jquery"
],
"extends": [
"eslint:recommended",
- "google",
- "plugin:node/recommended-module",
"plugin:import/errors",
"plugin:import/warnings",
- "plugin:jsdoc/recommended"
+ "plugin:jsdoc/recommended",
+ "plugin:jquery/deprecated"
],
- "globals": {
- "Atomics": "readonly",
- "SharedArrayBuffer": "readonly"
- },
- "parser": "babel-eslint",
"rules": {
- "require-jsdoc": "off",
- "valid-jsdoc": "off",
- "jsdoc/require-jsdoc": "off",
"max-len": ["warn", {
"code": 120
}],
+ "linebreak-style": "off",
+ "jsdoc/require-jsdoc": "off",
+ "import/unambiguous": "error",
"import/no-commonjs": "error",
"import/no-amd": "error",
- "linebreak-style": "off"
+ "import/no-nodejs-modules": "error",
+ "import/no-deprecated": "error",
+ "import/extensions": ["error", "always"],
+ "import/no-unresolved": ["error", {
+ "commonjs": true
+ }]
}
}
diff --git a/web/.gitattributes b/web/.gitattributes
new file mode 100644
index 0000000..f8a7cde
--- /dev/null
+++ b/web/.gitattributes
@@ -0,0 +1 @@
+package-lock.json text eol=lf
diff --git a/web/.gitignore b/web/.gitignore
index 40b878d..1436c17 100644
--- a/web/.gitignore
+++ b/web/.gitignore
@@ -1 +1,2 @@
-node_modules/
\ No newline at end of file
+!*
+node_modules/
diff --git a/web/babel.config.json b/web/babel.config.json
new file mode 100644
index 0000000..db84814
--- /dev/null
+++ b/web/babel.config.json
@@ -0,0 +1,5 @@
+{
+ "plugins": [
+ "@babel/plugin-proposal-class-properties"
+ ]
+}
diff --git a/web/js/app.mjs b/web/js/app.mjs
index b172543..4506030 100644
--- a/web/js/app.mjs
+++ b/web/js/app.mjs
@@ -1,13 +1,23 @@
-import {library, dom} from '@fortawesome/fontawesome-svg-core/index.es';
-import {fas} from '@fortawesome/free-solid-svg-icons/index.es';
-import {far} from '@fortawesome/free-regular-svg-icons/index.es';
-library.add(fas, far);
+import {library, dom} from '@fortawesome/fontawesome-svg-core/index.es.js';
+import {
+ faTimesCircle, faPlus, faCheck, faUpload, faTimes, faTrash, faPlay, faPause, faFastForward, faPlayCircle, faLightbulb,
+ faTrashAlt, faDownload, faSyncAlt, faEdit, faVolumeUp, faVolumeDown, faRobot, faRedo, faRandom, faTasks
+} from '@fortawesome/free-solid-svg-icons/index.es.js';
+import {faFileAlt} from '@fortawesome/free-regular-svg-icons/index.es.js';
+
+library.add(
+ // Solid
+ faTimesCircle, faPlus, faCheck, faUpload, faTimes, faTrash, faPlay, faPause, faFastForward, faPlayCircle, faLightbulb,
+ faTrashAlt, faDownload, faSyncAlt, faEdit, faVolumeUp, faVolumeDown, faRobot, faRedo, faRandom, faTasks,
+ // Regular
+ faFileAlt
+);
// Old application code
import './main.mjs';
// New application code
-import Theme from './theme.mjs';
+import Theme from './lib/theme.mjs';
document.addEventListener('DOMContentLoaded', () => {
Theme.init();
diff --git a/web/js/lib/text.mjs b/web/js/lib/text.mjs
new file mode 100644
index 0000000..6ee3f9b
--- /dev/null
+++ b/web/js/lib/text.mjs
@@ -0,0 +1,42 @@
+import {validateString, validateNumber} from './type.mjs';
+
+/**
+ * Truncate string length by characters.
+ *
+ * @param {string} text String to format.
+ * @param {number} limit Maximum number of characters in resulting string.
+ * @param {string} ending Ending to use if string is trucated.
+ *
+ * @returns {string} Formatted string.
+ */
+export function limitChars(text, limit = 50, ending = '...') {
+ validateString(text);
+ validateNumber(limit);
+ validateString(ending);
+
+ // Check if string is already below limit
+ if (text.length <= limit) {
+ return text;
+ }
+
+ // Limit string length by characters
+ return text.substring(0, limit - ending.length) + ending;
+}
+
+/**
+ * Truncate string length by words.
+ *
+ * @param {string} text String to format.
+ * @param {number} limit Maximum number of words in resulting string.
+ * @param {string} ending Ending to use if string is trucated.
+ *
+ * @returns {string} Formatted string.
+ */
+export function limitWords(text, limit = 10, ending = '...') {
+ validateString(text);
+ validateNumber(limit);
+ validateString(ending);
+
+ // Limit string length by words
+ return text.split(' ').splice(0, limit).join(' ') + ending;
+}
diff --git a/web/js/theme.mjs b/web/js/lib/theme.mjs
similarity index 92%
rename from web/js/theme.mjs
rename to web/js/lib/theme.mjs
index f83f921..70294d6 100644
--- a/web/js/theme.mjs
+++ b/web/js/lib/theme.mjs
@@ -1,6 +1,6 @@
export default class {
/**
- * @property {boolean} #dark Interal state for dark theme activation.
+ * @property {boolean} dark Interal state for dark theme activation.
* @private
*/
static #dark = false;
diff --git a/web/js/lib/type.mjs b/web/js/lib/type.mjs
new file mode 100644
index 0000000..6429dd8
--- /dev/null
+++ b/web/js/lib/type.mjs
@@ -0,0 +1,65 @@
+/**
+ * Checks if `value` is the type `Object` excluding `Function` and `null`
+ *
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an object, otherwise `false`.
+ */
+export function isObject(value) {
+ return (Object.prototype.toString.call(value) === '[object Object]');
+}
+
+/**
+ * Checks if `value` is the type `string`
+ *
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a string, otherwise `false`.
+ */
+export function isString(value) {
+ return (typeof value === 'string');
+}
+
+/**
+ * Checks if `value` is the type `number`
+ *
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a number, otherwise `false`.
+ */
+export function isNumber(value) {
+ return (typeof value === 'number');
+}
+
+/**
+ * Validate parameter is of type object.
+ *
+ * @param {string} value Variable to validate.
+ * @throws Error if not an object.
+ */
+export function validateObject(value) {
+ if (!isObject(value)) {
+ throw new TypeError('Parameter "value" must be of type object.');
+ }
+}
+
+/**
+ * Validate parameter is of type string.
+ *
+ * @param {string} value Variable to validate.
+ * @throws Error if not an string.
+ */
+export function validateString(value) {
+ if (!isString(value)) {
+ throw new TypeError('Parameter "value" must be of type string.');
+ }
+}
+
+/**
+ * Validate parameter is of type number.
+ *
+ * @param {number} value Variable to validate.
+ * @throws Error if not an number.
+ */
+export function validateNumber(value) {
+ if (!isNumber(value)) {
+ throw new TypeError('Parameter "value" must be of type number.');
+ }
+}
diff --git a/web/js/util.js b/web/js/lib/util.mjs
similarity index 60%
rename from web/js/util.js
rename to web/js/lib/util.mjs
index b5b2b1c..4bd7221 100644
--- a/web/js/util.js
+++ b/web/js/lib/util.mjs
@@ -3,13 +3,13 @@ export function isOverflown(element) {
}
export function hash(string) {
- if (typeof string != "string") return 0;
+ if (typeof string != 'string') return 0;
let hash = 0;
if (string.length === 0) {
return hash;
}
for (let i = 0; i < string.length; i++) {
- let char = string.charCodeAt(i);
+ const char = string.charCodeAt(i);
hash = ((hash<<5)-hash)+char;
hash = hash & hash; // Convert to 32bit integer
}
@@ -17,24 +17,25 @@ export function hash(string) {
}
export function getColor(string) {
- let num = hash(string) % 8;
- switch(num) {
+ const num = hash(string) % 8;
+
+ switch (num) {
case 0:
- return "primary";
+ return 'primary';
case 1:
- return "secondary";
+ return 'secondary';
case 2:
- return "success";
+ return 'success';
case 3:
- return "danger";
+ return 'danger';
case 4:
- return "warning";
+ return 'warning';
case 5:
- return "info";
+ return 'info';
case 6:
- return "light";
+ return 'light';
case 7:
- return "dark";
+ return 'dark';
}
}
@@ -52,20 +53,3 @@ export function secondsToStr(seconds) {
const secs = seconds % 60;
return ('00' + mins).slice(-2) + ':' + ('00' + secs).slice(-2);
}
-
-export function coverArtString(title) {
-
- let nameOfSong = "";
- // The maximum length before we start truncating
- const maxLength = 50;
-
- if (title.length > maxLength) {
- // Name = longTitleTooLongToBeAGoodAltTex...
- nameOfSong = title.substr(0, maxLength) + "\u2026";
- } else {
- // Name = shortTitle
- nameOfSong = title;
- }
-
- return 'Cover art for ' + nameOfSong;
-}
diff --git a/web/js/main.mjs b/web/js/main.mjs
index b6e51ef..c82acc6 100644
--- a/web/js/main.mjs
+++ b/web/js/main.mjs
@@ -1,20 +1,20 @@
-import 'jquery/src/jquery';
-import 'jquery-migrate/src/migrate';
-import Popper from 'popper.js/dist/esm/popper';
+import 'jquery/src/jquery.js';
+import 'jquery-migrate/src/migrate.js';
+import Popper from 'popper.js/dist/esm/popper.js';
import {
Modal,
Toast,
Tooltip,
-} from 'bootstrap/js/src/index';
+} from 'bootstrap/js/src/index.js';
import {
getColor,
isOverflown,
setProgressBar,
secondsToStr,
- coverArtString,
-} from './util';
+} from './lib/util.mjs';
+import {limitChars} from './lib/text.mjs';
-$('#uploadSelectFile').on('change', function () {
+$('#uploadSelectFile').on('change', function() {
// get the file name
const fileName = $(this).val().replace('C:\\fakepath\\', ' ');
// replace the "Choose a file" label
@@ -85,7 +85,7 @@ fastForwardBtn.on('click', () => {
});
document.getElementById('clear-playlist-btn').addEventListener('click', () => {
- request('post', { action: 'clear' });
+ request('post', {action: 'clear'});
});
// eslint-disable-next-line guard-for-in
@@ -102,14 +102,14 @@ function request(_url, _data, refresh = false) {
url: _url,
data: _data,
statusCode: {
- 200: function (data) {
+ 200: function(data) {
if (data.ver !== playlist_ver) {
checkForPlaylistUpdate();
}
updateControls(data.empty, data.play, data.mode, data.volume);
updatePlayerPlayhead(data.playhead);
},
- 403: function () {
+ 403: function() {
location.reload(true);
},
},
@@ -125,7 +125,7 @@ function addPlaylistItem(item) {
pl_title_element.html(item.title);
pl_artist_element.html(item.artist);
pl_thumb_element.attr('src', item.thumbnail);
- pl_thumb_element.attr('alt', coverArtString(item.title));
+ pl_thumb_element.attr('alt', limitChars(item.title));
pl_type_element.html(item.type);
pl_path_element.html(item.path);
@@ -137,13 +137,13 @@ function addPlaylistItem(item) {
tags.empty();
const tag_edit_copy = pl_tag_edit_element.clone();
- tag_edit_copy.click(function () {
+ tag_edit_copy.click(function() {
addTagModalShow(item.id, item.title, item.tags);
});
tag_edit_copy.appendTo(tags);
if (item.tags.length > 0) {
- item.tags.forEach(function (tag_tuple) {
+ item.tags.forEach(function(tag_tuple) {
const tag_copy = tag_element.clone();
tag_copy.html(tag_tuple[0]);
tag_copy.addClass('badge-' + tag_tuple[1]);
@@ -160,14 +160,14 @@ function addPlaylistItem(item) {
function displayPlaylist(data) {
playlist_table.animate({
opacity: 0,
- }, 200, function () {
+ }, 200, function() {
playlist_loading.hide();
$('.playlist-item').remove();
const items = data.items;
const length = data.length;
- if (items.length === 0){
+ if (items.length === 0) {
playlist_empty.removeClass('d-none');
- playlist_table.animate({ opacity: 1 }, 200);
+ playlist_table.animate({opacity: 1}, 200);
return;
}
playlist_items = {};
@@ -188,9 +188,9 @@ function displayPlaylist(data) {
}
items.forEach(
- function (item) {
- addPlaylistItem(item);
- },
+ function(item) {
+ addPlaylistItem(item);
+ },
);
if (items.length < length && start_from + items.length < length) {
@@ -205,7 +205,7 @@ function displayPlaylist(data) {
displayActiveItem(data.current_index);
updatePlayerInfo(playlist_items[data.current_index]);
bindPlaylistEvent();
- playlist_table.animate({ opacity: 1 }, 200);
+ playlist_table.animate({opacity: 1}, 200);
});
}
@@ -227,7 +227,7 @@ function insertExpandPrompt(real_from, real_to, display_from, display_to, total_
expand_copy.addClass('playlist-item');
expand_copy.appendTo(playlist_table);
- expand_copy.click(function () {
+ expand_copy.click(function() {
playlist_range_from = real_from;
playlist_range_to = real_to;
updatePlaylist();
@@ -237,7 +237,7 @@ function insertExpandPrompt(real_from, real_to, display_from, display_to, total_
function updatePlaylist() {
playlist_table.animate({
opacity: 0,
- }, 200, function () {
+ }, 200, function() {
playlist_empty.addClass('d-none');
playlist_loading.show();
playlist_table.find('.playlist-item').css('opacity', 0);
@@ -267,7 +267,7 @@ function checkForPlaylistUpdate() {
type: 'POST',
url: 'post',
statusCode: {
- 200: function (data) {
+ 200: function(data) {
if (data.ver !== playlist_ver) {
playlist_ver = data.ver;
playlist_range_from = 0;
@@ -298,18 +298,18 @@ function checkForPlaylistUpdate() {
function bindPlaylistEvent() {
$('.playlist-item-play').unbind().click(
- function (e) {
- request('post', {
- 'play_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1),
- });
- },
+ function(e) {
+ request('post', {
+ 'play_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1),
+ });
+ },
);
$('.playlist-item-trash').unbind().click(
- function (e) {
- request('post', {
- 'delete_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1),
- });
- },
+ function(e) {
+ request('post', {
+ 'delete_music': ($(e.currentTarget).parent().parent().parent().find('.playlist-item-index').html() - 1),
+ });
+ },
);
}
@@ -416,10 +416,10 @@ function setFilterType(event, type) {
}
-filter_dir.change(function () {
+filter_dir.change(function() {
updateResults();
});
-filter_keywords.change(function () {
+filter_keywords.change(function() {
updateResults();
});
@@ -427,69 +427,69 @@ const item_template = $('#library-item');
function bindLibraryResultEvent() {
$('.library-thumb-col').unbind().hover(
- function (e) {
- $(e.currentTarget).find('.library-thumb-grp').addClass('library-thumb-grp-hover');
- },
- function (e) {
- $(e.currentTarget).find('.library-thumb-grp').removeClass('library-thumb-grp-hover');
- },
+ function(e) {
+ $(e.currentTarget).find('.library-thumb-grp').addClass('library-thumb-grp-hover');
+ },
+ function(e) {
+ $(e.currentTarget).find('.library-thumb-grp').removeClass('library-thumb-grp-hover');
+ },
);
$('.library-info-title').unbind().hover(
- function (e) {
- $(e.currentTarget).parent().find('.library-thumb-grp').addClass('library-thumb-grp-hover');
- },
- function (e) {
- $(e.currentTarget).parent().find('.library-thumb-grp').removeClass('library-thumb-grp-hover');
- },
+ function(e) {
+ $(e.currentTarget).parent().find('.library-thumb-grp').addClass('library-thumb-grp-hover');
+ },
+ function(e) {
+ $(e.currentTarget).parent().find('.library-thumb-grp').removeClass('library-thumb-grp-hover');
+ },
);
$('.library-item-play').unbind().click(
- function (e) {
- request('post', {
- 'add_item_at_once': $(e.currentTarget).parent().parent().parent().find('.library-item-id').val(),
- });
- },
+ function(e) {
+ request('post', {
+ 'add_item_at_once': $(e.currentTarget).parent().parent().parent().find('.library-item-id').val(),
+ });
+ },
);
$('.library-item-trash').unbind().click(
- function (e) {
- request('post', {
- 'delete_item_from_library': $(e.currentTarget).parent().parent().find('.library-item-id').val(),
- });
- updateResults(active_page);
- },
+ function(e) {
+ request('post', {
+ 'delete_item_from_library': $(e.currentTarget).parent().parent().find('.library-item-id').val(),
+ });
+ updateResults(active_page);
+ },
);
$('.library-item-download').unbind().click(
- function (e) {
- const id = $(e.currentTarget).parent().parent().find('.library-item-id').val();
- // window.open('/download?id=' + id);
- downloadId(id);
- },
+ function(e) {
+ const id = $(e.currentTarget).parent().parent().find('.library-item-id').val();
+ // window.open('/download?id=' + id);
+ downloadId(id);
+ },
);
$('.library-item-add-next').unbind().click(
- function (e) {
- const id = $(e.currentTarget).parent().parent().find('.library-item-id').val();
- request('post', {
- 'add_item_next': id,
- });
- },
+ function(e) {
+ const id = $(e.currentTarget).parent().parent().find('.library-item-id').val();
+ request('post', {
+ 'add_item_next': id,
+ });
+ },
);
$('.library-item-add-bottom').unbind().click(
- function (e) {
- const id = $(e.currentTarget).parent().parent().find('.library-item-id').val();
- request('post', {
- 'add_item_bottom': id,
- });
- },
+ function(e) {
+ const id = $(e.currentTarget).parent().parent().find('.library-item-id').val();
+ request('post', {
+ 'add_item_bottom': id,
+ });
+ },
);
}
-const lib_filter_tag_group = $("#filter-tags");
-const lib_filter_tag_element = $(".filter-tag");
+const lib_filter_tag_group = $('#filter-tags');
+const lib_filter_tag_element = $('.filter-tag');
const lib_group = $('#library-group');
const id_element = $('.library-item-id');
@@ -503,62 +503,62 @@ const tag_edit_element = $('.library-item-edit');
// var notag_element = $(".library-item-notag");
// var tag_element = $(".library-item-tag");
-let library_tags = [];
+const library_tags = [];
-function updateLibraryControls(){
+function updateLibraryControls() {
$.ajax({
type: 'GET',
url: 'library/info',
statusCode: {
200: displayLibraryControls,
- 403: function () {
+ 403: function() {
location.reload(true);
},
},
});
}
-function displayLibraryControls(data){
- $("#maxUploadFileSize").val(data.max_upload_file_size);
+function displayLibraryControls(data) {
+ $('#maxUploadFileSize').val(data.max_upload_file_size);
if (data.upload_enabled) {
- $("#uploadDisabled").val("false");
- $("#upload").show();
+ $('#uploadDisabled').val('false');
+ $('#upload').show();
} else {
- $("#uploadDisabled").val("true");
- $("#upload").hide();
+ $('#uploadDisabled').val('true');
+ $('#upload').hide();
}
if (data.delete_allowed) {
- $("#deleteAllowed").val("true");
- $(".library-delete").show();
+ $('#deleteAllowed').val('true');
+ $('.library-delete').show();
} else {
- $("#uploadDisabled").val("false");
- $(".library-delete").hide();
+ $('#uploadDisabled').val('false');
+ $('.library-delete').hide();
}
- let select = $("#filter-dir");
- let dataList = $("#upload-target-dirs");
- select.find("option").remove();
- dataList.find("option").remove();
+ const select = $('#filter-dir');
+ const dataList = $('#upload-target-dirs');
+ select.find('option').remove();
+ dataList.find('option').remove();
if (data.dirs.length > 0) {
console.log(data.dirs);
- data.dirs.forEach(function (dir) {
- $("").appendTo(select);
- $("').appendTo(select);
+ $('