Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save m-elewa/73dce6af6a958c89e9d371515b7d17bc to your computer and use it in GitHub Desktop.
Save m-elewa/73dce6af6a958c89e9d371515b7d17bc to your computer and use it in GitHub Desktop.
// ==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;
}
`));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment