mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-10 10:11:46 +00:00
REMOVE adblocker AND no-google-login, and renaming
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
# YouTube Music
|
||||
# YTMD
|
||||
|
||||
[](https://github.com/th-ch/youtube-music/releases/)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/license)
|
||||
@ -65,9 +65,6 @@ Read this in other languages: [한국어](./docs/readme/README-ko.md), [Françai
|
||||
- And more ...
|
||||
|
||||
## Available plugins:
|
||||
|
||||
- **Ad Blocker**: Block all ads and tracking out of the box
|
||||
|
||||
- **Album Actions**: Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album
|
||||
|
||||
- **Album Color Theme**: Applies a dynamic theme and visual effects based on the album color palette
|
||||
@ -116,8 +113,6 @@ Read this in other languages: [한국어](./docs/readme/README-ko.md), [Françai
|
||||
|
||||
- **Navigation**: Next/Back navigation arrows directly integrated in the interface, like in your favorite browser
|
||||
|
||||
- **No Google Login**: Remove Google login buttons and links from the interface
|
||||
|
||||
- **Notifications**: Display a notification when a song starts
|
||||
playing ([interactive notifications](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png)
|
||||
are available on windows)
|
||||
|
||||
1
src/plugins/adblocker/.gitignore
vendored
1
src/plugins/adblocker/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/ad-blocker-engine.bin
|
||||
@ -1,58 +0,0 @@
|
||||
function skipAd(target: Element) {
|
||||
const skipButton = target.querySelector<HTMLButtonElement>(
|
||||
'button.ytp-ad-skip-button-modern',
|
||||
);
|
||||
if (skipButton) {
|
||||
skipButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
function speedUpAndMute(player: Element, isAdShowing: boolean) {
|
||||
const video = player.querySelector<HTMLVideoElement>('video');
|
||||
if (!video) return;
|
||||
if (isAdShowing) {
|
||||
video.playbackRate = 16;
|
||||
video.muted = true;
|
||||
} else if (!isAdShowing) {
|
||||
video.playbackRate = 1;
|
||||
video.muted = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const loadAdSpeedup = () => {
|
||||
const player = document.querySelector<HTMLVideoElement>('#movie_player');
|
||||
if (!player) return;
|
||||
|
||||
new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (
|
||||
mutation.type === 'attributes' &&
|
||||
mutation.attributeName === 'class'
|
||||
) {
|
||||
const target = mutation.target as HTMLElement;
|
||||
|
||||
const isAdShowing =
|
||||
target.classList.contains('ad-showing') ||
|
||||
target.classList.contains('ad-interrupting');
|
||||
speedUpAndMute(target, isAdShowing);
|
||||
}
|
||||
if (
|
||||
mutation.type === 'childList' &&
|
||||
mutation.addedNodes.length &&
|
||||
mutation.target instanceof HTMLElement
|
||||
) {
|
||||
skipAd(mutation.target);
|
||||
}
|
||||
}
|
||||
}).observe(player, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
const isAdShowing =
|
||||
player.classList.contains('ad-showing') ||
|
||||
player.classList.contains('ad-interrupting');
|
||||
speedUpAndMute(player, isAdShowing);
|
||||
skipAd(player);
|
||||
};
|
||||
@ -1,81 +0,0 @@
|
||||
// Used for caching
|
||||
import path from 'node:path';
|
||||
import fs, { promises } from 'node:fs';
|
||||
|
||||
import { ElectronBlocker } from '@ghostery/adblocker-electron';
|
||||
import { app, net } from 'electron';
|
||||
|
||||
const SOURCES = [
|
||||
'https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt',
|
||||
// UBlock Origin
|
||||
'https://raw.githubusercontent.com/ghostery/adblocker/master/packages/adblocker/assets/ublock-origin/filters.txt',
|
||||
'https://raw.githubusercontent.com/ghostery/adblocker/master/packages/adblocker/assets/ublock-origin/quick-fixes.txt',
|
||||
'https://raw.githubusercontent.com/ghostery/adblocker/master/packages/adblocker/assets/ublock-origin/unbreak.txt',
|
||||
'https://raw.githubusercontent.com/ghostery/adblocker/master/packages/adblocker/assets/ublock-origin/filters-2020.txt',
|
||||
'https://raw.githubusercontent.com/ghostery/adblocker/master/packages/adblocker/assets/ublock-origin/filters-2021.txt',
|
||||
'https://raw.githubusercontent.com/ghostery/adblocker/master/packages/adblocker/assets/ublock-origin/filters-2022.txt',
|
||||
'https://raw.githubusercontent.com/ghostery/adblocker/master/packages/adblocker/assets/ublock-origin/filters-2023.txt',
|
||||
// Fanboy Annoyances
|
||||
'https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt',
|
||||
// AdGuard
|
||||
'https://filters.adtidy.org/extension/ublock/filters/122_optimized.txt',
|
||||
];
|
||||
|
||||
let blocker: ElectronBlocker | undefined;
|
||||
|
||||
export const loadAdBlockerEngine = async (
|
||||
session: Electron.Session | undefined = undefined,
|
||||
cache: boolean = true,
|
||||
additionalBlockLists: string[] = [],
|
||||
disableDefaultLists: boolean | unknown[] = false,
|
||||
) => {
|
||||
// Only use cache if no additional blocklists are passed
|
||||
const cacheDirectory = path.join(app.getPath('userData'), 'adblock_cache');
|
||||
if (!fs.existsSync(cacheDirectory)) {
|
||||
fs.mkdirSync(cacheDirectory);
|
||||
}
|
||||
const cachingOptions =
|
||||
cache && additionalBlockLists.length === 0
|
||||
? {
|
||||
path: path.join(cacheDirectory, 'adblocker-engine.bin'),
|
||||
read: promises.readFile,
|
||||
write: promises.writeFile,
|
||||
}
|
||||
: undefined;
|
||||
const lists = [
|
||||
...((disableDefaultLists && !Array.isArray(disableDefaultLists)) ||
|
||||
(Array.isArray(disableDefaultLists) && disableDefaultLists.length > 0)
|
||||
? []
|
||||
: SOURCES),
|
||||
...additionalBlockLists,
|
||||
];
|
||||
|
||||
try {
|
||||
blocker = await ElectronBlocker.fromLists(
|
||||
(url: string) => net.fetch(url),
|
||||
lists,
|
||||
{
|
||||
enableCompression: true,
|
||||
// When generating the engine for caching, do not load network filters
|
||||
// So that enhancing the session works as expected
|
||||
// Allowing to define multiple webRequest listeners
|
||||
loadNetworkFilters: session !== undefined,
|
||||
},
|
||||
cachingOptions,
|
||||
);
|
||||
if (session) {
|
||||
blocker.enableBlockingInSession(session);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading adBlocker engine', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const unloadAdBlockerEngine = (session: Electron.Session) => {
|
||||
if (blocker) {
|
||||
blocker.disableBlockingInSession(session);
|
||||
}
|
||||
};
|
||||
|
||||
export const isBlockerEnabled = (session: Electron.Session) =>
|
||||
blocker !== undefined && blocker.isBlockingEnabled(session);
|
||||
@ -1,148 +0,0 @@
|
||||
import { contextBridge, webFrame } from 'electron';
|
||||
|
||||
import { blockers } from './types';
|
||||
import { createPlugin } from '@/utils';
|
||||
import {
|
||||
isBlockerEnabled,
|
||||
loadAdBlockerEngine,
|
||||
unloadAdBlockerEngine,
|
||||
} from './blocker';
|
||||
|
||||
import { inject, isInjected } from './injectors/inject';
|
||||
import { loadAdSpeedup } from './adSpeedup';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import type { BrowserWindow } from 'electron';
|
||||
|
||||
interface AdblockerConfig {
|
||||
/**
|
||||
* Whether to enable the adblocker.
|
||||
* @default true
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* When enabled, the adblocker will cache the blocklists.
|
||||
* @default true
|
||||
*/
|
||||
cache: boolean;
|
||||
/**
|
||||
* Which adblocker to use.
|
||||
* @default blockers.InPlayer
|
||||
*/
|
||||
blocker: (typeof blockers)[keyof typeof blockers];
|
||||
/**
|
||||
* Additional list of filters to use.
|
||||
* @example ["https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"]
|
||||
* @default []
|
||||
*/
|
||||
additionalBlockLists: string[];
|
||||
/**
|
||||
* Disable the default blocklists.
|
||||
* @default false
|
||||
*/
|
||||
disableDefaultLists: boolean;
|
||||
}
|
||||
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.adblocker.name'),
|
||||
description: () => t('plugins.adblocker.description'),
|
||||
restartNeeded: false,
|
||||
config: {
|
||||
enabled: true,
|
||||
cache: true,
|
||||
blocker: blockers.InPlayer,
|
||||
additionalBlockLists: [],
|
||||
disableDefaultLists: false,
|
||||
} as AdblockerConfig,
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
const config = await getConfig();
|
||||
|
||||
return [
|
||||
{
|
||||
label: t('plugins.adblocker.menu.blocker'),
|
||||
submenu: Object.values(blockers).map((blocker) => ({
|
||||
label: blocker,
|
||||
type: 'radio',
|
||||
checked: (config.blocker || blockers.WithBlocklists) === blocker,
|
||||
click() {
|
||||
setConfig({ blocker });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
renderer: {
|
||||
async onPlayerApiReady(_, { getConfig }) {
|
||||
const config = await getConfig();
|
||||
if (config.blocker === blockers.AdSpeedup) {
|
||||
loadAdSpeedup();
|
||||
}
|
||||
},
|
||||
},
|
||||
backend: {
|
||||
mainWindow: null as BrowserWindow | null,
|
||||
async start({ getConfig, window }) {
|
||||
const config = await getConfig();
|
||||
this.mainWindow = window;
|
||||
|
||||
if (config.blocker === blockers.WithBlocklists) {
|
||||
await loadAdBlockerEngine(
|
||||
window.webContents.session,
|
||||
config.cache,
|
||||
config.additionalBlockLists,
|
||||
config.disableDefaultLists,
|
||||
);
|
||||
}
|
||||
},
|
||||
stop({ window }) {
|
||||
if (isBlockerEnabled(window.webContents.session)) {
|
||||
unloadAdBlockerEngine(window.webContents.session);
|
||||
}
|
||||
},
|
||||
async onConfigChange(newConfig) {
|
||||
if (this.mainWindow) {
|
||||
if (
|
||||
newConfig.blocker === blockers.WithBlocklists &&
|
||||
!isBlockerEnabled(this.mainWindow.webContents.session)
|
||||
) {
|
||||
await loadAdBlockerEngine(
|
||||
this.mainWindow.webContents.session,
|
||||
newConfig.cache,
|
||||
newConfig.additionalBlockLists,
|
||||
newConfig.disableDefaultLists,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
preload: {
|
||||
// see #1478
|
||||
script: `const _prunerFn = window._pruner;
|
||||
window._pruner = undefined;
|
||||
JSON.parse = new Proxy(JSON.parse, {
|
||||
apply() {
|
||||
return _prunerFn(Reflect.apply(...arguments));
|
||||
},
|
||||
});
|
||||
Response.prototype.json = new Proxy(Response.prototype.json, {
|
||||
apply() {
|
||||
return Reflect.apply(...arguments).then((o) => _prunerFn(o));
|
||||
},
|
||||
}); 0`,
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
|
||||
if (config.blocker === blockers.InPlayer && !isInjected()) {
|
||||
inject(contextBridge);
|
||||
await webFrame.executeJavaScript(this.script);
|
||||
}
|
||||
},
|
||||
async onConfigChange(newConfig) {
|
||||
if (newConfig.blocker === blockers.InPlayer && !isInjected()) {
|
||||
inject(contextBridge);
|
||||
await webFrame.executeJavaScript(this.script);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -1,3 +0,0 @@
|
||||
export default async () => {
|
||||
await import('@ghostery/adblocker-electron-preload');
|
||||
};
|
||||
5
src/plugins/adblocker/injectors/inject.d.ts
vendored
5
src/plugins/adblocker/injectors/inject.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
import type { ContextBridge } from 'electron';
|
||||
|
||||
export const inject: (contextBridge: ContextBridge) => void;
|
||||
|
||||
export const isInjected: () => boolean;
|
||||
@ -1,259 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// Source: https://addons.mozilla.org/en-US/firefox/addon/adblock-for-youtube/
|
||||
// https://robwu.nl/crxviewer/?crx=https%3A%2F%2Faddons.mozilla.org%2Fen-US%2Ffirefox%2Faddon%2Fadblock-for-youtube%2F
|
||||
|
||||
/*
|
||||
Parts of this code is derived from set-constant.js:
|
||||
https://github.com/gorhill/uBlock/blob/5de0ce975753b7565759ac40983d31978d1f84ca/assets/resources/scriptlets.js#L704
|
||||
*/
|
||||
|
||||
let injected = false;
|
||||
|
||||
export const isInjected = () => injected;
|
||||
|
||||
/**
|
||||
* @param {Electron.ContextBridge} contextBridge
|
||||
* @returns {*}
|
||||
*/
|
||||
export const inject = (contextBridge) => {
|
||||
injected = true;
|
||||
{
|
||||
const pruner = function (o) {
|
||||
delete o.playerAds;
|
||||
delete o.adPlacements;
|
||||
delete o.adSlots;
|
||||
//
|
||||
if (o.playerResponse) {
|
||||
delete o.playerResponse.playerAds;
|
||||
delete o.playerResponse.adPlacements;
|
||||
delete o.playerResponse.adSlots;
|
||||
}
|
||||
if (o.ytInitialPlayerResponse) {
|
||||
delete o.ytInitialPlayerResponse.playerAds;
|
||||
delete o.ytInitialPlayerResponse.adPlacements;
|
||||
delete o.ytInitialPlayerResponse.adSlots;
|
||||
}
|
||||
|
||||
//
|
||||
return o;
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('_pruner', pruner);
|
||||
}
|
||||
|
||||
const chains = [
|
||||
{
|
||||
chain: 'playerResponse.adPlacements',
|
||||
cValue: 'undefined',
|
||||
},
|
||||
{
|
||||
chain: 'ytInitialPlayerResponse.playerAds',
|
||||
cValue: 'undefined',
|
||||
},
|
||||
{
|
||||
chain: 'ytInitialPlayerResponse.adPlacements',
|
||||
cValue: 'undefined',
|
||||
},
|
||||
{
|
||||
chain: 'ytInitialPlayerResponse.adSlots',
|
||||
cValue: 'undefined',
|
||||
}
|
||||
];
|
||||
|
||||
chains.forEach(function ({ chain, cValue }) {
|
||||
const thisScript = document.currentScript;
|
||||
//
|
||||
switch (cValue) {
|
||||
case 'null': {
|
||||
cValue = null;
|
||||
break;
|
||||
}
|
||||
|
||||
case "''": {
|
||||
cValue = '';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'true': {
|
||||
cValue = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'false': {
|
||||
cValue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'undefined': {
|
||||
cValue = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'noopFunc': {
|
||||
cValue = function () {};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'trueFunc': {
|
||||
cValue = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'falseFunc': {
|
||||
cValue = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (/^\d+$/.test(cValue)) {
|
||||
cValue = Number.parseFloat(cValue);
|
||||
//
|
||||
if (isNaN(cValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(cValue) > 0x7f_ff) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
let aborted = false;
|
||||
const mustAbort = function (v) {
|
||||
if (aborted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
aborted =
|
||||
v !== undefined &&
|
||||
v !== null &&
|
||||
cValue !== undefined &&
|
||||
cValue !== null &&
|
||||
typeof v !== typeof cValue;
|
||||
return aborted;
|
||||
};
|
||||
|
||||
/*
|
||||
Support multiple trappers for the same property:
|
||||
https://github.com/uBlockOrigin/uBlock-issues/issues/156
|
||||
*/
|
||||
|
||||
const trapProp = function (owner, prop, configurable, handler) {
|
||||
if (handler.init(owner[prop]) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
const odesc = Object.getOwnPropertyDescriptor(owner, prop);
|
||||
let previousGetter;
|
||||
let previousSetter;
|
||||
if (odesc instanceof Object) {
|
||||
if (odesc.configurable === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (odesc.get instanceof Function) {
|
||||
previousGetter = odesc.get;
|
||||
}
|
||||
|
||||
if (odesc.set instanceof Function) {
|
||||
previousSetter = odesc.set;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
Object.defineProperty(owner, prop, {
|
||||
configurable,
|
||||
get() {
|
||||
if (previousGetter !== undefined) {
|
||||
previousGetter();
|
||||
}
|
||||
|
||||
//
|
||||
return handler.getter();
|
||||
},
|
||||
set(a) {
|
||||
if (previousSetter !== undefined) {
|
||||
previousSetter(a);
|
||||
}
|
||||
|
||||
//
|
||||
handler.setter(a);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const trapChain = function (owner, chain) {
|
||||
const pos = chain.indexOf('.');
|
||||
if (pos === -1) {
|
||||
trapProp(owner, chain, false, {
|
||||
v: undefined,
|
||||
getter() {
|
||||
return document.currentScript === thisScript ? this.v : cValue;
|
||||
},
|
||||
setter(a) {
|
||||
if (mustAbort(a) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
cValue = a;
|
||||
},
|
||||
init(v) {
|
||||
if (mustAbort(v)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
this.v = v;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
//
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
const prop = chain.slice(0, pos);
|
||||
const v = owner[prop];
|
||||
//
|
||||
chain = chain.slice(pos + 1);
|
||||
if (v instanceof Object || (typeof v === 'object' && v !== null)) {
|
||||
trapChain(v, chain);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
trapProp(owner, prop, true, {
|
||||
v: undefined,
|
||||
getter() {
|
||||
return this.v;
|
||||
},
|
||||
setter(a) {
|
||||
this.v = a;
|
||||
if (a instanceof Object) {
|
||||
trapChain(a, chain);
|
||||
}
|
||||
},
|
||||
init(v) {
|
||||
this.v = v;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
trapChain(window, chain);
|
||||
});
|
||||
};
|
||||
@ -1,5 +0,0 @@
|
||||
export const blockers = {
|
||||
WithBlocklists: 'With blocklists',
|
||||
InPlayer: 'In player',
|
||||
AdSpeedup: 'Ad speedup',
|
||||
} as const;
|
||||
@ -1,26 +0,0 @@
|
||||
import style from './style.css?inline';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.no-google-login.name'),
|
||||
description: () => t('plugins.no-google-login.description'),
|
||||
restartNeeded: true,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
stylesheets: [style],
|
||||
renderer() {
|
||||
const elementsToRemove = [
|
||||
'.sign-in-link.ytmusic-nav-bar',
|
||||
'.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"]',
|
||||
];
|
||||
|
||||
for (const selector of elementsToRemove) {
|
||||
const node = document.querySelector(selector);
|
||||
if (node) {
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
.ytmusic-pivot-bar-renderer[tab-id='FEmusic_liked'],
|
||||
ytmusic-guide-signin-promo-renderer,
|
||||
a[href='/music_premium'],
|
||||
.sign-in-link {
|
||||
display: none !important;
|
||||
}
|
||||
Reference in New Issue
Block a user