0% found this document useful (0 votes)
59 views13 pages

Youtube Auto Sender

This document is a userscript for YouTube that automates the sending of chat messages (barrage) based on user-defined configurations. It includes features such as customizable message intervals, sentence segmentation modes, and options for random message selection. The script utilizes an event-driven architecture to manage message sending and user interactions with a graphical interface.

Uploaded by

janda kering
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
59 views13 pages

Youtube Auto Sender

This document is a userscript for YouTube that automates the sending of chat messages (barrage) based on user-defined configurations. It includes features such as customizable message intervals, sentence segmentation modes, and options for random message selection. The script utilizes an event-driven architecture to manage message sending and user interactions with a graphical interface.

Uploaded by

janda kering
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd

// ==UserScript==

// @icon https://www.youtube.com/favicon.ico
// @name the translated version of "Youtube 独轮车-Auto Youtube Chat
Sender"
// @author necros & dislido
// @translatedBy melewa
// @description youtube wheelbarrow
// @match *://www.youtube.com/*
// @version 2.10
// @grant none
// @namespace https://greasyfork.org/zh-CN/users/692472-necrosn
// ==/UserScript==

{
if (window.top !== window.self) throw new Error('Non-top frame');
const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1) +
min);
const eventBus = {
ee: new EventTarget(),
on(type, fn, opt) {
this.ee.addEventListener(type, fn, opt);
},
off(type, fn, opt) {
this.ee.removeEventListener(type, fn, opt);
},
emit(type, detail) {
const event = new CustomEvent(type, { detail });
this.ee.dispatchEvent(event);
},
};

const refs = {
sendBtn: null,
chatTxtInput: null,

runBtn: null,

remoteDanmakuConfig: [],
};

const config = {
splitMode: 2, // Sentence segmentation
minCycleSec: 6, // Minimum sending interval (seconds)
maxCycleSec: 6, // Maximum sending interval (seconds)
randomDanmaku: false, // Send randomly
text: '', // Send message list
maxDanmakuLength: 200, // Maximum barrage word limit
minDanmakuLength: 20, // Minimum barrage length
startTime: '2020-10-24 00:07:00',
stopTime : '2020-10-24 00:07:00',
splitChar: ',;:。!?…,.!?,', // Sentence break
remoteDanmakuBase: '',
};

eventBus.on('dlc.sendMsg', ({ detail: text }) => {


refs.chatTxtInput.textContent = text;
refs.chatTxtInput.dispatchEvent(new InputEvent('input'));
refs.sendBtn.click();
});
eventBus.on('setRef.remoteDanmakuConfig', ({ detail }) => {
refs.remoteDanmakuConfig = detail;
});

try {
const savedCfg = JSON.parse(localStorage.getItem('duluncheCfg'));
Object.assign(config, savedCfg);
} catch (_) {
// noop
}
let timer;// Timer

// Mini renderer
const h = (tagname, attributes = {}, children = [], option = {}) => {
if (tagname instanceof Node) return tagname;
if (tagname instanceof Array) {
const frag = document.createDocumentFragment();
tagname.forEach((it) => {
if (it instanceof Node) {
frag.appendChild(it);
} else if (Array.isArray(it)) {
frag.appendChild(h(it[0], it[1], it[2], it[3]));
} else if (['string', 'number'].includes(typeof it) || it) {
frag.appendChild(new Text(it));
}
});
return frag;
}
const el = document.createElement(tagname);
Object.entries(attributes).forEach(([key, value]) => {
if (key === 'style' && typeof value === 'object') {
Object.assign(el.style, value);
} else if (key.startsWith('$')) {
if (typeof value === 'function') {
el.addEventListener(key.slice(1), value);
} else {
el.addEventListener(key.slice(1), value.handleEvent, value);
}
} else el.setAttribute(key, value);
});
if (['string', 'number'].includes(typeof children)) {
el.textContent = children;
} else if (children) {
el.appendChild(h(children));
}
if (typeof option === 'function') {
option(el);
} else if (option.cb) { option.cb(el); }
return el;
};
const formatTime = (time) => {
let h = Math.floor(time / 3600);
let m = Math.floor(time / 60 % 60);
let s = Math.floor(time % 60)
return h + ':' + m + ':' + s
}
// Various components
/** toast 'Prompt component',msg 'Can be a renderer' children 'Types of' */
const Toast = (msg) => {
if (Toast.singletonDom) Toast.singletonDom.remove();
const dom = h('div', {
class: 'dlc-toast',
$click: () => {
dom.remove();
Toast.singletonDom = null;
},
}, msg);
Toast.singletonDom = dom;
document.body.appendChild(dom);
};
const ConfigField = ({
label, type, name, props = {}, children = [], valueProp = 'value', helpDesc,
}, option) => ['div', {}, [
helpDesc && ['span', {
class: 'help-icon',
$click: (ev) => {
ev.stopPropagation();
Toast(helpDesc);
},
}],
['label', {}, [
label,
[type, {
...props,
$change: (ev) => {
eventBus.emit(`setConfig.${name}`, ev.target[valueProp]);
if (props.$change) props.$change(ev);
},
}, children, (el) => {
el[valueProp] = config[name];
eventBus.on(`setConfig.${name}`, ({ detail }) => {
config[name] = detail;
el[valueProp] = detail;
});
}],
]],
], option];

// End launch
eventBus.on('dlc.stop', () => {
refs.runBtn.innerText = 'Dispatch';
clearTimeout(timer);
});

const splitter = {
// Single sentence pattern
0: (text) => [text.substr(0, config.maxDanmakuLength)],
// Multi-sentence runner
2: (text) => text
.split('\n')
.map((it) => it.trim().substr(0, config.maxDanmakuLength))
.filter(Boolean),
// Storytelling mode
1: (text) => {
const { maxDanmakuLength, minDanmakuLength, splitChar } = config;
const list = [];
text
.trim()
.replace(/\s+/g, ' ')
.split(new RegExp(`(?<=[${splitChar.replace(/(\\|])/g, '\\$1')}])`))
.reduce((buf, curr, currIndex, arr) => {
buf += curr;
while (buf.length > maxDanmakuLength) {
list.push(buf.substr(0, maxDanmakuLength));
buf = buf.substr(maxDanmakuLength);
}
if (currIndex === arr.length - 1) {
list.push(buf);
return '';
}
if (buf.length < minDanmakuLength) return buf;
list.push(buf);
return '';
}, '');
return list;
},
};

// Launch barrage
eventBus.on('dlc.run', () => {
const {
maxCycleSec, minCycleSec, text, splitMode, randomDanmaku,
} = config;

// Check the settings


if (Number(config.minCycleSec) < 1) eventBus.emit('setConfig.minCycleSec',
1);
if (Number(config.maxCycleSec) < config.minCycleSec)
eventBus.emit('setConfig.maxCycleSec', config.minCycleSec);
if (Number(config.minDanmakuLength) < 1)
eventBus.emit('setConfig.minDanmakuLength', 1);
if (Number(config.maxDanmakuLength) < config.minDanmakuLength) {
eventBus.emit('setConfig.maxDanmakuLength', config.minDanmakuLength);
}

const localDanmakuList = splitter[splitMode](text);


const danmakuList = refs.remoteDanmakuConfig
.filter(Boolean)
.reduce((list, data) => list.concat(data.list), localDanmakuList);

if (!danmakuList.length) {
Toast('The barrage list is empty! ');
return;
}

localStorage.setItem('duluncheCfg', JSON.stringify(config));
refs.runBtn.innerText = 'Cancel';

const minCycleTime = parseInt(minCycleSec * 1000, 10);


const maxCycleTime = parseInt(maxCycleSec * 1000, 10);

const danmakuGener = (function* gen() {


if (+splitMode === 2 && randomDanmaku) {
while (true) yield danmakuList[randomInt(0, danmakuList.length - 1)];
} else {
while (true) yield* danmakuList;
}
}());

const nextTimer = () => {


timer = setTimeout(
() => {
eventBus.emit('dlc.sendMsg', danmakuGener.next().value);
nextTimer();
},
randomInt(minCycleTime, maxCycleTime),
);
};
nextTimer();
});

// Console
const cmd = h('div', { class: 'dlc-cmd' }, [
['div', {
class: 'dlc-titlebar',
$mousedown(ev) {
if (ev.target !== this) return;
const mask = h('div', { style: {
position: 'fixed',
left: '0',
top: '0',
width: '100vw',
height: '100vh',
}});
this.style.cursor = 'all-scroll';
document.body.appendChild(mask);
const { layerX, layerY } = ev;
const move = (ev) => {
cmd.style.left = `${ev.clientX - layerX}px`;
cmd.style.top = `${ev.clientY - layerY}px`;
};
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', (ev) => {
document.removeEventListener('mousemove', move);
this.style.cursor = '';
mask.remove();
}, { once: true });
},
}, [
['button', {
class: 'dlc-btn',
$click: (ev) => {
ev.stopPropagation();
if (refs.runBtn.innerText === 'Dispatch') eventBus.emit('dlc.run');
else eventBus.emit('dlc.stop');
},
}, 'Dispatch', (el) => { refs.runBtn = el; }],
'v2.10',
['div', {
class: 'dlc-close-btn',
$click: (ev) => {
ev.stopPropagation();
cmd.style.setProperty('display', 'none');
},
}, 'X'],
]],
['div', { style: { margin: '0 auto' } }, [
ConfigField({
label: '',
name: 'text',
type: 'textarea',
props: {
placeholder: 'Enter the content to be launched here',
style: {
width: '265px',
height: '155px',
overflow: 'scroll',
whiteSpace: 'pre',
},
},
}),
ConfigField({
label: 'Minimum interval time(s):',
name: 'minCycleSec',
type: 'input',
props: {
type: 'number',
min: 3,
placeholder: 3,
style: { width: '48px', margin: '1px' },
},
}),
ConfigField({
label: 'Maximum interval time(s):',
name: 'maxCycleSec',
type: 'input',
props: {
type: 'number',
min: 3,
placeholder: 3,
style: { width: '48px', margin: '1px' },
},
}),
ConfigField({
label: 'Maximum length of barrage:',
name: 'maxDanmakuLength',
type: 'input',
props: {
type: 'number',
min: 1,
style: { width: '48px', margin: '1px' },
},
}),

ConfigField({
label: 'Sentence segmentation:',
name: 'splitMode',
type: 'select',
children: [
{ value: '2', text: 'Multi-sentence runner' },
{ value: '0', text: 'Single sentence pattern' },
{ value: '1', text: 'Storytelling mode' },
].map(({ text, value }) => ['option', { value }, text]),
helpDesc: 'Multi-sentence wheel: one line per barrage; single sentence
mode: continuous line; storytelling mode: break lines according to the form symbol
and the lower limit of storytelling length',
}),
ConfigField({
label: 'Random barrage:',
name: 'randomDanmaku',
type: 'input',
props: { type: 'checkbox' },
valueProp: 'checked',
helpDesc: 'Random Barrage: Whether to randomly select a barrage from the
barrage list to send, only used in the multi-sentence reel mode; enabling the
storytelling mode will cause confusion',
}, (el) => {
if (+config.splitMode !== 2) el.classList.add('hide');
eventBus.on('setConfig.splitMode', ({ detail: value }) => {
if (value !== '2') el.classList.add('hide');
else el.classList.remove('hide');
});
}),
ConfigField({
label: 'Minimum storytelling length:',
name: 'minDanmakuLength',
type: 'input',
props: {
type: 'number',
min: 1,
style: { width: '48px', margin: '1px' },
},
helpDesc: 'Lower limit of storytelling length: Only the storytelling mode
takes effect, try to control the barrage length above this when segmenting
sentences',
}, (el) => {
if (+config.splitMode !== 1) el.classList.add('hide');
eventBus.on('setConfig.splitMode', ({ detail: value }) => {
if (value !== '1') el.classList.add('hide');
else el.classList.remove('hide');
});
}),
ConfigField({
label: 'Storyteller:',
name: 'splitChar',
type: 'input',
props: {
type: 'text',
min: 1,
style: { width: '48px', margin: '1px' },
},
helpDesc: 'Storytelling breaker: In storytelling mode, the article is
divided into multiple bullet screens with the configured symbol, and then merged
into the length of the bullet screen above the lower limit of the storytelling
length. When it is empty, the sentence is fixed according to the lower limit of the
storytelling length',
}, (el) => {
if (+config.splitMode !== 1) el.classList.add('hide');
eventBus.on('setConfig.splitMode', ({ detail: value }) => {
if (value !== '1') el.classList.add('hide');
else el.classList.remove('hide');
});
}),
ConfigField({
label: h([
[
'button', {
class: 'dlc-btn',
$click: ({target}) => {
if (target.innerText !== 'Timing start') return;
const {startTime} = config;
const timeStamp = Date.parse(new Date(startTime));
let timeRemain = timeStamp - new Date().getTime();
target.innerText = formatTime(parseInt(timeRemain/1000));
const startTimer = () => {
setTimeout(
() => {
if (timeRemain > 0){
timeRemain -= 1000;
target.innerText = formatTime(parseInt(timeRemain/1000));
startTimer();
}else{
eventBus.emit('dlc.run');
target.innerText = 'Timing start'
}
},
1000,
);
};
startTimer();
}
}, 'Timing start'
]
]),
name: 'startTime',
type: 'input',
props: {
type: 'text',
min: 1,
style: { width: '150px', margin: '1px' },
},
// helpDesc: 'Input time timing start,Format example:2020-10-21
17:31:00',
}),
ConfigField({
label: h([
[
'button', {
class: 'dlc-btn',
$click: ({target}) => {
if (target.innerText !== 'Timing ends') return;
const {stopTime} = config;
const timeStamp = Date.parse(new Date(stopTime));
let timeRemain = timeStamp - new Date().getTime();
target.innerText = formatTime(parseInt(timeRemain/1000));
const stopTimer = () => {
setTimeout(
() => {
if (timeRemain > 0){
timeRemain -= 1000;
target.innerText = formatTime(parseInt(timeRemain/1000));
stopTimer();
}else{
eventBus.emit('dlc.stop');
target.innerText = 'Timing ends'
}
},
1000,
);
};
stopTimer();
}
}, 'Timing ends'
]
]),
name: 'stopTime',
type: 'input',
props: {
type: 'text',
min: 1,
style: { width: '150px', margin: '1px' },
},
// helpDesc: 'Timing end of input time,Format example:2020-10-21
17:31:00',
}),

ConfigField({
label: h([
'Load remote barrage library:',
['button', {
class: 'dlc-btn',
$click: ({ target }) => {
if (target.innerText !== 'Update') return;
target.innerText = 'updating...';
const { remoteDanmakuBase } = config;
const urlList = remoteDanmakuBase.split('\n').map((it) =>
it.trim()).filter(Boolean);
const queued = new Set();
const allRemoteUrl = new Set();
const loaded = [];
const loadFinish = () => {
eventBus.emit('setRef.remoteDanmakuConfig', loaded);
target.innerText = 'Update';
Toast(h(
'pre',
{ style: { color: 'blue' } },
refs.remoteDanmakuConfig
.map((data) => data.error || `${data.name || 'Anonymous
Barrage Library'}: ${data.list.length}Article`)
.join('\n'),
));
};
const loadRemoteDanmaku = (url) => {
if (allRemoteUrl.has(url)) return;
queued.add(url);
allRemoteUrl.add(url);
fetch(url)
.then((data) => data.json())
.then((data) => {
if (!data) {
loaded.push({ error: `[Get failed]${url}` });
return;
}
if (Array.isArray(data.extends)) {
data.extends.forEach((extUrl) =>
loadRemoteDanmaku(extUrl));
}
if (Array.isArray(data.list)) loaded.push(data);
})
.catch((err) => {
console.error(err);
loaded.push({ error: `[Get failed]${url}` });
})
.finally(() => {
queued.delete(url);
if (queued.size === 0) loadFinish();
});
};
urlList.forEach((url) => loadRemoteDanmaku(url));
},
}, 'Update'],
['span', {}, '(Barrage library not loaded)', (el) => {
eventBus.on('setRef.remoteDanmakuConfig', ({ detail }) => {
const totalLength = detail.reduce(
(total, data) => total + ((data && data.list.length) || 0),
0,
);
el.innerText = `(Loaded${totalLength}Article)`;
});
}],
]),
name: 'remoteDanmakuBase',
type: 'textarea',
props: {
placeholder: 'Support multiple URLs, each line',
style: {
width: '265px',
height: '70px',
overflow: 'scroll',
whiteSpace: 'pre',
},
},
helpDesc: 'Load the barrage list from the specified address, support
adding multiple addresses, one line each; the remote barrage list will be added to
the local barrage list; the updated barrage will take effect the next time it is
dispatched',
}),

]],
]);

// Default floating window


let tip = false;
const suspension = h('div', {
class: 'dlc-suspension',
$click: () => {
cmd.style.setProperty('display', 'block');
if (!tip) {
tip = true;
Toast('In the multi-sentence runner mode, please separate each sentence
with a carriage return. For your own account, it is recommended to adjust the
speaking interval to more than 8000;');
}
},
}, 'Initializing...', (el) => {
eventBus.on('dlc.ready', () => {
el.textContent = 'Wheelbarrow console';
}, { once: true });
});

const initChatCtx = async () => {


try {
const chatFrameCtx = document.getElementById('chatframe').contentWindow;
refs.sendBtn = chatFrameCtx.document.querySelector('#send-button
button'); // Send button
refs.chatTxtInput = chatFrameCtx.document.querySelector('#input.yt-live-
chat-text-input-field-renderer'); // Input box
if (!refs.sendBtn || !refs.chatTxtInput) throw new Error('Initializing
retrying..');
eventBus.emit('dlc.ready');
} catch (_) {
setTimeout(initChatCtx, 1000);
}
};
initChatCtx();
// Window width changes will cause the chat bar to reload
setInterval(() => {
try {
const chatFrameCtx = document.getElementById('chatframe').contentWindow;
const sendBtn = chatFrameCtx.document.querySelector('#send-button button');
// Send button
const chatTxtInput = chatFrameCtx.document.querySelector('#input.yt-live-
chat-text-input-field-renderer'); // Input box
if (sendBtn) refs.sendBtn = sendBtn;
if (chatTxtInput) refs.chatTxtInput = chatTxtInput;
} catch (_) {
}
}, 60000);

window.dulunche = {
config: new Proxy(config, {
set(_, p, value) {
eventBus.emit(`setConfig.${p}`, value);
},
}),
eventBus,
};

document.body.appendChild(suspension);
document.body.appendChild(cmd);

document.body.appendChild(h('style', {}, `
.dlc-cmd {
background: #FFFFFF;
overflow-y: auto;
overflow-x: hidden;
z-index: 998;
position: fixed;
padding:5px;
width: 290px;
height: 370px;
box-sizing: content-box;
border: 1px solid #ff921a;
border-radius: 5px;
right: 10px;
top: 30%;
display: none;
}
.dlc-titlebar {
user-select: none;
background-color: #fb5;
}
.dlc-close-btn {
display: inline-block;
margin-top: 3px;
position: relative;
text-align: center;
width: 19px;
height: 19px;
color: white;
cursor: pointer;
float: right;
margin-right: 5px;
background-color: black;
border: gray 1px solid;
line-height: 21px;
}
.dlc-btn {
display: inline-block;
background: #f70;
color: #FFFFFF;
width: 70px;
height: 24px;
margin: 2px;
}
.dlc-suspension {
background: #1A59B7;
color:#ffffff;
overflow: hidden;
z-index: 997;
position: fixed;
padding:5px;
text-align:center;
width: 85px;
height: 22px;
border-radius: 5px;
right: 10px;
top: 30%;
}
.dlc-toast {
z-index: 10000;
position: fixed;
top: 10px;
right: 10px;
background: lightgrey;
max-width: 300px;
padding: 16px;
border: gray 1px solid;
}
.dlc-toast::after {
content: '(Click close)';
}
.help-icon::after {
content: '?';
margin-right: 4px;
display: inline-block;
background-color: #ddd;
border-radius: 50%;
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
border: #999 1px solid;
font-size: 12px;
}
.hide {
display: none;
}
`));
}

You might also like