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;