mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-15 12:21:47 +00:00
feat: migration to TypeScript part 2
Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
@ -1,15 +0,0 @@
|
||||
const { ipcMain } = require('electron');
|
||||
const { Innertube } = require('youtubei.js');
|
||||
|
||||
require('./config');
|
||||
|
||||
module.exports = async () => {
|
||||
const yt = await Innertube.create();
|
||||
|
||||
ipcMain.handle('audio-url', async (_, videoID) => {
|
||||
const info = await yt.getBasicInfo(videoID);
|
||||
const url = info.streaming_data?.formats[0].decipher(yt.session.player);
|
||||
|
||||
return url;
|
||||
});
|
||||
};
|
||||
15
plugins/crossfade/back.ts
Normal file
15
plugins/crossfade/back.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { Innertube } from 'youtubei.js';
|
||||
|
||||
import config from './config';
|
||||
|
||||
export default async () => {
|
||||
const yt = await Innertube.create();
|
||||
|
||||
ipcMain.handle('audio-url', async (_, videoID: string) => {
|
||||
const info = await yt.getBasicInfo(videoID);
|
||||
const url = info.streaming_data?.formats[0].decipher(yt.session.player);
|
||||
|
||||
return url;
|
||||
});
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
const { PluginConfig } = require('../../config/dynamic');
|
||||
|
||||
const config = new PluginConfig('crossfade', { enableFront: true });
|
||||
module.exports = { ...config };
|
||||
4
plugins/crossfade/config.ts
Normal file
4
plugins/crossfade/config.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { PluginConfig } from '../../config/dynamic';
|
||||
|
||||
const config = new PluginConfig('crossfade', { enableFront: true });
|
||||
export default { ...config } as PluginConfig<'crossfade'>;
|
||||
@ -18,7 +18,7 @@
|
||||
'use strict';
|
||||
|
||||
// Internal utility: check if value is a valid volume level and throw if not
|
||||
const validateVolumeLevel = (value) => {
|
||||
const validateVolumeLevel = (value: number) => {
|
||||
// Number between 0 and 1?
|
||||
if (!Number.isNaN(value) && value >= 0 && value <= 1) {
|
||||
// Yup, that's fine
|
||||
@ -29,8 +29,51 @@ const validateVolumeLevel = (value) => {
|
||||
}
|
||||
};
|
||||
|
||||
type VolumeLogger = <Params extends unknown[]>(message: string, ...args: Params) => void;
|
||||
interface VolumeFaderOptions {
|
||||
/**
|
||||
* logging `function(stuff, …)` for execution information (default: no logging)
|
||||
*/
|
||||
logger?: VolumeLogger;
|
||||
/**
|
||||
* either 'linear', 'logarithmic' or a positive number in dB (default: logarithmic)
|
||||
*/
|
||||
fadeScaling?: string | number;
|
||||
/**
|
||||
* media volume 0…1 to apply during setup (volume not touched by default)
|
||||
*/
|
||||
initialVolume?: number;
|
||||
/**
|
||||
* time in milliseconds to complete a fade (default: 1000 ms)
|
||||
*/
|
||||
fadeDuration?: number;
|
||||
}
|
||||
|
||||
interface VolumeFade {
|
||||
volume: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
time: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
callback?: () => void;
|
||||
}
|
||||
|
||||
// Main class
|
||||
class VolumeFader {
|
||||
export class VolumeFader {
|
||||
private media: HTMLMediaElement;
|
||||
private logger: VolumeLogger | false;
|
||||
private scale: {
|
||||
internalToVolume: (level: number) => number;
|
||||
volumeToInternal: (level: number) => number;
|
||||
};
|
||||
private fadeDuration: number = 1000;
|
||||
private active: boolean = false;
|
||||
private fade: VolumeFade | undefined;
|
||||
|
||||
|
||||
/**
|
||||
* VolumeFader Constructor
|
||||
*
|
||||
@ -38,13 +81,8 @@ class VolumeFader {
|
||||
* @param options {Object} - an object with optional settings
|
||||
* @throws {TypeError} if options.initialVolume or options.fadeDuration are invalid
|
||||
*
|
||||
* options:
|
||||
* .logger: {Function} logging `function(stuff, …)` for execution information (default: no logging)
|
||||
* .fadeScaling: {Mixed} either 'linear', 'logarithmic' or a positive number in dB (default: logarithmic)
|
||||
* .initialVolume: {Number} media volume 0…1 to apply during setup (volume not touched by default)
|
||||
* .fadeDuration: {Number} time in milliseconds to complete a fade (default: 1000 ms)
|
||||
*/
|
||||
constructor(media, options) {
|
||||
constructor(media: HTMLMediaElement, options: VolumeFaderOptions) {
|
||||
// Passed media element of correct type?
|
||||
if (media instanceof HTMLMediaElement) {
|
||||
// Save reference to media element
|
||||
@ -70,8 +108,8 @@ class VolumeFader {
|
||||
if (options.fadeScaling === 'linear') {
|
||||
// Pass levels unchanged
|
||||
this.scale = {
|
||||
internalToVolume: (level) => level,
|
||||
volumeToInternal: (level) => level,
|
||||
internalToVolume: (level: number) => level,
|
||||
volumeToInternal: (level: number) => level,
|
||||
};
|
||||
|
||||
// Log setting
|
||||
@ -79,7 +117,7 @@ class VolumeFader {
|
||||
}
|
||||
// No linear, but logarithmic fading…
|
||||
else {
|
||||
let dynamicRange;
|
||||
let dynamicRange: number;
|
||||
|
||||
// Default dynamic range?
|
||||
if (
|
||||
@ -91,7 +129,8 @@ class VolumeFader {
|
||||
}
|
||||
// Custom dynamic range?
|
||||
else if (
|
||||
!Number.isNaN(options.fadeScaling)
|
||||
typeof options.fadeScaling === 'number'
|
||||
&& !Number.isNaN(options.fadeScaling)
|
||||
&& options.fadeScaling > 0
|
||||
) {
|
||||
// Turn amplitude dB into a multiple of 10 power dB
|
||||
@ -107,9 +146,9 @@ class VolumeFader {
|
||||
|
||||
// Use exponential/logarithmic scaler for expansion/compression
|
||||
this.scale = {
|
||||
internalToVolume: (level) =>
|
||||
internalToVolume: (level: number) =>
|
||||
this.exponentialScaler(level, dynamicRange),
|
||||
volumeToInternal: (level) =>
|
||||
volumeToInternal: (level: number) =>
|
||||
this.logarithmicScaler(level, dynamicRange),
|
||||
};
|
||||
|
||||
@ -193,7 +232,7 @@ class VolumeFader {
|
||||
* @throws {TypeError} if fadeDuration is not a number greater than zero
|
||||
* @return {Object} VolumeFader instance for chaining
|
||||
*/
|
||||
setFadeDuration(fadeDuration) {
|
||||
setFadeDuration(fadeDuration: number) {
|
||||
// If duration is a valid number > 0…
|
||||
if (!Number.isNaN(fadeDuration) && fadeDuration > 0) {
|
||||
// Set fade duration
|
||||
@ -219,7 +258,7 @@ class VolumeFader {
|
||||
* @throws {TypeError} if targetVolume is not in the range 0…1
|
||||
* @return {Object} VolumeFader instance for chaining
|
||||
*/
|
||||
fadeTo(targetVolume, callback) {
|
||||
fadeTo(targetVolume: number, callback?: () => void) {
|
||||
// Validate volume and throw if invalid
|
||||
validateVolumeLevel(targetVolume);
|
||||
|
||||
@ -250,11 +289,11 @@ class VolumeFader {
|
||||
}
|
||||
|
||||
// Convenience shorthand methods for common fades
|
||||
fadeIn(callback) {
|
||||
fadeIn(callback: () => void) {
|
||||
this.fadeTo(1, callback);
|
||||
}
|
||||
|
||||
fadeOut(callback) {
|
||||
fadeOut(callback: () => void) {
|
||||
this.fadeTo(0, callback);
|
||||
}
|
||||
|
||||
@ -313,7 +352,7 @@ class VolumeFader {
|
||||
* @param {Number} dynamicRange - expanded output range, in multiples of 10 dB (float, 0…∞)
|
||||
* @return {Number} - expanded level (float, 0…1)
|
||||
*/
|
||||
exponentialScaler(input, dynamicRange) {
|
||||
exponentialScaler(input: number, dynamicRange: number) {
|
||||
// Special case: make zero (or any falsy input) return zero
|
||||
if (input === 0) {
|
||||
// Since the dynamic range is limited,
|
||||
@ -336,7 +375,7 @@ class VolumeFader {
|
||||
* @param {Number} dynamicRange - coerced input range, in multiples of 10 dB (float, 0…∞)
|
||||
* @return {Number} - compressed level (float, 0…1)
|
||||
*/
|
||||
logarithmicScaler(input, dynamicRange) {
|
||||
logarithmicScaler(input: number, dynamicRange: number) {
|
||||
// Special case: make zero (or any falsy input) return zero
|
||||
if (input === 0) {
|
||||
// Logarithm of zero would be -∞, which would map to zero anyway
|
||||
@ -351,6 +390,6 @@ class VolumeFader {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
VolumeFader
|
||||
};
|
||||
@ -1,38 +1,39 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
const { Howl } = require('howler');
|
||||
/* eslint-disable @typescript-eslint/await-thenable */
|
||||
/* renderer */
|
||||
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Howl } from 'howler';
|
||||
|
||||
// Extracted from https://github.com/bitfasching/VolumeFader
|
||||
const { VolumeFader } = require('./fader');
|
||||
import { VolumeFader } from './fader';
|
||||
|
||||
let transitionAudio; // Howler audio used to fade out the current music
|
||||
import configProvider from './config';
|
||||
|
||||
import defaultConfigs from '../../config/defaults';
|
||||
import { ConfigType } from '../../config/dynamic';
|
||||
|
||||
let transitionAudio: Howl; // Howler audio used to fade out the current music
|
||||
let firstVideo = true;
|
||||
let waitForTransition;
|
||||
let waitForTransition: Promise<unknown>;
|
||||
|
||||
/**
|
||||
* @type {PluginConfig}
|
||||
*/
|
||||
const configProvider = require('./config');
|
||||
const defaultConfig = defaultConfigs.plugins.crossfade;
|
||||
|
||||
const defaultConfig = require('../../config/defaults').plugins.crossfade;
|
||||
let config: ConfigType<'crossfade'>;
|
||||
|
||||
let config;
|
||||
const configGetNumber = (key: keyof ConfigType<'crossfade'>): number => Number(config[key]) || (defaultConfig[key] as number);
|
||||
|
||||
const configGetNumber = (key) => Number(config[key]) || defaultConfig[key];
|
||||
const getStreamURL = async (videoID: string) => ipcRenderer.invoke('audio-url', videoID) as Promise<string>;
|
||||
|
||||
const getStreamURL = async (videoID) => {
|
||||
return await ipcRenderer.invoke('audio-url', videoID);
|
||||
};
|
||||
|
||||
const getVideoIDFromURL = (url) => new URLSearchParams(url.split('?')?.at(-1)).get('v');
|
||||
const getVideoIDFromURL = (url: string) => new URLSearchParams(url.split('?')?.at(-1)).get('v');
|
||||
|
||||
const isReadyToCrossfade = () => transitionAudio && transitionAudio.state() === 'loaded';
|
||||
|
||||
const watchVideoIDChanges = (cb) => {
|
||||
const watchVideoIDChanges = (cb: (id: string) => void) => {
|
||||
window.navigation.addEventListener('navigate', (event) => {
|
||||
const currentVideoID = getVideoIDFromURL(
|
||||
event.currentTarget.currentEntry.url,
|
||||
(event.currentTarget as Navigation).currentEntry?.url ?? '',
|
||||
);
|
||||
const nextVideoID = getVideoIDFromURL(event.destination.url);
|
||||
const nextVideoID = getVideoIDFromURL(event.destination.url ?? '');
|
||||
|
||||
if (
|
||||
nextVideoID
|
||||
@ -51,7 +52,7 @@ const watchVideoIDChanges = (cb) => {
|
||||
});
|
||||
};
|
||||
|
||||
const createAudioForCrossfade = async (url) => {
|
||||
const createAudioForCrossfade = (url: string) => {
|
||||
if (transitionAudio) {
|
||||
transitionAudio.unload();
|
||||
}
|
||||
@ -61,19 +62,19 @@ const createAudioForCrossfade = async (url) => {
|
||||
html5: true,
|
||||
volume: 0,
|
||||
});
|
||||
await syncVideoWithTransitionAudio();
|
||||
syncVideoWithTransitionAudio();
|
||||
};
|
||||
|
||||
const syncVideoWithTransitionAudio = async () => {
|
||||
const video = document.querySelector('video');
|
||||
const syncVideoWithTransitionAudio = () => {
|
||||
const video = document.querySelector('video')!;
|
||||
|
||||
const videoFader = new VolumeFader(video, {
|
||||
fadeScaling: configGetNumber('fadeScaling'),
|
||||
fadeDuration: configGetNumber('fadeInDuration'),
|
||||
});
|
||||
|
||||
await transitionAudio.play();
|
||||
await transitionAudio.seek(video.currentTime);
|
||||
transitionAudio.play();
|
||||
transitionAudio.seek(video.currentTime);
|
||||
|
||||
video.addEventListener('seeking', () => {
|
||||
transitionAudio.seek(video.currentTime);
|
||||
@ -83,9 +84,9 @@ const syncVideoWithTransitionAudio = async () => {
|
||||
transitionAudio.pause();
|
||||
});
|
||||
|
||||
video.addEventListener('play', async () => {
|
||||
await transitionAudio.play();
|
||||
await transitionAudio.seek(video.currentTime);
|
||||
video.addEventListener('play', () => {
|
||||
transitionAudio.play();
|
||||
transitionAudio.seek(video.currentTime);
|
||||
|
||||
// Fade in
|
||||
const videoVolume = video.volume;
|
||||
@ -102,7 +103,7 @@ const syncVideoWithTransitionAudio = async () => {
|
||||
video.removeEventListener('timeupdate', transitionBeforeEnd);
|
||||
|
||||
// Go to next video - XXX: does not support "repeat 1" mode
|
||||
document.querySelector('.next-button').click();
|
||||
(document.querySelector('.next-button') as HTMLButtonElement).click();
|
||||
}
|
||||
};
|
||||
|
||||
@ -121,18 +122,18 @@ const onApiLoaded = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const crossfade = async (cb) => {
|
||||
const crossfade = (cb: () => void) => {
|
||||
if (!isReadyToCrossfade()) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
let resolveTransition;
|
||||
waitForTransition = new Promise((resolve) => {
|
||||
let resolveTransition: () => void;
|
||||
waitForTransition = new Promise<void>((resolve) => {
|
||||
resolveTransition = resolve;
|
||||
});
|
||||
|
||||
const video = document.querySelector('video');
|
||||
const video = document.querySelector('video')!;
|
||||
|
||||
const fader = new VolumeFader(transitionAudio._sounds[0]._node, {
|
||||
initialVolume: video.volume,
|
||||
@ -148,7 +149,7 @@ const crossfade = async (cb) => {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = async () => {
|
||||
export default async () => {
|
||||
config = await configProvider.getAll();
|
||||
|
||||
configProvider.subscribeAll((newConfig) => {
|
||||
@ -1,11 +1,16 @@
|
||||
const prompt = require('custom-electron-prompt');
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
const config = require('./config');
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
const promptOptions = require('../../providers/prompt-options');
|
||||
const defaultOptions = require('../../config/defaults').plugins.crossfade;
|
||||
import config from './config';
|
||||
|
||||
module.exports = (win) => [
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
import configOptions from '../../config/defaults';
|
||||
import { ConfigType } from '../../config/dynamic';
|
||||
|
||||
const defaultOptions = configOptions.plugins.crossfade;
|
||||
|
||||
export default (win: BrowserWindow) => [
|
||||
{
|
||||
label: 'Advanced',
|
||||
async click() {
|
||||
@ -17,7 +22,7 @@ module.exports = (win) => [
|
||||
},
|
||||
];
|
||||
|
||||
async function promptCrossfadeValues(win, options) {
|
||||
async function promptCrossfadeValues(win: BrowserWindow, options: ConfigType<'crossfade'>): Promise<Partial<ConfigType<'crossfade'>> | undefined> {
|
||||
const res = await prompt(
|
||||
{
|
||||
title: 'Crossfade Options',
|
||||
@ -29,8 +34,8 @@ async function promptCrossfadeValues(win, options) {
|
||||
inputAttrs: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
min: 0,
|
||||
step: 100,
|
||||
min: '0',
|
||||
step: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -39,8 +44,8 @@ async function promptCrossfadeValues(win, options) {
|
||||
inputAttrs: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
min: 0,
|
||||
step: 100,
|
||||
min: '0',
|
||||
step: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -50,7 +55,7 @@ async function promptCrossfadeValues(win, options) {
|
||||
inputAttrs: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
min: 0,
|
||||
min: '0',
|
||||
},
|
||||
},
|
||||
{
|
||||
Reference in New Issue
Block a user