mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
Initial commit - app + 4 plugins
This commit is contained in:
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
* text=auto
|
||||||
|
*.js text eol=lf
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
/assets/generated
|
||||||
BIN
assets/youtube-music.png
Normal file
BIN
assets/youtube-music.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
137
index.js
Normal file
137
index.js
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
"use strict";
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const electron = require("electron");
|
||||||
|
const isDev = require("electron-is-dev");
|
||||||
|
|
||||||
|
const { setApplicationMenu } = require("./menu");
|
||||||
|
const { getEnabledPlugins, store } = require("./store");
|
||||||
|
const { fileExists, injectCSS } = require("./plugins/utils");
|
||||||
|
|
||||||
|
const app = electron.app;
|
||||||
|
|
||||||
|
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||||
|
require("electron-debug")();
|
||||||
|
|
||||||
|
// Prevent window being garbage collected
|
||||||
|
let mainWindow;
|
||||||
|
|
||||||
|
let icon = "assets/youtube-music.png";
|
||||||
|
if (process.platform == "win32") {
|
||||||
|
icon = "assets/generated/icon.ico";
|
||||||
|
} else if (process.platform == "darwin") {
|
||||||
|
icon = "assets/generated/icon.icns";
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClosed() {
|
||||||
|
// Dereference the window
|
||||||
|
// For multiple windows store them in an array
|
||||||
|
mainWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMainWindow() {
|
||||||
|
const windowSize = store.get("window-size");
|
||||||
|
const windowMaximized = store.get("window-maximized");
|
||||||
|
|
||||||
|
const win = new electron.BrowserWindow({
|
||||||
|
icon : icon,
|
||||||
|
width : windowSize.width,
|
||||||
|
height : windowSize.height,
|
||||||
|
backgroundColor: "#000",
|
||||||
|
show : false,
|
||||||
|
webPreferences : {
|
||||||
|
nodeIntegration: false,
|
||||||
|
preload : path.join(__dirname, "preload.js")
|
||||||
|
},
|
||||||
|
frame : false,
|
||||||
|
titleBarStyle: "hiddenInset"
|
||||||
|
});
|
||||||
|
if (windowMaximized) {
|
||||||
|
win.maximize();
|
||||||
|
}
|
||||||
|
|
||||||
|
win.webContents.loadURL(store.get("url"));
|
||||||
|
win.on("closed", onClosed);
|
||||||
|
|
||||||
|
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
|
||||||
|
win.webContents.on("did-finish-load", () => {
|
||||||
|
if (isDev) {
|
||||||
|
console.log("did finish load");
|
||||||
|
win.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getEnabledPlugins().forEach(plugin => {
|
||||||
|
console.log("Loaded plugin - " + plugin);
|
||||||
|
const pluginPath = path.join(__dirname, "plugins", plugin, "back.js");
|
||||||
|
fileExists(pluginPath, () => {
|
||||||
|
const handle = require(pluginPath);
|
||||||
|
handle(win);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
win.webContents.on("did-navigate-in-page", () => {
|
||||||
|
const url = win.webContents.getURL();
|
||||||
|
if (url.startsWith("https://music.youtube.com")) {
|
||||||
|
store.set("url", url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
win.on("move", () => {
|
||||||
|
let position = win.getPosition();
|
||||||
|
store.set("window-position", { x: position[0], y: position[1] });
|
||||||
|
});
|
||||||
|
|
||||||
|
win.on("resize", () => {
|
||||||
|
const windowSize = win.getSize();
|
||||||
|
|
||||||
|
store.set("window-maximized", win.isMaximized());
|
||||||
|
if (!win.isMaximized()) {
|
||||||
|
store.set("window-size", { width: windowSize[0], height: windowSize[1] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
win.once("ready-to-show", () => {
|
||||||
|
win.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister all shortcuts.
|
||||||
|
electron.globalShortcut.unregisterAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("activate", () => {
|
||||||
|
// On OS X it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
if (mainWindow === null) {
|
||||||
|
mainWindow = createMainWindow();
|
||||||
|
} else if (!mainWindow.isVisible()) {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("ready", () => {
|
||||||
|
setApplicationMenu();
|
||||||
|
mainWindow = createMainWindow();
|
||||||
|
|
||||||
|
// Optimized for Mac OS X
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
var forceQuit = false;
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
forceQuit = true;
|
||||||
|
});
|
||||||
|
mainWindow.on("close", event => {
|
||||||
|
if (!forceQuit) {
|
||||||
|
event.preventDefault();
|
||||||
|
mainWindow.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
21
license
Normal file
21
license
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) th-ch <th-ch@users.noreply.github.com> (https://github.com/th-ch/youtube-music)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
56
menu.js
Normal file
56
menu.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
const { app, Menu } = require("electron");
|
||||||
|
|
||||||
|
const { getAllPlugins } = require("./plugins/utils");
|
||||||
|
const { isPluginEnabled, enablePlugin, disablePlugin } = require("./store");
|
||||||
|
|
||||||
|
module.exports.setApplicationMenu = () => {
|
||||||
|
const menuTemplate = [
|
||||||
|
{
|
||||||
|
label : "Plugins",
|
||||||
|
submenu: getAllPlugins().map(plugin => {
|
||||||
|
return {
|
||||||
|
label : plugin,
|
||||||
|
type : "checkbox",
|
||||||
|
checked: isPluginEnabled(plugin),
|
||||||
|
click : item => {
|
||||||
|
if (item.checked) {
|
||||||
|
enablePlugin(plugin);
|
||||||
|
} else {
|
||||||
|
disablePlugin(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
const name = app.getName();
|
||||||
|
menuTemplate.unshift({
|
||||||
|
label : name,
|
||||||
|
submenu: [
|
||||||
|
{ role: "about" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "hide" },
|
||||||
|
{ role: "hideothers" },
|
||||||
|
{ role: "unhide" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label : "Select All",
|
||||||
|
accelerator: "CmdOrCtrl+A",
|
||||||
|
selector : "selectAll:"
|
||||||
|
},
|
||||||
|
{ label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
|
||||||
|
{ label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
|
||||||
|
{ label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "minimize" },
|
||||||
|
{ role: "close" },
|
||||||
|
{ role: "quit" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
};
|
||||||
7065
package-lock.json
generated
Normal file
7065
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
package.json
Normal file
43
package.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name" : "youtube-music",
|
||||||
|
"productName": "YouTube Music",
|
||||||
|
"version" : "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"license" : "MIT",
|
||||||
|
"repository" : "th-ch/youtube-music",
|
||||||
|
"author" : {
|
||||||
|
"name" : "th-ch",
|
||||||
|
"email": "th-ch@users.noreply.github.com",
|
||||||
|
"url" : "https://github.com/th-ch/youtube-music"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test" : "xo",
|
||||||
|
"start" : "electron .",
|
||||||
|
"icon" : "electron-icon-maker --input=assets/youtube-music.png --output=assets/generated",
|
||||||
|
"postinstall": "npm run icon && npm rebuild && node plugins/adblocker/generator.js && electron-rebuild",
|
||||||
|
"build" : "electron-packager . --out=dist --asar --overwrite --all --icon=assets/generated/icons/mac/icon.icns --prune=true",
|
||||||
|
"build:macos": "electron-packager . --platform=darwin --arch=x64 --out=dist --asar --overwrite --icon=assets/generated/icons/mac/icon.icns --prune=true"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ad-block" : "^4.1.3",
|
||||||
|
"electron-debug" : "^2.0.0",
|
||||||
|
"electron-is-dev" : "^1.0.1",
|
||||||
|
"electron-localshortcut": "^3.1.0",
|
||||||
|
"electron-store" : "^2.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"devtron" : "^1.4.0",
|
||||||
|
"electron" : "^4.0.8",
|
||||||
|
"electron-devtools-installer": "^2.2.4",
|
||||||
|
"electron-icon-maker" : "0.0.4",
|
||||||
|
"electron-packager" : "^13.1.1",
|
||||||
|
"electron-rebuild" : "^1.8.4",
|
||||||
|
"xo" : "^0.24.0"
|
||||||
|
},
|
||||||
|
"xo": {
|
||||||
|
"envs": [
|
||||||
|
"node",
|
||||||
|
"browser"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
1
plugins/adblocker/.gitignore
vendored
Normal file
1
plugins/adblocker/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
detector.buffer
|
||||||
3
plugins/adblocker/back.js
Normal file
3
plugins/adblocker/back.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const { blockWindowAds } = require("./blocker");
|
||||||
|
|
||||||
|
module.exports = win => blockWindowAds(win.webContents);
|
||||||
12
plugins/adblocker/blocker.js
Normal file
12
plugins/adblocker/blocker.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
const { initialize, containsAds } = require("./contains-ads");
|
||||||
|
|
||||||
|
module.exports.blockWindowAds = webContents => {
|
||||||
|
initialize();
|
||||||
|
webContents.session.webRequest.onBeforeRequest(
|
||||||
|
["*://*./*"],
|
||||||
|
(details, cb) => {
|
||||||
|
const shouldBeBlocked = containsAds(details.url);
|
||||||
|
cb({ cancel: shouldBeBlocked });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
24
plugins/adblocker/contains-ads.js
Normal file
24
plugins/adblocker/contains-ads.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const Blocker = require("ad-block");
|
||||||
|
|
||||||
|
const client = new Blocker.AdBlockClient();
|
||||||
|
const file = path.resolve(__dirname, "detector.buffer");
|
||||||
|
|
||||||
|
module.exports.client = client;
|
||||||
|
module.exports.initialize = () =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
fs.readFile(file, (err, buffer) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
client.deserialize(buffer);
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const none = Blocker.FilterOptions.noFilterOption;
|
||||||
|
const isAd = (req, base) => client.matches(req, none, base);
|
||||||
|
|
||||||
|
module.exports.containsAds = (req, base) => isAd(req, base);
|
||||||
|
module.exports.isAd = isAd;
|
||||||
67
plugins/adblocker/generator.js
Normal file
67
plugins/adblocker/generator.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// This file generates the detector buffer
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const Blocker = require("ad-block");
|
||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
const SOURCES = [
|
||||||
|
"https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt"
|
||||||
|
];
|
||||||
|
|
||||||
|
function parseAdblockList(client, adblockList) {
|
||||||
|
const urls = adblockList.split("\n");
|
||||||
|
const totalSize = urls.length;
|
||||||
|
console.log(
|
||||||
|
"Parsing " + totalSize + " urls (this can take a couple minutes)."
|
||||||
|
);
|
||||||
|
urls.map(line => client.parse(line));
|
||||||
|
console.log("Created buffer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeBuffer(client) {
|
||||||
|
const output = path.resolve(__dirname, "detector.buffer");
|
||||||
|
fs.writeFile(output, client.serialize(64), err => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Wrote buffer to detector.buffer!");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDetectorBuffer() {
|
||||||
|
const client = new Blocker.AdBlockClient();
|
||||||
|
let nbSourcesFetched = 0;
|
||||||
|
|
||||||
|
// fetch updated versions
|
||||||
|
SOURCES.forEach(source => {
|
||||||
|
console.log("Downloading " + source);
|
||||||
|
https
|
||||||
|
.get(source, resp => {
|
||||||
|
let data = "";
|
||||||
|
|
||||||
|
// A chunk of data has been recieved.
|
||||||
|
resp.on("data", chunk => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The whole response has been received. Print out the result.
|
||||||
|
resp.on("end", () => {
|
||||||
|
parseAdblockList(client, data);
|
||||||
|
nbSourcesFetched++;
|
||||||
|
|
||||||
|
if (nbSourcesFetched === SOURCES.length) {
|
||||||
|
writeBuffer(client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on("error", err => {
|
||||||
|
console.log("Error: " + err.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = generateDetectorBuffer;
|
||||||
|
if (require.main === module) {
|
||||||
|
generateDetectorBuffer();
|
||||||
|
}
|
||||||
24
plugins/navigation/actions.js
Normal file
24
plugins/navigation/actions.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const { triggerAction } = require('../utils');
|
||||||
|
|
||||||
|
const CHANNEL = "navigation";
|
||||||
|
const ACTIONS = {
|
||||||
|
NEXT: "next",
|
||||||
|
BACK: 'back',
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToNextPage() {
|
||||||
|
triggerAction(CHANNEL, ACTIONS.NEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPreviousPage() {
|
||||||
|
triggerAction(CHANNEL, ACTIONS.BACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CHANNEL: CHANNEL,
|
||||||
|
ACTIONS: ACTIONS,
|
||||||
|
global: {
|
||||||
|
goToNextPage: goToNextPage,
|
||||||
|
goToPreviousPage: goToPreviousPage,
|
||||||
|
}
|
||||||
|
};
|
||||||
23
plugins/navigation/back.js
Normal file
23
plugins/navigation/back.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const { listenAction } = require("../utils");
|
||||||
|
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||||
|
|
||||||
|
function handle(win) {
|
||||||
|
listenAction(CHANNEL, (event, action) => {
|
||||||
|
switch (action) {
|
||||||
|
case ACTIONS.NEXT:
|
||||||
|
if (win.webContents.canGoForward()) {
|
||||||
|
win.webContents.goForward();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ACTIONS.BACK:
|
||||||
|
if (win.webContents.canGoBack()) {
|
||||||
|
win.webContents.goBack();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Unknown action: " + action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = handle;
|
||||||
14
plugins/navigation/front.js
Normal file
14
plugins/navigation/front.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const { ElementFromFile, templatePath } = require('../utils');
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
const forwardButton = ElementFromFile(templatePath(__dirname, 'forward.html'));
|
||||||
|
const backButton = ElementFromFile(templatePath(__dirname, 'back.html'));
|
||||||
|
const menu = document.querySelector("ytmusic-pivot-bar-renderer");
|
||||||
|
|
||||||
|
if (menu) {
|
||||||
|
menu.prepend(forwardButton);
|
||||||
|
menu.prepend(backButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = run;
|
||||||
21
plugins/navigation/templates/back.html
Normal file
21
plugins/navigation/templates/back.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<ytmusic-pivot-bar-item-renderer class="style-scope ytmusic-pivot-bar-renderer" tab-id="FEmusic_back" role="tab" onclick="goToPreviousPage()">
|
||||||
|
<yt-icon class="tab-icon style-scope ytmusic-pivot-bar-item-renderer">
|
||||||
|
<svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope yt-icon" style="pointer-events: none; display: block; width: 100%; height: 100%;">
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path class="st0" d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z" class="style-scope yt-icon">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</yt-icon>
|
||||||
|
|
||||||
|
<paper-icon-button class="search-icon style-scope ytmusic-search-box" role="button" tabindex="0" aria-disabled="false" title="Go to previous page">
|
||||||
|
<iron-icon id="icon" class="style-scope paper-icon-button">
|
||||||
|
<svg viewBox="0 0 492 492" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope iron-icon" style="pointer-events: none; display: block; width: 100%; height: 100%;">
|
||||||
|
<g class="style-scope iron-icon">
|
||||||
|
<path class="st0" d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</iron-icon>
|
||||||
|
</paper-icon-button>
|
||||||
|
</ytmusic-pivot-bar-item-renderer>
|
||||||
26
plugins/navigation/templates/forward.html
Normal file
26
plugins/navigation/templates/forward.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<ytmusic-pivot-bar-item-renderer class="style-scope ytmusic-pivot-bar-renderer" tab-id="FEmusic_next" role="tab" onclick="goToNextPage()">
|
||||||
|
<yt-icon class="tab-icon style-scope ytmusic-pivot-bar-item-renderer">
|
||||||
|
<svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope yt-icon" style="pointer-events: none; display: block; width: 100%; height: 100%;">
|
||||||
|
<g class="style-scope yt-icon">
|
||||||
|
<path d="M382.678,226.804L163.73,7.86C158.666,2.792,151.906,0,144.698,0s-13.968,2.792-19.032,7.86l-16.124,16.12
|
||||||
|
c-10.492,10.504-10.492,27.576,0,38.064L293.398,245.9l-184.06,184.06c-5.064,5.068-7.86,11.824-7.86,19.028
|
||||||
|
c0,7.212,2.796,13.968,7.86,19.04l16.124,16.116c5.068,5.068,11.824,7.86,19.032,7.86s13.968-2.792,19.032-7.86L382.678,265
|
||||||
|
c5.076-5.084,7.864-11.872,7.848-19.088C390.542,238.668,387.754,231.884,382.678,226.804z" class="style-scope yt-icon">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</yt-icon>
|
||||||
|
|
||||||
|
<paper-icon-button class="search-icon style-scope ytmusic-search-box" role="button" tabindex="0" aria-disabled="false" title="Go to next page">
|
||||||
|
<iron-icon id="icon" class="style-scope paper-icon-button">
|
||||||
|
<svg viewBox="0 0 492 492" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope iron-icon" style="pointer-events: none; display: block; width: 100%; height: 100%;">
|
||||||
|
<g class="style-scope iron-icon">
|
||||||
|
<path class="st0" d="M382.7,226.8L163.7,7.9c-5.1-5.1-11.8-7.9-19-7.9s-14,2.8-19,7.9L109.5,24c-10.5,10.5-10.5,27.6,0,38.1
|
||||||
|
l183.9,183.9L109.3,430c-5.1,5.1-7.9,11.8-7.9,19c0,7.2,2.8,14,7.9,19l16.1,16.1c5.1,5.1,11.8,7.9,19,7.9s14-2.8,19-7.9L382.7,265
|
||||||
|
c5.1-5.1,7.9-11.9,7.8-19.1C390.5,238.7,387.8,231.9,382.7,226.8z">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</iron-icon>
|
||||||
|
</paper-icon-button>
|
||||||
|
</ytmusic-pivot-bar-item-renderer>
|
||||||
6
plugins/no-google-login/back.js
Normal file
6
plugins/no-google-login/back.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const { injectCSS } = require("../utils");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = win => {
|
||||||
|
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||||
|
};
|
||||||
15
plugins/no-google-login/front.js
Normal file
15
plugins/no-google-login/front.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
function removeLoginElements() {
|
||||||
|
const elementsToRemove = [
|
||||||
|
".sign-in-link.ytmusic-nav-bar",
|
||||||
|
'.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"]'
|
||||||
|
];
|
||||||
|
|
||||||
|
elementsToRemove.forEach(selector => {
|
||||||
|
const node = document.querySelector(selector);
|
||||||
|
if (node) {
|
||||||
|
node.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = removeLoginElements;
|
||||||
3
plugins/no-google-login/style.css
Normal file
3
plugins/no-google-login/style.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
31
plugins/shortcuts/back.js
Normal file
31
plugins/shortcuts/back.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const { globalShortcut } = require("electron");
|
||||||
|
const electronLocalshortcut = require("electron-localshortcut");
|
||||||
|
|
||||||
|
const {
|
||||||
|
playPause,
|
||||||
|
nextTrack,
|
||||||
|
previousTrack,
|
||||||
|
startSearch
|
||||||
|
} = require("./youtube.js");
|
||||||
|
|
||||||
|
function _registerGlobalShortcut(webContents, shortcut, action) {
|
||||||
|
globalShortcut.register(shortcut, () => {
|
||||||
|
action(webContents);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _registerLocalShortcut(win, shortcut, action) {
|
||||||
|
electronLocalshortcut.register(win, shortcut, () => {
|
||||||
|
action(win.webContents);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerShortcuts(win) {
|
||||||
|
_registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause);
|
||||||
|
_registerGlobalShortcut(win.webContents, "MediaNextTrack", nextTrack);
|
||||||
|
_registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previousTrack);
|
||||||
|
_registerLocalShortcut(win, "CommandOrControl+F", startSearch);
|
||||||
|
_registerLocalShortcut(win, "CommandOrControl+L", startSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerShortcuts;
|
||||||
29
plugins/shortcuts/youtube.js
Normal file
29
plugins/shortcuts/youtube.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
function _keyboardInput(webContents, key) {
|
||||||
|
return webContents.sendInputEvent({
|
||||||
|
type : "keydown",
|
||||||
|
keyCode: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function playPause(webContents) {
|
||||||
|
return _keyboardInput(webContents, "Space");
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextTrack(webContents) {
|
||||||
|
return _keyboardInput(webContents, "j");
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousTrack(webContents) {
|
||||||
|
return _keyboardInput(webContents, "k");
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSearch(webContents) {
|
||||||
|
return _keyboardInput(webContents, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
playPause : playPause,
|
||||||
|
nextTrack : nextTrack,
|
||||||
|
previousTrack: previousTrack,
|
||||||
|
startSearch : startSearch
|
||||||
|
};
|
||||||
54
plugins/utils.js
Normal file
54
plugins/utils.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const { ipcMain, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
// Creates a DOM element from a HTML string
|
||||||
|
module.exports.ElementFromHtml = html => {
|
||||||
|
var template = document.createElement("template");
|
||||||
|
html = html.trim(); // Never return a text node of whitespace as the result
|
||||||
|
template.innerHTML = html;
|
||||||
|
return template.content.firstChild;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Creates a DOM element from a HTML file
|
||||||
|
module.exports.ElementFromFile = filepath => {
|
||||||
|
return module.exports.ElementFromHtml(fs.readFileSync(filepath, "utf8"));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.templatePath = (pluginPath, name) => {
|
||||||
|
return path.join(pluginPath, "templates", name);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.triggerAction = (channel, action) => {
|
||||||
|
return ipcRenderer.send(channel, action);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.listenAction = (channel, callback) => {
|
||||||
|
return ipcMain.on(channel, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.fileExists = (path, callbackIfExists) => {
|
||||||
|
fs.access(path, fs.F_OK, err => {
|
||||||
|
if (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callbackIfExists();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.injectCSS = (webContents, filepath) => {
|
||||||
|
webContents.on("did-finish-load", () => {
|
||||||
|
webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.getAllPlugins = () => {
|
||||||
|
const isDirectory = source => fs.lstatSync(source).isDirectory();
|
||||||
|
return fs
|
||||||
|
.readdirSync(__dirname)
|
||||||
|
.map(name => path.join(__dirname, name))
|
||||||
|
.filter(isDirectory)
|
||||||
|
.map(name => path.basename(name));
|
||||||
|
};
|
||||||
26
preload.js
Normal file
26
preload.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const { getEnabledPlugins } = require("./store");
|
||||||
|
const { fileExists } = require("./plugins/utils");
|
||||||
|
|
||||||
|
const plugins = getEnabledPlugins();
|
||||||
|
|
||||||
|
plugins.forEach(plugin => {
|
||||||
|
const pluginPath = path.join(__dirname, "plugins", plugin, "actions.js");
|
||||||
|
fileExists(pluginPath, () => {
|
||||||
|
const actions = require(pluginPath).global || {};
|
||||||
|
Object.keys(actions).forEach(actionName => {
|
||||||
|
global[actionName] = actions[actionName];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
plugins.forEach(plugin => {
|
||||||
|
const pluginPath = path.join(__dirname, "plugins", plugin, "front.js");
|
||||||
|
fileExists(pluginPath, () => {
|
||||||
|
const run = require(pluginPath);
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
87
readme.md
Normal file
87
readme.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# YouTube Music [](https://github.com/sindresorhus/xo)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Electron wrapper around YouTube Music featuring:**
|
||||||
|
|
||||||
|
- Native look & feel, aims at keeping the original interface
|
||||||
|
- Framework for custom plugins: change YouTube Music to your needs (style, content, features), enable/disable plugins in one click
|
||||||
|
|
||||||
|
## Available plugins:
|
||||||
|
|
||||||
|
- **Ad Blocker**: block all ads and tracking out of the box
|
||||||
|
- **No Google Login**: remove Google login buttons and links from the interface
|
||||||
|
- **Shortcuts**: use your usual shortcuts (media keys, Ctrl/CMD + F…) to control YouTube Music
|
||||||
|
- **Navigation**: next/back navigation arrows directly integrated in the interface, like in your favorite browser
|
||||||
|
|
||||||
|
## Dev
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/th-ch/youtube-music
|
||||||
|
cd youtube-music
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build your own plugins
|
||||||
|
|
||||||
|
Using plugins, you can:
|
||||||
|
|
||||||
|
- manipulate the app - the `BrowserWindow` from electron is passed to the plugin handler
|
||||||
|
- change the front by manipulating the HTML/CSS
|
||||||
|
|
||||||
|
### Creating a plugin
|
||||||
|
|
||||||
|
Create a folder in `plugins/YOUR-PLUGIN-NAME`:
|
||||||
|
|
||||||
|
- if you need to manipulate the BrowserWindow, create a file `back.js` with the following template:
|
||||||
|
|
||||||
|
```
|
||||||
|
module.exports = win => {
|
||||||
|
// win is the BrowserWindow object
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- if you need to change the front, create a file `front.js` with the following template:
|
||||||
|
|
||||||
|
```
|
||||||
|
module.exports = () => {
|
||||||
|
// This function will be called as a preload script
|
||||||
|
// So you can use front features like `document.querySelector`
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common use cases
|
||||||
|
|
||||||
|
- injecting custom CSS: create a `style.css` file in the same folder then:
|
||||||
|
|
||||||
|
```
|
||||||
|
// back.js
|
||||||
|
module.exports = win => {
|
||||||
|
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- changing the HTML:
|
||||||
|
|
||||||
|
```
|
||||||
|
// front.js
|
||||||
|
module.exports = () => {
|
||||||
|
// Remove the login button
|
||||||
|
document.querySelector('.sign-in-link.ytmusic-nav-bar').remove();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- communicating between the front and back: can be done using the ipcMain module from electron. See `utils.js` file and example in `navigation` plugin.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Builds the app for macOS, Linux, and Windows, using [electron-packager](https://github.com/electron-userland/electron-packager).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT © [th-ch](https://github.com/th-ch/youtube-music)
|
||||||
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1024 KiB |
21
store/index.js
Normal file
21
store/index.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const Store = require("electron-store");
|
||||||
|
const plugins = require("./plugins");
|
||||||
|
|
||||||
|
const store = new Store({
|
||||||
|
defaults: {
|
||||||
|
"window-size": {
|
||||||
|
width : 1100,
|
||||||
|
height: 550
|
||||||
|
},
|
||||||
|
url : "https://music.youtube.com",
|
||||||
|
plugins: ["navigation", "shortcuts", "adblocker", "no-google-login"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
store : store,
|
||||||
|
isPluginEnabled : plugin => plugins.isEnabled(store, plugin),
|
||||||
|
getEnabledPlugins: () => plugins.getEnabledPlugins(store),
|
||||||
|
enableplugin : plugin => plugins.enablePlugin(store, plugin),
|
||||||
|
disablePlugin : plugin => plugins.disablePlugin(store, plugin)
|
||||||
|
};
|
||||||
31
store/plugins.js
Normal file
31
store/plugins.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
function getEnabledPlugins(store) {
|
||||||
|
return store.get("plugins");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnabled(store, plugin) {
|
||||||
|
return store.get("plugins").indexOf(plugin) > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enablePlugin(store, plugin) {
|
||||||
|
let plugins = getEnabledPlugins(store);
|
||||||
|
if (plugins.indexOf(plugin) === -1) {
|
||||||
|
plugins.push(plugin);
|
||||||
|
store.set("plugins", plugins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disablePlugin(store, plugin) {
|
||||||
|
let plugins = getEnabledPlugins(store);
|
||||||
|
let index = plugins.indexOf(plugin);
|
||||||
|
if (index > -1) {
|
||||||
|
plugins.splice(index, 1);
|
||||||
|
store.set("plugins", plugins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isEnabled : isEnabled,
|
||||||
|
getEnabledPlugins: getEnabledPlugins,
|
||||||
|
enableplugin : enablePlugin,
|
||||||
|
disableplugin : disablePlugin
|
||||||
|
};
|
||||||
25
youtube-music.css
Normal file
25
youtube-music.css
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Overriding YouTube Music style
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Allow window dragging */
|
||||||
|
ytmusic-nav-bar {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-app-region : drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
iron-icon,
|
||||||
|
ytmusic-pivot-bar-item-renderer,
|
||||||
|
.tab-title,
|
||||||
|
a {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* custom style for navbar */
|
||||||
|
ytmusic-app-layout {
|
||||||
|
--ytmusic-nav-bar-height: 85px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ytmusic-search-box.ytmusic-nav-bar {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user