restructure files onto electron-vite boilerplate

This commit is contained in:
jeffvli 2025-05-18 14:03:18 -07:00
parent 91ce2cd8a1
commit 1cf587bc8f
457 changed files with 9927 additions and 11705 deletions

View file

@ -1,12 +1,13 @@
import axios, { AxiosResponse } from 'axios';
import { load } from 'cheerio';
import { orderSearchResults } from './shared';
import {
LyricSource,
InternetProviderLyricResponse,
InternetProviderLyricSearchResponse,
LyricSearchQuery,
} from '../../../../renderer/api/types';
LyricSource,
} from '.';
import { orderSearchResults } from './shared';
const SEARCH_URL = 'https://genius.com/api/search/song';
@ -17,20 +18,6 @@ export interface GeniusResponse {
response: Response;
}
export interface Meta {
status: number;
}
export interface Response {
next_page: number;
sections: Section[];
}
export interface Section {
hits: Hit[];
type: string;
}
export interface Hit {
highlights: any[];
index: string;
@ -38,6 +25,35 @@ export interface Hit {
type: string;
}
export interface Meta {
status: number;
}
export interface PrimaryArtist {
_type: string;
api_path: string;
header_image_url: string;
id: number;
image_url: string;
index_character: string;
is_meme_verified: boolean;
is_verified: boolean;
name: string;
slug: string;
url: string;
}
export interface ReleaseDateComponents {
day: number;
month: number;
year: number;
}
export interface Response {
next_page: number;
sections: Section[];
}
export interface Result {
_type: string;
annotation_count: number;
@ -69,24 +85,9 @@ export interface Result {
url: string;
}
export interface PrimaryArtist {
_type: string;
api_path: string;
header_image_url: string;
id: number;
image_url: string;
index_character: string;
is_meme_verified: boolean;
is_verified: boolean;
name: string;
slug: string;
url: string;
}
export interface ReleaseDateComponents {
day: number;
month: number;
year: number;
export interface Section {
hits: Hit[];
type: string;
}
export interface Stats {
@ -94,6 +95,27 @@ export interface Stats {
unreviewed_annotations: number;
}
export async function getLyricsBySongId(url: string): Promise<null | string> {
let result: AxiosResponse<string, any>;
try {
result = await axios.get<string>(url, { responseType: 'text' });
} catch (e) {
console.error('Genius lyrics request got an error!', e);
return null;
}
const $ = load(result.data.split('<br/>').join('\n'));
const lyricsDiv = $('div.lyrics');
if (lyricsDiv.length > 0) return lyricsDiv.text().trim();
const lyricSections = $('div[class^=Lyrics__Container]')
.map((_, e) => $(e).text())
.toArray()
.join('\n');
return lyricSections;
}
export async function getSearchResults(
params: LyricSearchQuery,
): Promise<InternetProviderLyricSearchResponse[] | null> {
@ -133,9 +155,33 @@ export async function getSearchResults(
return orderSearchResults({ params, results: songResults });
}
export async function query(
params: LyricSearchQuery,
): Promise<InternetProviderLyricResponse | null> {
const response = await getSongId(params);
if (!response) {
console.error('Could not find the song on Genius!');
return null;
}
const lyrics = await getLyricsBySongId(response.id);
if (!lyrics) {
console.error('Could not get lyrics on Genius!');
return null;
}
return {
artist: response.artist,
id: response.id,
lyrics,
name: response.name,
source: LyricSource.GENIUS,
};
}
async function getSongId(
params: LyricSearchQuery,
): Promise<Omit<InternetProviderLyricResponse, 'lyrics'> | null> {
): Promise<null | Omit<InternetProviderLyricResponse, 'lyrics'>> {
let result: AxiosResponse<GeniusResponse>;
try {
result = await axios.get(SEARCH_URL, {
@ -162,48 +208,3 @@ async function getSongId(
source: LyricSource.GENIUS,
};
}
export async function getLyricsBySongId(url: string): Promise<string | null> {
let result: AxiosResponse<string, any>;
try {
result = await axios.get<string>(url, { responseType: 'text' });
} catch (e) {
console.error('Genius lyrics request got an error!', e);
return null;
}
const $ = load(result.data.split('<br/>').join('\n'));
const lyricsDiv = $('div.lyrics');
if (lyricsDiv.length > 0) return lyricsDiv.text().trim();
const lyricSections = $('div[class^=Lyrics__Container]')
.map((_, e) => $(e).text())
.toArray()
.join('\n');
return lyricSections;
}
export async function query(
params: LyricSearchQuery,
): Promise<InternetProviderLyricResponse | null> {
const response = await getSongId(params);
if (!response) {
console.error('Could not find the song on Genius!');
return null;
}
const lyrics = await getLyricsBySongId(response.id);
if (!lyrics) {
console.error('Could not get lyrics on Genius!');
return null;
}
return {
artist: response.artist,
id: response.id,
lyrics,
name: response.name,
source: LyricSource.GENIUS,
};
}

View file

@ -1,36 +1,76 @@
import { ipcMain } from 'electron';
import { store } from '../settings/index';
import {
getLyricsBySongId as getGenius,
query as queryGenius,
getSearchResults as searchGenius,
getLyricsBySongId as getGenius,
} from './genius';
import {
getLyricsBySongId as getLrcLib,
query as queryLrclib,
getSearchResults as searchLrcLib,
getLyricsBySongId as getLrcLib,
} from './lrclib';
import {
getLyricsBySongId as getNetease,
query as queryNetease,
getSearchResults as searchNetease,
getLyricsBySongId as getNetease,
} from './netease';
import {
InternetProviderLyricResponse,
InternetProviderLyricSearchResponse,
LyricSearchQuery,
QueueSong,
LyricGetQuery,
LyricSource,
} from '../../../../renderer/api/types';
import { store } from '../settings/index';
type SongFetcher = (params: LyricSearchQuery) => Promise<InternetProviderLyricResponse | null>;
export enum LyricSource {
GENIUS = 'Genius',
LRCLIB = 'lrclib.net',
NETEASE = 'NetEase',
}
export type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'lyrics' | 'source'> & {
lyrics: LyricsResponse;
remote: boolean;
source: string;
};
export type InternetProviderLyricResponse = {
artist: string;
id: string;
lyrics: string;
name: string;
source: LyricSource;
};
export type InternetProviderLyricSearchResponse = {
artist: string;
id: string;
name: string;
score?: number;
source: LyricSource;
};
export type LyricGetQuery = {
remoteSongId: string;
remoteSource: LyricSource;
song: Song;
};
export type LyricOverride = Omit<InternetProviderLyricResponse, 'lyrics'>;
export type LyricSearchQuery = {
album?: string;
artist?: string;
duration?: number;
name?: string;
};
export type LyricsResponse = string | SynchronizedLyricsArray;
export type SynchronizedLyricsArray = Array<[number, string]>;
type CachedLyrics = Record<LyricSource, InternetProviderLyricResponse>;
type GetFetcher = (id: string) => Promise<null | string>;
type SearchFetcher = (
params: LyricSearchQuery,
) => Promise<InternetProviderLyricSearchResponse[] | null>;
type GetFetcher = (id: string) => Promise<string | null>;
type CachedLyrics = Record<LyricSource, InternetProviderLyricResponse>;
type SongFetcher = (params: LyricSearchQuery) => Promise<InternetProviderLyricResponse | null>;
const FETCHERS: Record<LyricSource, SongFetcher> = {
[LyricSource.GENIUS]: queryGenius,
@ -54,7 +94,7 @@ const MAX_CACHED_ITEMS = 10;
const lyricCache = new Map<string, CachedLyrics>();
const getRemoteLyrics = async (song: QueueSong) => {
const getRemoteLyrics = async (song: any) => {
const sources = store.get('lyrics', []) as LyricSource[];
const cached = lyricCache.get(song.id);
@ -122,7 +162,7 @@ const searchRemoteLyrics = async (params: LyricSearchQuery) => {
return results;
};
const getRemoteLyricsById = async (params: LyricGetQuery): Promise<string | null> => {
const getRemoteLyricsById = async (params: LyricGetQuery): Promise<null | string> => {
const { remoteSongId, remoteSource } = params;
const response = await GET_FETCHERS[remoteSource](remoteSongId);
@ -133,7 +173,7 @@ const getRemoteLyricsById = async (params: LyricGetQuery): Promise<string | null
return response;
};
ipcMain.handle('lyric-by-song', async (_event, song: QueueSong) => {
ipcMain.handle('lyric-by-song', async (_event, song: any) => {
const lyric = await getRemoteLyrics(song);
return lyric;
});

View file

@ -1,12 +1,13 @@
// Credits to https://github.com/tranxuanthang/lrcget for API implementation
import axios, { AxiosResponse } from 'axios';
import { orderSearchResults } from './shared';
import {
InternetProviderLyricResponse,
InternetProviderLyricSearchResponse,
LyricSearchQuery,
LyricSource,
} from '../../../../renderer/api/types';
} from '.';
import { orderSearchResults } from './shared';
const FETCH_URL = 'https://lrclib.net/api/get';
const SEEARCH_URL = 'https://lrclib.net/api/search';
@ -29,10 +30,23 @@ export interface LrcLibTrackResponse {
isrc: string;
lang: string;
name: string;
plainLyrics: string | null;
plainLyrics: null | string;
releaseDate: string;
spotifyId: string;
syncedLyrics: string | null;
syncedLyrics: null | string;
}
export async function getLyricsBySongId(songId: string): Promise<null | string> {
let result: AxiosResponse<LrcLibTrackResponse, any>;
try {
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
} catch (e) {
console.error('LrcLib lyrics request got an error!', e);
return null;
}
return result.data.syncedLyrics || result.data.plainLyrics || null;
}
export async function getSearchResults(
@ -69,19 +83,6 @@ export async function getSearchResults(
return orderSearchResults({ params, results: songResults });
}
export async function getLyricsBySongId(songId: string): Promise<string | null> {
let result: AxiosResponse<LrcLibTrackResponse, any>;
try {
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
} catch (e) {
console.error('LrcLib lyrics request got an error!', e);
return null;
}
return result.data.syncedLyrics || result.data.plainLyrics || null;
}
export async function query(
params: LyricSearchQuery,
): Promise<InternetProviderLyricResponse | null> {

View file

@ -1,46 +1,18 @@
import axios, { AxiosResponse } from 'axios';
import { LyricSource } from '../../../../renderer/api/types';
import { orderSearchResults } from './shared';
import type {
import {
InternetProviderLyricResponse,
InternetProviderLyricSearchResponse,
LyricSearchQuery,
} from '/@/renderer/api/types';
LyricSource,
} from '.';
import { orderSearchResults } from './shared';
const SEARCH_URL = 'https://music.163.com/api/search/get';
const LYRICS_URL = 'https://music.163.com/api/song/lyric';
// Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/netease.ts
export interface NetEaseResponse {
code: number;
result: Result;
}
export interface Result {
hasMore: boolean;
songCount: number;
songs: Song[];
}
export interface Song {
album: Album;
alias: string[];
artists: Artist[];
copyrightId: number;
duration: number;
fee: number;
ftype: number;
id: number;
mark: number;
mvid: number;
name: string;
rUrl: null;
rtype: number;
status: number;
transNames?: string[];
}
export interface Album {
artist: Artist;
copyrightId: number;
@ -67,6 +39,53 @@ export interface Artist {
trans: null;
}
export interface NetEaseResponse {
code: number;
result: Result;
}
export interface Result {
hasMore: boolean;
songCount: number;
songs: Song[];
}
export interface Song {
album: Album;
alias: string[];
artists: Artist[];
copyrightId: number;
duration: number;
fee: number;
ftype: number;
id: number;
mark: number;
mvid: number;
name: string;
rtype: number;
rUrl: null;
status: number;
transNames?: string[];
}
export async function getLyricsBySongId(songId: string): Promise<null | string> {
let result: AxiosResponse<any, any>;
try {
result = await axios.get(LYRICS_URL, {
params: {
id: songId,
kv: '-1',
lv: '-1',
},
});
} catch (e) {
console.error('NetEase lyrics request got an error!', e);
return null;
}
return result.data.klyric?.lyric || result.data.lrc?.lyric;
}
export async function getSearchResults(
params: LyricSearchQuery,
): Promise<InternetProviderLyricSearchResponse[] | null> {
@ -110,38 +129,6 @@ export async function getSearchResults(
return orderSearchResults({ params, results: songResults });
}
async function getMatchedLyrics(
params: LyricSearchQuery,
): Promise<Omit<InternetProviderLyricResponse, 'lyrics'> | null> {
const results = await getSearchResults(params);
const firstMatch = results?.[0];
if (!firstMatch || (firstMatch?.score && firstMatch.score > 0.5)) {
return null;
}
return firstMatch;
}
export async function getLyricsBySongId(songId: string): Promise<string | null> {
let result: AxiosResponse<any, any>;
try {
result = await axios.get(LYRICS_URL, {
params: {
id: songId,
kv: '-1',
lv: '-1',
},
});
} catch (e) {
console.error('NetEase lyrics request got an error!', e);
return null;
}
return result.data.klyric?.lyric || result.data.lrc?.lyric;
}
export async function query(
params: LyricSearchQuery,
): Promise<InternetProviderLyricResponse | null> {
@ -165,3 +152,17 @@ export async function query(
source: LyricSource.NETEASE,
};
}
async function getMatchedLyrics(
params: LyricSearchQuery,
): Promise<null | Omit<InternetProviderLyricResponse, 'lyrics'>> {
const results = await getSearchResults(params);
const firstMatch = results?.[0];
if (!firstMatch || (firstMatch?.score && firstMatch.score > 0.5)) {
return null;
}
return firstMatch;
}

View file

@ -1,4 +1,5 @@
import Fuse from 'fuse.js';
import {
InternetProviderLyricSearchResponse,
LyricSearchQuery,

View file

@ -1,10 +1,11 @@
import console from 'console';
import { rm } from 'fs/promises';
import { pid } from 'node:process';
import { app, ipcMain } from 'electron';
import { rm } from 'fs/promises';
import uniq from 'lodash/uniq';
import MpvAPI from 'node-mpv';
import { getMainWindow, sendToastToRenderer } from '../../../main';
import { pid } from 'node:process';
import { getMainWindow, sendToastToRenderer } from '../../../index';
import { createLog, isWindows } from '../../../utils';
import { store } from '../settings';
@ -86,7 +87,7 @@ const createMpv = async (data: {
extraParameters?: string[];
properties?: Record<string, any>;
}): Promise<MpvAPI> => {
const { extraParameters, properties, binaryPath } = data;
const { binaryPath, extraParameters, properties } = data;
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
@ -174,7 +175,7 @@ ipcMain.on('player-set-properties', async (_event, data: Record<string, any>) =>
} else {
getMpvInstance()?.setMultipleProperties(data);
}
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to set properties: ${JSON.stringify(data)}` }, err);
}
});
@ -199,7 +200,7 @@ ipcMain.handle(
mpvInstance = await createMpv(data);
mpvLog({ action: 'Restarted mpv', toast: 'success' });
setAudioPlayerFallback(false);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to restart mpv, falling back to web player' }, err);
setAudioPlayerFallback(true);
}
@ -215,7 +216,7 @@ ipcMain.handle(
});
mpvInstance = await createMpv(data);
setAudioPlayerFallback(false);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to initialize mpv, falling back to web player' }, err);
setAudioPlayerFallback(true);
}
@ -226,7 +227,7 @@ ipcMain.on('player-quit', async () => {
try {
await getMpvInstance()?.stop();
await quit();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to quit mpv' }, err);
} finally {
mpvInstance = null;
@ -245,7 +246,7 @@ ipcMain.handle('player-clean-up', async () => {
ipcMain.on('player-start', async () => {
try {
await getMpvInstance()?.play();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to start mpv playback' }, err);
}
});
@ -254,7 +255,7 @@ ipcMain.on('player-start', async () => {
ipcMain.on('player-play', async () => {
try {
await getMpvInstance()?.play();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to start mpv playback' }, err);
}
});
@ -263,7 +264,7 @@ ipcMain.on('player-play', async () => {
ipcMain.on('player-pause', async () => {
try {
await getMpvInstance()?.pause();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to pause mpv playback' }, err);
}
});
@ -272,7 +273,7 @@ ipcMain.on('player-pause', async () => {
ipcMain.on('player-stop', async () => {
try {
await getMpvInstance()?.stop();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to stop mpv playback' }, err);
}
});
@ -281,7 +282,7 @@ ipcMain.on('player-stop', async () => {
ipcMain.on('player-next', async () => {
try {
await getMpvInstance()?.next();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to go to next track' }, err);
}
});
@ -290,7 +291,7 @@ ipcMain.on('player-next', async () => {
ipcMain.on('player-previous', async () => {
try {
await getMpvInstance()?.prev();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: 'Failed to go to previous track' }, err);
}
});
@ -299,7 +300,7 @@ ipcMain.on('player-previous', async () => {
ipcMain.on('player-seek', async (_event, time: number) => {
try {
await getMpvInstance()?.seek(time);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to seek by ${time} seconds` }, err);
}
});
@ -308,7 +309,7 @@ ipcMain.on('player-seek', async (_event, time: number) => {
ipcMain.on('player-seek-to', async (_event, time: number) => {
try {
await getMpvInstance()?.goToPosition(time);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to seek to ${time} seconds` }, err);
}
});
@ -320,7 +321,7 @@ ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, p
await getMpvInstance()?.clearPlaylist();
await getMpvInstance()?.pause();
return;
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to clear play queue` }, err);
}
}
@ -344,7 +345,7 @@ ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, p
// Only force play if pause is explicitly false
await getMpvInstance()?.play();
}
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to set play queue` }, err);
}
});
@ -365,7 +366,7 @@ ipcMain.on('player-set-queue-next', async (_event, url?: string) => {
if (url) {
await getMpvInstance()?.load(url, 'append');
}
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to set play queue` }, err);
}
});
@ -385,7 +386,7 @@ ipcMain.on('player-auto-next', async (_event, url?: string) => {
if (url) {
await getMpvInstance()?.load(url, 'append');
}
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to load next song` }, err);
}
});
@ -398,7 +399,7 @@ ipcMain.on('player-volume', async (_event, value: number) => {
}
await getMpvInstance()?.volume(value);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to set volume to ${value}` }, err);
}
});
@ -407,7 +408,7 @@ ipcMain.on('player-volume', async (_event, value: number) => {
ipcMain.on('player-mute', async (_event, mute: boolean) => {
try {
await getMpvInstance()?.mute(mute);
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to set mute status` }, err);
}
});
@ -415,7 +416,7 @@ ipcMain.on('player-mute', async (_event, mute: boolean) => {
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
try {
return getMpvInstance()?.getTimePosition();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to get current time` }, err);
return 0;
}
@ -442,7 +443,7 @@ app.on('before-quit', async (event) => {
event.preventDefault();
await getMpvInstance()?.stop();
await quit();
} catch (err: NodeMpvError | any) {
} catch (err: any | NodeMpvError) {
mpvLog({ action: `Failed to cleanly before-quit` }, err);
} finally {
mpvState = MpvState.DONE;

View file

@ -1,5 +1,6 @@
/* eslint-disable promise/always-return */
import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
import { isMacOS } from '../../../utils';
import { store } from '../settings';

View file

@ -1,32 +1,22 @@
import { Stats, promises } from 'fs';
import { readFile } from 'fs/promises';
import { IncomingMessage, Server, ServerResponse, createServer } from 'http';
import { join } from 'path';
import { deflate, gzip } from 'zlib';
import axios from 'axios';
import { app, ipcMain } from 'electron';
import { Server as WsServer, WebSocketServer, WebSocket } from 'ws';
import manifest from './manifest.json';
import { ClientEvent, ServerEvent } from '../../../../remote/types';
import { PlayerRepeat, PlayerStatus, SongState } from '../../../../renderer/types';
import { getMainWindow } from '../../../main';
import { promises, Stats } from 'fs';
import { readFile } from 'fs/promises';
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
import { join } from 'path';
import { WebSocket, WebSocketServer, Server as WsServer } from 'ws';
import { deflate, gzip } from 'zlib';
import { getMainWindow } from '../../..';
import { isLinux } from '../../../utils';
import type { QueueSong } from '/@/renderer/api/types';
import manifest from './manifest.json';
let mprisPlayer: any | undefined;
if (isLinux()) {
// eslint-disable-next-line global-require
mprisPlayer = require('../../linux/mpris').mprisPlayer;
}
interface RemoteConfig {
enabled: boolean;
password: string;
port: number;
username: string;
}
interface MimeType {
css: string;
html: string;
@ -34,6 +24,13 @@ interface MimeType {
js: string;
}
interface RemoteConfig {
enabled: boolean;
password: string;
port: number;
username: string;
}
declare class StatefulWebSocket extends WebSocket {
alive: boolean;
@ -41,7 +38,7 @@ declare class StatefulWebSocket extends WebSocket {
}
let server: Server | undefined;
let wsServer: WsServer<typeof StatefulWebSocket> | undefined;
let wsServer: undefined | WsServer<typeof StatefulWebSocket>;
const settings: RemoteConfig = {
enabled: false,
@ -54,14 +51,6 @@ type SendData = ServerEvent & {
client: StatefulWebSocket;
};
function send({ client, event, data }: SendData): void {
if (client.readyState === WebSocket.OPEN) {
if (client.alive && client.auth) {
client.send(JSON.stringify({ data, event }));
}
}
}
function broadcast(message: ServerEvent): void {
if (wsServer) {
for (const client of wsServer.clients) {
@ -70,6 +59,14 @@ function broadcast(message: ServerEvent): void {
}
}
function send({ client, data, event }: SendData): void {
if (client.readyState === WebSocket.OPEN) {
if (client.alive && client.auth) {
client.send(JSON.stringify({ data, event }));
}
}
}
const shutdownServer = () => {
if (wsServer) {
wsServer.clients.forEach((client) => client.close(4000));
@ -121,21 +118,17 @@ const getEncoding = (encoding: string | string[]): Encoding => {
const cache = new Map<string, Map<Encoding, [number, Buffer]>>();
function setOk(
res: ServerResponse,
mtimeMs: number,
extension: keyof MimeType,
encoding: Encoding,
data?: Buffer,
) {
res.statusCode = data ? 200 : 304;
function authorize(req: IncomingMessage): boolean {
if (settings.username || settings.password) {
// https://stackoverflow.com/questions/23616371/basic-http-authentication-with-node-and-express-4
res.setHeader('Content-Type', MIME_TYPES[extension]);
res.setHeader('ETag', `"${mtimeMs}"`);
res.setHeader('Cache-Control', 'public');
const authorization = req.headers.authorization?.split(' ')[1] || '';
const [login, password] = Buffer.from(authorization, 'base64').toString().split(':');
if (encoding !== 'none') res.setHeader('Content-Encoding', encoding);
res.end(data);
return login === settings.username && password === settings.password;
}
return true;
}
async function serveFile(
@ -252,17 +245,21 @@ async function serveFile(
return Promise.resolve();
}
function authorize(req: IncomingMessage): boolean {
if (settings.username || settings.password) {
// https://stackoverflow.com/questions/23616371/basic-http-authentication-with-node-and-express-4
function setOk(
res: ServerResponse,
mtimeMs: number,
extension: keyof MimeType,
encoding: Encoding,
data?: Buffer,
) {
res.statusCode = data ? 200 : 304;
const authorization = req.headers.authorization?.split(' ')[1] || '';
const [login, password] = Buffer.from(authorization, 'base64').toString().split(':');
res.setHeader('Content-Type', MIME_TYPES[extension]);
res.setHeader('ETag', `"${mtimeMs}"`);
res.setHeader('Cache-Control', 'public');
return login === settings.username && password === settings.password;
}
return true;
if (encoding !== 'none') res.setHeader('Content-Encoding', encoding);
res.end(data);
}
const enableServer = (config: RemoteConfig): Promise<void> => {
@ -286,28 +283,28 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
await serveFile(req, 'index', 'html', res);
break;
}
case '/credentials': {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(req.headers.authorization);
break;
}
case '/favicon.ico': {
await serveFile(req, 'favicon', 'ico', res);
break;
}
case '/remote.css': {
await serveFile(req, 'remote', 'css', res);
break;
}
case '/remote.js': {
await serveFile(req, 'remote', 'js', res);
break;
}
case '/manifest.json': {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(manifest));
break;
}
case '/credentials': {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(req.headers.authorization);
case '/remote.css': {
await serveFile(req, 'remote', 'css', res);
break;
}
case '/remote.js': {
await serveFile(req, 'remote', 'js', res);
break;
}
default: {
@ -371,6 +368,21 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
}
switch (event) {
case 'favorite': {
const { favorite, id } = json;
if (id && id === currentState.song?.id) {
getMainWindow()?.webContents.send('request-favorite', {
favorite,
id,
serverId: currentState.song.serverId,
});
}
break;
}
case 'next': {
getMainWindow()?.webContents.send('renderer-player-next');
break;
}
case 'pause': {
getMainWindow()?.webContents.send('renderer-player-pause');
break;
@ -379,10 +391,6 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
getMainWindow()?.webContents.send('renderer-player-play');
break;
}
case 'next': {
getMainWindow()?.webContents.send('renderer-player-next');
break;
}
case 'previous': {
getMainWindow()?.webContents.send('renderer-player-previous');
break;
@ -421,6 +429,17 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
break;
}
case 'rating': {
const { id, rating } = json;
if (id && id === currentState.song?.id) {
getMainWindow()?.webContents.send('request-rating', {
id,
rating,
serverId: currentState.song.serverId,
});
}
break;
}
case 'repeat': {
getMainWindow()?.webContents.send('renderer-player-toggle-repeat');
break;
@ -450,28 +469,6 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
}
break;
}
case 'favorite': {
const { favorite, id } = json;
if (id && id === currentState.song?.id) {
getMainWindow()?.webContents.send('request-favorite', {
favorite,
id,
serverId: currentState.song.serverId,
});
}
break;
}
case 'rating': {
const { rating, id } = json;
if (id && id === currentState.song?.id) {
getMainWindow()?.webContents.send('request-rating', {
id,
rating,
serverId: currentState.song.serverId,
});
}
break;
}
case 'position': {
const { position } = json;
if (mprisPlayer) {
@ -631,12 +628,7 @@ ipcMain.on('update-volume', (_event, volume: number) => {
if (mprisPlayer) {
mprisPlayer.on('loopStatus', (event: string) => {
const repeat =
event === 'Playlist'
? PlayerRepeat.ALL
: event === 'Track'
? PlayerRepeat.ONE
: PlayerRepeat.NONE;
const repeat = event === 'Playlist' ? 'all' : event === 'Track' ? 'one' : 'none';
currentState.repeat = repeat;
broadcast({ data: repeat, event: 'repeat' });

View file

@ -1,6 +1,7 @@
import type { TitleTheme } from '/@/renderer/types';
import { ipcMain, nativeTheme, safeStorage } from 'electron';
import Store from 'electron-store';
import type { TitleTheme } from '/@/renderer/types';
export const store = new Store();
@ -12,7 +13,7 @@ ipcMain.on('settings-set', (__event, data: { property: string; value: any }) =>
store.set(`${data.property}`, data.value);
});
ipcMain.handle('password-get', (_event, server: string): string | null => {
ipcMain.handle('password-get', (_event, server: string): null | string => {
if (safeStorage.isEncryptionAvailable()) {
const servers = store.get('server') as Record<string, string> | undefined;

View file

@ -1,4 +1,3 @@
import './core';
// eslint-disable-next-line import/no-dynamic-require
require(`./${process.platform}`);
// require(`./${process.platform}`)

View file

@ -1,7 +1,9 @@
import { ipcMain } from 'electron';
import Player from 'mpris-service';
import { PlayerRepeat, PlayerStatus } from '../../../renderer/types';
import { getMainWindow } from '../../main';
import { QueueSong } from '/@/renderer/api/types';
const mprisPlayer = Player({
@ -124,8 +126,8 @@ ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
const REPEAT_TO_MPRIS: Record<PlayerRepeat, string> = {
[PlayerRepeat.ALL]: 'Playlist',
[PlayerRepeat.ONE]: 'Track',
[PlayerRepeat.NONE]: 'None',
[PlayerRepeat.ONE]: 'Track',
};
ipcMain.on('update-repeat', (_event, arg: PlayerRepeat) => {

View file

@ -0,0 +1,3 @@
// Dummy file to satisfy the build system
export {};

View file

@ -1,51 +1,40 @@
/* eslint global-require: off, no-console: off, promise/always-return: off */
/**
* This module executes inside of electron's main process. You can start
* electron renderer process from here and communicate with the other processes
* through IPC.
*
* When running `npm run build` or `npm run build:main`, this file is compiled to
* `./src/main.js` using webpack. This gives us some performance wins.
*/
import { access, constants, readFile, writeFile } from 'fs';
import path, { join } from 'path';
import { deflate, inflate } from 'zlib';
import { electronApp, is, optimizer } from '@electron-toolkit/utils';
import { constants } from 'buffer';
import {
app,
BrowserWindow,
shell,
ipcMain,
BrowserWindowConstructorOptions,
globalShortcut,
Tray,
ipcMain,
Menu,
nativeImage,
nativeTheme,
BrowserWindowConstructorOptions,
protocol,
net,
protocol,
Rectangle,
screen,
shell,
Tray,
} from 'electron';
import electronLocalShortcut from 'electron-localshortcut';
import log from 'electron-log/main';
import { autoUpdater } from 'electron-updater';
import { access, readFile, writeFile } from 'fs';
import path, { join } from 'path';
import { deflate, inflate } from 'zlib';
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
import { store } from './features/core/settings/index';
import { store } from './features/core/settings';
import MenuBuilder from './menu';
import {
autoUpdaterLogInterface,
createLog,
hotkeyToElectronAccelerator,
isLinux,
isMacOS,
isWindows,
resolveHtmlPath,
createLog,
autoUpdaterLogInterface,
} from './utils';
import './features';
import type { TitleTheme } from '/@/renderer/types';
declare module 'node-mpv';
export default class AppUpdater {
constructor() {
@ -72,7 +61,7 @@ if (isLinux() && !process.argv.some((a) => a.startsWith('--password-store='))) {
}
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
let tray: null | Tray = null;
let exitFromTray = false;
let forceQuit = false;
@ -117,7 +106,7 @@ export const sendToastToRenderer = ({
type,
}: {
message: string;
type: 'success' | 'error' | 'warning' | 'info';
type: 'error' | 'info' | 'success' | 'warning';
}) => {
getMainWindow()?.webContents.send('toast-from-main', {
message,
@ -208,7 +197,7 @@ const createTray = () => {
tray.setContextMenu(contextMenu);
};
const createWindow = async (first = true) => {
async function createWindow(first = true): Promise<void> {
if (isDevelopment) {
await installExtensions().catch(console.log);
}
@ -233,6 +222,7 @@ const createWindow = async (first = true) => {
},
};
// Create the browser window.
mainWindow = new BrowserWindow({
autoHideMenuBar: true,
frame: false,
@ -247,9 +237,8 @@ const createWindow = async (first = true) => {
contextIsolation: true,
devTools: true,
nodeIntegration: true,
preload: app.isPackaged
? path.join(__dirname, 'preload.js')
: path.join(__dirname, '../../.erb/dll/preload.js'),
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: !store.get('ignore_cors'),
},
width: 1440,
@ -374,11 +363,11 @@ const createWindow = async (first = true) => {
enableMediaKeys(mainWindow);
}
mainWindow.loadURL(resolveHtmlPath('index.html'));
const startWindowMinimized = store.get('window_start_minimized', false) as boolean;
mainWindow.on('ready-to-show', () => {
// mainWindow.show()
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
@ -404,7 +393,7 @@ const createWindow = async (first = true) => {
mainWindow = null;
});
let saved = false;
const saved = false;
mainWindow.on('close', (event) => {
store.set('bounds', mainWindow?.getNormalBounds());
@ -484,13 +473,25 @@ const createWindow = async (first = true) => {
});
if (store.get('disable_auto_updates') !== true) {
// eslint-disable-next-line
new AppUpdater();
}
const theme = store.get('theme') as TitleTheme | undefined;
nativeTheme.themeSource = theme || 'dark';
};
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
return { action: 'deny' };
});
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
}
}
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
@ -519,6 +520,8 @@ enum BindingActions {
}
const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
[BindingActions.GLOBAL_SEARCH]: () => {},
[BindingActions.LOCAL_SEARCH]: () => {},
[BindingActions.MUTE]: () => getMainWindow()?.webContents.send('renderer-player-volume-mute'),
[BindingActions.NEXT]: () => getMainWindow()?.webContents.send('renderer-player-next'),
[BindingActions.PAUSE]: () => getMainWindow()?.webContents.send('renderer-player-pause'),
@ -533,16 +536,14 @@ const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
[BindingActions.SKIP_FORWARD]: () =>
getMainWindow()?.webContents.send('renderer-player-skip-forward'),
[BindingActions.STOP]: () => getMainWindow()?.webContents.send('renderer-player-stop'),
[BindingActions.TOGGLE_FULLSCREEN_PLAYER]: () => {},
[BindingActions.TOGGLE_QUEUE]: () => {},
[BindingActions.TOGGLE_REPEAT]: () =>
getMainWindow()?.webContents.send('renderer-player-toggle-repeat'),
[BindingActions.VOLUME_UP]: () =>
getMainWindow()?.webContents.send('renderer-player-volume-up'),
[BindingActions.VOLUME_DOWN]: () =>
getMainWindow()?.webContents.send('renderer-player-volume-down'),
[BindingActions.GLOBAL_SEARCH]: () => {},
[BindingActions.LOCAL_SEARCH]: () => {},
[BindingActions.TOGGLE_QUEUE]: () => {},
[BindingActions.TOGGLE_FULLSCREEN_PLAYER]: () => {},
[BindingActions.VOLUME_UP]: () =>
getMainWindow()?.webContents.send('renderer-player-volume-up'),
};
ipcMain.on(
@ -585,7 +586,7 @@ ipcMain.on(
_event,
data: {
message: string;
type: 'debug' | 'verbose' | 'success' | 'error' | 'warning' | 'info';
type: 'debug' | 'error' | 'info' | 'success' | 'verbose' | 'warning';
},
) => {
createLog(data);
@ -597,7 +598,6 @@ app.on('window-all-closed', () => {
// Respect the OSX convention of having the application in memory even
// after all windows have been closed
if (isMacOS()) {
ipcMain.removeHandler('window-clear-cache');
mainWindow = null;
} else {
app.quit();
@ -632,22 +632,22 @@ if (!singleInstance) {
app.whenReady()
.then(() => {
protocol.handle('feishin', async (request) => {
const filePath = `file://${request.url.slice('feishin://'.length)}`;
const response = await net.fetch(filePath);
const contentType = response.headers.get('content-type');
// protocol.handle('feishin', async (request) => {
// const filePath = `file://${request.url.slice('feishin://'.length)}`
// const response = await net.fetch(filePath)
// const contentType = response.headers.get('content-type')
if (!contentType || !FONT_HEADERS.includes(contentType)) {
getMainWindow()?.webContents.send('custom-font-error', filePath);
// if (!contentType || !FONT_HEADERS.includes(contentType)) {
// getMainWindow()?.webContents.send('custom-font-error', filePath)
return new Response(null, {
status: 403,
statusText: 'Forbidden',
});
}
// return new Response(null, {
// status: 403,
// statusText: 'Forbidden'
// })
// }
return response;
});
// return response
// })
createWindow();
if (store.get('window_enable_tray', true)) {

View file

@ -1,4 +1,4 @@
import { app, Menu, shell, BrowserWindow, MenuItemConstructorOptions } from 'electron';
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';
interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
selector?: string;
@ -12,37 +12,6 @@ export default class MenuBuilder {
this.mainWindow = mainWindow;
}
buildMenu(): Menu {
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
this.setupDevelopmentEnvironment();
}
const template =
process.platform === 'darwin'
? this.buildDarwinTemplate()
: this.buildDefaultTemplate();
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
return menu;
}
setupDevelopmentEnvironment(): void {
this.mainWindow.webContents.on('context-menu', (_, props) => {
const { x, y } = props;
Menu.buildFromTemplate([
{
click: () => {
this.mainWindow.webContents.inspectElement(x, y);
},
label: 'Inspect element',
},
]).popup({ window: this.mainWindow });
});
}
buildDarwinTemplate(): MenuItemConstructorOptions[] {
const subMenuAbout: DarwinMenuItemConstructorOptions = {
label: 'Electron',
@ -276,4 +245,35 @@ export default class MenuBuilder {
return templateDefault;
}
buildMenu(): Menu {
if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
this.setupDevelopmentEnvironment();
}
const template =
process.platform === 'darwin'
? this.buildDarwinTemplate()
: this.buildDefaultTemplate();
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
return menu;
}
setupDevelopmentEnvironment(): void {
this.mainWindow.webContents.on('context-menu', (_, props) => {
const { x, y } = props;
Menu.buildFromTemplate([
{
click: () => {
this.mainWindow.webContents.inspectElement(x, y);
},
label: 'Inspect element',
},
]).popup({ window: this.mainWindow });
});
}
}

View file

@ -1,23 +0,0 @@
import { contextBridge } from 'electron';
import { browser } from './preload/browser';
import { discordRpc } from './preload/discord-rpc';
import { ipc } from './preload/ipc';
import { localSettings } from './preload/local-settings';
import { lyrics } from './preload/lyrics';
import { mpris } from './preload/mpris';
import { mpvPlayer, mpvPlayerListener } from './preload/mpv-player';
import { remote } from './preload/remote';
import { utils } from './preload/utils';
contextBridge.exposeInMainWorld('electron', {
browser,
discordRpc,
ipc,
localSettings,
lyrics,
mpris,
mpvPlayer,
mpvPlayerListener,
remote,
utils,
});

View file

@ -1,41 +0,0 @@
import { ipcRenderer } from 'electron';
const exit = () => {
ipcRenderer.send('window-close');
};
const maximize = () => {
ipcRenderer.send('window-maximize');
};
const minimize = () => {
ipcRenderer.send('window-minimize');
};
const unmaximize = () => {
ipcRenderer.send('window-unmaximize');
};
const quit = () => {
ipcRenderer.send('window-quit');
};
const devtools = () => {
ipcRenderer.send('window-dev-tools');
};
const clearCache = (): Promise<void> => {
return ipcRenderer.invoke('window-clear-cache');
};
export const browser = {
clearCache,
devtools,
exit,
maximize,
minimize,
quit,
unmaximize,
};
export type Browser = typeof browser;

View file

@ -1,28 +0,0 @@
import { SetActivity } from '@xhayper/discord-rpc';
import { ipcRenderer } from 'electron';
const initialize = (clientId: string) => {
const client = ipcRenderer.invoke('discord-rpc-initialize', clientId);
return client;
};
const clearActivity = () => {
ipcRenderer.invoke('discord-rpc-clear-activity');
};
const setActivity = (activity: SetActivity) => {
ipcRenderer.invoke('discord-rpc-set-activity', activity);
};
const quit = () => {
ipcRenderer.invoke('discord-rpc-quit');
};
export const discordRpc = {
clearActivity,
initialize,
quit,
setActivity,
};
export type DiscordRpc = typeof discordRpc;

View file

@ -1,16 +0,0 @@
import { ipcRenderer } from 'electron';
const removeAllListeners = (channel: string) => {
ipcRenderer.removeAllListeners(channel);
};
const send = (channel: string, ...args: any[]) => {
ipcRenderer.send(channel, ...args);
};
export const ipc = {
removeAllListeners,
send,
};
export type Ipc = typeof ipc;

View file

@ -1,85 +0,0 @@
import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron';
import Store from 'electron-store';
import { toServerType, type TitleTheme } from '/@/renderer/types';
const store = new Store();
const set = (
property: string,
value: string | Record<string, unknown> | boolean | string[] | undefined,
) => {
if (value === undefined) {
store.delete(property);
return;
}
store.set(`${property}`, value);
};
const get = (property: string) => {
return store.get(`${property}`);
};
const restart = () => {
ipcRenderer.send('app-restart');
};
const enableMediaKeys = () => {
ipcRenderer.send('global-media-keys-enable');
};
const disableMediaKeys = () => {
ipcRenderer.send('global-media-keys-disable');
};
const passwordGet = async (server: string): Promise<string | null> => {
return ipcRenderer.invoke('password-get', server);
};
const passwordRemove = (server: string) => {
ipcRenderer.send('password-remove', server);
};
const passwordSet = async (password: string, server: string): Promise<boolean> => {
return ipcRenderer.invoke('password-set', password, server);
};
const setZoomFactor = (zoomFactor: number) => {
webFrame.setZoomFactor(zoomFactor / 100);
};
const fontError = (cb: (event: IpcRendererEvent, file: string) => void) => {
ipcRenderer.on('custom-font-error', cb);
};
const themeSet = (theme: TitleTheme): void => {
ipcRenderer.send('theme-set', theme);
};
const SERVER_TYPE = toServerType(process.env.SERVER_TYPE);
const env = {
SERVER_LOCK:
SERVER_TYPE !== null ? process.env.SERVER_LOCK?.toLocaleLowerCase() === 'true' : false,
SERVER_NAME: process.env.SERVER_NAME ?? '',
SERVER_TYPE,
SERVER_URL: process.env.SERVER_URL ?? 'http://',
START_MAXIMIZED: store.get('maximized'),
};
export const localSettings = {
disableMediaKeys,
enableMediaKeys,
env,
fontError,
get,
passwordGet,
passwordRemove,
passwordSet,
restart,
set,
setZoomFactor,
themeSet,
};
export type LocalSettings = typeof localSettings;

View file

@ -1,33 +0,0 @@
import { ipcRenderer } from 'electron';
import {
InternetProviderLyricSearchResponse,
LyricGetQuery,
LyricSearchQuery,
LyricSource,
QueueSong,
} from '/@/renderer/api/types';
const getRemoteLyricsBySong = (song: QueueSong) => {
const result = ipcRenderer.invoke('lyric-by-song', song);
return result;
};
const searchRemoteLyrics = (
params: LyricSearchQuery,
): Promise<Record<LyricSource, InternetProviderLyricSearchResponse[]>> => {
const result = ipcRenderer.invoke('lyric-search', params);
return result;
};
const getRemoteLyricsByRemoteId = (id: LyricGetQuery) => {
const result = ipcRenderer.invoke('lyric-by-remote-id', id);
return result;
};
export const lyrics = {
getRemoteLyricsByRemoteId,
getRemoteLyricsBySong,
searchRemoteLyrics,
};
export type Lyrics = typeof lyrics;

View file

@ -1,41 +0,0 @@
import { IpcRendererEvent, ipcRenderer } from 'electron';
import type { PlayerRepeat } from '/@/renderer/types';
const updatePosition = (timeSec: number) => {
ipcRenderer.send('mpris-update-position', timeSec);
};
const updateSeek = (timeSec: number) => {
ipcRenderer.send('mpris-update-seek', timeSec);
};
const toggleRepeat = () => {
ipcRenderer.send('mpris-toggle-repeat');
};
const toggleShuffle = () => {
ipcRenderer.send('mpris-toggle-shuffle');
};
const requestToggleRepeat = (
cb: (event: IpcRendererEvent, data: { repeat: PlayerRepeat }) => void,
) => {
ipcRenderer.on('mpris-request-toggle-repeat', cb);
};
const requestToggleShuffle = (
cb: (event: IpcRendererEvent, data: { shuffle: boolean }) => void,
) => {
ipcRenderer.on('mpris-request-toggle-shuffle', cb);
};
export const mpris = {
requestToggleRepeat,
requestToggleShuffle,
toggleRepeat,
toggleShuffle,
updatePosition,
updateSeek,
};
export type Mpris = typeof mpris;

View file

@ -1,205 +0,0 @@
import { ipcRenderer, IpcRendererEvent } from 'electron';
import { PlayerData } from '/@/renderer/store';
const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
return ipcRenderer.invoke('player-initialize', data);
};
const restart = (data: {
binaryPath?: string;
extraParameters?: string[];
properties?: Record<string, any>;
}) => {
return ipcRenderer.invoke('player-restart', data);
};
const isRunning = () => {
return ipcRenderer.invoke('player-is-running');
};
const cleanup = () => {
return ipcRenderer.invoke('player-clean-up');
};
const setProperties = (data: Record<string, any>) => {
ipcRenderer.send('player-set-properties', data);
};
const autoNext = (url?: string) => {
ipcRenderer.send('player-auto-next', url);
};
const currentTime = () => {
ipcRenderer.send('player-current-time');
};
const mute = (mute: boolean) => {
ipcRenderer.send('player-mute', mute);
};
const next = () => {
ipcRenderer.send('player-next');
};
const pause = () => {
ipcRenderer.send('player-pause');
};
const play = () => {
ipcRenderer.send('player-play');
};
const previous = () => {
ipcRenderer.send('player-previous');
};
const seek = (seconds: number) => {
ipcRenderer.send('player-seek', seconds);
};
const seekTo = (seconds: number) => {
ipcRenderer.send('player-seek-to', seconds);
};
const setQueue = (current?: string, next?: string, pause?: boolean) => {
ipcRenderer.send('player-set-queue', current, next, pause);
};
const setQueueNext = (url?: string) => {
ipcRenderer.send('player-set-queue-next', url);
};
const stop = () => {
ipcRenderer.send('player-stop');
};
const volume = (value: number) => {
ipcRenderer.send('player-volume', value);
};
const quit = () => {
ipcRenderer.send('player-quit');
};
const getCurrentTime = async () => {
return ipcRenderer.invoke('player-get-time');
};
const rendererAutoNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-auto-next', cb);
};
const rendererCurrentTime = (cb: (event: IpcRendererEvent, data: number) => void) => {
ipcRenderer.on('renderer-player-current-time', cb);
};
const rendererNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-next', cb);
};
const rendererPause = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-pause', cb);
};
const rendererPlay = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-play', cb);
};
const rendererPlayPause = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-play-pause', cb);
};
const rendererPrevious = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-previous', cb);
};
const rendererStop = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-stop', cb);
};
const rendererSkipForward = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-skip-forward', cb);
};
const rendererSkipBackward = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-skip-backward', cb);
};
const rendererVolumeUp = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-volume-up', cb);
};
const rendererVolumeDown = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-volume-down', cb);
};
const rendererVolumeMute = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-volume-mute', cb);
};
const rendererToggleRepeat = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-toggle-repeat', cb);
};
const rendererToggleShuffle = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-toggle-shuffle', cb);
};
const rendererQuit = (cb: (event: IpcRendererEvent) => void) => {
ipcRenderer.on('renderer-player-quit', cb);
};
const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
ipcRenderer.on('renderer-player-error', cb);
};
const rendererPlayerFallback = (cb: (event: IpcRendererEvent, data: boolean) => void) => {
ipcRenderer.on('renderer-player-fallback', cb);
};
export const mpvPlayer = {
autoNext,
cleanup,
currentTime,
getCurrentTime,
initialize,
isRunning,
mute,
next,
pause,
play,
previous,
quit,
restart,
seek,
seekTo,
setProperties,
setQueue,
setQueueNext,
stop,
volume,
};
export const mpvPlayerListener = {
rendererAutoNext,
rendererCurrentTime,
rendererError,
rendererNext,
rendererPause,
rendererPlay,
rendererPlayPause,
rendererPlayerFallback,
rendererPrevious,
rendererQuit,
rendererSkipBackward,
rendererSkipForward,
rendererStop,
rendererToggleRepeat,
rendererToggleShuffle,
rendererVolumeDown,
rendererVolumeMute,
rendererVolumeUp,
};
export type MpvPLayer = typeof mpvPlayer;
export type MpvPlayerListener = typeof mpvPlayerListener;

View file

@ -1,112 +0,0 @@
import { IpcRendererEvent, ipcRenderer } from 'electron';
import { QueueSong } from '/@/renderer/api/types';
import { PlayerStatus } from '/@/renderer/types';
const requestFavorite = (
cb: (
event: IpcRendererEvent,
data: { favorite: boolean; id: string; serverId: string },
) => void,
) => {
ipcRenderer.on('request-favorite', cb);
};
const requestPosition = (cb: (event: IpcRendererEvent, data: { position: number }) => void) => {
ipcRenderer.on('request-position', cb);
};
const requestRating = (
cb: (event: IpcRendererEvent, data: { id: string; rating: number; serverId: string }) => void,
) => {
ipcRenderer.on('request-rating', cb);
};
const requestSeek = (cb: (event: IpcRendererEvent, data: { offset: number }) => void) => {
ipcRenderer.on('request-seek', cb);
};
const requestVolume = (cb: (event: IpcRendererEvent, data: { volume: number }) => void) => {
ipcRenderer.on('request-volume', cb);
};
const setRemoteEnabled = (enabled: boolean): Promise<string | null> => {
const result = ipcRenderer.invoke('remote-enable', enabled);
return result;
};
const setRemotePort = (port: number): Promise<string | null> => {
const result = ipcRenderer.invoke('remote-port', port);
return result;
};
const updateFavorite = (favorite: boolean, serverId: string, ids: string[]) => {
ipcRenderer.send('update-favorite', favorite, serverId, ids);
};
const updatePassword = (password: string) => {
ipcRenderer.send('remote-password', password);
};
const updatePlayback = (playback: PlayerStatus) => {
ipcRenderer.send('update-playback', playback);
};
const updateSetting = (
enabled: boolean,
port: number,
username: string,
password: string,
): Promise<string | null> => {
return ipcRenderer.invoke('remote-settings', enabled, port, username, password);
};
const updateRating = (rating: number, serverId: string, ids: string[]) => {
ipcRenderer.send('update-rating', rating, serverId, ids);
};
const updateRepeat = (repeat: string) => {
ipcRenderer.send('update-repeat', repeat);
};
const updateShuffle = (shuffle: boolean) => {
ipcRenderer.send('update-shuffle', shuffle);
};
const updateSong = (args: QueueSong | undefined) => {
ipcRenderer.send('update-song', args);
};
const updateUsername = (username: string) => {
ipcRenderer.send('remote-username', username);
};
const updateVolume = (volume: number) => {
ipcRenderer.send('update-volume', volume);
};
const updatePosition = (timeSec: number) => {
ipcRenderer.send('update-position', timeSec);
};
export const remote = {
requestFavorite,
requestPosition,
requestRating,
requestSeek,
requestVolume,
setRemoteEnabled,
setRemotePort,
updateFavorite,
updatePassword,
updatePlayback,
updatePosition,
updateRating,
updateRepeat,
updateSetting,
updateShuffle,
updateSong,
updateUsername,
updateVolume,
};
export type Remote = typeof remote;

View file

@ -1,69 +0,0 @@
import { IpcRendererEvent, ipcRenderer } from 'electron';
import { isMacOS, isWindows, isLinux } from '../utils';
import { PlayerState } from '/@/renderer/store';
const saveQueue = (data: Record<string, any>) => {
ipcRenderer.send('player-save-queue', data);
};
const restoreQueue = () => {
ipcRenderer.send('player-restore-queue');
};
const openItem = async (path: string) => {
return ipcRenderer.invoke('open-item', path);
};
const onSaveQueue = (cb: (event: IpcRendererEvent) => void) => {
ipcRenderer.on('renderer-save-queue', cb);
};
const onRestoreQueue = (cb: (event: IpcRendererEvent, data: Partial<PlayerState>) => void) => {
ipcRenderer.on('renderer-restore-queue', cb);
};
const playerErrorListener = (cb: (event: IpcRendererEvent, data: { code: number }) => void) => {
ipcRenderer.on('player-error-listener', cb);
};
const mainMessageListener = (
cb: (
event: IpcRendererEvent,
data: { message: string; type: 'success' | 'error' | 'warning' | 'info' },
) => void,
) => {
ipcRenderer.on('toast-from-main', cb);
};
const logger = (
cb: (
event: IpcRendererEvent,
data: {
message: string;
type: 'debug' | 'verbose' | 'error' | 'warning' | 'info';
},
) => void,
) => {
ipcRenderer.send('logger', cb);
};
const download = (url: string) => {
ipcRenderer.send('download-url', url);
};
export const utils = {
download,
isLinux,
isMacOS,
isWindows,
logger,
mainMessageListener,
onRestoreQueue,
onSaveQueue,
openItem,
playerErrorListener,
restoreQueue,
saveQueue,
};
export type Utils = typeof utils;

View file

@ -1,8 +1,7 @@
/* eslint import/prefer-default-export: off, import/no-mutable-exports: off */
import log from 'electron-log/main';
import path from 'path';
import process from 'process';
import { URL } from 'url';
import log from 'electron-log/main';
export let resolveHtmlPath: (htmlFileName: string) => string;
@ -76,7 +75,7 @@ const logColor = {
export const createLog = (data: {
message: string;
type: 'debug' | 'verbose' | 'success' | 'error' | 'warning' | 'info';
type: 'debug' | 'error' | 'info' | 'success' | 'verbose' | 'warning';
}) => {
logMethod[data.type](`%c${data.message}`, `color: ${logColor[data.type]}`);
};