Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9221bcb
add tempermonkey scripts and readme info
KrX3D Mar 8, 2026
c79f72b
add visual logger
KrX3D Mar 8, 2026
78ba5ca
chore: bump version to 1.12.23 + rebuild dist [skip ci]
github-actions[bot] Mar 8, 2026
694cf2e
add version and
KrX3D Mar 8, 2026
d2e1a35
chore: bump version to 1.12.33 + rebuild dist [skip ci]
github-actions[bot] Mar 8, 2026
d71a663
Populating key null with default value undefined fix
KrX3D Mar 8, 2026
b4e8b0c
chore: bump version to 1.12.43 + rebuild dist [skip ci]
github-actions[bot] Mar 8, 2026
1caea7b
fix
KrX3D Mar 9, 2026
710fc19
chore: bump version to 1.12.53 + rebuild dist [skip ci]
github-actions[bot] Mar 9, 2026
7b61dcd
fix
KrX3D Mar 9, 2026
478bb40
chore: bump version to 1.12.63 + rebuild dist [skip ci]
github-actions[bot] Mar 9, 2026
e44f10e
f
KrX3D Mar 9, 2026
6a96b51
chore: bump version to 1.12.73 + rebuild dist [skip ci]
github-actions[bot] Mar 9, 2026
a5a218a
Merge upstream/main into get_debug_logs_on_browser
Apr 14, 2026
f25fd74
Merge pull request #506 from KrX3D/sync/upstream-to-get_debug_logs_on…
KrX3D Apr 14, 2026
645724c
Update resolveCommand.js
KrX3D Apr 14, 2026
0928aeb
Update rollup.config.js
KrX3D Apr 14, 2026
0bc3b5f
chore: bump version to 1.13.12 + rebuild dist [skip ci]
github-actions[bot] Apr 14, 2026
2b80211
Refactor whitespace and comments in ui.js
KrX3D Apr 14, 2026
81cec78
chore: bump version to 1.13.22 + rebuild dist [skip ci]
github-actions[bot] Apr 14, 2026
0bdf3e8
Update config.js
KrX3D Apr 14, 2026
692df4b
chore: bump version to 1.13.32 + rebuild dist [skip ci]
github-actions[bot] Apr 14, 2026
f8ca1a8
Delete dist/userScript.js
KrX3D Apr 19, 2026
37198ed
Delete .github/workflows/build-publish-cleanup.yml
KrX3D Apr 19, 2026
6da26a7
Delete dist/service.js
KrX3D Apr 19, 2026
174751b
Update package.json
KrX3D Apr 19, 2026
c730e36
Update package.json
KrX3D Apr 19, 2026
7f48132
Remove app version and update settings summary text
KrX3D Apr 19, 2026
afc7501
Merge pull request #519 from KrX3D/get_debug_logs_on_browser
KrX3D Apr 19, 2026
16d09b1
fix: improve deArrowify with queue, safer iteration and stricter guards
Apr 19, 2026
f02f637
fix: remove redundant capturedItem alias and restore thumbnail format…
Apr 19, 2026
c80cd46
Update adblock.js
KrX3D Apr 19, 2026
a902f71
Update ui.js
KrX3D Apr 19, 2026
54d289f
Update ui.js
KrX3D Apr 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,73 @@ Looking for an app for Android TVs? Check out [TizenTube Cobalt](https://github.
- [DeArrow](https://dearrow.ajay.app/) Support
- Customizable Themes (Custom Coloring)
- More to come, if you [request](https://github.com/reisxd/TizenTube/issues/new) it!

# Tampermonkey local debugging helpers (Windows + Chrome)

Use this when you want to test TizenTube locally in Chrome instead of on-device.

Tampermonkey runs the userscripts, while a User-Agent switcher extension makes Chrome present itself as a TV browser so `https://www.youtube.com/tv` stays on the TV UI.

## Scripts in this repository

Tampermonkey scripts are stored in:

- `scripts/tampermonkey/tizentube-loader.user.js` (loads `dist/userScript.js` via `@require`)
- `scripts/tampermonkey/tizentube-log-button.user.js` (adds floating **TT Logs** button that calls `window.downloadTizenTubeLogs()`)

## Full setup steps

1. Install **Tampermonkey** in Chrome.
2. Open `chrome://extensions/`:
- Enable **Developer mode** (top-right).
- Open Tampermonkey details and enable **Allow in Incognito**.
- If you use a User-Agent extension, also enable **Allow in Incognito** for it.
3. Install a User-Agent switching extension (for TV UA testing).
4. Set a TV-like User-Agent for YouTube. Example that usually works (can be in Incognito Mode):

`Mozilla/5.0 (SMART-TV; Linux; Tizen 6.5) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.0 TV Safari/537.36`


Real TV User-Agent examples:

- Tizen 5.0 TV:
`Mozilla/5.0 (SMART-TV; LINUX; Tizen 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/5.0 TV Safari/537.36`
- Tizen 6.5 TV:
`Mozilla/5.0 (SMART-TV; LINUX; Tizen 6.5) AppleWebKit/537.36 (KHTML, like Gecko) 108.0.5359.1/6.5 TV Safari/537.36 UWE/0.2.27108`

5. In Tampermonkey, create/import `scripts/tampermonkey/tizentube-loader.user.js` (configured for `youtube.com/tv*` only).
6. In Tampermonkey, create/import `scripts/tampermonkey/tizentube-log-button.user.js` (configured for `youtube.com/tv*` only).
7. In Tampermonkey script settings, set **Sandbox mode = ALL** for these scripts.
8. Open `https://www.youtube.com/tv` and sign in if needed.
9. Verify TizenTube loaded:
- Open DevTools Console (F12) and run `typeof window.toggleDebugConsole` (should return `"function"`).
10. Click **TT Logs** (bottom-right corner of the page, floating above the YouTube TV UI) to download logs without typing console commands.


### Keyboard shortcuts for Windows testing (no TV remote)

When testing in Chrome on desktop, TizenTube maps TV color-button actions to normal keys:

- **GREEN** (open TizenTube settings): `G`, `F2`, or `2`
- **RED** (open theme settings): `R`, `F1`, or `1`
- **YELLOW** (toggle debug console): `Y`, `F3`, or `3`
- **BLUE**: `B`, `F4`, or `4`

### Where should I see the button?

After both userscripts are enabled and `/tv` has loaded, the **TT Logs** button appears in the **bottom-right corner** as a small black/green floating button.

If you do not see it:
- confirm `tizentube-log-button.user.js` is enabled in Tampermonkey,
- refresh the page once,
- and verify Tampermonkey script sandbox is set to **ALL**.

## Notes

- These helper scripts are intentionally limited to `/tv` URLs so they do not affect normal desktop YouTube pages.
- In Tampermonkey, you can force-refresh `@require` files via **TizenTube Loader → Externals → Requires → Update**.
- If Tampermonkey seems stale in Incognito, open script dashboard and use **Utilities → Check for userscript updates**, then hard-refresh YouTube TV.
- Tampermonkey only refreshes `@require` when it checks script updates; bumping loader `@version` and running update check forces newest bundle.
- If `/tv` redirects back to desktop YouTube, re-check User-Agent override and extension scope.
- Keep both scripts enabled: loader + log-button.
- The log button is external (Tampermonkey UI helper), not an in-app visual-console button.
17 changes: 15 additions & 2 deletions mods/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,14 @@ const defaultConfig = {
autoFrameRatePauseVideoFor: 0,
enableSigninReminder: false,
sortSubscriptionsByAlphabet: false,
enableDebugConsole: false,
enableDebugLogging: false,
debugConsolePosition: 'top-left',
debugConsoleHeight: 500
};

let localConfig;
const populatedConfigWarnings = new Set();

try {
localConfig = JSON.parse(window.localStorage[CONFIG_KEY]);
Expand All @@ -67,10 +72,18 @@ try {
localConfig = defaultConfig;
}

if (!localConfig || typeof localConfig !== 'object') {
localConfig = { ...defaultConfig };
}

export function configRead(key) {
if (localConfig[key] === undefined) {
console.warn('Populating key', key, 'with default value', defaultConfig[key]);
localConfig[key] = defaultConfig[key];
const hasDefault = Object.prototype.hasOwnProperty.call(defaultConfig, key);
localConfig[key] = hasDefault ? defaultConfig[key] : undefined;
if (hasDefault && !populatedConfigWarnings.has(key)) {
populatedConfigWarnings.add(key);
console.warn('Populating key', key, 'with default value', defaultConfig[key]);
}
}

return localConfig[key];
Expand Down
3 changes: 1 addition & 2 deletions mods/features/adblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,6 @@ function deArrowify(items) {
}
}


function hqify(items) {
for (const item of items) {
if (!item.tileRenderer) continue;
Expand Down Expand Up @@ -411,4 +410,4 @@ function hideVideo(items) {
const percentWatched = (progressBar.percentDurationWatched || 0);
return percentWatched <= configRead('hideWatchedVideosThreshold');
});
}
}
5 changes: 3 additions & 2 deletions mods/features/moreSubtitles.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,12 @@ function patchSubtitleMenu() {
isPatched = true;
}

// Wait for the YouTube TV app to be ready
// Wait for the YouTube TV app to be ready, keep retrying until patch succeeds
const interval = setInterval(() => {
if (window._yttv && Object.keys(window._yttv).length > 0) {
patchSubtitleMenu();
clearInterval(interval);
// Only clear the interval if patching actually succeeded
if (isPatched) clearInterval(interval);
}
}, 1000);

Expand Down
2 changes: 1 addition & 1 deletion mods/features/sponsorblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ class SponsorBlockHandler {

this.sliderInterval = setInterval(() => {
this.slider = document.querySelector('ytlr-redux-connect-ytlr-progress-bar');
if (this.slider) {
if (this.slider && this.slider.isConnected) { // add isConnected check
clearInterval(this.sliderInterval);
this.sliderInterval = null;
this.observer.observe(this.slider, {
Expand Down
227 changes: 227 additions & 0 deletions mods/features/visualConsole.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { configRead, configWrite, configChangeEmitter } from '../config.js';
import resolveCommand from '../resolveCommand.js';
import rootPkg from '../../package.json';

const APP_VERSION_LABEL = 'TizenTube';
const APP_VERSION = rootPkg.version;

function detectTvModel() {
let modelName = null;
let modelCode = null;

try {
const h5vccModel = window?.h5vcc?.system?.getDeviceInfo?.()?.modelName;
if (h5vccModel) modelName = String(h5vccModel);
} catch (_) { }

try {
const webapisModel = window?.webapis?.productinfo?.getModel?.();
if (webapisModel) modelName = String(webapisModel);
} catch (_) { }

try {
const webapisRealModel = window?.webapis?.productinfo?.getRealModel?.();
if (webapisRealModel) modelName = String(webapisRealModel);
} catch (_) { }

try {
const maybeCode = window?.webapis?.productinfo?.getModelCode?.();
if (maybeCode) modelCode = String(maybeCode);
} catch (_) { }

try {
const ua = String(navigator.userAgent || '');
const match = ua.match(/\(([^)]*?TV[^)]*?)\)/i);
if (!modelName && match?.[1]) modelName = match[1];
} catch (_) { }

if (!modelName && !modelCode) return 'unknown';
if (modelName && modelCode) return `${modelName} (${modelCode})`;
return modelName || modelCode;
}

function initVisualConsole() {
const positions = {
'top-left': { top: '0', left: '0', right: '', bottom: '', transform: '' },
'top-right': { top: '0', right: '0', left: '', bottom: '', transform: '' },
'bottom-left': { bottom: '0', left: '0', right: '', top: '', transform: '' },
'bottom-right': { bottom: '0', right: '0', left: '', top: '', transform: '' },
center: { top: '50%', left: '50%', right: '', bottom: '', transform: 'translate(-50%, -50%)' }
};

const consoleDiv = document.createElement('div');
consoleDiv.id = 'tv-debug-console';

const applyPosition = () => {
const pos = configRead('debugConsolePosition') || 'bottom-right';
Object.assign(consoleDiv.style, positions[pos] || positions['bottom-right']);
};

const applyHeight = () => {
const h = Number(configRead('debugConsoleHeight') || 500);
consoleDiv.style.height = `${h}px`;
};

consoleDiv.style.cssText = `
position: fixed;
width: 1054px;
background: rgba(0, 0, 0, 0.95);
color: #0f0;
font-family: monospace;
font-size: 13px;
padding: 10px;
overflow-y: auto;
overflow-x: hidden;
z-index: 999999;
border: 3px solid #0f0;
display: none;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.5);
pointer-events: none;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
`;

applyPosition();
applyHeight();

const mount = () => {
if (document.body && !document.getElementById('tv-debug-console')) document.body.appendChild(consoleDiv);
};
mount();
document.addEventListener('DOMContentLoaded', mount);

let logs = [];
if (!Array.isArray(window.__ttFileOnlyLogs)) window.__ttFileOnlyLogs = [];
const original = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
debug: console.debug
};

const renderLogs = () => {
while (consoleDiv.firstChild) {
consoleDiv.removeChild(consoleDiv.firstChild);
}

for (const entry of logs) {
const row = document.createElement('div');
row.style.color = entry.color;
row.style.marginBottom = '4px';
row.style.whiteSpace = 'pre-wrap';
row.style.wordWrap = 'break-word';
row.textContent = `[${entry.time}] ${entry.msg}`;
consoleDiv.appendChild(row);
}
};

const syncVisible = () => {
const enabled = !!configRead('enableDebugConsole');
consoleDiv.style.display = enabled ? 'block' : 'none';
if (enabled) {
applyPosition();
applyHeight();
renderLogs();
consoleDiv.scrollTop = 0;
}
};

const addLog = (type, args) => {
if (!configRead('enableDebugConsole') && !configRead('enableDebugLogging')) return;
const color = type === 'error' ? '#f55' : type === 'warn' ? '#ff0' : '#0f0';
const msg = args.map((a) => {
if (typeof a === 'string') return a;
try { return JSON.stringify(a); } catch (_) { return String(a); }
}).join(' ');
logs.unshift({
color,
msg,
time: new Date().toLocaleTimeString()
});
if (logs.length > 600) logs.pop();
if (consoleDiv.style.display !== 'none') {
renderLogs();
consoleDiv.scrollTop = 0;
}
};

console.log = (...args) => { original.log.apply(console, args); addLog('log', args); };
console.info = (...args) => { original.info.apply(console, args); addLog('info', args); };
console.warn = (...args) => { original.warn.apply(console, args); addLog('warn', args); };
console.error = (...args) => { original.error.apply(console, args); addLog('error', args); };
console.debug = (...args) => { original.debug.apply(console, args); addLog('debug', args); };

const downloadLogs = () => {
try {
const plainTextLogs = logs
.map((entry) => `[${entry.time}] ${entry.msg}`)
.join('\n');
const fileOnlyLogs = Array.isArray(window.__ttFileOnlyLogs) ? window.__ttFileOnlyLogs.join('\n') : '';
const combinedLogs = `${plainTextLogs}\n\n===== FILE-ONLY DEBUG LOGS =====\n${fileOnlyLogs}`;
const blob = new Blob([combinedLogs], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `tizentube-logs-${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
} catch (err) {
original.error('Failed to download logs', err);
}
};

window.downloadTizenTubeLogs = downloadLogs;

const toggleDebugConsole = function () {
configWrite('enableDebugConsole', !configRead('enableDebugConsole'));
syncVisible();
};

window.toggleDebugConsole = toggleDebugConsole;

configChangeEmitter.addEventListener('configChange', (event) => {
if (event?.detail?.key === 'enableDebugConsole' || event?.detail?.key === 'debugConsolePosition' || event?.detail?.key === 'debugConsoleHeight') {
syncVisible();
}
});

syncVisible();

console.log('[Console] ========================================');
console.log('[Console] Use TizenTube settings to configure position/height');
console.log(`[Console] Visual Console ${APP_VERSION_LABEL} v${APP_VERSION}`);
console.log(`[Console] TV Model: ${detectTvModel()}`);
console.log(`[Console] User-Agent: ${navigator.userAgent}`);
console.log('[Console] ========================================');

const versionToastCmd = {
openPopupAction: {
popupType: 'TOAST',
popupDurationSeconds: 5,
popup: {
overlayToastRenderer: {
title: { simpleText: 'TizenTube started' },
subtitle: { simpleText: 'Version ' + APP_VERSION }
}
}
}
};

setTimeout(() => {
try { resolveCommand(versionToastCmd); } catch (_) { }
}, 1200);
}

if (typeof window.toggleDebugConsole !== 'function') {
window.toggleDebugConsole = function () {
configWrite('enableDebugConsole', !configRead('enableDebugConsole'));
};
}

const interval = setInterval(() => {
if (document.querySelector('video')) {
initVisualConsole();
clearInterval(interval);
}
}, 500);
Loading