mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-13 03:11:46 +00:00
feat: migration to TypeScript part 3
Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
@ -3,6 +3,39 @@ export interface WindowSizeConfig {
|
|||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DefaultConfig {
|
||||||
|
'window-size': {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
'window-maximized': boolean;
|
||||||
|
'window-position': {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
url: string;
|
||||||
|
options: {
|
||||||
|
tray: boolean;
|
||||||
|
appVisible: boolean;
|
||||||
|
autoUpdates: boolean;
|
||||||
|
alwaysOnTop: boolean;
|
||||||
|
hideMenu: boolean;
|
||||||
|
hideMenuWarned: boolean;
|
||||||
|
startAtLogin: boolean;
|
||||||
|
disableHardwareAcceleration: boolean;
|
||||||
|
removeUpgradeButton: boolean;
|
||||||
|
restartOnConfigChanges: boolean;
|
||||||
|
trayClickPlayPause: boolean;
|
||||||
|
autoResetAppCache: boolean;
|
||||||
|
resumeOnStart: boolean;
|
||||||
|
likeButtons: string;
|
||||||
|
proxy: string;
|
||||||
|
startingPage: string;
|
||||||
|
overrideUserAgent: boolean;
|
||||||
|
themes: string[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
'window-size': {
|
'window-size': {
|
||||||
width: 1100,
|
width: 1100,
|
||||||
@ -54,15 +87,22 @@ const defaultConfig = {
|
|||||||
'downloader': {
|
'downloader': {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
ffmpegArgs: [], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
|
ffmpegArgs: [], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
|
||||||
downloadFolder: undefined, // Custom download folder (absolute path)
|
downloadFolder: undefined as string | undefined, // Custom download folder (absolute path)
|
||||||
preset: 'mp3',
|
preset: 'mp3',
|
||||||
|
skipExisting: false,
|
||||||
|
playlistMaxItems: undefined as number | undefined,
|
||||||
},
|
},
|
||||||
'last-fm': {
|
'last-fm': {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
token: undefined as string | undefined, // Token used for authentication
|
||||||
|
session_key: undefined as string | undefined, // Session key used for scrobbling
|
||||||
api_root: 'http://ws.audioscrobbler.com/2.0/',
|
api_root: 'http://ws.audioscrobbler.com/2.0/',
|
||||||
api_key: '04d76faaac8726e60988e14c105d421a', // Api key registered by @semvis123
|
api_key: '04d76faaac8726e60988e14c105d421a', // Api key registered by @semvis123
|
||||||
secret: 'a5d2a36fdf64819290f6982481eaffa2',
|
secret: 'a5d2a36fdf64819290f6982481eaffa2',
|
||||||
},
|
},
|
||||||
|
'lyric-genius': {
|
||||||
|
romanizedLyrics: false,
|
||||||
|
},
|
||||||
'discord': {
|
'discord': {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
autoReconnect: true, // If enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect
|
autoReconnect: true, // If enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect
|
||||||
|
|||||||
47
package-lock.json
generated
47
package-lock.json
generated
@ -36,6 +36,7 @@
|
|||||||
"keyboardevents-areequal": "0.2.2",
|
"keyboardevents-areequal": "0.2.2",
|
||||||
"md5": "2.3.0",
|
"md5": "2.3.0",
|
||||||
"mpris-service": "2.1.2",
|
"mpris-service": "2.1.2",
|
||||||
|
"node-id3": "0.2.6",
|
||||||
"simple-youtube-age-restriction-bypass": "git+https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.9",
|
"simple-youtube-age-restriction-bypass": "git+https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.9",
|
||||||
"vudio": "2.1.1",
|
"vudio": "2.1.1",
|
||||||
"youtubei.js": "6.1.0",
|
"youtubei.js": "6.1.0",
|
||||||
@ -44,7 +45,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.37.1",
|
"@playwright/test": "1.37.1",
|
||||||
"@total-typescript/ts-reset": "0.5.1",
|
"@total-typescript/ts-reset": "0.5.1",
|
||||||
|
"@types/electron-localshortcut": "^3.1.0",
|
||||||
"@types/howler": "^2.2.8",
|
"@types/howler": "^2.2.8",
|
||||||
|
"@types/html-to-text": "^9.0.1",
|
||||||
|
"@types/md5": "^2.3.2",
|
||||||
"@types/youtube-player": "^5.5.7",
|
"@types/youtube-player": "^5.5.7",
|
||||||
"@typescript-eslint/eslint-plugin": "6.5.0",
|
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||||
"auto-changelog": "2.4.0",
|
"auto-changelog": "2.4.0",
|
||||||
@ -1169,6 +1173,15 @@
|
|||||||
"@types/ms": "*"
|
"@types/ms": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/electron-localshortcut": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/electron-localshortcut/-/electron-localshortcut-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-upKSXMxBPRdz5kmcXfdfn+hWH9PCAvwhyVozDXTIwwHQ1lUJcdSgGUfxOC1QBlnAPKPqcW/r4icWfMosKz8ibg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"electron": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/filesystem": {
|
"node_modules/@types/filesystem": {
|
||||||
"version": "0.0.32",
|
"version": "0.0.32",
|
||||||
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz",
|
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz",
|
||||||
@ -1207,6 +1220,12 @@
|
|||||||
"integrity": "sha512-7OK+cGHTWIDCOvBlEc61Lzj2tJhCpmeqiqdeNbZvTxLHluBMF6xz/2wjoQkK1M8mJIStp40OdPnkp8xIvwwsuw==",
|
"integrity": "sha512-7OK+cGHTWIDCOvBlEc61Lzj2tJhCpmeqiqdeNbZvTxLHluBMF6xz/2wjoQkK1M8mJIStp40OdPnkp8xIvwwsuw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/html-to-text": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-sHu702QGb0SP2F0Zt+CxdCmGZIZ0gEaaCjqOh/V4iba1wTxPVntEPOM/vHm5bel08TILhB3+OxUTkDJWnr/zHQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/http-cache-semantics": {
|
"node_modules/@types/http-cache-semantics": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||||
@ -1232,6 +1251,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/md5": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/minimist": {
|
"node_modules/@types/minimist": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
|
||||||
@ -6571,6 +6596,25 @@
|
|||||||
"node": "^12.13 || ^14.13 || >=16"
|
"node": "^12.13 || ^14.13 || >=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-id3": {
|
||||||
|
"version": "0.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.2.6.tgz",
|
||||||
|
"integrity": "sha512-w8GuKXLlPpDjTxLowCt/uYMhRQzED3cg2GdSG1i6RSGKeDzPvxlXeLQuQInKljahPZ0aDnmyX7FX8BbJOM7REg==",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-id3/node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nopt": {
|
"node_modules/nopt": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
|
||||||
@ -7783,8 +7827,7 @@
|
|||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/sanitize-filename": {
|
"node_modules/sanitize-filename": {
|
||||||
"version": "1.6.3",
|
"version": "1.6.3",
|
||||||
|
|||||||
@ -98,7 +98,7 @@
|
|||||||
"lint": "xo",
|
"lint": "xo",
|
||||||
"changelog": "auto-changelog",
|
"changelog": "auto-changelog",
|
||||||
"plugins": "npm run plugin:adblocker && npm run plugin:bypass-age-restrictions",
|
"plugins": "npm run plugin:adblocker && npm run plugin:bypass-age-restrictions",
|
||||||
"plugin:adblocker": "del-cli plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.ts",
|
"plugin:adblocker": "del-cli plugins/adblocker/ad-blocker-engine.bin && tsc plugins/adblocker/blocker.ts && node dist/plugins/adblocker/blocker.js",
|
||||||
"plugin:bypass-age-restrictions": "del-cli node_modules/simple-youtube-age-restriction-bypass/package.json && npm run generate:package simple-youtube-age-restriction-bypass",
|
"plugin:bypass-age-restrictions": "del-cli node_modules/simple-youtube-age-restriction-bypass/package.json && npm run generate:package simple-youtube-age-restriction-bypass",
|
||||||
"release:linux": "npm run clean && electron-builder --linux -p always -c.snap.publish=github",
|
"release:linux": "npm run clean && electron-builder --linux -p always -c.snap.publish=github",
|
||||||
"release:mac": "npm run clean && electron-builder --mac -p always",
|
"release:mac": "npm run clean && electron-builder --mac -p always",
|
||||||
@ -135,6 +135,7 @@
|
|||||||
"keyboardevents-areequal": "0.2.2",
|
"keyboardevents-areequal": "0.2.2",
|
||||||
"md5": "2.3.0",
|
"md5": "2.3.0",
|
||||||
"mpris-service": "2.1.2",
|
"mpris-service": "2.1.2",
|
||||||
|
"node-id3": "0.2.6",
|
||||||
"simple-youtube-age-restriction-bypass": "git+https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.9",
|
"simple-youtube-age-restriction-bypass": "git+https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.9",
|
||||||
"vudio": "2.1.1",
|
"vudio": "2.1.1",
|
||||||
"youtubei.js": "6.1.0",
|
"youtubei.js": "6.1.0",
|
||||||
@ -149,7 +150,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.37.1",
|
"@playwright/test": "1.37.1",
|
||||||
"@total-typescript/ts-reset": "0.5.1",
|
"@total-typescript/ts-reset": "0.5.1",
|
||||||
|
"@types/electron-localshortcut": "^3.1.0",
|
||||||
"@types/howler": "^2.2.8",
|
"@types/howler": "^2.2.8",
|
||||||
|
"@types/html-to-text": "^9.0.1",
|
||||||
|
"@types/md5": "^2.3.2",
|
||||||
"@types/youtube-player": "^5.5.7",
|
"@types/youtube-player": "^5.5.7",
|
||||||
"@typescript-eslint/eslint-plugin": "6.5.0",
|
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||||
"auto-changelog": "2.4.0",
|
"auto-changelog": "2.4.0",
|
||||||
|
|||||||
@ -162,7 +162,7 @@ export default (
|
|||||||
largeImageKey: songInfo.imageSrc ?? '',
|
largeImageKey: songInfo.imageSrc ?? '',
|
||||||
largeImageText: songInfo.album ?? '',
|
largeImageText: songInfo.album ?? '',
|
||||||
buttons: options.listenAlong ? [
|
buttons: options.listenAlong ? [
|
||||||
{ label: 'Listen Along', url: songInfo.url },
|
{ label: 'Listen Along', url: songInfo.url ?? '' },
|
||||||
] : undefined,
|
] : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ export default (
|
|||||||
}
|
}
|
||||||
} else if (!options.hideDurationLeft) {
|
} else if (!options.hideDurationLeft) {
|
||||||
// Add the start and end time of the song
|
// Add the start and end time of the song
|
||||||
const songStartTime = Date.now() - (songInfo.elapsedSeconds * 1000);
|
const songStartTime = Date.now() - ((songInfo.elapsedSeconds ?? 0) * 1000);
|
||||||
activityInfo.startTimestamp = songStartTime;
|
activityInfo.startTimestamp = songStartTime;
|
||||||
activityInfo.endTimestamp
|
activityInfo.endTimestamp
|
||||||
= songStartTime + (songInfo.songDuration * 1000);
|
= songStartTime + (songInfo.songDuration * 1000);
|
||||||
|
|||||||
@ -1,58 +1,64 @@
|
|||||||
const {
|
|
||||||
|
import {
|
||||||
existsSync,
|
existsSync,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
createWriteStream,
|
createWriteStream,
|
||||||
writeFileSync,
|
writeFileSync,
|
||||||
} = require('node:fs');
|
} from 'node:fs';
|
||||||
const { join } = require('node:path');
|
import { join } from 'node:path';
|
||||||
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
const { randomBytes } = require('node:crypto');
|
import { app, BrowserWindow, dialog, ipcMain } from 'electron';
|
||||||
|
import { ClientType, Innertube, UniversalCache, Utils } from 'youtubei.js';
|
||||||
|
import is from 'electron-is';
|
||||||
|
import ytpl from 'ytpl';
|
||||||
|
// REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid
|
||||||
|
import filenamify from 'filenamify';
|
||||||
|
import { Mutex } from 'async-mutex';
|
||||||
|
import { createFFmpeg } from '@ffmpeg/ffmpeg';
|
||||||
|
|
||||||
const { ipcMain, app, dialog } = require('electron');
|
import NodeID3, { TagConstants } from 'node-id3';
|
||||||
const is = require('electron-is');
|
|
||||||
const { Innertube, UniversalCache, Utils, ClientType } = require('youtubei.js');
|
import PlayerErrorMessage from 'youtubei.js/dist/src/parser/classes/PlayerErrorMessage';
|
||||||
const ytpl = require('ytpl'); // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid
|
import { FormatOptions } from 'youtubei.js/dist/src/types/FormatUtils';
|
||||||
const filenamify = require('filenamify');
|
|
||||||
const ID3Writer = require('browser-id3-writer');
|
import TrackInfo from 'youtubei.js/dist/src/parser/ytmusic/TrackInfo';
|
||||||
const { Mutex } = require('async-mutex');
|
|
||||||
const ffmpeg = require('@ffmpeg/ffmpeg').createFFmpeg({
|
import { VideoInfo } from 'youtubei.js/dist/src/parser/youtube';
|
||||||
|
|
||||||
|
import { cropMaxWidth, getFolder, presets, sendFeedback as sendFeedback_, setBadge } from './utils';
|
||||||
|
|
||||||
|
import config from './config';
|
||||||
|
|
||||||
|
import { fetchFromGenius } from '../lyrics-genius/back';
|
||||||
|
import { isEnabled } from '../../config/plugins';
|
||||||
|
import { cleanupName, getImage, SongInfo } from '../../providers/song-info';
|
||||||
|
import { injectCSS } from '../utils';
|
||||||
|
import { cache } from '../../providers/decorators';
|
||||||
|
import { GetPlayerResponse } from '../../types/get-player-response';
|
||||||
|
|
||||||
|
type CustomSongInfo = SongInfo & { trackId?: string };
|
||||||
|
|
||||||
|
const ffmpeg = createFFmpeg({
|
||||||
log: false,
|
log: false,
|
||||||
logger() {
|
logger() {
|
||||||
}, // Console.log,
|
}, // Console.log,
|
||||||
progress() {
|
progress() {
|
||||||
}, // Console.log,
|
}, // Console.log,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
|
||||||
presets,
|
|
||||||
cropMaxWidth,
|
|
||||||
getFolder,
|
|
||||||
setBadge,
|
|
||||||
sendFeedback: sendFeedback_,
|
|
||||||
} = require('./utils');
|
|
||||||
|
|
||||||
const config = require('./config');
|
|
||||||
|
|
||||||
const { fetchFromGenius } = require('../lyrics-genius/back');
|
|
||||||
const { isEnabled } = require('../../config/plugins');
|
|
||||||
const { getImage, cleanupName } = require('../../providers/song-info');
|
|
||||||
const { injectCSS } = require('../utils');
|
|
||||||
const { cache } = require('../../providers/decorators');
|
|
||||||
|
|
||||||
const ffmpegMutex = new Mutex();
|
const ffmpegMutex = new Mutex();
|
||||||
|
|
||||||
/** @type {Innertube} */
|
let yt: Innertube;
|
||||||
let yt;
|
let win: BrowserWindow;
|
||||||
let win;
|
let playingUrl: string;
|
||||||
let playingUrl;
|
|
||||||
|
|
||||||
const sendError = (error, source) => {
|
const sendError = (error: Error, source?: string) => {
|
||||||
win.setProgressBar(-1); // Close progress bar
|
win.setProgressBar(-1); // Close progress bar
|
||||||
setBadge(0); // Close badge
|
setBadge(0); // Close badge
|
||||||
sendFeedback_(win); // Reset feedback
|
sendFeedback_(win); // Reset feedback
|
||||||
|
|
||||||
const songNameMessage = source ? `\nin ${source}` : '';
|
const songNameMessage = source ? `\nin ${source}` : '';
|
||||||
const cause = error.cause ? `\n\n${error.cause.toString()}` : '';
|
const cause = error.cause ? `\n\n${String(error.cause)}` : '';
|
||||||
const message = `${error.toString()}${songNameMessage}${cause}`;
|
const message = `${error.toString()}${songNameMessage}${cause}`;
|
||||||
|
|
||||||
console.error(message);
|
console.error(message);
|
||||||
@ -65,7 +71,7 @@ const sendError = (error, source) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = async (win_) => {
|
export default async (win_: BrowserWindow) => {
|
||||||
win = win_;
|
win = win_;
|
||||||
injectCSS(win.webContents, join(__dirname, 'style.css'));
|
injectCSS(win.webContents, join(__dirname, 'style.css'));
|
||||||
|
|
||||||
@ -73,52 +79,48 @@ module.exports = async (win_) => {
|
|||||||
cache: new UniversalCache(false),
|
cache: new UniversalCache(false),
|
||||||
generate_session_locally: true,
|
generate_session_locally: true,
|
||||||
});
|
});
|
||||||
ipcMain.on('download-song', (_, url) => downloadSong(url));
|
ipcMain.on('download-song', (_, url: string) => downloadSong(url));
|
||||||
ipcMain.on('video-src-changed', async (_, data) => {
|
ipcMain.on('video-src-changed', (_, data: string) => {
|
||||||
playingUrl
|
playingUrl = (JSON.parse(data) as GetPlayerResponse).microformat.microformatDataRenderer.urlCanonical;
|
||||||
= JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical;
|
|
||||||
});
|
});
|
||||||
ipcMain.on('download-playlist-request', async (_event, url) =>
|
ipcMain.on('download-playlist-request', async (_event, url: string) =>
|
||||||
downloadPlaylist(url),
|
downloadPlaylist(url),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.downloadSong = downloadSong;
|
export async function downloadSong(
|
||||||
module.exports.downloadPlaylist = downloadPlaylist;
|
url: string,
|
||||||
|
playlistFolder: string | undefined = undefined,
|
||||||
async function downloadSong(
|
trackId: string | undefined = undefined,
|
||||||
url,
|
increasePlaylistProgress: (value: number) => void = () => {
|
||||||
playlistFolder = undefined,
|
|
||||||
trackId = undefined,
|
|
||||||
increasePlaylistProgress = () => {
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
let resolvedName;
|
let resolvedName;
|
||||||
try {
|
try {
|
||||||
await downloadSongUnsafe(
|
await downloadSongUnsafe(
|
||||||
url,
|
url,
|
||||||
(name) => resolvedName = name,
|
(name: string) => resolvedName = name,
|
||||||
playlistFolder,
|
playlistFolder,
|
||||||
trackId,
|
trackId,
|
||||||
increasePlaylistProgress,
|
increasePlaylistProgress,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
sendError(error, resolvedName || url);
|
sendError(error as Error, resolvedName || url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadSongUnsafe(
|
async function downloadSongUnsafe(
|
||||||
url,
|
url: string,
|
||||||
setName,
|
setName: (name: string) => void,
|
||||||
playlistFolder = undefined,
|
playlistFolder: string | undefined = undefined,
|
||||||
trackId = undefined,
|
trackId: string | undefined = undefined,
|
||||||
increasePlaylistProgress = () => {
|
increasePlaylistProgress: (value: number) => void = () => {
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const sendFeedback = (message, progress) => {
|
const sendFeedback = (message: unknown, progress?: number) => {
|
||||||
if (!playlistFolder) {
|
if (!playlistFolder) {
|
||||||
sendFeedback_(win, message);
|
sendFeedback_(win, message);
|
||||||
if (!isNaN(progress)) {
|
if (progress && !isNaN(progress)) {
|
||||||
win.setProgressBar(progress);
|
win.setProgressBar(progress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,7 +129,9 @@ async function downloadSongUnsafe(
|
|||||||
sendFeedback('Downloading...', 2);
|
sendFeedback('Downloading...', 2);
|
||||||
|
|
||||||
const id = getVideoId(url);
|
const id = getVideoId(url);
|
||||||
let info = await yt.music.getInfo(id);
|
if (typeof id !== 'string') throw new Error('Video not found');
|
||||||
|
|
||||||
|
let info: TrackInfo | VideoInfo = await yt.music.getInfo(id);
|
||||||
|
|
||||||
if (!info) {
|
if (!info) {
|
||||||
throw new Error('Video not found');
|
throw new Error('Video not found');
|
||||||
@ -164,19 +168,19 @@ async function downloadSongUnsafe(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (playabilityStatus.status === 'UNPLAYABLE') {
|
if (playabilityStatus.status === 'UNPLAYABLE') {
|
||||||
/**
|
const errorScreen = playabilityStatus.error_screen as PlayerErrorMessage | null;
|
||||||
* @typedef {import('youtubei.js/dist/src/parser/classes/PlayerErrorMessage').default} PlayerErrorMessage
|
|
||||||
* @type {PlayerErrorMessage}
|
|
||||||
*/
|
|
||||||
const errorScreen = playabilityStatus.error_screen;
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${playabilityStatus.status}] ${errorScreen.reason.text}: ${errorScreen.subreason.text}`,
|
`[${playabilityStatus.status}] ${errorScreen?.reason.text}: ${errorScreen?.subreason.text}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const extension = presets[config.get('preset')]?.extension || 'mp3';
|
const preset = config.get('preset') ?? 'mp3';
|
||||||
|
let presetSetting: { extension: string; ffmpegArgs: string[] } | null = null;
|
||||||
|
if (preset === 'opus') {
|
||||||
|
presetSetting = presets[preset];
|
||||||
|
}
|
||||||
|
|
||||||
const filename = filenamify(`${name}.${extension}`, {
|
const filename = filenamify(`${name}.${presetSetting?.extension ?? 'mp3'}`, {
|
||||||
replacement: '_',
|
replacement: '_',
|
||||||
maxLength: 255,
|
maxLength: 255,
|
||||||
});
|
});
|
||||||
@ -187,7 +191,7 @@ async function downloadSongUnsafe(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadOptions = {
|
const downloadOptions: FormatOptions = {
|
||||||
type: 'audio', // Audio, video or video+audio
|
type: 'audio', // Audio, video or video+audio
|
||||||
quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
||||||
format: 'any', // Media container format
|
format: 'any', // Media container format
|
||||||
@ -197,7 +201,7 @@ async function downloadSongUnsafe(
|
|||||||
const stream = await info.download(downloadOptions);
|
const stream = await info.download(downloadOptions);
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
`Downloading ${metadata.artist} - ${metadata.title} [${metadata.id}]`,
|
`Downloading ${metadata.artist} - ${metadata.title} [${metadata.videoId}]`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const iterableStream = Utils.streamToIterable(stream);
|
const iterableStream = Utils.streamToIterable(stream);
|
||||||
@ -206,10 +210,10 @@ async function downloadSongUnsafe(
|
|||||||
mkdirSync(dir);
|
mkdirSync(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (presets[config.get('preset')]) {
|
if (presetSetting) {
|
||||||
const file = createWriteStream(filePath);
|
const file = createWriteStream(filePath);
|
||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
const total = format.content_length;
|
const total: number = format.content_length ?? 1;
|
||||||
|
|
||||||
for await (const chunk of iterableStream) {
|
for await (const chunk of iterableStream) {
|
||||||
downloaded += chunk.length;
|
downloaded += chunk.length;
|
||||||
@ -223,18 +227,23 @@ async function downloadSongUnsafe(
|
|||||||
await ffmpegWriteTags(
|
await ffmpegWriteTags(
|
||||||
filePath,
|
filePath,
|
||||||
metadata,
|
metadata,
|
||||||
presets[config.get('preset')]?.ffmpegArgs,
|
presetSetting.ffmpegArgs,
|
||||||
);
|
);
|
||||||
sendFeedback(null, -1);
|
sendFeedback(null, -1);
|
||||||
} else {
|
} else {
|
||||||
const fileBuffer = await iterableStreamToMP3(
|
const fileBuffer = await iterableStreamToMP3(
|
||||||
iterableStream,
|
iterableStream,
|
||||||
metadata,
|
metadata,
|
||||||
format.content_length,
|
format.content_length ?? 0,
|
||||||
sendFeedback,
|
sendFeedback,
|
||||||
increasePlaylistProgress,
|
increasePlaylistProgress,
|
||||||
);
|
);
|
||||||
writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback));
|
if (fileBuffer) {
|
||||||
|
const buffer = await writeID3(Buffer.from(fileBuffer), metadata, sendFeedback);
|
||||||
|
if (buffer) {
|
||||||
|
writeFileSync(filePath, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendFeedback(null, -1);
|
sendFeedback(null, -1);
|
||||||
@ -242,11 +251,11 @@ async function downloadSongUnsafe(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function iterableStreamToMP3(
|
async function iterableStreamToMP3(
|
||||||
stream,
|
stream: AsyncGenerator<Uint8Array, void>,
|
||||||
metadata,
|
metadata: CustomSongInfo,
|
||||||
contentLength,
|
contentLength: number,
|
||||||
sendFeedback,
|
sendFeedback: (str: string, value?: number) => void,
|
||||||
increasePlaylistProgress = () => {
|
increasePlaylistProgress: (value: number) => void = () => {
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
@ -294,66 +303,69 @@ async function iterableStreamToMP3(
|
|||||||
sendFeedback('Saving…');
|
sendFeedback('Saving…');
|
||||||
|
|
||||||
return ffmpeg.FS('readFile', `${safeVideoName}.mp3`);
|
return ffmpeg.FS('readFile', `${safeVideoName}.mp3`);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
sendError(error, safeVideoName);
|
sendError(error as Error, safeVideoName);
|
||||||
} finally {
|
} finally {
|
||||||
releaseFFmpegMutex();
|
releaseFFmpegMutex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCoverBuffer = cache(async (url) => {
|
const getCoverBuffer = cache(async (url: string) => {
|
||||||
const nativeImage = cropMaxWidth(await getImage(url));
|
const nativeImage = cropMaxWidth(await getImage(url));
|
||||||
return nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null;
|
return nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function writeID3(buffer, metadata, sendFeedback) {
|
async function writeID3(buffer: Buffer, metadata: CustomSongInfo, sendFeedback: (str: string, value?: number) => void) {
|
||||||
try {
|
try {
|
||||||
sendFeedback('Writing ID3 tags...');
|
sendFeedback('Writing ID3 tags...');
|
||||||
|
const tags: NodeID3.Tags = {};
|
||||||
const coverBuffer = await getCoverBuffer(metadata.image);
|
|
||||||
|
|
||||||
const writer = new ID3Writer(buffer);
|
|
||||||
|
|
||||||
// Create the metadata tags
|
// Create the metadata tags
|
||||||
writer.setFrame('TIT2', metadata.title).setFrame('TPE1', [metadata.artist]);
|
tags.title = metadata.title;
|
||||||
|
tags.artist = metadata.artist;
|
||||||
|
|
||||||
if (metadata.album) {
|
if (metadata.album) {
|
||||||
writer.setFrame('TALB', metadata.album);
|
tags.album = metadata.album;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const coverBuffer = await getCoverBuffer(metadata.imageSrc ?? '');
|
||||||
if (coverBuffer) {
|
if (coverBuffer) {
|
||||||
writer.setFrame('APIC', {
|
tags.image = {
|
||||||
type: 3,
|
mime: 'image/png',
|
||||||
data: coverBuffer,
|
type: {
|
||||||
description: '',
|
id: TagConstants.AttachedPicture.PictureType.FRONT_COVER,
|
||||||
});
|
},
|
||||||
|
description: 'thumbnail',
|
||||||
|
imageBuffer: coverBuffer,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnabled('lyrics-genius')) {
|
if (isEnabled('lyrics-genius')) {
|
||||||
const lyrics = await fetchFromGenius(metadata);
|
const lyrics = await fetchFromGenius(metadata);
|
||||||
if (lyrics) {
|
if (lyrics) {
|
||||||
writer.setFrame('USLT', {
|
tags.unsynchronisedLyrics = {
|
||||||
description: '',
|
language: '',
|
||||||
lyrics,
|
text: lyrics,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadata.trackId) {
|
if (metadata.trackId) {
|
||||||
writer.setFrame('TRCK', metadata.trackId);
|
tags.trackNumber = metadata.trackId;
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.addTag();
|
return NodeID3.write(tags, buffer);
|
||||||
return Buffer.from(writer.arrayBuffer);
|
} catch (error: unknown) {
|
||||||
} catch (error) {
|
sendError(error as Error, `${metadata.artist} - ${metadata.title}`);
|
||||||
sendError(error, `${metadata.artist} - ${metadata.title}`);
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadPlaylist(givenUrl) {
|
export async function downloadPlaylist(givenUrl?: string | URL) {
|
||||||
try {
|
try {
|
||||||
givenUrl = new URL(givenUrl);
|
givenUrl = new URL(givenUrl ?? '');
|
||||||
} catch {
|
} catch {
|
||||||
givenUrl = undefined;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playlistId
|
const playlistId
|
||||||
@ -366,18 +378,18 @@ async function downloadPlaylist(givenUrl) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendFeedback = (message) => sendFeedback_(win, message);
|
const sendFeedback = (message?: unknown) => sendFeedback_(win, message);
|
||||||
|
|
||||||
console.log(`trying to get playlist ID: '${playlistId}'`);
|
console.log(`trying to get playlist ID: '${playlistId}'`);
|
||||||
sendFeedback('Getting playlist info…');
|
sendFeedback('Getting playlist info…');
|
||||||
let playlist;
|
let playlist: ytpl.Result;
|
||||||
try {
|
try {
|
||||||
playlist = await ytpl(playlistId, {
|
playlist = await ytpl(playlistId, {
|
||||||
limit: config.get('playlistMaxItems') || Number.POSITIVE_INFINITY,
|
limit: config.get('playlistMaxItems') || Number.POSITIVE_INFINITY,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
sendError(
|
sendError(
|
||||||
`Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${error}`,
|
Error(`Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${String(error)}`),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -432,8 +444,8 @@ async function downloadPlaylist(givenUrl) {
|
|||||||
|
|
||||||
const progressStep = 1 / playlist.items.length;
|
const progressStep = 1 / playlist.items.length;
|
||||||
|
|
||||||
const increaseProgress = (itemPercentage) => {
|
const increaseProgress = (itemPercentage: number) => {
|
||||||
const currentProgress = (counter - 1) / playlist.items.length;
|
const currentProgress = (counter - 1) / (playlist.items.length ?? 1);
|
||||||
const newProgress = currentProgress + (progressStep * itemPercentage);
|
const newProgress = currentProgress + (progressStep * itemPercentage);
|
||||||
win.setProgressBar(newProgress);
|
win.setProgressBar(newProgress);
|
||||||
};
|
};
|
||||||
@ -445,11 +457,11 @@ async function downloadPlaylist(givenUrl) {
|
|||||||
await downloadSong(
|
await downloadSong(
|
||||||
song.url,
|
song.url,
|
||||||
playlistFolder,
|
playlistFolder,
|
||||||
trackId,
|
trackId?.toString(),
|
||||||
increaseProgress,
|
increaseProgress,
|
||||||
).catch((error) =>
|
).catch((error) =>
|
||||||
sendError(
|
sendError(
|
||||||
`Error downloading "${song.author.name} - ${song.title}":\n ${error}`,
|
new Error(`Error downloading "${song.author.name} - ${song.title}":\n ${error}`)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -457,8 +469,8 @@ async function downloadPlaylist(givenUrl) {
|
|||||||
setBadge(playlist.items.length - counter);
|
setBadge(playlist.items.length - counter);
|
||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
sendError(error);
|
sendError(error as Error);
|
||||||
} finally {
|
} finally {
|
||||||
win.setProgressBar(-1); // Close progress bar
|
win.setProgressBar(-1); // Close progress bar
|
||||||
setBadge(0); // Close badge counter
|
setBadge(0); // Close badge counter
|
||||||
@ -466,7 +478,7 @@ async function downloadPlaylist(givenUrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ffmpegWriteTags(filePath, metadata, ffmpegArgs = []) {
|
async function ffmpegWriteTags(filePath: string, metadata: CustomSongInfo, ffmpegArgs: string[] = []) {
|
||||||
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -481,16 +493,16 @@ async function ffmpegWriteTags(filePath, metadata, ffmpegArgs = []) {
|
|||||||
...ffmpegArgs,
|
...ffmpegArgs,
|
||||||
filePath,
|
filePath,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
sendError(error);
|
sendError(error as Error);
|
||||||
} finally {
|
} finally {
|
||||||
releaseFFmpegMutex();
|
releaseFFmpegMutex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFFmpegMetadataArgs(metadata) {
|
function getFFmpegMetadataArgs(metadata: CustomSongInfo) {
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
return;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -504,7 +516,7 @@ function getFFmpegMetadataArgs(metadata) {
|
|||||||
// Playlist radio modifier needs to be cut from playlist ID
|
// Playlist radio modifier needs to be cut from playlist ID
|
||||||
const INVALID_PLAYLIST_MODIFIER = 'RDAMPL';
|
const INVALID_PLAYLIST_MODIFIER = 'RDAMPL';
|
||||||
|
|
||||||
const getPlaylistID = (aURL) => {
|
const getPlaylistID = (aURL: URL) => {
|
||||||
const result
|
const result
|
||||||
= aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist');
|
= aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist');
|
||||||
if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) {
|
if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) {
|
||||||
@ -514,7 +526,7 @@ const getPlaylistID = (aURL) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVideoId = (url) => {
|
const getVideoId = (url: URL | string): string | null => {
|
||||||
if (typeof url === 'string') {
|
if (typeof url === 'string') {
|
||||||
url = new URL(url);
|
url = new URL(url);
|
||||||
}
|
}
|
||||||
@ -522,18 +534,21 @@ const getVideoId = (url) => {
|
|||||||
return url.searchParams.get('v');
|
return url.searchParams.get('v');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMetadata = (info) => ({
|
const getMetadata = (info: TrackInfo): CustomSongInfo => ({
|
||||||
id: info.basic_info.id,
|
videoId: info.basic_info.id!,
|
||||||
title: cleanupName(info.basic_info.title),
|
title: cleanupName(info.basic_info.title!),
|
||||||
artist: cleanupName(info.basic_info.author),
|
artist: cleanupName(info.basic_info.author!),
|
||||||
album: info.player_overlays?.browser_media_session?.album?.text,
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
|
||||||
image: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))?.url,
|
album: (info.player_overlays?.browser_media_session as any)?.album?.text as string | undefined,
|
||||||
|
imageSrc: info.basic_info.thumbnail?.find((t) => !t.url.endsWith('.webp'))?.url,
|
||||||
|
views: info.basic_info.view_count!,
|
||||||
|
songDuration: info.basic_info.duration!,
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is used to bypass age restrictions
|
// This is used to bypass age restrictions
|
||||||
const getAndroidTvInfo = async (id) => {
|
const getAndroidTvInfo = async (id: string): Promise<VideoInfo> => {
|
||||||
const innertube = await Innertube.create({
|
const innertube = await Innertube.create({
|
||||||
clientType: ClientType.TV_EMBEDDED,
|
client_type: ClientType.TV_EMBEDDED,
|
||||||
generate_session_locally: true,
|
generate_session_locally: true,
|
||||||
retrieve_player: true,
|
retrieve_player: true,
|
||||||
});
|
});
|
||||||
@ -1,4 +0,0 @@
|
|||||||
const { PluginConfig } = require('../../config/dynamic');
|
|
||||||
|
|
||||||
const config = new PluginConfig('downloader');
|
|
||||||
module.exports = { ...config };
|
|
||||||
4
plugins/downloader/config.ts
Normal file
4
plugins/downloader/config.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { PluginConfig } from '../../config/dynamic';
|
||||||
|
|
||||||
|
const config = new PluginConfig('downloader');
|
||||||
|
export default { ...config } as PluginConfig<'downloader'>;
|
||||||
@ -1,11 +1,12 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
const { defaultConfig } = require('../../config');
|
import defaultConfig from '../../config/defaults';
|
||||||
const { getSongMenu } = require('../../providers/dom-elements');
|
import { getSongMenu } from '../../providers/dom-elements';
|
||||||
const { ElementFromFile, templatePath } = require('../utils');
|
import { ElementFromFile, templatePath } from '../utils';
|
||||||
|
import { getSongInfo } from '../../providers/song-info-front';
|
||||||
|
|
||||||
let menu = null;
|
let menu: Element | null = null;
|
||||||
let progress = null;
|
let progress: Element | null = null;
|
||||||
const downloadButton = ElementFromFile(
|
const downloadButton = ElementFromFile(
|
||||||
templatePath(__dirname, 'download.html'),
|
templatePath(__dirname, 'download.html'),
|
||||||
);
|
);
|
||||||
@ -24,7 +25,7 @@ const menuObserver = new MutationObserver(() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href;
|
const menuUrl = (document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint') as HTMLAnchorElement | undefined)?.href;
|
||||||
if (!menuUrl?.includes('watch?') && doneFirstLoad) {
|
if (!menuUrl?.includes('watch?') && doneFirstLoad) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -42,7 +43,7 @@ const menuObserver = new MutationObserver(() => {
|
|||||||
// TODO: re-enable once contextIsolation is set to true
|
// TODO: re-enable once contextIsolation is set to true
|
||||||
// contextBridge.exposeInMainWorld("downloader", {
|
// contextBridge.exposeInMainWorld("downloader", {
|
||||||
// download: () => {
|
// download: () => {
|
||||||
global.download = () => {
|
export const download = () => {
|
||||||
let videoUrl = getSongMenu()
|
let videoUrl = getSongMenu()
|
||||||
// Selector of first button which is always "Start Radio"
|
// Selector of first button which is always "Start Radio"
|
||||||
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint')
|
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint')
|
||||||
@ -57,21 +58,21 @@ global.download = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
videoUrl = global.songInfo.url || window.location.href;
|
videoUrl = getSongInfo().url || window.location.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcRenderer.send('download-song', videoUrl);
|
ipcRenderer.send('download-song', videoUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = () => {
|
export default () => {
|
||||||
document.addEventListener('apiLoaded', () => {
|
document.addEventListener('apiLoaded', () => {
|
||||||
menuObserver.observe(document.querySelector('ytmusic-popup-container'), {
|
menuObserver.observe(document.querySelector('ytmusic-popup-container')!, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
});
|
});
|
||||||
}, { once: true, passive: true });
|
}, { once: true, passive: true });
|
||||||
|
|
||||||
ipcRenderer.on('downloader-feedback', (_, feedback) => {
|
ipcRenderer.on('downloader-feedback', (_, feedback: string) => {
|
||||||
if (progress) {
|
if (progress) {
|
||||||
progress.innerHTML = feedback || 'Download';
|
progress.innerHTML = feedback || 'Download';
|
||||||
} else {
|
} else {
|
||||||
@ -1,10 +1,12 @@
|
|||||||
const { dialog } = require('electron');
|
import { dialog } from 'electron';
|
||||||
|
|
||||||
const { downloadPlaylist } = require('./back');
|
import { downloadPlaylist } from './back';
|
||||||
const { defaultMenuDownloadLabel, getFolder, presets } = require('./utils');
|
import { defaultMenuDownloadLabel, getFolder, presets } from './utils';
|
||||||
const config = require('./config');
|
import config from './config';
|
||||||
|
|
||||||
module.exports = () => [
|
import { MenuTemplate } from '../../menu';
|
||||||
|
|
||||||
|
export default (): MenuTemplate => [
|
||||||
{
|
{
|
||||||
label: defaultMenuDownloadLabel,
|
label: defaultMenuDownloadLabel,
|
||||||
click: () => downloadPlaylist(),
|
click: () => downloadPlaylist(),
|
||||||
@ -1,14 +1,14 @@
|
|||||||
const { app } = require('electron');
|
import { app, BrowserWindow } from 'electron';
|
||||||
const is = require('electron-is');
|
import is from 'electron-is';
|
||||||
|
|
||||||
module.exports.getFolder = (customFolder) => customFolder || app.getPath('downloads');
|
export const getFolder = (customFolder: string) => customFolder || app.getPath('downloads');
|
||||||
module.exports.defaultMenuDownloadLabel = 'Download playlist';
|
export const defaultMenuDownloadLabel = 'Download playlist';
|
||||||
|
|
||||||
module.exports.sendFeedback = (win, message) => {
|
export const sendFeedback = (win: BrowserWindow, message?: unknown) => {
|
||||||
win.webContents.send('downloader-feedback', message);
|
win.webContents.send('downloader-feedback', message);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.cropMaxWidth = (image) => {
|
export const cropMaxWidth = (image: Electron.NativeImage) => {
|
||||||
const imageSize = image.getSize();
|
const imageSize = image.getSize();
|
||||||
// Standart youtube artwork width with margins from both sides is 280 + 720 + 280
|
// Standart youtube artwork width with margins from both sides is 280 + 720 + 280
|
||||||
if (imageSize.width === 1280 && imageSize.height === 720) {
|
if (imageSize.width === 1280 && imageSize.height === 720) {
|
||||||
@ -24,7 +24,7 @@ module.exports.cropMaxWidth = (image) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Presets for FFmpeg
|
// Presets for FFmpeg
|
||||||
module.exports.presets = {
|
export const presets = {
|
||||||
'None (defaults to mp3)': undefined,
|
'None (defaults to mp3)': undefined,
|
||||||
'opus': {
|
'opus': {
|
||||||
extension: 'opus',
|
extension: 'opus',
|
||||||
@ -32,7 +32,7 @@ module.exports.presets = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.setBadge = (n) => {
|
export const setBadge = (n: number) => {
|
||||||
if (is.linux() || is.macOS()) {
|
if (is.linux() || is.macOS()) {
|
||||||
app.setBadgeCount(n);
|
app.setBadgeCount(n);
|
||||||
}
|
}
|
||||||
@ -1,23 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
|
|
||||||
const electronLocalshortcut = require('electron-localshortcut');
|
|
||||||
const { setupTitlebar, attachTitlebarToWindow } = require('custom-electron-titlebar/main');
|
|
||||||
|
|
||||||
const { injectCSS } = require('../utils');
|
|
||||||
|
|
||||||
setupTitlebar();
|
|
||||||
|
|
||||||
// Tracks menu visibility
|
|
||||||
|
|
||||||
module.exports = (win) => {
|
|
||||||
// Css for custom scrollbar + disable drag area(was causing bugs)
|
|
||||||
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
|
||||||
|
|
||||||
win.once('ready-to-show', () => {
|
|
||||||
attachTitlebarToWindow(win);
|
|
||||||
|
|
||||||
electronLocalshortcut.register(win, '`', () => {
|
|
||||||
win.webContents.send('toggleMenu');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
27
plugins/in-app-menu/back.ts
Normal file
27
plugins/in-app-menu/back.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { register } from 'electron-localshortcut';
|
||||||
|
// eslint-disable-next-line import/no-unresolved
|
||||||
|
import { attachTitlebarToWindow, setupTitlebar } from 'custom-electron-titlebar/main';
|
||||||
|
|
||||||
|
import { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
|
import { injectCSS } from '../utils';
|
||||||
|
|
||||||
|
|
||||||
|
setupTitlebar();
|
||||||
|
|
||||||
|
// Tracks menu visibility
|
||||||
|
|
||||||
|
module.exports = (win: BrowserWindow) => {
|
||||||
|
// Css for custom scrollbar + disable drag area(was causing bugs)
|
||||||
|
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
||||||
|
|
||||||
|
win.once('ready-to-show', () => {
|
||||||
|
attachTitlebarToWindow(win);
|
||||||
|
|
||||||
|
register(win, '`', () => {
|
||||||
|
win.webContents.send('toggleMenu');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
9
plugins/in-app-menu/custom-electron-titlebar.d.ts
vendored
Normal file
9
plugins/in-app-menu/custom-electron-titlebar.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
declare module 'custom-electron-titlebar' {
|
||||||
|
// eslint-disable-next-line import/no-unresolved
|
||||||
|
import OriginalTitlebar from 'custom-electron-titlebar/dist/titlebar';
|
||||||
|
// eslint-disable-next-line import/no-unresolved
|
||||||
|
import { Color as OriginalColor } from 'custom-electron-titlebar/dist/vs/base/common/color';
|
||||||
|
|
||||||
|
export const Color: typeof OriginalColor;
|
||||||
|
export const Titlebar: typeof OriginalTitlebar;
|
||||||
|
}
|
||||||
@ -1,78 +0,0 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
|
||||||
const { Titlebar, Color } = require('custom-electron-titlebar');
|
|
||||||
|
|
||||||
const config = require('../../config');
|
|
||||||
const { isEnabled } = require('../../config/plugins');
|
|
||||||
|
|
||||||
function $(selector) {
|
|
||||||
return document.querySelector(selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
const visible = () => Boolean($('.cet-menubar').firstChild);
|
|
||||||
const bar = new Titlebar({
|
|
||||||
icon: 'https://cdn-icons-png.flaticon.com/512/5358/5358672.png',
|
|
||||||
backgroundColor: Color.fromHex('#050505'),
|
|
||||||
itemBackgroundColor: Color.fromHex('#1d1d1d'),
|
|
||||||
svgColor: Color.WHITE,
|
|
||||||
menu: config.get('options.hideMenu') ? null : undefined,
|
|
||||||
});
|
|
||||||
bar.updateTitle(' ');
|
|
||||||
document.title = 'Youtube Music';
|
|
||||||
|
|
||||||
const toggleMenu = () => {
|
|
||||||
if (visible()) {
|
|
||||||
bar.updateMenu(null);
|
|
||||||
} else {
|
|
||||||
bar.refreshMenu();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$('.cet-window-icon').addEventListener('click', toggleMenu);
|
|
||||||
ipcRenderer.on('toggleMenu', toggleMenu);
|
|
||||||
|
|
||||||
ipcRenderer.on('refreshMenu', () => {
|
|
||||||
if (visible()) {
|
|
||||||
bar.refreshMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isEnabled('picture-in-picture')) {
|
|
||||||
ipcRenderer.on('pip-toggle', () => {
|
|
||||||
bar.refreshMenu();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
|
|
||||||
document.addEventListener('apiLoaded', () => {
|
|
||||||
setNavbarMargin();
|
|
||||||
const playPageObserver = new MutationObserver(setNavbarMargin);
|
|
||||||
playPageObserver.observe($('ytmusic-app-layout'), { attributeFilter: ['player-page-open_', 'playerPageOpen_'] });
|
|
||||||
setupSearchOpenObserver();
|
|
||||||
setupMenuOpenObserver();
|
|
||||||
}, { once: true, passive: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
function setupSearchOpenObserver() {
|
|
||||||
const searchOpenObserver = new MutationObserver((mutations) => {
|
|
||||||
$('#nav-bar-background').style.webkitAppRegion
|
|
||||||
= mutations[0].target.opened ? 'no-drag' : 'drag';
|
|
||||||
});
|
|
||||||
searchOpenObserver.observe($('ytmusic-search-box'), { attributeFilter: ['opened'] });
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupMenuOpenObserver() {
|
|
||||||
const menuOpenObserver = new MutationObserver(() => {
|
|
||||||
$('#nav-bar-background').style.webkitAppRegion
|
|
||||||
= [...$('.cet-menubar').childNodes].some((c) => c.classList.contains('open'))
|
|
||||||
? 'no-drag' : 'drag';
|
|
||||||
});
|
|
||||||
menuOpenObserver.observe($('.cet-menubar'), { subtree: true, attributeFilter: ['class'] });
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNavbarMargin() {
|
|
||||||
$('#nav-bar-background').style.right
|
|
||||||
= $('ytmusic-app-layout').playerPageOpen_
|
|
||||||
? '0px'
|
|
||||||
: '12px';
|
|
||||||
}
|
|
||||||
96
plugins/in-app-menu/front.ts
Normal file
96
plugins/in-app-menu/front.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { ipcRenderer, Menu } from 'electron';
|
||||||
|
// eslint-disable-next-line import/no-unresolved
|
||||||
|
import { Color, Titlebar } from 'custom-electron-titlebar';
|
||||||
|
|
||||||
|
import config from '../../config';
|
||||||
|
import { isEnabled } from '../../config/plugins';
|
||||||
|
|
||||||
|
function $(selector: string) {
|
||||||
|
return document.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
const visible = () => Boolean($('.cet-menubar')?.firstChild);
|
||||||
|
const bar = new Titlebar({
|
||||||
|
icon: 'https://cdn-icons-png.flaticon.com/512/5358/5358672.png',
|
||||||
|
backgroundColor: Color.fromHex('#050505'),
|
||||||
|
itemBackgroundColor: Color.fromHex('#1d1d1d') ,
|
||||||
|
svgColor: Color.WHITE,
|
||||||
|
menu: config.get('options.hideMenu') ? null as unknown as Menu : undefined,
|
||||||
|
});
|
||||||
|
bar.updateTitle(' ');
|
||||||
|
document.title = 'Youtube Music';
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
if (visible()) {
|
||||||
|
bar.updateMenu(null as unknown as Menu);
|
||||||
|
} else {
|
||||||
|
bar.refreshMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.cet-window-icon')?.addEventListener('click', toggleMenu);
|
||||||
|
ipcRenderer.on('toggleMenu', toggleMenu);
|
||||||
|
|
||||||
|
ipcRenderer.on('refreshMenu', () => {
|
||||||
|
if (visible()) {
|
||||||
|
bar.refreshMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isEnabled('picture-in-picture')) {
|
||||||
|
ipcRenderer.on('pip-toggle', () => {
|
||||||
|
bar.refreshMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
|
||||||
|
document.addEventListener('apiLoaded', () => {
|
||||||
|
setNavbarMargin();
|
||||||
|
const playPageObserver = new MutationObserver(setNavbarMargin);
|
||||||
|
const appLayout = $('ytmusic-app-layout');
|
||||||
|
if (appLayout) {
|
||||||
|
playPageObserver.observe(appLayout, { attributeFilter: ['player-page-open_', 'playerPageOpen_'] });
|
||||||
|
}
|
||||||
|
setupSearchOpenObserver();
|
||||||
|
setupMenuOpenObserver();
|
||||||
|
}, { once: true, passive: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupSearchOpenObserver() {
|
||||||
|
const searchOpenObserver = new MutationObserver((mutations) => {
|
||||||
|
($('#nav-bar-background') as HTMLElement)
|
||||||
|
.style
|
||||||
|
.setProperty(
|
||||||
|
'-webkit-app-region',
|
||||||
|
(mutations[0].target as HTMLElement & { opened: boolean }).opened ? 'no-drag' : 'drag',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const searchBox = $('ytmusic-search-box');
|
||||||
|
if (searchBox) {
|
||||||
|
searchOpenObserver.observe(searchBox, { attributeFilter: ['opened'] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMenuOpenObserver() {
|
||||||
|
const cetMenubar = $('.cet-menubar');
|
||||||
|
if (cetMenubar) {
|
||||||
|
const menuOpenObserver = new MutationObserver(() => {
|
||||||
|
($('#nav-bar-background') as HTMLElement)
|
||||||
|
.style
|
||||||
|
.setProperty(
|
||||||
|
'-webkit-app-region',
|
||||||
|
Array.from(cetMenubar.childNodes).some((c) => (c as HTMLElement).classList.contains('open')) ? 'no-drag' : 'drag',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
menuOpenObserver.observe(cetMenubar, { subtree: true, attributeFilter: ['class'] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNavbarMargin() {
|
||||||
|
const navBarBackground = $('#nav-bar-background') as HTMLElement;
|
||||||
|
navBarBackground.style.right
|
||||||
|
= ($('ytmusic-app-layout') as HTMLElement & { playerPageOpen_: boolean }).playerPageOpen_
|
||||||
|
? '0px'
|
||||||
|
: '12px';
|
||||||
|
}
|
||||||
@ -1,32 +1,41 @@
|
|||||||
const { shell, net } = require('electron');
|
import { BrowserWindow, net, shell } from 'electron';
|
||||||
const md5 = require('md5');
|
import md5 from 'md5';
|
||||||
|
|
||||||
const { setOptions } = require('../../config/plugins');
|
import { setOptions } from '../../config/plugins';
|
||||||
const registerCallback = require('../../providers/song-info');
|
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||||
const defaultConfig = require('../../config/defaults');
|
import defaultConfig from '../../config/defaults';
|
||||||
|
import config from '../../config';
|
||||||
|
|
||||||
const createFormData = (parameters) => {
|
const LastFMOptionsObj = config.get('plugins.last-fm');
|
||||||
|
type LastFMOptions = typeof LastFMOptionsObj;
|
||||||
|
|
||||||
|
interface LastFmData {
|
||||||
|
method: string,
|
||||||
|
timestamp?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFormData = (parameters: Record<string, unknown>) => {
|
||||||
// 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, parameters[key]);
|
formData.append(key, String(parameters[key]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createQueryString = (parameters, apiSignature) => {
|
const createQueryString = (parameters: Record<string, unknown>, apiSignature: string) => {
|
||||||
// Creates a querystring
|
// Creates a querystring
|
||||||
const queryData = [];
|
const queryData = [];
|
||||||
parameters.api_sig = apiSignature;
|
parameters.api_sig = apiSignature;
|
||||||
for (const key in parameters) {
|
for (const key in parameters) {
|
||||||
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(parameters[key])}`);
|
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(parameters[key]))}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return '?' + queryData.join('&');
|
return '?' + queryData.join('&');
|
||||||
};
|
};
|
||||||
|
|
||||||
const createApiSig = (parameters, secret) => {
|
const createApiSig = (parameters: Record<string, unknown>, 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 = [];
|
const keys = [];
|
||||||
for (const key in parameters) {
|
for (const key in parameters) {
|
||||||
@ -40,7 +49,7 @@ const createApiSig = (parameters, secret) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
sig += `${key}${parameters[key]}`;
|
sig += `${key}${String(parameters[key])}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
sig += secret;
|
sig += secret;
|
||||||
@ -48,7 +57,7 @@ const createApiSig = (parameters, secret) => {
|
|||||||
return sig;
|
return sig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createToken = async ({ apiKey, apiRoot, secret }) => {
|
const createToken = async ({ api_key: apiKey, api_root: apiRoot, secret }: LastFMOptions) => {
|
||||||
// Creates and stores the auth token
|
// Creates and stores the auth token
|
||||||
const data = {
|
const data = {
|
||||||
method: 'auth.gettoken',
|
method: 'auth.gettoken',
|
||||||
@ -56,20 +65,18 @@ const createToken = async ({ apiKey, apiRoot, secret }) => {
|
|||||||
format: 'json',
|
format: 'json',
|
||||||
};
|
};
|
||||||
const apiSigature = createApiSig(data, secret);
|
const apiSigature = createApiSig(data, secret);
|
||||||
let response = await net.fetch(`${apiRoot}${createQueryString(data, apiSigature)}`);
|
const response = await net.fetch(`${apiRoot}${createQueryString(data, apiSigature)}`);
|
||||||
response = await response.json();
|
const json = await response.json() as Record<string, string>;
|
||||||
return response?.token;
|
return json?.token;
|
||||||
};
|
};
|
||||||
|
|
||||||
const authenticate = async (config) => {
|
const authenticateAndGetToken = async (config: LastFMOptions) => {
|
||||||
// Asks the user for authentication
|
// Asks the user for authentication
|
||||||
config.token = await createToken(config);
|
await shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
|
||||||
setOptions('last-fm', config);
|
return await createToken(config);
|
||||||
shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
|
|
||||||
return config;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAndSetSessionKey = async (config) => {
|
const getAndSetSessionKey = async (config: LastFMOptions) => {
|
||||||
// Get and store the session key
|
// Get and store the session key
|
||||||
const data = {
|
const data = {
|
||||||
api_key: config.api_key,
|
api_key: config.api_key,
|
||||||
@ -78,18 +85,25 @@ const getAndSetSessionKey = async (config) => {
|
|||||||
token: config.token,
|
token: config.token,
|
||||||
};
|
};
|
||||||
const apiSignature = createApiSig(data, config.secret);
|
const apiSignature = createApiSig(data, config.secret);
|
||||||
let res = await net.fetch(`${config.api_root}${createQueryString(data, apiSignature)}`);
|
const response = await net.fetch(`${config.api_root}${createQueryString(data, apiSignature)}`);
|
||||||
res = await res.json();
|
const json = await response.json() as {
|
||||||
if (res.error) {
|
error?: string,
|
||||||
await authenticate(config);
|
session?: {
|
||||||
|
key: string,
|
||||||
}
|
}
|
||||||
|
};
|
||||||
config.session_key = res?.session?.key;
|
if (json.error) {
|
||||||
|
config.token = await authenticateAndGetToken(config);
|
||||||
setOptions('last-fm', config);
|
setOptions('last-fm', config);
|
||||||
|
}
|
||||||
|
if (json.session) {
|
||||||
|
config.session_key = json?.session?.key;
|
||||||
|
setOptions('last-fm', config);
|
||||||
|
}
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
const postSongDataToAPI = async (songInfo, config, data) => {
|
const postSongDataToAPI = async (songInfo: SongInfo, config: LastFMOptions, data: LastFmData) => {
|
||||||
// This sends a post request to the api, and adds the common data
|
// This sends a post request to the api, and adds the common data
|
||||||
if (!config.session_key) {
|
if (!config.session_key) {
|
||||||
await getAndSetSessionKey(config);
|
await getAndSetSessionKey(config);
|
||||||
@ -101,6 +115,7 @@ const postSongDataToAPI = async (songInfo, config, data) => {
|
|||||||
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,
|
||||||
@ -108,26 +123,32 @@ const postSongDataToAPI = async (songInfo, config, 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) })
|
net.fetch('https://ws.audioscrobbler.com/2.0/', { method: 'POST', body: createFormData(postData) })
|
||||||
.catch((error) => {
|
.catch(async (error: {
|
||||||
if (error.response.data.error === 9) {
|
response?: {
|
||||||
|
data?: {
|
||||||
|
error: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
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);
|
||||||
setOptions('last-fm', config);
|
setOptions('last-fm', config);
|
||||||
authenticate(config);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addScrobble = (songInfo, config) => {
|
const addScrobble = (songInfo: SongInfo, config: LastFMOptions) => {
|
||||||
// This adds one scrobbled song to last.fm
|
// This adds one scrobbled song to last.fm
|
||||||
const data = {
|
const data = {
|
||||||
method: 'track.scrobble',
|
method: 'track.scrobble',
|
||||||
timestamp: Math.trunc((Date.now() - songInfo.elapsedSeconds) / 1000),
|
timestamp: Math.trunc((Date.now() - (songInfo.elapsedSeconds ?? 0)) / 1000),
|
||||||
};
|
};
|
||||||
postSongDataToAPI(songInfo, config, data);
|
postSongDataToAPI(songInfo, config, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setNowPlaying = (songInfo, config) => {
|
const setNowPlaying = (songInfo: SongInfo, config: LastFMOptions) => {
|
||||||
// This sets the now playing status in last.fm
|
// This sets the now playing status in last.fm
|
||||||
const data = {
|
const data = {
|
||||||
method: 'track.updateNowPlaying',
|
method: 'track.updateNowPlaying',
|
||||||
@ -136,9 +157,9 @@ const setNowPlaying = (songInfo, config) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// This will store the timeout that will trigger addScrobble
|
// This will store the timeout that will trigger addScrobble
|
||||||
let scrobbleTimer;
|
let scrobbleTimer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
const lastfm = async (_win, config) => {
|
const lastfm = async (_win: BrowserWindow, config: LastFMOptions) => {
|
||||||
if (!config.api_root) {
|
if (!config.api_root) {
|
||||||
// Settings are not present, creating them with the default values
|
// Settings are not present, creating them with the default values
|
||||||
config = defaultConfig.plugins['last-fm'];
|
config = defaultConfig.plugins['last-fm'];
|
||||||
@ -156,11 +177,11 @@ const lastfm = async (_win, config) => {
|
|||||||
clearTimeout(scrobbleTimer);
|
clearTimeout(scrobbleTimer);
|
||||||
if (!songInfo.isPaused) {
|
if (!songInfo.isPaused) {
|
||||||
setNowPlaying(songInfo, config);
|
setNowPlaying(songInfo, config);
|
||||||
// Scrobble when the song is half way through, or has passed the 4 minute mark
|
// Scrobble when the song is halfway through, or has passed the 4-minute mark
|
||||||
const scrobbleTime = Math.min(Math.ceil(songInfo.songDuration / 2), 4 * 60);
|
const scrobbleTime = Math.min(Math.ceil(songInfo.songDuration / 2), 4 * 60);
|
||||||
if (scrobbleTime > songInfo.elapsedSeconds) {
|
if (scrobbleTime > (songInfo.elapsedSeconds ?? 0)) {
|
||||||
// Scrobble still needs to happen
|
// Scrobble still needs to happen
|
||||||
const timeToWait = (scrobbleTime - songInfo.elapsedSeconds) * 1000;
|
const timeToWait = (scrobbleTime - (songInfo.elapsedSeconds ?? 0)) * 1000;
|
||||||
scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config);
|
scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,36 +1,43 @@
|
|||||||
const { join } = require('node:path');
|
import { join } from 'node:path';
|
||||||
|
|
||||||
const { ipcMain, net } = require('electron');
|
import { BrowserWindow, ipcMain, net } from 'electron';
|
||||||
const is = require('electron-is');
|
import is from 'electron-is';
|
||||||
const { convert } = require('html-to-text');
|
import { convert } from 'html-to-text';
|
||||||
|
|
||||||
const { cleanupName } = require('../../providers/song-info');
|
import { GetGeniusLyric } from './types';
|
||||||
const { injectCSS } = require('../utils');
|
|
||||||
|
import { cleanupName, SongInfo } from '../../providers/song-info';
|
||||||
|
|
||||||
|
import { injectCSS } from '../utils';
|
||||||
|
import config from '../../config';
|
||||||
|
|
||||||
const eastAsianChars = /\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
|
const eastAsianChars = /\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
|
||||||
let revRomanized = false;
|
let revRomanized = false;
|
||||||
|
|
||||||
module.exports = async (win, options) => {
|
const LyricGeniusTypeObj = config.get('plugins.lyric-genius');
|
||||||
|
export type LyricGeniusType = typeof LyricGeniusTypeObj;
|
||||||
|
|
||||||
|
export default (win: BrowserWindow, options: LyricGeniusType) => {
|
||||||
if (options.romanizedLyrics) {
|
if (options.romanizedLyrics) {
|
||||||
revRomanized = true;
|
revRomanized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
injectCSS(win.webContents, join(__dirname, 'style.css'));
|
injectCSS(win.webContents, join(__dirname, 'style.css'));
|
||||||
|
|
||||||
ipcMain.on('search-genius-lyrics', async (event, extractedSongInfo) => {
|
ipcMain.handle('search-genius-lyrics', async (_, extractedSongInfo: string) => {
|
||||||
const metadata = JSON.parse(extractedSongInfo);
|
const metadata = JSON.parse(extractedSongInfo) as SongInfo;
|
||||||
event.returnValue = await fetchFromGenius(metadata);
|
return await fetchFromGenius(metadata);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleRomanized = () => {
|
export const toggleRomanized = () => {
|
||||||
revRomanized = !revRomanized;
|
revRomanized = !revRomanized;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFromGenius = async (metadata) => {
|
export const fetchFromGenius = async (metadata: SongInfo) => {
|
||||||
const songTitle = `${cleanupName(metadata.title)}`;
|
const songTitle = `${cleanupName(metadata.title)}`;
|
||||||
const songArtist = `${cleanupName(metadata.artist)}`;
|
const songArtist = `${cleanupName(metadata.artist)}`;
|
||||||
let lyrics;
|
let lyrics: string | null;
|
||||||
|
|
||||||
/* Uses Regex to test the title and artist first for said characters if romanization is enabled. Otherwise normal
|
/* Uses Regex to test the title and artist first for said characters if romanization is enabled. Otherwise normal
|
||||||
Genius Lyrics behavior is observed.
|
Genius Lyrics behavior is observed.
|
||||||
@ -46,7 +53,7 @@ const fetchFromGenius = async (metadata) => {
|
|||||||
/* If the romanization toggle is on, and we did not detect any characters in the title or artist, we do a check
|
/* If the romanization toggle is on, and we did not detect any characters in the title or artist, we do a check
|
||||||
for characters in the lyrics themselves. If this check proves true, we search for Romanized lyrics.
|
for characters in the lyrics themselves. If this check proves true, we search for Romanized lyrics.
|
||||||
*/
|
*/
|
||||||
if (revRomanized && !hasAsianChars && eastAsianChars.test(lyrics)) {
|
if (revRomanized && !hasAsianChars && lyrics && eastAsianChars.test(lyrics)) {
|
||||||
lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`);
|
lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +65,7 @@ const fetchFromGenius = async (metadata) => {
|
|||||||
* @param {*} queryString
|
* @param {*} queryString
|
||||||
* @returns The lyrics of the first song found using the Genius-Lyrics API
|
* @returns The lyrics of the first song found using the Genius-Lyrics API
|
||||||
*/
|
*/
|
||||||
const getLyricsList = async (queryString) => {
|
const getLyricsList = async (queryString: string): Promise<string | null> => {
|
||||||
const response = await net.fetch(
|
const response = await net.fetch(
|
||||||
`https://genius.com/api/search/multi?per_page=5&q=${encodeURIComponent(queryString)}`,
|
`https://genius.com/api/search/multi?per_page=5&q=${encodeURIComponent(queryString)}`,
|
||||||
);
|
);
|
||||||
@ -69,17 +76,17 @@ const getLyricsList = async (queryString) => {
|
|||||||
/* Fetch the first URL with the api, giving a collection of song results.
|
/* Fetch the first URL with the api, giving a collection of song results.
|
||||||
Pick the first song, parsing the json given by the API.
|
Pick the first song, parsing the json given by the API.
|
||||||
*/
|
*/
|
||||||
const info = await response.json();
|
const info = await response.json() as GetGeniusLyric;
|
||||||
let url = '';
|
const url = info
|
||||||
try {
|
.response
|
||||||
url = info.response.sections.find((section) => section.type === 'song')
|
.sections
|
||||||
.hits[0].result.url;
|
.find((section) => section.type === 'song')?.hits[0].result.url;
|
||||||
} catch {
|
|
||||||
|
if (url) {
|
||||||
|
return await getLyrics(url);
|
||||||
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lyrics = await getLyrics(url);
|
|
||||||
return lyrics;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -87,7 +94,7 @@ const getLyricsList = async (queryString) => {
|
|||||||
* @param {*} url
|
* @param {*} url
|
||||||
* @returns The lyrics of the song URL provided, null if none
|
* @returns The lyrics of the song URL provided, null if none
|
||||||
*/
|
*/
|
||||||
const getLyrics = async (url) => {
|
const getLyrics = async (url: string): Promise<string | null> => {
|
||||||
const response = await net.fetch(url);
|
const response = await net.fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return null;
|
return null;
|
||||||
@ -116,6 +123,3 @@ const getLyrics = async (url) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.toggleRomanized = toggleRomanized;
|
|
||||||
module.exports.fetchFromGenius = fetchFromGenius;
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
import { ipcRenderer } from 'electron';
|
||||||
const is = require('electron-is');
|
import is from 'electron-is';
|
||||||
|
|
||||||
module.exports = () => {
|
export default () => {
|
||||||
ipcRenderer.on('update-song-info', (_, extractedSongInfo) => setTimeout(() => {
|
ipcRenderer.on('update-song-info', (_, extractedSongInfo: string) => setTimeout(async () => {
|
||||||
const tabList = document.querySelectorAll('tp-yt-paper-tab');
|
const tabList = document.querySelectorAll('tp-yt-paper-tab');
|
||||||
const tabs = {
|
const tabs = {
|
||||||
upNext: tabList[0],
|
upNext: tabList[0],
|
||||||
@ -17,10 +17,10 @@ module.exports = () => {
|
|||||||
|
|
||||||
let hasLyrics = true;
|
let hasLyrics = true;
|
||||||
|
|
||||||
const lyrics = ipcRenderer.sendSync(
|
const lyrics = await ipcRenderer.invoke(
|
||||||
'search-genius-lyrics',
|
'search-genius-lyrics',
|
||||||
extractedSongInfo,
|
extractedSongInfo,
|
||||||
);
|
) as string;
|
||||||
if (!lyrics) {
|
if (!lyrics) {
|
||||||
// Delete previous lyrics if tab is open and couldn't get new lyrics
|
// Delete previous lyrics if tab is open and couldn't get new lyrics
|
||||||
checkLyricsContainer(() => {
|
checkLyricsContainer(() => {
|
||||||
@ -40,8 +40,9 @@ module.exports = () => {
|
|||||||
|
|
||||||
checkLyricsContainer();
|
checkLyricsContainer();
|
||||||
|
|
||||||
tabs.lyrics.addEventListener('click', () => {
|
const lyricsTabHandler = () => {
|
||||||
const tabContainer = document.querySelector('ytmusic-tab-renderer');
|
const tabContainer = document.querySelector('ytmusic-tab-renderer');
|
||||||
|
if (tabContainer) {
|
||||||
const observer = new MutationObserver((_, observer) => {
|
const observer = new MutationObserver((_, observer) => {
|
||||||
checkLyricsContainer(() => observer.disconnect());
|
checkLyricsContainer(() => observer.disconnect());
|
||||||
});
|
});
|
||||||
@ -50,7 +51,10 @@ module.exports = () => {
|
|||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tabs.lyrics.addEventListener('click', lyricsTabHandler);
|
||||||
|
|
||||||
function checkLyricsContainer(callback = () => {
|
function checkLyricsContainer(callback = () => {
|
||||||
}) {
|
}) {
|
||||||
@ -63,7 +67,7 @@ module.exports = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLyrics(lyricsContainer) {
|
function setLyrics(lyricsContainer: Element) {
|
||||||
lyricsContainer.innerHTML = `<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics">
|
lyricsContainer.innerHTML = `<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics">
|
||||||
${
|
${
|
||||||
hasLyrics
|
hasLyrics
|
||||||
@ -74,15 +78,20 @@ module.exports = () => {
|
|||||||
</div>
|
</div>
|
||||||
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"></yt-formatted-string>`;
|
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"></yt-formatted-string>`;
|
||||||
if (hasLyrics) {
|
if (hasLyrics) {
|
||||||
lyricsContainer.querySelector('.footer').textContent = 'Source: Genius';
|
const footer = lyricsContainer.querySelector('.footer');
|
||||||
|
if (footer) {
|
||||||
|
footer.textContent = 'Source: Genius';
|
||||||
enableLyricsTab();
|
enableLyricsTab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setTabsOnclick(callback) {
|
const defaultHandler = () => {};
|
||||||
|
|
||||||
|
function setTabsOnclick(callback: EventListenerOrEventListenerObject | undefined) {
|
||||||
for (const tab of [tabs.upNext, tabs.discover]) {
|
for (const tab of [tabs.upNext, tabs.discover]) {
|
||||||
if (tab) {
|
if (tab) {
|
||||||
tab.addEventListener('click', callback);
|
tab.addEventListener('click', callback ?? defaultHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,16 +0,0 @@
|
|||||||
const { toggleRomanized } = require('./back');
|
|
||||||
|
|
||||||
const { setOptions } = require('../../config/plugins');
|
|
||||||
|
|
||||||
module.exports = (win, options) => [
|
|
||||||
{
|
|
||||||
label: 'Romanized Lyrics',
|
|
||||||
type: 'checkbox',
|
|
||||||
checked: options.romanizedLyrics,
|
|
||||||
click(item) {
|
|
||||||
options.romanizedLyrics = item.checked;
|
|
||||||
setOptions('lyrics-genius', options);
|
|
||||||
toggleRomanized();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
18
plugins/lyrics-genius/menu.ts
Normal file
18
plugins/lyrics-genius/menu.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { BrowserWindow, MenuItem } from 'electron';
|
||||||
|
|
||||||
|
import { LyricGeniusType, toggleRomanized } from './back';
|
||||||
|
|
||||||
|
import { setOptions } from '../../config/plugins';
|
||||||
|
|
||||||
|
module.exports = (win: BrowserWindow, options: LyricGeniusType) => [
|
||||||
|
{
|
||||||
|
label: 'Romanized Lyrics',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: options.romanizedLyrics,
|
||||||
|
click(item: MenuItem) {
|
||||||
|
options.romanizedLyrics = item.checked;
|
||||||
|
setOptions('lyrics-genius', options);
|
||||||
|
toggleRomanized();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
121
plugins/lyrics-genius/types.ts
Normal file
121
plugins/lyrics-genius/types.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
export interface GetGeniusLyric {
|
||||||
|
meta: Meta;
|
||||||
|
response: Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Meta {
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
sections: Section[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Section {
|
||||||
|
type: string;
|
||||||
|
hits: Hit[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Hit {
|
||||||
|
highlights: Highlight[];
|
||||||
|
index: Index;
|
||||||
|
type: Index;
|
||||||
|
result: Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Highlight {
|
||||||
|
property: string;
|
||||||
|
value: string;
|
||||||
|
snippet: boolean;
|
||||||
|
ranges: Range[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Range {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Index {
|
||||||
|
Album = 'album',
|
||||||
|
Lyric = 'lyric',
|
||||||
|
Song = 'song',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Result {
|
||||||
|
_type: Index;
|
||||||
|
annotation_count?: number;
|
||||||
|
api_path: string;
|
||||||
|
artist_names?: string;
|
||||||
|
full_title: string;
|
||||||
|
header_image_thumbnail_url?: string;
|
||||||
|
header_image_url?: string;
|
||||||
|
id: number;
|
||||||
|
instrumental?: boolean;
|
||||||
|
lyrics_owner_id?: number;
|
||||||
|
lyrics_state?: LyricsState;
|
||||||
|
lyrics_updated_at?: number;
|
||||||
|
path?: string;
|
||||||
|
pyongs_count?: number | null;
|
||||||
|
relationships_index_url?: string;
|
||||||
|
release_date_components: ReleaseDateComponents;
|
||||||
|
release_date_for_display: string;
|
||||||
|
release_date_with_abbreviated_month_for_display?: string;
|
||||||
|
song_art_image_thumbnail_url?: string;
|
||||||
|
song_art_image_url?: string;
|
||||||
|
stats?: Stats;
|
||||||
|
title?: string;
|
||||||
|
title_with_featured?: string;
|
||||||
|
updated_by_human_at?: number;
|
||||||
|
url: string;
|
||||||
|
featured_artists?: string[];
|
||||||
|
primary_artist?: Artist;
|
||||||
|
cover_art_thumbnail_url?: string;
|
||||||
|
cover_art_url?: string;
|
||||||
|
name?: string;
|
||||||
|
name_with_artist?: string;
|
||||||
|
artist?: Artist;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Artist {
|
||||||
|
_type: Type;
|
||||||
|
api_path: string;
|
||||||
|
header_image_url: string;
|
||||||
|
id: number;
|
||||||
|
image_url: string;
|
||||||
|
index_character: IndexCharacter;
|
||||||
|
is_meme_verified: boolean;
|
||||||
|
is_verified: boolean;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
url: string;
|
||||||
|
iq?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add more types
|
||||||
|
export enum Type {
|
||||||
|
Artist = 'artist',
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add more index characters
|
||||||
|
export enum IndexCharacter {
|
||||||
|
G = 'g',
|
||||||
|
Y = 'y',
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add more states
|
||||||
|
export enum LyricsState {
|
||||||
|
Complete = 'complete',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReleaseDateComponents {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Stats {
|
||||||
|
unreviewed_annotations: number;
|
||||||
|
concurrents?: number;
|
||||||
|
hot: boolean;
|
||||||
|
pageviews?: number;
|
||||||
|
}
|
||||||
@ -61,7 +61,7 @@ module.exports = (win: BrowserWindow) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data.duration = secToMilisec(songInfo.songDuration);
|
data.duration = secToMilisec(songInfo.songDuration);
|
||||||
data.progress = secToMilisec(songInfo.elapsedSeconds);
|
data.progress = secToMilisec(songInfo.elapsedSeconds ?? 0);
|
||||||
data.cover = songInfo.imageSrc ?? '';
|
data.cover = songInfo.imageSrc ?? '';
|
||||||
data.cover_url = songInfo.imageSrc ?? '';
|
data.cover_url = songInfo.imageSrc ?? '';
|
||||||
data.album_url = songInfo.imageSrc ?? '';
|
data.album_url = songInfo.imageSrc ?? '';
|
||||||
|
|||||||
@ -7,9 +7,10 @@ import { YoutubePlayer } from '../types/youtube-player';
|
|||||||
import { GetState } from '../types/datahost-get-state';
|
import { GetState } from '../types/datahost-get-state';
|
||||||
|
|
||||||
let songInfo: SongInfo = {} as SongInfo;
|
let songInfo: SongInfo = {} as SongInfo;
|
||||||
|
export const getSongInfo = () => songInfo;
|
||||||
|
|
||||||
const $ = <E extends HTMLElement>(s: string): E => document.querySelector(s) as E;
|
const $ = <E extends HTMLElement>(s: string): E => document.querySelector(s) as E;
|
||||||
const $$ = <E extends HTMLElement>(s: string): E[] => [...document.querySelectorAll(s)!] as E[];
|
const $$ = <E extends HTMLElement>(s: string): E[] => Array.from(document.querySelectorAll(s));
|
||||||
|
|
||||||
ipcRenderer.on('update-song-info', async (_, extractedSongInfo: string) => {
|
ipcRenderer.on('update-song-info', async (_, extractedSongInfo: string) => {
|
||||||
songInfo = JSON.parse(extractedSongInfo) as SongInfo;
|
songInfo = JSON.parse(extractedSongInfo) as SongInfo;
|
||||||
|
|||||||
@ -9,16 +9,16 @@ export interface SongInfo {
|
|||||||
title: string;
|
title: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
views: number;
|
views: number;
|
||||||
uploadDate: string;
|
uploadDate?: string;
|
||||||
imageSrc?: string | null;
|
imageSrc?: string | null;
|
||||||
image?: Electron.NativeImage | null;
|
image?: Electron.NativeImage | null;
|
||||||
isPaused?: boolean;
|
isPaused?: boolean;
|
||||||
songDuration: number;
|
songDuration: number;
|
||||||
elapsedSeconds: number;
|
elapsedSeconds?: number;
|
||||||
url: string;
|
url?: string;
|
||||||
album?: string | null;
|
album?: string | null;
|
||||||
videoId: string;
|
videoId: string;
|
||||||
playlistId: string;
|
playlistId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill songInfo with empty values
|
// Fill songInfo with empty values
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"lib": ["dom"],
|
"lib": ["dom", "es2022"],
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user