Import from
-
Currently only quotes from Streamlabs Chatbot (desktop bot) can be imported.
+
Currently only quotes from Streamlabs Chatbot (desktop bot) and Mix It Up can be imported.
Choose file
-
To get the export file in Streamlabs Chatbot, go to Connections -> Cloud -> Create Split Excel and find the file called Quotes.xlsx.
+
To get the export file in Streamlabs Chatbot, go to Connections -> Cloud -> Create Split Excel and find the file called Quotes.xlsx.
+
To get the export file in Mix It Up, go to Quotes -> Export Quotes and find the file called Quotes.txt.
@@ -97,7 +98,14 @@
];
$ctrl.onFileSelected = (filepath) => {
- const data = importService.parseStreamlabsChatbotData(filepath);
+ //get the file type from the filepath
+ const fileType = filepath.split(".").pop();
+ let data;
+ if (fileType === "xlsx") {
+ data = importService.parseStreamlabsChatbotData(filepath);
+ } else if (fileType === "txt") {
+ data = importService.parseMixItUpData(filepath, "quotes");
+ }
if (data && data.quotes) {
$ctrl.quotes = data.quotes;
$ctrl.search = "";
diff --git a/src/gui/app/index.html b/src/gui/app/index.html
index 4fc3e9221..20d5df52a 100644
--- a/src/gui/app/index.html
+++ b/src/gui/app/index.html
@@ -89,6 +89,8 @@
+
+
@@ -202,6 +204,7 @@
+
@@ -270,6 +273,7 @@
+
diff --git a/src/gui/app/services/effect-queues.service.js b/src/gui/app/services/effect-queues.service.js
index 35cd8f5ca..41fcfe804 100644
--- a/src/gui/app/services/effect-queues.service.js
+++ b/src/gui/app/services/effect-queues.service.js
@@ -32,6 +32,20 @@
}
});
+ backendCommunicator.on("updateQueueLength", queue => {
+ const index = service.effectQueues.findIndex(eq => eq.id === queue.id);
+ if (service.effectQueues[index] != null) {
+ service.effectQueues[index].length = queue.length;
+ }
+ });
+
+ backendCommunicator.on("updateQueueStatus", queue => {
+ const index = service.effectQueues.findIndex(eq => eq.id === queue.id);
+ if (service.effectQueues[index] != null) {
+ service.effectQueues[index].active = queue.active;
+ }
+ });
+
service.queueModes = [
{
id: "custom",
@@ -73,6 +87,15 @@
return false;
};
+ service.toggleEffectQueue = (queue) => {
+ backendCommunicator.fireEvent("toggleEffectQueue", queue.id);
+ queue.active = !queue.active;
+ };
+
+ service.clearEffectQueue = (queueId) => {
+ backendCommunicator.fireEvent("clearEffectQueue", queueId);
+ };
+
service.saveAllEffectQueues = (effectQueues) => {
service.effectQueues = effectQueues;
backendCommunicator.fireEvent("saveAllEffectQueues", effectQueues);
diff --git a/src/gui/app/services/import.service.js b/src/gui/app/services/import.service.js
index 56fe0e4f4..66f79bc5d 100644
--- a/src/gui/app/services/import.service.js
+++ b/src/gui/app/services/import.service.js
@@ -104,6 +104,40 @@
return data;
};
+ service.parseMixItUpData = (filepath, dataType) => {
+ if (dataType === "quotes") {
+ const data = {};
+ //split the file into lines either \r\n or \n
+ const file = fs.readFileSync(filepath, "utf8").split(/\r?\n/);
+ const header = file.shift();
+ if (header !== "# Quote Game Date/Time") {
+ return data;
+ }
+ //remove any empty lines
+ file.forEach((line, index) => {
+ if (line === "") {
+ file.splice(index, 1);
+ }
+ });
+ //split the file into quotes
+ const quotes = file.map(q => {
+ const splittedQuote = q.split("\t");
+ return {
+ _id: splittedQuote[0],
+ text: splittedQuote[1],
+ originator: "",
+ creator: "",
+ game: splittedQuote[2],
+ createdAt: splittedQuote[3]
+ };
+ });
+ //set the quotes property on the data object
+ data.quotes = quotes;
+ //return the data object
+ return data;
+ }
+ };
+
return service;
});
}());
\ No newline at end of file
diff --git a/src/gui/app/services/video.service.js b/src/gui/app/services/video.service.js
new file mode 100644
index 000000000..2b0f1e8e8
--- /dev/null
+++ b/src/gui/app/services/video.service.js
@@ -0,0 +1,90 @@
+"use strict";
+
+(function() {
+
+ const uuid = require("uuid");
+
+ // This provides methods for playing sounds
+
+ angular
+ .module("firebotApp")
+ .factory("videoService", function(backendCommunicator) {
+ const service = {};
+
+ service.getVideoDuration = function (path) {
+ return new Promise((resolve) => {
+ const id = "video-" + uuid();
+ const videoElement = `
`;
+ $(document.documentElement).append(videoElement);
+ const video = document.getElementById(id);
+ video.onloadedmetadata = () => {
+ resolve(video.duration);
+ video.remove();
+ };
+
+ video.onerror = () => {
+ const result = {
+ error: video.error.message,
+ path: video.src
+ };
+ video.remove();
+ resolve(result);
+ };
+
+ video.src = path;
+ });
+ };
+
+ service.getYoutubeVideoDuration = function (videoId) {
+ return new Promise((resolve) => {
+ const id = "video-" + uuid();
+ $(document.documentElement).append(`
`);
+ // eslint-disable-next-line no-undef
+ const player = new YT.Player(id, {
+ videoId: videoId,
+ events: {
+ onReady: (event) => {
+ event.target.setVolume(0);
+ event.target.playVideo();
+ if (player.getDuration() === 0) {
+ return;
+ }
+ resolve(player.getDuration());
+ document.getElementById(id).remove();
+ },
+ onError: (event) => {
+ const error = {
+ code: event.data,
+ youtubeId: videoId
+ };
+ if (event.data === "2") {
+ error.error = "The request contains an invalid parameter value.";
+ }
+ if (event.data === "5") {
+ error.error = "The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.";
+ }
+ if (event.data === "100") {
+ error.error = "The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.";
+ }
+ if (event.data === "101" || event.data === "150") {
+ error.error = "The owner of the requested video does not allow it to be played in embedded players.";
+ }
+ resolve(error);
+ document.getElementById(id).remove();
+ }
+ }
+ });
+ });
+ };
+
+ backendCommunicator.onAsync("getVideoDuration", async (path) => {
+ return await service.getVideoDuration(path);
+ });
+
+ backendCommunicator.onAsync("getYoutubeVideoDuration", async (youtubeId) => {
+ return await service.getYoutubeVideoDuration(youtubeId);
+ });
+
+ return service;
+ });
+}(window.angular));
diff --git a/src/gui/app/templates/_effect-queues.html b/src/gui/app/templates/_effect-queues.html
index f338940a3..e21174f92 100644
--- a/src/gui/app/templates/_effect-queues.html
+++ b/src/gui/app/templates/_effect-queues.html
@@ -10,4 +10,5 @@
no-data-message="No effect queues saved. You should make one! :)"
none-found-message="No effect queues found."
search-placeholder="Search effect queues"
+ status-field="active"
>
\ No newline at end of file
diff --git a/src/gui/js/plugins/select.js b/src/gui/js/plugins/select.js
index 13af545dc..c1b4605e5 100644
--- a/src/gui/js/plugins/select.js
+++ b/src/gui/js/plugins/select.js
@@ -28,7 +28,7 @@ var KEY = {
DELETE: 46,
COMMAND: 91,
- MAP: { 91 : "COMMAND", 8 : "BACKSPACE" , 9 : "TAB" , 13 : "ENTER" , 16 : "SHIFT" , 17 : "CTRL" , 18 : "ALT" , 19 : "PAUSEBREAK" , 20 : "CAPSLOCK" , 27 : "ESC" , 32 : "SPACE" , 33 : "PAGE_UP", 34 : "PAGE_DOWN" , 35 : "END" , 36 : "HOME" , 37 : "LEFT" , 38 : "UP" , 39 : "RIGHT" , 40 : "DOWN" , 43 : "+" , 44 : "PRINTSCREEN" , 45 : "INSERT" , 46 : "DELETE", 48 : "0" , 49 : "1" , 50 : "2" , 51 : "3" , 52 : "4" , 53 : "5" , 54 : "6" , 55 : "7" , 56 : "8" , 57 : "9" , 59 : ";", 61 : "=" , 65 : "A" , 66 : "B" , 67 : "C" , 68 : "D" , 69 : "E" , 70 : "F" , 71 : "G" , 72 : "H" , 73 : "I" , 74 : "J" , 75 : "K" , 76 : "L", 77 : "M" , 78 : "N" , 79 : "O" , 80 : "P" , 81 : "Q" , 82 : "R" , 83 : "S" , 84 : "T" , 85 : "U" , 86 : "V" , 87 : "W" , 88 : "X" , 89 : "Y" , 90 : "Z", 96 : "0" , 97 : "1" , 98 : "2" , 99 : "3" , 100 : "4" , 101 : "5" , 102 : "6" , 103 : "7" , 104 : "8" , 105 : "9", 106 : "*" , 107 : "+" , 109 : "-" , 110 : "." , 111 : "/", 112 : "F1" , 113 : "F2" , 114 : "F3" , 115 : "F4" , 116 : "F5" , 117 : "F6" , 118 : "F7" , 119 : "F8" , 120 : "F9" , 121 : "F10" , 122 : "F11" , 123 : "F12", 144 : "NUMLOCK" , 145 : "SCROLLLOCK" , 186 : ";" , 187 : "=" , 188 : "," , 189 : "-" , 190 : "." , 191 : "/" , 192 : "`" , 219 : "[" , 220 : "\\" , 221 : "]" , 222 : "'"
+ MAP: { 91 : "COMMAND", 8 : "BACKSPACE" , 9 : "TAB" , 13 : "ENTER" , 16 : "SHIFT" , 17 : "CTRL" , 18 : "ALT" , 19 : "PAUSEBREAK" , 20 : "CAPSLOCK" , 27 : "ESC" , 32 : "SPACE" , 33 : "PAGE_UP", 34 : "PAGE_DOWN" , 35 : "END" , 36 : "HOME" , 37 : "LEFT" , 38 : "UP" , 39 : "RIGHT" , 40 : "DOWN" , 43 : "+" , 44 : "PRINTSCREEN" , 45 : "INSERT" , 46 : "DELETE", 48 : "0" , 49 : "1" , 50 : "2" , 51 : "3" , 52 : "4" , 53 : "5" , 54 : "6" , 55 : "7" , 56 : "8" , 57 : "9" , 59 : ";", 61 : "=" , 65 : "A" , 66 : "B" , 67 : "C" , 68 : "D" , 69 : "E" , 70 : "F" , 71 : "G" , 72 : "H" , 73 : "I" , 74 : "J" , 75 : "K" , 76 : "L", 77 : "M" , 78 : "N" , 79 : "O" , 80 : "P" , 81 : "Q" , 82 : "R" , 83 : "S" , 84 : "T" , 85 : "U" , 86 : "V" , 87 : "W" , 88 : "X" , 89 : "Y" , 90 : "Z", 96 : "0" , 97 : "1" , 98 : "2" , 99 : "3" , 100 : "4" , 101 : "5" , 102 : "6" , 103 : "7" , 104 : "8" , 105 : "9", 106 : "*" , 107 : "+" , 109 : "-" , 110 : "." , 111 : "/", 112 : "F1" , 113 : "F2" , 114 : "F3" , 115 : "F4" , 116 : "F5" , 117 : "F6" , 118 : "F7" , 119 : "F8" , 120 : "F9" , 121 : "F10" , 122 : "F11" , 123 : "F12", 124: "F13" , 125 : "F14" , 126 : "F15" , 127 : "F16" , 128 : "F17" , 129 : "F18" , 130 : "F19" , 131 : "F20" , 132 : "F21" , 133 : "F22" , 134 : "F23" , 135 : "F24" , 144 : "NUMLOCK" , 145 : "SCROLLLOCK" , 186 : ";" , 187 : "=" , 188 : "," , 189 : "-" , 190 : "." , 191 : "/" , 192 : "`" , 219 : "[" , 220 : "\\" , 221 : "]" , 222 : "'"
},
isControl: function (e) {
diff --git a/src/resources/overlay/js/main.js b/src/resources/overlay/js/main.js
index 191d6800c..82fdfa915 100644
--- a/src/resources/overlay/js/main.js
+++ b/src/resources/overlay/js/main.js
@@ -49,22 +49,6 @@ function overlaySocketConnect(){
return;
}
- if(event == "getVideoDuration") {
- const token = encodeURIComponent(data.meta.resourceToken);
- const url = `http://${window.location.hostname}:7472/resource/${token}`;
- const videoElement = `
-
- `;
- $(".wrapper").append(videoElement);
- const video = document.getElementById(token);
- video.oncanplay = () => {
- sendWebsocketEvent("video-duration", {resourceToken: token, duration: Math.ceil(video.duration)});
- video.remove();
- }
- }
-
firebotOverlay.emit(event, data.meta);
};
diff --git a/src/server/api/v1/controllers/countersApiController.ts b/src/server/api/v1/controllers/countersApiController.ts
index d6ed68ca7..1b1012546 100644
--- a/src/server/api/v1/controllers/countersApiController.ts
+++ b/src/server/api/v1/controllers/countersApiController.ts
@@ -34,4 +34,57 @@ export async function getCounterById(req: Request, res: Response): Promise
{
+ const counterId: string = req.params.counterId;
+ const value: number = req.body.value;
+ const override: boolean = req.body.override ?? false;
+
+ if (!(counterId.length > 0)) {
+ return res.status(400).send({
+ status: "error",
+ message: "No counterId provided"
+ });
+ }
+
+ if (value == null) {
+ return res.status(400).send({
+ status: "error",
+ message: "value not present."
+ });
+ }
+
+ if (typeof value !== "number") {
+ return res.status(400).send({
+ status: "error",
+ message: "value must be a number."
+ });
+ }
+
+ if (typeof override !== "boolean") {
+ return res.status(400).send({
+ status: "error",
+ message: "override must be a boolean."
+ });
+ }
+
+ const counter = counterManager.getItem(counterId);
+
+ if (counter == null) {
+ return res.status(404).send({
+ status: "error",
+ message: `Counter '${counterId}' not found`
+ });
+ }
+
+ let response = {
+ oldValue: counter.value,
+ newValue: 0
+ };
+
+ // @ts-ignore
+ await counterManager.updateCounterValue(counter.id, value, override);
+ response.newValue = counterManager.getItem(counterId).value;
+ return res.json(response);
};
\ No newline at end of file
diff --git a/src/server/api/v1/controllers/effectQueuesApiController.ts b/src/server/api/v1/controllers/effectQueuesApiController.ts
new file mode 100644
index 000000000..aa550de3a
--- /dev/null
+++ b/src/server/api/v1/controllers/effectQueuesApiController.ts
@@ -0,0 +1,83 @@
+import effectQueueManager from "../../../../backend/effects/queues/effect-queue-manager";
+import effectQueueRunner from "../../../../backend/effects/queues/effect-queue-runner";
+import { Request, Response } from "express";
+
+function checkQueue(req: Request, res: Response): boolean {
+ const queueId: string = req.params.queueId;
+ if (!(queueId.length > 0)) {
+ res.status(400).send({
+ status: "error",
+ message: "No queueId provided"
+ });
+ return false;
+ }
+ const queue = effectQueueManager.getItem(queueId);
+ if (queue == null) {
+ res.status(404).send({
+ status: "error",
+ message: `Queue '${queueId}' not found.`
+ });
+ return false;
+ }
+ return true;
+}
+
+export async function getQueues(req: Request, res: Response): Promise {
+ return res.json(effectQueueManager.getAllItems());
+}
+
+export async function getQueueById(req: Request, res: Response): Promise {
+ if (!checkQueue(req, res)) {
+ return res;
+ }
+
+ return res.json(effectQueueManager.getItem(req.params.queueId));
+}
+
+export async function pauseQueue(req: Request, res: Response): Promise {
+ if (!checkQueue(req, res)) {
+ return res;
+ }
+
+ const queueId = req.params.queueId;
+
+ effectQueueManager.pauseQueue(queueId);
+
+ return res.json(effectQueueManager.getItem(queueId));
+}
+
+export async function resumeQueue(req: Request, res: Response): Promise {
+ if (!checkQueue(req, res)) {
+ return res;
+ }
+
+ const queueId = req.params.queueId;
+
+ effectQueueManager.resumeQueue(queueId);
+
+ return res.json(effectQueueManager.getItem(queueId));
+}
+
+export async function toggleQueue(req: Request, res: Response): Promise {
+ if (!checkQueue(req, res)) {
+ return res;
+ }
+
+ const queueId = req.params.queueId;
+
+ effectQueueManager.toggleQueue(queueId);
+
+ return res.json(effectQueueManager.getItem(queueId));
+}
+
+export async function clearQueue(req: Request, res: Response): Promise {
+ if (!checkQueue(req, res)) {
+ return res;
+ }
+
+ const queueId = req.params.queueId;
+
+ effectQueueRunner.removeQueue(queueId);
+
+ return res.json(effectQueueManager.getItem(queueId));
+}
diff --git a/src/server/api/v1/v1Router.js b/src/server/api/v1/v1Router.js
index b7364a97d..a3e456b3d 100644
--- a/src/server/api/v1/v1Router.js
+++ b/src/server/api/v1/v1Router.js
@@ -177,7 +177,8 @@ router
router
.route("/counters/:counterId")
- .get(counters.getCounterById);
+ .get(counters.getCounterById)
+ .post(counters.updateCounter);
// Timers
const timers = require("./controllers/timersApiController");
@@ -190,4 +191,34 @@ router
.route("/timers/:timerId")
.get(timers.getTimerById);
+const queues = require("./controllers/effectQueuesApiController");
+
+router
+ .route("/queues")
+ .get(queues.getQueues);
+
+router
+ .route("/queues/:queueId")
+ .get(queues.getQueueById);
+
+router
+ .route("/queues/:queueId/pause")
+ .get(queues.pauseQueue)
+ .post(queues.pauseQueue);
+
+router
+ .route("/queues/:queueId/resume")
+ .get(queues.resumeQueue)
+ .post(queues.resumeQueue);
+
+router
+ .route("/queues/:queueId/toggle")
+ .get(queues.toggleQueue)
+ .post(queues.toggleQueue);
+
+router
+ .route("/queues/:queueId/clear")
+ .get(queues.clearQueue)
+ .post(queues.clearQueue);
+
module.exports = router;
diff --git a/src/shared/youtube-url-parser.js b/src/shared/youtube-url-parser.js
new file mode 100644
index 000000000..a2a7bcc51
--- /dev/null
+++ b/src/shared/youtube-url-parser.js
@@ -0,0 +1,44 @@
+'use strict';
+
+function parseYoutubeId(id) {
+ if (id.length === 11) { // string is same length as YouTube id
+ return {id: id};
+ }
+
+ let url;
+ let finalId = id;
+ let startTime;
+
+ if (!id.startsWith("http")) {
+ id = "http://" + id;
+ }
+
+ try {
+ url = new URL(id);
+
+ if (url.hostname === "youtube.com" || url.hostname === "www.youtube.com") {
+ if (url.pathname.includes("/shorts/")) {
+ return {id: url.pathname.replace("/shorts/", "")};
+ }
+ } else if (url.hostname === "youtu.be") {
+ finalId = url.pathname.replace("/", "");
+ } else {
+ return {id: finalId};
+ }
+
+ for (const [key, value] of url.searchParams) {
+ if (key === "v") {
+ finalId = value;
+ } else if (key === "t") {
+ startTime = value;
+ }
+ }
+
+ } catch (error) {
+ return {id: finalId};
+ }
+
+ return {id: finalId, startTime: startTime};
+}
+
+exports.parseYoutubeId = parseYoutubeId;
\ No newline at end of file