Compare commits

...

24 Commits

Author SHA1 Message Date
b5dbfaf686 Bump version to 2.0.2 2023-10-08 17:35:40 +09:00
6b7fd5ba63 Merge pull request #1272 from th-ch/feat/resolves-1265 2023-10-08 17:19:19 +09:00
73a049a7bc Merge pull request #1279 from th-ch/fix/1274 2023-10-08 17:18:54 +09:00
ef0c30e23a Merge pull request #1280 from th-ch/revert-scale-factor-patch 2023-10-08 17:18:33 +09:00
59ed2326d9 Revert "Fix for windows zoom (ScaleFactor) #1159"
This reverts commit d36fb592d0.
2023-10-08 17:06:30 +09:00
07a02c8c82 Revert "hotfix: fixed app launching offscreen"
This reverts commit ca92031e89.
2023-10-08 17:05:54 +09:00
f1050cb676 remove: remove useless CSS property 2023-10-08 15:35:08 +09:00
7131893f1c chore: update README
Added a guide to install YTM without a network connection.
2023-10-08 15:21:04 +09:00
e4dfb2ff33 feat(discord): remove hacky solution for calling callbacks 2023-10-08 15:01:26 +09:00
187fad6834 Merge branch 'master' into fix/1274 2023-10-08 15:00:30 +09:00
26df435db0 fix: fallback to DOM window controls on platforms without native support 2023-10-08 14:57:58 +09:00
0bee281d1d fix: discord-rpc (#1278) 2023-10-08 14:44:48 +09:00
26de5802a0 fix: prevent multiple RPCs from being registered 2023-10-08 13:55:40 +09:00
c258a4855e Merge pull request #1277 from th-ch/hotfix/1273 2023-10-08 12:56:16 +09:00
b7b6d50ba2 Merge pull request #1276 from jkrei0/master 2023-10-08 12:56:03 +09:00
0376a30fbb fix: prevent name shadowing 2023-10-08 12:39:24 +09:00
ca92031e89 hotfix: fixed app launching offscreen 2023-10-08 12:29:09 +09:00
986d2ad5b1 chore: add window control icons 2023-10-08 12:12:44 +09:00
d9b8d8c48d Fix in-app-menu squishing sub-menu items 2023-10-07 22:50:32 -04:00
0ef34d7c71 feat: use nsis-web instead of nsis 2023-10-08 03:04:53 +09:00
f87607d25d Merge pull request #1271 from th-ch/fix/lastfm 2023-10-08 02:54:13 +09:00
cc0bfae067 fix(last-fm): fix last-fm plugin 2023-10-08 02:41:06 +09:00
e7d2d04f5a fix(test): Add a test to check the title
It failed because of @cliqz/adblocker-electron.
Check see this issue: https://github.com/ghostery/adblocker/issues/3462
2023-10-08 01:16:11 +09:00
f4319ebc6b Update changelog for v2.0.1 2023-10-07 15:57:46 +00:00
17 changed files with 219 additions and 85 deletions

View File

@ -2,7 +2,15 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v2.0.0](https://github.com/th-ch/youtube-music/compare/v1.20.0...v2.0.0) #### [v2.0.1](https://github.com/th-ch/youtube-music/compare/v2.0.0...v2.0.1)
- Update changelog for v2.0.0 [`2d69dfd`](https://github.com/th-ch/youtube-music/commit/2d69dfd333c3223ecc7de13a0abc98fd99aa3a2b)
- hotfix: hotfix for #1267 [`c002263`](https://github.com/th-ch/youtube-music/commit/c002263c3bdd51890b8ffb431283afb60405d8fe)
- Bump version to 2.0.1 [`a1f025e`](https://github.com/th-ch/youtube-music/commit/a1f025e23c599fe5eb63b32ea38ee81200d232d6)
### [v2.0.0](https://github.com/th-ch/youtube-music/compare/v1.20.0...v2.0.0)
> 7 October 2023
- Bump version to 2.0.0 [`#1257`](https://github.com/th-ch/youtube-music/pull/1257) - Bump version to 2.0.0 [`#1257`](https://github.com/th-ch/youtube-music/pull/1257)
- feat(GitHub): add issue template [`#1264`](https://github.com/th-ch/youtube-music/pull/1264) - feat(GitHub): add issue template [`#1264`](https://github.com/th-ch/youtube-music/pull/1264)

View File

@ -226,15 +226,15 @@ function createMainWindow() {
loadPlugins(win); loadPlugins(win);
if (windowPosition) { if (windowPosition) {
const { x, y } = windowPosition; const { x: windowX, y: windowY } = windowPosition;
const winSize = win.getSize(); const winSize = win.getSize();
const displaySize const displaySize
= screen.getDisplayNearestPoint(windowPosition).bounds; = screen.getDisplayNearestPoint(windowPosition).bounds;
if ( if (
x + winSize[0] < displaySize.x - 8 windowX + winSize[0] < displaySize.x - 8
|| x - winSize[0] > displaySize.x + displaySize.width || windowX - winSize[0] > displaySize.x + displaySize.width
|| y < displaySize.y - 8 || windowY < displaySize.y - 8
|| y > displaySize.y + displaySize.height || windowY > displaySize.y + displaySize.height
) { ) {
// Window is offscreen // Window is offscreen
if (is.dev()) { if (is.dev()) {
@ -243,7 +243,7 @@ function createMainWindow() {
); );
} }
} else { } else {
win.setPosition(x, y); win.setPosition(windowX, windowY);
} }
} }
@ -261,26 +261,6 @@ function createMainWindow() {
win.webContents.loadURL(urlToLoad); win.webContents.loadURL(urlToLoad);
win.on('closed', onClosed); win.on('closed', onClosed);
const scaleFactor = screen.getAllDisplays().length > 1 ? screen.getPrimaryDisplay().scaleFactor : 1;
const size = config.get('window-size');
const position = config.get('window-position');
if (size && size.width && size.height) {
const scaledSize = {
width: size.width / scaleFactor,
height: size.height / scaleFactor,
};
win.setSize(scaledSize.width, scaledSize.height);
}
if (position && position.x && position.y) {
const scaledPosition = {
x: position.x / scaleFactor,
y: position.y / scaleFactor,
};
win.setPosition(scaledPosition.x, scaledPosition.y);
}
type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture']; type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture'];
const setPiPOptions = config.plugins.isEnabled('picture-in-picture') const setPiPOptions = config.plugins.isEnabled('picture-in-picture')
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"version": "2.0.1", "version": "2.0.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "youtube-music", "name": "youtube-music",
"version": "2.0.1", "version": "2.0.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -1,7 +1,7 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "2.0.1", "version": "2.0.2",
"description": "YouTube Music Desktop App - including custom plugins", "description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/index.js", "main": "./dist/index.js",
"license": "MIT", "license": "MIT",
@ -59,7 +59,7 @@
"icon": "assets/generated/icons/win/icon.ico", "icon": "assets/generated/icons/win/icon.ico",
"target": [ "target": [
{ {
"target": "nsis", "target": "nsis-web",
"arch": [ "arch": [
"x64", "x64",
"ia32", "ia32",
@ -76,7 +76,7 @@
} }
] ]
}, },
"nsis": { "nsisWeb": {
"runAfterFinish": false "runAfterFinish": false
}, },
"linux": { "linux": {
@ -119,7 +119,7 @@
"dist:mac": "npm run clean && npm run build && electron-builder --mac dmg:x64 -p never", "dist:mac": "npm run clean && npm run build && electron-builder --mac dmg:x64 -p never",
"dist:mac:arm64": "npm run clean && npm run build && electron-builder --mac dmg:arm64 -p never", "dist:mac:arm64": "npm run clean && npm run build && electron-builder --mac dmg:arm64 -p never",
"dist:win": "npm run clean && npm run build && electron-builder --win -p never", "dist:win": "npm run clean && npm run build && electron-builder --win -p never",
"dist:win:x64": "npm run clean && npm run build && electron-builder --win nsis:x64 -p never", "dist:win:x64": "npm run clean && npm run build && electron-builder --win nsis-web:x64 -p never",
"lint": "eslint .", "lint": "eslint .",
"changelog": "auto-changelog", "changelog": "auto-changelog",
"plugins": "npm run plugin:bypass-age-restrictions", "plugins": "npm run plugin:bypass-age-restrictions",

View File

@ -4,7 +4,7 @@ import { dev } from 'electron-is';
import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser'; import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
import registerCallback from '../../providers/song-info'; import registerCallback, { type SongInfoCallback, type SongInfo } from '../../providers/song-info';
import type { ConfigType } from '../../config/dynamic'; import type { ConfigType } from '../../config/dynamic';
@ -15,7 +15,7 @@ export interface Info {
rpc: DiscordClient; rpc: DiscordClient;
ready: boolean; ready: boolean;
autoReconnect: boolean; autoReconnect: boolean;
lastSongInfo?: import('../../providers/song-info').SongInfo; lastSongInfo?: SongInfo;
} }
const info: Info = { const info: Info = {
@ -44,31 +44,6 @@ const resetInfo = () => {
} }
}; };
info.rpc.on('connected', () => {
if (dev()) {
console.log('discord connected');
}
for (const cb of refreshCallbacks) {
cb();
}
});
info.rpc.on('ready', () => {
info.ready = true;
if (info.lastSongInfo) {
updateActivity(info.lastSongInfo);
}
});
info.rpc.on('disconnected', () => {
resetInfo();
if (info.autoReconnect) {
connectTimeout();
}
});
const connectTimeout = () => new Promise((resolve, reject) => setTimeout(() => { const connectTimeout = () => new Promise((resolve, reject) => setTimeout(() => {
if (!info.autoReconnect || info.rpc.isConnected) { if (!info.autoReconnect || info.rpc.isConnected) {
return; return;
@ -117,7 +92,7 @@ export const connect = (showError = false) => {
}; };
let clearActivity: NodeJS.Timeout | undefined; let clearActivity: NodeJS.Timeout | undefined;
let updateActivity: import('../../providers/song-info').SongInfoCallback; let updateActivity: SongInfoCallback;
type DiscordOptions = ConfigType<'discord'>; type DiscordOptions = ConfigType<'discord'>;
@ -125,6 +100,31 @@ export default (
win: Electron.BrowserWindow, win: Electron.BrowserWindow,
options: DiscordOptions, options: DiscordOptions,
) => { ) => {
info.rpc.on('connected', () => {
if (dev()) {
console.log('discord connected');
}
for (const cb of refreshCallbacks) {
cb();
}
});
info.rpc.on('ready', () => {
info.ready = true;
if (info.lastSongInfo) {
updateActivity(info.lastSongInfo);
}
});
info.rpc.on('disconnected', () => {
resetInfo();
if (info.autoReconnect) {
connectTimeout();
}
});
info.autoReconnect = options.autoReconnect; info.autoReconnect = options.autoReconnect;
window = win; window = win;

View File

@ -0,0 +1,3 @@
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@ -0,0 +1,3 @@
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M6 3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6Z"/>
</svg>

After

Width:  |  Height:  |  Size: 252 B

View File

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 338 B

View File

@ -0,0 +1,3 @@
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z" />
</svg>

After

Width:  |  Height:  |  Size: 174 B

View File

@ -0,0 +1,3 @@
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M7.518 5H6.009a3.25 3.25 0 0 1 3.24-3h8.001A4.75 4.75 0 0 1 22 6.75v8a3.25 3.25 0 0 1-3 3.24v-1.508a1.75 1.75 0 0 0 1.5-1.732v-8a3.25 3.25 0 0 0-3.25-3.25h-8A1.75 1.75 0 0 0 7.518 5ZM5.25 6A3.25 3.25 0 0 0 2 9.25v9.5A3.25 3.25 0 0 0 5.25 22h9.5A3.25 3.25 0 0 0 18 18.75v-9.5A3.25 3.25 0 0 0 14.75 6h-9.5ZM3.5 9.25c0-.966.784-1.75 1.75-1.75h9.5c.967 0 1.75.784 1.75 1.75v9.5a1.75 1.75 0 0 1-1.75 1.75h-9.5a1.75 1.75 0 0 1-1.75-1.75v-9.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 546 B

View File

@ -1,5 +1,3 @@
import path from 'node:path';
import { register } from 'electron-localshortcut'; import { register } from 'electron-localshortcut';
import { BrowserWindow, Menu, MenuItem, ipcMain } from 'electron'; import { BrowserWindow, Menu, MenuItem, ipcMain } from 'electron';
@ -25,7 +23,7 @@ export default (win: BrowserWindow) => {
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined), (key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
), ),
); );
const getMenuItemById = (commandId: number): MenuItem | null => { const getMenuItemById = (commandId: number): MenuItem | null => {
const menu = Menu.getApplicationMenu(); const menu = Menu.getApplicationMenu();
@ -40,7 +38,7 @@ export default (win: BrowserWindow) => {
break; break;
} }
} }
return target; return target;
}; };
@ -57,4 +55,11 @@ export default (win: BrowserWindow) => {
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined), (key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
); );
}); });
ipcMain.handle('window-is-maximized', () => win.isMaximized());
ipcMain.handle('window-close', () => win.close());
ipcMain.handle('window-minimize', () => win.minimize());
ipcMain.handle('window-maximize', () => win.maximize());
ipcMain.handle('window-unmaximize', () => win.unmaximize());
}; };

View File

@ -2,7 +2,12 @@ import { ipcRenderer, Menu } from 'electron';
import { createPanel } from './menu/panel'; import { createPanel } from './menu/panel';
import logo from '../../assets/menu.svg'; import logo from './assets/menu.svg';
import close from './assets/close.svg';
import minimize from './assets/minimize.svg';
import maximize from './assets/maximize.svg';
import unmaximize from './assets/unmaximize.svg';
import { isEnabled } from '../../config/plugins'; import { isEnabled } from '../../config/plugins';
import config from '../../config'; import config from '../../config';
@ -11,8 +16,9 @@ function $<E extends Element = Element>(selector: string) {
} }
const isMacOS = navigator.userAgent.includes('Macintosh'); const isMacOS = navigator.userAgent.includes('Macintosh');
const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS;
export default () => { export default async () => {
let hideMenu = config.get('options.hideMenu'); let hideMenu = config.get('options.hideMenu');
const titleBar = document.createElement('title-bar'); const titleBar = document.createElement('title-bar');
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background'); const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
@ -39,6 +45,60 @@ export default () => {
if (!isMacOS) titleBar.appendChild(logo); if (!isMacOS) titleBar.appendChild(logo);
document.body.appendChild(titleBar); document.body.appendChild(titleBar);
titleBar.appendChild(logo);
const addWindowControls = async () => {
// Create window control buttons
const minimizeButton = document.createElement('button');
minimizeButton.classList.add('window-control');
minimizeButton.appendChild(minimize);
minimizeButton.onclick = () => ipcRenderer.invoke('window-minimize');
const maximizeButton = document.createElement('button');
if (await ipcRenderer.invoke('window-is-maximized')) {
maximizeButton.classList.add('window-control');
maximizeButton.appendChild(unmaximize);
} else {
maximizeButton.classList.add('window-control');
maximizeButton.appendChild(maximize);
}
maximizeButton.onclick = async () => {
if (await ipcRenderer.invoke('window-is-maximized')) {
// change icon to maximize
maximizeButton.removeChild(maximizeButton.firstChild!);
maximizeButton.appendChild(maximize);
// call unmaximize
await ipcRenderer.invoke('window-unmaximize');
} else {
// change icon to unmaximize
maximizeButton.removeChild(maximizeButton.firstChild!);
maximizeButton.appendChild(unmaximize);
// call maximize
await ipcRenderer.invoke('window-maximize');
}
};
const closeButton = document.createElement('button');
closeButton.classList.add('window-control');
closeButton.appendChild(close);
closeButton.onclick = () => ipcRenderer.invoke('window-close');
// Create a container div for the window control buttons
const windowControlsContainer = document.createElement('div');
windowControlsContainer.classList.add('window-controls-container');
windowControlsContainer.appendChild(minimizeButton);
windowControlsContainer.appendChild(maximizeButton);
windowControlsContainer.appendChild(closeButton);
// Add window control buttons to the title bar
titleBar.appendChild(windowControlsContainer);
};
if (isNotWindowsOrMacOS) await addWindowControls();
if (navBar) { if (navBar) {
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
mutations.forEach(() => { mutations.forEach(() => {
@ -69,8 +129,9 @@ export default () => {
menu.style.visibility = 'hidden'; menu.style.visibility = 'hidden';
} }
}); });
if (isNotWindowsOrMacOS) await addWindowControls();
}; };
updateMenu(); await updateMenu();
document.title = 'Youtube Music'; document.title = 'Youtube Music';

View File

@ -101,6 +101,15 @@ export const createPanel = (
} }
panel.setAttribute('open', 'true'); panel.setAttribute('open', 'true');
// Children are placed below their parent item, which can cause
// long lists to squeeze their children at the bottom of the screen
// (This needs to be done *after* setAttribute)
panel.classList.remove('position-by-bottom');
if (options.placement === 'right' && panel.scrollHeight > panel.clientHeight ) {
panel.style.setProperty('--y', `${rect.y + rect.height}px`);
panel.classList.add('position-by-bottom');
}
}; };
anchor.addEventListener('click', () => { anchor.addEventListener('click', () => {

View File

@ -80,6 +80,11 @@ menu-panel[open="true"] {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
menu-panel.position-by-bottom {
top: unset;
bottom: calc(100vh - var(--y, 100%));
max-height: calc(var(--y, 0) - var(--menu-bar-height, 36px) - 16px);
}
menu-item { menu-item {
-webkit-app-region: none; -webkit-app-region: none;
@ -121,6 +126,33 @@ menu-separator {
margin-left: -4px; margin-left: -4px;
} }
/* Window control container */
.window-controls-container {
-webkit-app-region: no-drag;
display: flex;
justify-content: flex-end; /* Align to the right end of the title-bar */
align-items: center;
gap: 4px; /* Add spacing between the window control buttons */
position: absolute; /* Position it absolutely within title-bar */
right: 4px; /* Adjust the right position as needed */
}
/* Window control buttons */
.window-control {
width: 24px;
height: 24px;
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
color: #f1f1f1;
font-size: 14px;
padding: 0;
}
/* youtube-music style */ /* youtube-music style */
ytmusic-app-layout { ytmusic-app-layout {

View File

@ -15,11 +15,24 @@ interface LastFmData {
timestamp?: number, timestamp?: number,
} }
const createFormData = (parameters: Record<string, unknown>) => { interface LastFmSongData {
track?: string,
duration?: number,
artist?: string,
album?: string,
api_key: string,
sk?: string,
format: string,
method: string,
timestamp?: number,
api_sig?: string,
}
const createFormData = (parameters: LastFmSongData) => {
// Creates the body for in the post request // Creates the body for in the post request
const formData = new URLSearchParams(); const formData = new URLSearchParams();
for (const key in parameters) { for (const key in parameters) {
formData.append(key, String(parameters[key])); formData.append(key, String(parameters[key as keyof LastFmSongData]));
} }
return formData; return formData;
@ -36,7 +49,7 @@ const createQueryString = (parameters: Record<string, unknown>, apiSignature: st
return '?' + queryData.join('&'); return '?' + queryData.join('&');
}; };
const createApiSig = (parameters: Record<string, unknown>, secret: string) => { const createApiSig = (parameters: LastFmSongData, secret: string) => {
// This function creates the api signature, see: https://www.last.fm/api/authspec // This function creates the api signature, see: https://www.last.fm/api/authspec
const keys = Object.keys(parameters); const keys = Object.keys(parameters);
@ -47,7 +60,7 @@ const createApiSig = (parameters: Record<string, unknown>, secret: string) => {
continue; continue;
} }
sig += `${key}${String(parameters[key])}`; sig += `${key}${parameters[key as keyof LastFmSongData]}`;
} }
sig += secret; sig += secret;
@ -59,7 +72,7 @@ const createToken = async ({ api_key: apiKey, api_root: apiRoot, secret }: LastF
// Creates and stores the auth token // Creates and stores the auth token
const data = { const data = {
method: 'auth.gettoken', method: 'auth.gettoken',
apiKey, api_key: apiKey,
format: 'json', format: 'json',
}; };
const apiSigature = createApiSig(data, secret); const apiSigature = createApiSig(data, secret);
@ -68,10 +81,9 @@ const createToken = async ({ api_key: apiKey, api_root: apiRoot, secret }: LastF
return json?.token; return json?.token;
}; };
const authenticateAndGetToken = async (config: LastFMOptions) => { const authenticate = async (config: LastFMOptions) => {
// Asks the user for authentication // Asks the user for authentication
await shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`); await shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
return await createToken(config);
}; };
const getAndSetSessionKey = async (config: LastFMOptions) => { const getAndSetSessionKey = async (config: LastFMOptions) => {
@ -86,18 +98,19 @@ const getAndSetSessionKey = async (config: LastFMOptions) => {
const response = await net.fetch(`${config.api_root}${createQueryString(data, apiSignature)}`); const response = await net.fetch(`${config.api_root}${createQueryString(data, apiSignature)}`);
const json = await response.json() as { const json = await response.json() as {
error?: string, error?: string,
session?: { session?: {
key: string, key: string,
} }
}; };
if (json.error) { if (json.error) {
config.token = await authenticateAndGetToken(config); config.token = await createToken(config);
await authenticate(config);
setOptions('last-fm', config); setOptions('last-fm', config);
} }
if (json.session) { if (json.session) {
config.session_key = json?.session?.key; config.session_key = json.session.key;
setOptions('last-fm', config);
} }
setOptions('last-fm', config);
return config; return config;
}; };
@ -107,20 +120,20 @@ const postSongDataToAPI = async (songInfo: SongInfo, config: LastFMOptions, data
await getAndSetSessionKey(config); await getAndSetSessionKey(config);
} }
const postData = { const postData: LastFmSongData = {
track: songInfo.title, track: songInfo.title,
duration: songInfo.songDuration, duration: songInfo.songDuration,
artist: songInfo.artist, artist: songInfo.artist,
...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video ...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video
api_key: config.api_key, api_key: config.api_key,
api_sig: '',
sk: config.session_key, sk: config.session_key,
format: 'json', format: 'json',
...data, ...data,
}; };
postData.api_sig = createApiSig(postData, config.secret); postData.api_sig = createApiSig(postData, config.secret);
net.fetch('https://ws.audioscrobbler.com/2.0/', { method: 'POST', body: createFormData(postData) }) const formData = createFormData(postData);
net.fetch('https://ws.audioscrobbler.com/2.0/', { method: 'POST', body: formData })
.catch(async (error: { .catch(async (error: {
response?: { response?: {
data?: { data?: {
@ -131,7 +144,8 @@ const postSongDataToAPI = async (songInfo: SongInfo, config: LastFMOptions, data
if (error?.response?.data?.error === 9) { if (error?.response?.data?.error === 9) {
// Session key is invalid, so remove it from the config and reauthenticate // Session key is invalid, so remove it from the config and reauthenticate
config.session_key = undefined; config.session_key = undefined;
config.token = await authenticateAndGetToken(config); config.token = await createToken(config);
await authenticate(config);
setOptions('last-fm', config); setOptions('last-fm', config);
} }
}); });

View File

@ -65,6 +65,16 @@ file).*
winget install th-ch.YouTubeMusic winget install th-ch.YouTubeMusic
``` ```
#### How to install without a network connection? (in Windows)
- Download the `*.nsis.7z` file for _your device architecture_ in [release page](https://github.com/th-ch/youtube-music/releases/latest).
- `x64` for 64-bit Windows
- `ia32` for 32-bit Windows
- `arm64` for ARM64 Windows
- Download installer in release page. (`*-Setup.exe`)
- Place them in the **same directory**.
- Run the installer.
## 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

View File

@ -30,6 +30,9 @@ test('YouTube Music App - With default settings, app is launched and visible', a
await consentForm.click('button'); await consentForm.click('button');
} }
const title = await window.title();
expect(title.replaceAll(/\s/g, ' ')).toEqual('YouTube Music');
const url = window.url(); const url = window.url();
expect(url.startsWith('https://music.youtube.com')).toBe(true); expect(url.startsWith('https://music.youtube.com')).toBe(true);