Watch Later became a graveyard. MyTube is a Chrome extension that gives you a YouTube home page you actually curate: save videos straight from YouTube into categories you define, then come back to a clean library with nothing recommended and nothing autoplaying.
![]() | ![]() |
| Save from anywhere. Every video card on YouTube gets a Save button: pick a category, or make a new one right from the menu. | Organize it your way. Your own categories with your own icons; move any video between them in two clicks. |
![]() | ![]() |
| Nothing recommended. Search and filter what you saved, from the home page or the toolbar popup. | Make it yours. Accent colors, a retro CRT skin, and English or Portuguese. |
chrome.storage.sync.The home is not a new-tab override. Overriding the new tab hijacks every tab and triggers Chrome's "keep this page?" prompt, so the home is a normal page you open on demand: from the toolbar popup, or with Ctrl+Shift+Y (Cmd+Shift+Y on Mac). See src/home-page.ts.
From the Chrome Web Store (recommended): MyTube on the Chrome Web Store → Add to Chrome.
From source:
npm install && npm run buildchrome://extensions and enable Developer modedist/ folderMyTube asks for exactly storage plus access to https://www.youtube.com/*:
no tabs, no history. There is no server, no account, no ads and no
analytics. Your library lives in your own browser storage and never reaches the
developer.
The one request the extension makes on its own is a best-effort lookup to YouTube's public oEmbed endpoint, to fill in a title or channel name it couldn't read from the page. A failed lookup never blocks a save.
See docs/PRIVACY.md for the full policy, and specs/security-hardening.spec.md for the message-validation, CSP and least-privilege rules.
Built with Vite + CRXJS, React 18, TypeScript, Tailwind
CSS, Lucide icons and @dnd-kit for drag & drop.
npm install
npm run dev # Vite with HMR — load dist/ in Chrome as above
npm run build # tsc --noEmit && vite build → dist/
npm test # Vitest: reducer, content-script helpers, popup, home, tooling
npm run test:watch
npm run test:e2e # Playwright: loads the built extension in headed Chromium
npm run test:e2e needs npx playwright install chromium once. CI runs
npm test and the build on every pull request.
The service worker owns storage and validates every message at the trust
boundary before it reaches the MyTubeStore reducer. The typed contract is the
Message union and the StorageData schema in src/types.ts;
tests inject a fake storage backend, so they never need a Chrome runtime.
Every image and video of the extension is generated from the real packaged extension, driven by Playwright, so it stays in sync with the UI:
| Command | Produces |
|---|---|
npm run store:promo | Designed store listing images, in English and Portuguese (docs/store-assets/listing/); the YouTube thumbnail and the Shorts cover (docs/promo-video/); the support-form header; this README's banner |
npm run promo:video | The promo video, landscape 1080p and vertical 1080×1920, silent so music and narration can be added (docs/promo-video/NARRATION.md) |
node scripts/make-promo-video.mjs gif | This README's demo GIF, cut from the 1080p video |
npm run store:assets | Raw UI screenshots of every surface (docs/store-assets/) |
Interface text lives in one catalog, src/i18n.ts, keyed by the saved language setting, so you can switch language without changing your browser locale. English is the default; Portuguese (Brazil) is picked automatically on a Portuguese browser and can be switched in Settings.
The store-facing manifest strings live separately in _locales/
and follow the browser language, which is what the Chrome Web Store and
chrome://extensions display.
Work is spec-first for anything beyond a trivial fix. Each feature owns one
specs/<feature>.spec.md with a table of stable, observable acceptance criteria
(SAVE-3, DUR-1, …), and every criterion is bound to a test named after its
ID, so grepping an ID shows spec ↔ test in one shot.
openspec/ is frozen, read-only history; new behavior is specced only in
specs/*.spec.md.
docs/chrome-web-store-submission.md holds the listing copy (English and Portuguese), the permission justifications, and the packaging checklist for the Chrome Web Store.
MIT © Danilo Mikhael da Silva Melo
MyTube is an independent project, not affiliated with, sponsored by, or endorsed by YouTube or Google LLC. YouTube is a trademark of Google LLC.
TypeScript
68.6%
JavaScript
25.3%
CSS
5.5%
Watch Later became a graveyard. MyTube is a Chrome extension that gives you a YouTube home page you actually curate: save videos straight from YouTube into categories you define, then come back to a clean library with nothing recommended and nothing autoplaying.
![]() | ![]() |
| Save from anywhere. Every video card on YouTube gets a Save button: pick a category, or make a new one right from the menu. | Organize it your way. Your own categories with your own icons; move any video between them in two clicks. |
![]() | ![]() |
| Nothing recommended. Search and filter what you saved, from the home page or the toolbar popup. | Make it yours. Accent colors, a retro CRT skin, and English or Portuguese. |
chrome.storage.sync.The home is not a new-tab override. Overriding the new tab hijacks every tab and triggers Chrome's "keep this page?" prompt, so the home is a normal page you open on demand: from the toolbar popup, or with Ctrl+Shift+Y (Cmd+Shift+Y on Mac). See src/home-page.ts.
From the Chrome Web Store (recommended): MyTube on the Chrome Web Store → Add to Chrome.
From source:
npm install && npm run buildchrome://extensions and enable Developer modedist/ folderMyTube asks for exactly storage plus access to https://www.youtube.com/*:
no tabs, no history. There is no server, no account, no ads and no
analytics. Your library lives in your own browser storage and never reaches the
developer.
The one request the extension makes on its own is a best-effort lookup to YouTube's public oEmbed endpoint, to fill in a title or channel name it couldn't read from the page. A failed lookup never blocks a save.
See docs/PRIVACY.md for the full policy, and specs/security-hardening.spec.md for the message-validation, CSP and least-privilege rules.
Built with Vite + CRXJS, React 18, TypeScript, Tailwind
CSS, Lucide icons and @dnd-kit for drag & drop.
npm install
npm run dev # Vite with HMR — load dist/ in Chrome as above
npm run build # tsc --noEmit && vite build → dist/
npm test # Vitest: reducer, content-script helpers, popup, home, tooling
npm run test:watch
npm run test:e2e # Playwright: loads the built extension in headed Chromium
npm run test:e2e needs npx playwright install chromium once. CI runs
npm test and the build on every pull request.
The service worker owns storage and validates every message at the trust
boundary before it reaches the MyTubeStore reducer. The typed contract is the
Message union and the StorageData schema in src/types.ts;
tests inject a fake storage backend, so they never need a Chrome runtime.
Every image and video of the extension is generated from the real packaged extension, driven by Playwright, so it stays in sync with the UI:
| Command | Produces |
|---|---|
npm run store:promo | Designed store listing images, in English and Portuguese (docs/store-assets/listing/); the YouTube thumbnail and the Shorts cover (docs/promo-video/); the support-form header; this README's banner |
npm run promo:video | The promo video, landscape 1080p and vertical 1080×1920, silent so music and narration can be added (docs/promo-video/NARRATION.md) |
node scripts/make-promo-video.mjs gif | This README's demo GIF, cut from the 1080p video |
npm run store:assets | Raw UI screenshots of every surface (docs/store-assets/) |
Interface text lives in one catalog, src/i18n.ts, keyed by the saved language setting, so you can switch language without changing your browser locale. English is the default; Portuguese (Brazil) is picked automatically on a Portuguese browser and can be switched in Settings.
The store-facing manifest strings live separately in _locales/
and follow the browser language, which is what the Chrome Web Store and
chrome://extensions display.
Work is spec-first for anything beyond a trivial fix. Each feature owns one
specs/<feature>.spec.md with a table of stable, observable acceptance criteria
(SAVE-3, DUR-1, …), and every criterion is bound to a test named after its
ID, so grepping an ID shows spec ↔ test in one shot.
openspec/ is frozen, read-only history; new behavior is specced only in
specs/*.spec.md.
docs/chrome-web-store-submission.md holds the listing copy (English and Portuguese), the permission justifications, and the packaging checklist for the Chrome Web Store.
MIT © Danilo Mikhael da Silva Melo
MyTube is an independent project, not affiliated with, sponsored by, or endorsed by YouTube or Google LLC. YouTube is a trademark of Google LLC.
TypeScript
68.6%
JavaScript
25.3%
CSS
5.5%