mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-13 11:21:46 +00:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d775e3d588 | |||
| e7ec15e90f | |||
| 403470be69 | |||
| 6dc0ba74c4 | |||
| 6dcfb336c2 | |||
| 84516b2ac1 | |||
| 57cf2a8cdd | |||
| e3ae97fec4 | |||
| ee76e2cb45 | |||
| de01bb6e75 | |||
| 42668c3e99 | |||
| 05f3c56e47 | |||
| d54977b9ee | |||
| b89fb4dc2f | |||
| a0cf77edfb | |||
| 069f9855d1 | |||
| e3e0775401 | |||
| d255e5ffe1 | |||
| fea460a374 | |||
| 302d3f693f | |||
| 9cc320d74b | |||
| e255777283 | |||
| fe0f213919 | |||
| e888b5c896 | |||
| f27ff52689 | |||
| acbe0ac25d | |||
| c66ff2bf05 | |||
| d089487aa8 | |||
| 6bc1d1606f | |||
| 9df5d921c7 | |||
| 4b1dfa1173 | |||
| f98318e737 | |||
| 7fa1278b31 | |||
| 878ec1f6c1 | |||
| 086048780a | |||
| aff0415816 | |||
| 6040fe1cbd | |||
| 52f4e9d796 | |||
| 09fe80cae7 | |||
| 817b48dc9d | |||
| c6f8c42c45 | |||
| 0535686129 | |||
| 53a77255ca | |||
| c01506dc44 | |||
| 6f5f9386ff | |||
| fddd0607e6 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
|||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: "12.x"
|
node-version: "14.x"
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
|
|||||||
14
index.js
14
index.js
@ -207,8 +207,18 @@ app.once("browser-window-created", (event, win) => {
|
|||||||
// Force user-agent "Firefox Windows" for Google OAuth to work
|
// Force user-agent "Firefox Windows" for Google OAuth to work
|
||||||
// From https://github.com/firebase/firebase-js-sdk/issues/2478#issuecomment-571356751
|
// From https://github.com/firebase/firebase-js-sdk/issues/2478#issuecomment-571356751
|
||||||
// Only set on accounts.google.com, otherwise querySelectors in preload scripts fail (?)
|
// Only set on accounts.google.com, otherwise querySelectors in preload scripts fail (?)
|
||||||
const userAgent =
|
// Uses custom user agent to Google alert with a correct device type (https://github.com/th-ch/youtube-music/issues/327)
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0";
|
// User agents are from https://developers.whatismybrowser.com/useragents/explore/
|
||||||
|
const userAgents = {
|
||||||
|
mac: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0",
|
||||||
|
windows: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0",
|
||||||
|
linux: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:90.0) Gecko/20100101 Firefox/90.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgent =
|
||||||
|
is.macOS() ? userAgents.mac :
|
||||||
|
is.windows() ? userAgents.windows :
|
||||||
|
userAgents.linux;
|
||||||
|
|
||||||
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
|
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
|
||||||
details.requestHeaders["User-Agent"] = userAgent;
|
details.requestHeaders["User-Agent"] = userAgent;
|
||||||
|
|||||||
15
package.json
15
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "1.12.2",
|
"version": "1.13.0",
|
||||||
"description": "YouTube Music Desktop App - including custom plugins",
|
"description": "YouTube Music Desktop App - including custom plugins",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "th-ch/youtube-music",
|
"repository": "th-ch/youtube-music",
|
||||||
@ -60,17 +60,17 @@
|
|||||||
"release:win": "yarn run clean && electron-builder --win -p always"
|
"release:win": "yarn run clean && electron-builder --win -p always"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.20",
|
"node": ">=14.0.0",
|
||||||
"npm": "Please use yarn and not npm"
|
"npm": "Please use yarn and not npm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cliqz/adblocker-electron": "^1.22.1",
|
"@cliqz/adblocker-electron": "^1.22.5",
|
||||||
"@ffmpeg/core": "^0.10.0",
|
"@ffmpeg/core": "^0.10.0",
|
||||||
"@ffmpeg/ffmpeg": "^0.10.0",
|
"@ffmpeg/ffmpeg": "^0.10.0",
|
||||||
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.9.0",
|
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.9.0",
|
||||||
"async-mutex": "^0.3.1",
|
"async-mutex": "^0.3.1",
|
||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.2",
|
||||||
"custom-electron-titlebar": "^3.2.7",
|
"custom-electron-titlebar": "^3.2.7",
|
||||||
"discord-rpc": "^3.2.0",
|
"discord-rpc": "^3.2.0",
|
||||||
"electron-better-web-request": "^1.0.1",
|
"electron-better-web-request": "^1.0.1",
|
||||||
@ -79,13 +79,14 @@
|
|||||||
"electron-localshortcut": "^3.2.1",
|
"electron-localshortcut": "^3.2.1",
|
||||||
"electron-store": "^7.0.3",
|
"electron-store": "^7.0.3",
|
||||||
"electron-unhandled": "^3.0.2",
|
"electron-unhandled": "^3.0.2",
|
||||||
"electron-updater": "^4.3.9",
|
"electron-updater": "^4.4.6",
|
||||||
"filenamify": "^4.3.0",
|
"filenamify": "^4.3.0",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
|
"mpris-service": "^2.1.2",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"node-notifier": "^9.0.1",
|
"node-notifier": "^9.0.1",
|
||||||
"ytdl-core": "^4.8.3",
|
"ytdl-core": "^4.9.1",
|
||||||
"ytpl": "^2.2.1"
|
"ytpl": "^2.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^12.0.8",
|
"electron": "^12.0.8",
|
||||||
|
|||||||
@ -29,6 +29,9 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
|
|||||||
songInfo.uploadDate,
|
songInfo.uploadDate,
|
||||||
songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views"
|
songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views"
|
||||||
].join(' || '),
|
].join(' || '),
|
||||||
|
buttons: [
|
||||||
|
{ label: "Listen Along", url: songInfo.url },
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (songInfo.isPaused) {
|
if (songInfo.isPaused) {
|
||||||
|
|||||||
@ -6,8 +6,16 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-item > .yt-simple-endpoint:hover {
|
||||||
|
background-color: var(--ytmusic-menu-item-hover-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
.menu-icon {
|
.menu-icon {
|
||||||
flex: var(--ytmusic-menu-item-icon_-_flex);
|
flex: var(--ytmusic-menu-item-icon_-_flex);
|
||||||
margin: var(--ytmusic-menu-item-icon_-_margin);
|
margin: var(--ytmusic-menu-item-icon_-_margin);
|
||||||
fill: var(--ytmusic-menu-item-icon_-_fill);
|
fill: var(--ytmusic-menu-item-icon_-_fill);
|
||||||
|
stroke: var(--iron-icon-stroke-color, none);
|
||||||
|
width: var(--iron-icon-width, 24px);
|
||||||
|
height: var(--iron-icon-height, 24px);
|
||||||
|
animation: var(--iron-icon_-_animation);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<div
|
<div
|
||||||
class="menu-item ytmusic-menu-popup-renderer"
|
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
||||||
role="option"
|
role="option"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
@ -7,31 +7,39 @@
|
|||||||
onclick="download()"
|
onclick="download()"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="menu-icon yt-icon-container yt-icon ytmusic-toggle-menu-service-item-renderer"
|
id="navigation-endpoint"
|
||||||
|
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<svg
|
<div
|
||||||
viewBox="0 0 24 24"
|
class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
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">
|
<svg
|
||||||
<path
|
viewBox="0 0 24 24"
|
||||||
d="M25.462,19.105v6.848H4.515v-6.848H0.489v8.861c0,1.111,0.9,2.012,2.016,2.012h24.967c1.115,0,2.016-0.9,2.016-2.012v-8.861H25.462z"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
class="style-scope yt-icon" fill="#aaaaaa"
|
focusable="false"
|
||||||
/>
|
class="style-scope yt-icon"
|
||||||
<path
|
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
||||||
d="M14.62,18.426l-5.764-6.965c0,0-0.877-0.828,0.074-0.828s3.248,0,3.248,0s0-0.557,0-1.416c0-2.449,0-6.906,0-8.723c0,0-0.129-0.494,0.615-0.494c0.75,0,4.035,0,4.572,0c0.536,0,0.524,0.416,0.524,0.416c0,1.762,0,6.373,0,8.742c0,0.768,0,1.266,0,1.266s1.842,0,2.998,0c1.154,0,0.285,0.867,0.285,0.867s-4.904,6.51-5.588,7.193C15.092,18.979,14.62,18.426,14.62,18.426z"
|
>
|
||||||
class="style-scope yt-icon" fill="#aaaaaa"
|
<g class="style-scope yt-icon">
|
||||||
/>
|
<path
|
||||||
</g>
|
d="M25.462,19.105v6.848H4.515v-6.848H0.489v8.861c0,1.111,0.9,2.012,2.016,2.012h24.967c1.115,0,2.016-0.9,2.016-2.012v-8.861H25.462z"
|
||||||
</svg>
|
class="style-scope yt-icon"
|
||||||
</div>
|
fill="#aaaaaa"
|
||||||
<div
|
/>
|
||||||
class="text style-scope ytmusic-toggle-menu-service-item-renderer"
|
<path
|
||||||
id="ytmcustom-download"
|
d="M14.62,18.426l-5.764-6.965c0,0-0.877-0.828,0.074-0.828s3.248,0,3.248,0s0-0.557,0-1.416c0-2.449,0-6.906,0-8.723c0,0-0.129-0.494,0.615-0.494c0.75,0,4.035,0,4.572,0c0.536,0,0.524,0.416,0.524,0.416c0,1.762,0,6.373,0,8.742c0,0.768,0,1.266,0,1.266s1.842,0,2.998,0c1.154,0,0.285,0.867,0.285,0.867s-4.904,6.51-5.588,7.193C15.092,18.979,14.62,18.426,14.62,18.426z"
|
||||||
>
|
class="style-scope yt-icon"
|
||||||
Download
|
fill="#aaaaaa"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
|
id="ytmcustom-download"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const ytdl = require("ytdl-core");
|
|||||||
const { triggerAction, triggerActionSync } = require("../utils");
|
const { triggerAction, triggerActionSync } = require("../utils");
|
||||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||||
const { getFolder, urlToJPG } = require("./utils");
|
const { getFolder, urlToJPG } = require("./utils");
|
||||||
const { cleanupArtistName } = require("../../providers/song-info");
|
const { cleanupName } = require("../../providers/song-info");
|
||||||
|
|
||||||
const { createFFmpeg } = FFmpeg;
|
const { createFFmpeg } = FFmpeg;
|
||||||
const ffmpeg = createFFmpeg({
|
const ffmpeg = createFFmpeg({
|
||||||
@ -40,7 +40,10 @@ const downloadVideoToMP3 = async (
|
|||||||
const { videoDetails } = await ytdl.getInfo(videoUrl);
|
const { videoDetails } = await ytdl.getInfo(videoUrl);
|
||||||
const thumbnails = videoDetails?.thumbnails;
|
const thumbnails = videoDetails?.thumbnails;
|
||||||
metadata = {
|
metadata = {
|
||||||
artist: videoDetails?.media?.artist || cleanupArtistName(videoDetails?.author?.name) || "",
|
artist:
|
||||||
|
videoDetails?.media?.artist ||
|
||||||
|
cleanupName(videoDetails?.author?.name) ||
|
||||||
|
"",
|
||||||
title: videoDetails?.media?.song || videoDetails?.title || "",
|
title: videoDetails?.media?.song || videoDetails?.title || "",
|
||||||
imageSrcYTPL: thumbnails ?
|
imageSrcYTPL: thumbnails ?
|
||||||
urlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId)
|
urlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId)
|
||||||
|
|||||||
52
plugins/lyrics-genius/back.js
Normal file
52
plugins/lyrics-genius/back.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const { join } = require("path");
|
||||||
|
|
||||||
|
const { ipcMain } = require("electron");
|
||||||
|
const is = require("electron-is");
|
||||||
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
|
const { cleanupName } = require("../../providers/song-info");
|
||||||
|
const { injectCSS } = require("../utils");
|
||||||
|
|
||||||
|
module.exports = async (win) => {
|
||||||
|
injectCSS(win.webContents, join(__dirname, "style.css"));
|
||||||
|
|
||||||
|
ipcMain.on("search-genius-lyrics", async (event, extractedSongInfo) => {
|
||||||
|
const metadata = JSON.parse(extractedSongInfo);
|
||||||
|
const queryString = `${cleanupName(metadata.artist)} ${cleanupName(
|
||||||
|
metadata.title
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
let response = await fetch(
|
||||||
|
`https://genius.com/api/search/multi?per_page=5&q=${encodeURI(
|
||||||
|
queryString
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
event.returnValue = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = await response.json();
|
||||||
|
let url = "";
|
||||||
|
try {
|
||||||
|
url = info.response.sections.filter(
|
||||||
|
(section) => section.type === "song"
|
||||||
|
)[0].hits[0].result.url;
|
||||||
|
} catch {
|
||||||
|
event.returnValue = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is.dev()) {
|
||||||
|
console.log("Fetching lyrics from Genius:", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
event.returnValue = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.returnValue = await response.text();
|
||||||
|
});
|
||||||
|
};
|
||||||
65
plugins/lyrics-genius/front.js
Normal file
65
plugins/lyrics-genius/front.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
ipcRenderer.on("update-song-info", (_, extractedSongInfo) => {
|
||||||
|
const lyricsTab = document.querySelector('tp-yt-paper-tab[tabindex="-1"]');
|
||||||
|
|
||||||
|
// Check if disabled
|
||||||
|
if (!lyricsTab || !lyricsTab.hasAttribute("disabled")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = ipcRenderer.sendSync(
|
||||||
|
"search-genius-lyrics",
|
||||||
|
extractedSongInfo
|
||||||
|
);
|
||||||
|
if (!html) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.innerHTML = html;
|
||||||
|
const lyricsSelector1 = wrapper.querySelector(".lyrics");
|
||||||
|
const lyricsSelector2 = wrapper.querySelector(
|
||||||
|
'[class^="Lyrics__Container"]'
|
||||||
|
);
|
||||||
|
const lyrics = lyricsSelector1
|
||||||
|
? lyricsSelector1.innerHTML
|
||||||
|
: lyricsSelector2
|
||||||
|
? lyricsSelector2.innerHTML
|
||||||
|
: null;
|
||||||
|
if (!lyrics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lyricsTab.removeAttribute("disabled");
|
||||||
|
lyricsTab.removeAttribute("aria-disabled");
|
||||||
|
document.querySelector("tp-yt-paper-tab").onclick = () => {
|
||||||
|
lyricsTab.removeAttribute("disabled");
|
||||||
|
lyricsTab.removeAttribute("aria-disabled");
|
||||||
|
};
|
||||||
|
|
||||||
|
lyricsTab.onclick = () => {
|
||||||
|
const tabContainer = document.querySelector("ytmusic-tab-renderer");
|
||||||
|
console.log("tabContainer", tabContainer);
|
||||||
|
const observer = new MutationObserver((_, observer) => {
|
||||||
|
const lyricsContainer = document.querySelector(
|
||||||
|
'[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"] > ytmusic-message-renderer'
|
||||||
|
);
|
||||||
|
if (lyricsContainer) {
|
||||||
|
lyricsContainer.innerHTML = `<div id="contents" class="style-scope ytmusic-section-list-renderer genius-lyrics">
|
||||||
|
${lyrics}
|
||||||
|
|
||||||
|
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer">Source : Genius</yt-formatted-string>
|
||||||
|
</div>`;
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(tabContainer, {
|
||||||
|
attributes: true,
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
7
plugins/lyrics-genius/style.css
Normal file
7
plugins/lyrics-genius/style.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/* Disable links in Genius lyrics */
|
||||||
|
.genius-lyrics a {
|
||||||
|
color: var(--ytmusic-text-primary);
|
||||||
|
display: inline-block;
|
||||||
|
pointer-events: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
@ -1,79 +1,87 @@
|
|||||||
<div
|
<div
|
||||||
class="menu-item ytmusic-menu-popup-renderer"
|
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
||||||
role="option"
|
role="option"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-selected="false"
|
aria-selected="false"
|
||||||
>
|
>
|
||||||
<tp-yt-paper-slider
|
<div
|
||||||
id="playback-speed-slider"
|
id="navigation-endpoint"
|
||||||
class="volume-slider style-scope ytmusic-player-bar on-hover"
|
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
max="100"
|
tabindex="-1"
|
||||||
min="0"
|
>
|
||||||
step="5"
|
<tp-yt-paper-slider
|
||||||
dir="ltr"
|
id="playback-speed-slider"
|
||||||
title="Playback speed"
|
class="volume-slider style-scope ytmusic-player-bar on-hover"
|
||||||
aria-label="Playback speed"
|
max="100"
|
||||||
role="slider"
|
min="0"
|
||||||
tabindex="0"
|
step="5"
|
||||||
aria-valuemin="0"
|
dir="ltr"
|
||||||
aria-valuemax="100"
|
title="Playback speed"
|
||||||
aria-valuenow="50"
|
aria-label="Playback speed"
|
||||||
aria-disabled="false"
|
role="slider"
|
||||||
value="50"
|
tabindex="0"
|
||||||
><!--css-build:shady-->
|
aria-valuemin="0"
|
||||||
<div id="sliderContainer" class="style-scope tp-yt-paper-slider">
|
aria-valuemax="100"
|
||||||
<div class="bar-container style-scope tp-yt-paper-slider">
|
aria-valuenow="50"
|
||||||
<tp-yt-paper-progress
|
aria-disabled="false"
|
||||||
id="sliderBar"
|
value="50"
|
||||||
aria-hidden="true"
|
><!--css-build:shady-->
|
||||||
class="style-scope tp-yt-paper-slider"
|
<div id="sliderContainer" class="style-scope tp-yt-paper-slider">
|
||||||
role="progressbar"
|
<div class="bar-container style-scope tp-yt-paper-slider">
|
||||||
value="50"
|
<tp-yt-paper-progress
|
||||||
aria-valuenow="50"
|
id="sliderBar"
|
||||||
aria-valuemin="0"
|
aria-hidden="true"
|
||||||
aria-valuemax="100"
|
class="style-scope tp-yt-paper-slider"
|
||||||
aria-disabled="false"
|
role="progressbar"
|
||||||
style="touch-action: none"
|
value="50"
|
||||||
><!--css-build:shady-->
|
aria-valuenow="50"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
aria-disabled="false"
|
||||||
|
style="touch-action: none"
|
||||||
|
><!--css-build:shady-->
|
||||||
|
|
||||||
<div id="progressContainer" class="style-scope tp-yt-paper-progress">
|
|
||||||
<div
|
<div
|
||||||
id="secondaryProgress"
|
id="progressContainer"
|
||||||
class="style-scope tp-yt-paper-progress"
|
class="style-scope tp-yt-paper-progress"
|
||||||
hidden="true"
|
>
|
||||||
style="transform: scaleX(0)"
|
<div
|
||||||
></div>
|
id="secondaryProgress"
|
||||||
<div
|
class="style-scope tp-yt-paper-progress"
|
||||||
id="primaryProgress"
|
hidden="true"
|
||||||
class="style-scope tp-yt-paper-progress"
|
style="transform: scaleX(0)"
|
||||||
style="transform: scaleX(0.5)"
|
></div>
|
||||||
></div>
|
<div
|
||||||
</div>
|
id="primaryProgress"
|
||||||
</tp-yt-paper-progress>
|
class="style-scope tp-yt-paper-progress"
|
||||||
|
style="transform: scaleX(0.5)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</tp-yt-paper-progress>
|
||||||
|
</div>
|
||||||
|
<dom-if class="style-scope tp-yt-paper-slider"
|
||||||
|
><template is="dom-if"></template
|
||||||
|
></dom-if>
|
||||||
|
<div
|
||||||
|
id="sliderKnob"
|
||||||
|
class="slider-knob style-scope tp-yt-paper-slider"
|
||||||
|
style="left: 50%; touch-action: none"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="slider-knob-inner style-scope tp-yt-paper-slider"
|
||||||
|
value="50"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<dom-if class="style-scope tp-yt-paper-slider"
|
<dom-if class="style-scope tp-yt-paper-slider"
|
||||||
><template is="dom-if"></template
|
><template is="dom-if"></template></dom-if
|
||||||
></dom-if>
|
></tp-yt-paper-slider>
|
||||||
<div
|
<div
|
||||||
id="sliderKnob"
|
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
class="slider-knob style-scope tp-yt-paper-slider"
|
id="ytmcustom-playback-speed"
|
||||||
style="left: 50%; touch-action: none"
|
>
|
||||||
>
|
Speed (<span id="playback-speed-value">1</span>)
|
||||||
<div
|
|
||||||
class="slider-knob-inner style-scope tp-yt-paper-slider"
|
|
||||||
value="50"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<dom-if class="style-scope tp-yt-paper-slider"
|
|
||||||
><template is="dom-if"></template></dom-if
|
|
||||||
></tp-yt-paper-slider>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="text style-scope ytmusic-toggle-menu-service-item-renderer"
|
|
||||||
id="ytmcustom-playback-speed"
|
|
||||||
>
|
|
||||||
Speed (<span id="playback-speed-value">1</span>)
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
const { globalShortcut } = require("electron");
|
const { globalShortcut } = require("electron");
|
||||||
|
const is = require("electron-is");
|
||||||
const electronLocalshortcut = require("electron-localshortcut");
|
const electronLocalshortcut = require("electron-localshortcut");
|
||||||
|
|
||||||
const getSongControls = require("../../providers/song-controls");
|
const getSongControls = require("../../providers/song-controls");
|
||||||
|
const { setupMPRIS } = require("./mpris");
|
||||||
|
|
||||||
function _registerGlobalShortcut(webContents, shortcut, action) {
|
function _registerGlobalShortcut(webContents, shortcut, action) {
|
||||||
globalShortcut.register(shortcut, () => {
|
globalShortcut.register(shortcut, () => {
|
||||||
@ -25,6 +27,22 @@ function registerShortcuts(win, options) {
|
|||||||
_registerLocalShortcut(win, "CommandOrControl+F", search);
|
_registerLocalShortcut(win, "CommandOrControl+F", search);
|
||||||
_registerLocalShortcut(win, "CommandOrControl+L", search);
|
_registerLocalShortcut(win, "CommandOrControl+L", search);
|
||||||
|
|
||||||
|
if (is.linux()) {
|
||||||
|
try {
|
||||||
|
const player = setupMPRIS();
|
||||||
|
|
||||||
|
player.on("raise", () => {
|
||||||
|
win.setSkipTaskbar(false);
|
||||||
|
win.show();
|
||||||
|
});
|
||||||
|
player.on("playpause", playPause);
|
||||||
|
player.on("next", next);
|
||||||
|
player.on("previous", previous);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error in MPRIS", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { global, local } = options;
|
const { global, local } = options;
|
||||||
(global || []).forEach(({ shortcut, action }) => {
|
(global || []).forEach(({ shortcut, action }) => {
|
||||||
console.debug("Registering global shortcut", shortcut, ":", action);
|
console.debug("Registering global shortcut", shortcut, ":", action);
|
||||||
|
|||||||
19
plugins/shortcuts/mpris.js
Normal file
19
plugins/shortcuts/mpris.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const mpris = require("mpris-service");
|
||||||
|
|
||||||
|
function setupMPRIS() {
|
||||||
|
const player = mpris({
|
||||||
|
name: "youtube-music",
|
||||||
|
identity: "YouTube Music",
|
||||||
|
canRaise: true,
|
||||||
|
supportedUriSchemes: ["https"],
|
||||||
|
supportedMimeTypes: ["audio/mpeg"],
|
||||||
|
supportedInterfaces: ["player"],
|
||||||
|
desktopEntry: "youtube-music",
|
||||||
|
});
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
setupMPRIS,
|
||||||
|
};
|
||||||
33
plugins/tuna-obs/back.js
Normal file
33
plugins/tuna-obs/back.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
const registerCallback = require("../../providers/song-info");
|
||||||
|
|
||||||
|
const post = (data) => {
|
||||||
|
const port = 1608;
|
||||||
|
headers = {'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Access-Control-Allow-Headers': '*',
|
||||||
|
'Access-Control-Allow-Origin': '*'}
|
||||||
|
const url = `http://localhost:${port}/`;
|
||||||
|
fetch(url, {method: 'POST', headers, body:JSON.stringify({data})});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (win) => {
|
||||||
|
registerCallback((songInfo) => {
|
||||||
|
|
||||||
|
// Register the callback
|
||||||
|
if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = Number(songInfo.songDuration)*1000
|
||||||
|
const progress = Number(songInfo.elapsedSeconds)*1000
|
||||||
|
const cover_url = songInfo.imageSrc
|
||||||
|
const album_url = songInfo.imageSrc
|
||||||
|
const title = songInfo.title
|
||||||
|
const artists = [songInfo.artist]
|
||||||
|
const status = !songInfo.isPaused ? 'Playing': 'Paused'
|
||||||
|
post({ cover_url, title, artists, status, progress, duration, album_url});
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -55,8 +55,9 @@ const songInfo = {
|
|||||||
|
|
||||||
const handleData = async (responseText, win) => {
|
const handleData = async (responseText, win) => {
|
||||||
let data = JSON.parse(responseText);
|
let data = JSON.parse(responseText);
|
||||||
songInfo.title = data?.videoDetails?.title;
|
songInfo.title = cleanupName(data?.videoDetails?.title);
|
||||||
songInfo.artist = await getArtist(win) || cleanupArtistName(data?.videoDetails?.author);
|
songInfo.artist =
|
||||||
|
(await getArtist(win)) || cleanupName(data?.videoDetails?.author);
|
||||||
songInfo.views = data?.videoDetails?.viewCount;
|
songInfo.views = data?.videoDetails?.viewCount;
|
||||||
songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url;
|
songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url;
|
||||||
songInfo.songDuration = data?.videoDetails?.lengthSeconds;
|
songInfo.songDuration = data?.videoDetails?.lengthSeconds;
|
||||||
@ -98,8 +99,15 @@ const registerProvider = (win) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const suffixesToRemove = [' - Topic', 'VEVO'];
|
const suffixesToRemove = [
|
||||||
function cleanupArtistName(artist) {
|
" - Topic",
|
||||||
|
"VEVO",
|
||||||
|
" (Performance Video)",
|
||||||
|
" (Official Music Video)",
|
||||||
|
" (Official Video)",
|
||||||
|
" (Clip officiel)",
|
||||||
|
];
|
||||||
|
function cleanupName(artist) {
|
||||||
if (!artist) {
|
if (!artist) {
|
||||||
return artist;
|
return artist;
|
||||||
}
|
}
|
||||||
@ -114,4 +122,4 @@ function cleanupArtistName(artist) {
|
|||||||
module.exports = registerCallback;
|
module.exports = registerCallback;
|
||||||
module.exports.setupSongInfo = registerProvider;
|
module.exports.setupSongInfo = registerProvider;
|
||||||
module.exports.getImage = getImage;
|
module.exports.getImage = getImage;
|
||||||
module.exports.cleanupArtistName = cleanupArtistName;
|
module.exports.cleanupName = cleanupName;
|
||||||
|
|||||||
21
readme.md
21
readme.md
@ -34,15 +34,22 @@ You can check out the [latest release](https://github.com/th-ch/youtube-music/re
|
|||||||
Install the `youtube-music-bin` package from the AUR. For AUR installation instructions, take a look at this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
Install the `youtube-music-bin` package from the AUR. For AUR installation instructions, take a look at this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
||||||
|
|
||||||
## Available plugins:
|
## Available plugins:
|
||||||
|
|
||||||
- **Ad Blocker**: block all ads and tracking out of the box
|
- **Ad Blocker**: block all ads and tracking out of the box
|
||||||
- **Downloader**: download to MP3 directly from the interface (youtube-dl)
|
- **Auto confirm when paused**: when the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) modal appears, automatically click "Yes"
|
||||||
- **No Google Login**: remove Google login buttons and links from the interface
|
- **Disable autoplay**: makes every song start in "paused" mode
|
||||||
- **Shortcuts**: use your usual shortcuts (media keys, Ctrl/CMD + F…) to control YouTube Music
|
- [**Discord**](https://discord.com/): show your friends what you listen to with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
|
||||||
- **Navigation**: next/back navigation arrows directly integrated in the interface, like in your favorite browser
|
- **Downloader**: downloads MP3 [directly from the interface](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
|
||||||
- **Auto confirm when paused**: when the "Continue Watching?" modal appears, automatically click "Yes"
|
|
||||||
- **Hide video player**: no video in the interface when playing music
|
- **Hide video player**: no video in the interface when playing music
|
||||||
- **Notifications**: display a notification when a song starts playing
|
- **In-app menu**: [gives bars a fancy, dark look](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
|
||||||
|
- [**Last.fm**](https://www.last.fm/): scrobbles support
|
||||||
|
- **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](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) when a song starts playing
|
||||||
|
- **Playback speed**: listen fast, listen slow! [Adds a slider that controls song speed](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
||||||
|
- **Precise volume**: customizable volume steps for more comfort, allows controlling the volume precisely using mousewheel
|
||||||
|
- **Shortcuts**: use your usual shortcuts (media keys, Ctrl/CMD + F…) to control YouTube Music, you may setup custom global hotkeys for play/pause/next/previous song
|
||||||
|
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): skips non-music parts
|
||||||
|
- **Taskbar media control**: control app from your [Windows taskbar](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
|
||||||
- **Touchbar**: custom TouchBar layout for macOS
|
- **Touchbar**: custom TouchBar layout for macOS
|
||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
|||||||
Reference in New Issue
Block a user