mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 02:13:33 +00:00
restructure files onto electron-vite boilerplate
This commit is contained in:
parent
91ce2cd8a1
commit
1cf587bc8f
457 changed files with 9927 additions and 11705 deletions
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Fuse from 'fuse.js';
|
||||
|
||||
import {
|
||||
InternetProviderLyricSearchResponse,
|
||||
LyricSearchQuery,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable promise/always-return */
|
||||
import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
|
||||
|
||||
import { isMacOS } from '../../../utils';
|
||||
import { store } from '../settings';
|
||||
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import './core';
|
||||
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
require(`./${process.platform}`);
|
||||
// require(`./${process.platform}`)
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
// Dummy file to satisfy the build system
|
||||
|
||||
export {};
|
||||
Loading…
Add table
Add a link
Reference in a new issue