Skip to content

Commit

Permalink
refactor + better error messages
Browse files Browse the repository at this point in the history
  • Loading branch information
ArjixWasTaken committed Oct 28, 2024
1 parent b41f2e1 commit 3d46f07
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 196 deletions.
83 changes: 83 additions & 0 deletions src/plugins/synced-lyrics/parsers/lrc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
interface LRCTag {}
interface LRCLine {
time: string;
timeInMs: number;
duration: number;
text: string;
}

interface LRC {
tags: LRCTag[];
lines: LRCLine[];
}

const tagRegex = /^\[(?<tag>\w+):\s*(?<value>.+?)\s*\]$/;
// prettier-ignore
const lyricRegex = /^\[(?<minutes>\d+):(?<seconds>\d+)\.(?<milliseconds>\d+)\](?<text>.+)$/;

export const LRC = {
parse: (text: string): LRC => {
let offset = 0;
const lrc: LRC = {
tags: [],
lines: [],
};

let previousLine: LRCLine | null = null;
for (const line of text.split('\n')) {
if (!line.trim().startsWith('[')) continue;

const lyric = line.match(lyricRegex)?.groups;
if (!lyric) {
const tag = line.match(tagRegex)?.groups;
if (tag) {
if (tag.tag === 'offset') {
offset = parseInt(tag.value);
continue;
}

lrc.tags.push({
tag: tag.tag,
value: tag.value,
});
}
continue;
}

const { minutes, seconds, milliseconds, text } = lyric;
const timeInMs =
parseInt(minutes) * 60 * 1000 +
parseInt(seconds) * 1000 +
parseInt(milliseconds);

const currentLine: LRCLine = {
time: `${minutes}:${seconds}:${milliseconds}`,
timeInMs,
text,
duration: Infinity,
};
if (previousLine) {
previousLine.duration = timeInMs - previousLine.timeInMs;
}

previousLine = currentLine;
lrc.lines.push(currentLine);
}

const first = lrc.lines.at(0);
if (first && first.timeInMs > 300) {
lrc.lines.unshift({
time: '0:0:0',
timeInMs: 0,
duration: first.timeInMs,
text: '',
});
}

for (const line of lrc.lines) {
line.timeInMs += offset;
}

return lrc;
},
};
76 changes: 18 additions & 58 deletions src/plugins/synced-lyrics/providers/LRCLib.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,8 @@
import { jaroWinkler } from '@skyra/jaro-winkler';

import {
type LineLyrics,
type LRCLIBSearchResponse,
LyricProvider,
} from '../types';
import type { LyricProvider } from '../types';
import { config } from '../renderer/renderer';

// TODO: Use an LRC parser instead of this.
function extractTimeAndText(line: string, index: number): LineLyrics | null {
const groups = /\[(\d+):(\d+)\.(\d+)\](.+)/.exec(line);
if (!groups) return null;

const [, rMinutes, rSeconds, rMillis, text] = groups;
const [minutes, seconds, millis] = [
parseInt(rMinutes),
parseInt(rSeconds),
parseInt(rMillis),
];

// prettier-ignore
const timeInMs = (minutes * 60 * 1000) + (seconds * 1000) + millis;

// prettier-ignore
return {
index,
timeInMs,
time: `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}:${millis}`,
text: text?.trim() ?? config()!.defaultTextString,
status: 'upcoming',
duration: 0,
};
}
import { LRC } from '../parsers/lrc';

export const LRCLib: LyricProvider = {
name: 'LRCLib',
Expand Down Expand Up @@ -131,37 +102,26 @@ export const LRCLib: LyricProvider = {
};
}

const raw = closestResult.syncedLyrics?.split('\n') ?? [];
if (!raw.length) {
return null;
}

// Add a blank line at the beginning
raw.unshift('[0:0.0] ');

const syncedLyricList = raw.reduce<LineLyrics[]>((acc, line, index) => {
const syncedLine = extractTimeAndText(line, index);
if (syncedLine) {
acc.push(syncedLine);
}

return acc;
}, []);

for (const line of syncedLyricList) {
const next = syncedLyricList[line.index + 1];
if (!next) {
line.duration = Infinity;
break;
}

line.duration = next.timeInMs - line.timeInMs;
}
const raw = closestResult.syncedLyrics;
if (!raw) return null;

const lyrics = LRC.parse(raw);
return {
title: closestResult.trackName,
artists: closestResult.artistName.split(/[&,]/g),
lines: syncedLyricList,
lines: lyrics.lines.map((l) => ({ ...l, status: 'upcoming' })),
};
},
} as const;

type LRCLIBSearchResponse = {
id: number;
name: string;
trackName: string;
artistName: string;
albumName: string;
duration: number;
instrumental: boolean;
plainLyrics: string;
syncedLyrics: string;
}[];
4 changes: 2 additions & 2 deletions src/plugins/synced-lyrics/providers/LyricsGenius.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ export const LyricsGenius: LyricProvider = {
name: 'Genius',
baseUrl: 'https://genius.com',

async search({ title, artist, album, songDuration }) {
async search({ title, artist }) {
// Only supports plain lyrics, not synced, for now it won't be used.
return null;
throw new Error('Not implemented');

const query = new URLSearchParams({
q: `${artist} ${title}`,
Expand Down
80 changes: 16 additions & 64 deletions src/plugins/synced-lyrics/providers/Megalobiz.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LineLyrics, LyricProvider } from '@/plugins/synced-lyrics/types';
import { LyricProvider } from '@/plugins/synced-lyrics/types';
import { jaroWinkler } from '@skyra/jaro-winkler';
import { config } from '../renderer/renderer';
import { LRC } from '../parsers/lrc';

const removeNoise = (text: string) => {
return text
Expand All @@ -12,32 +12,6 @@ const removeNoise = (text: string) => {
.replace(/\s+by$/, '');
};

// TODO: Use an LRC parser instead of this.
function extractTimeAndText(line: string, index: number): LineLyrics | null {
const groups = /\[(\d+):(\d+)\.(\d+)\](.+)/.exec(line);
if (!groups) return null;

const [, rMinutes, rSeconds, rMillis, text] = groups;
const [minutes, seconds, millis] = [
parseInt(rMinutes),
parseInt(rSeconds),
parseInt(rMillis),
];

// prettier-ignore
const timeInMs = (minutes * 60 * 1000) + (seconds * 1000) + millis;

// prettier-ignore
return {
index,
timeInMs,
time: `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}:${millis}`,
text: text?.trim() ?? config()!.defaultTextString,
status: 'upcoming',
duration: 0,
};
}

export const Megalobiz = {
name: 'Megalobiz',
baseUrl: 'https://www.megalobiz.com',
Expand All @@ -51,31 +25,28 @@ export const Megalobiz = {

const response = await fetch(`${this.baseUrl}/search/all?${query}`);
if (!response.ok) {
throw new Error('Failed to fetch lyrics');
throw new Error(response.statusText);
}

const data = await response.text();
const searchDoc = this.domParser.parseFromString(data, 'text/html');

// prettier-ignore
const searchResults = Array.prototype.map
.call(
searchDoc.querySelectorAll(
`a.entity_name[href^="/lrc/maker/"][name][title]`,
),
.call(searchDoc.querySelectorAll(`a.entity_name[href^="/lrc/maker/"][name][title]`),
(anchor: HTMLAnchorElement) => {
const { minutes, seconds, millis } = anchor
.getAttribute('title')!
.match(
/\[(?<minutes>\d+):(?<seconds>\d+)\.(?<millis>\d+)\]/,
)!.groups!;
.match(/\[(?<minutes>\d+):(?<seconds>\d+)\.(?<millis>\d+)\]/)!
.groups!;

let name = anchor.getAttribute('name')!;

// prettier-ignore
const artists = [
removeNoise(name.match(/\(?[Ff]eat\. (.+)\)?/)?.[1] ?? ""),
...(removeNoise(name).match(/(?<artists>.*?) [-•] (?<title>.*)/)?.groups?.artists?.split(/[&,]/)?.map(removeNoise) ?? []),
...(removeNoise(name).match(/(?<title>.*) by (?<artists>.*)/)?.groups?.artists?.split(/[&,]/)?.map(removeNoise) ?? []),
].filter(Boolean);
removeNoise(name.match(/\(?[Ff]eat\. (.+)\)?/)?.[1] ?? ""),
...(removeNoise(name).match(/(?<artists>.*?) [-•] (?<title>.*)/)?.groups?.artists?.split(/[&,]/)?.map(removeNoise) ?? []),
...(removeNoise(name).match(/(?<title>.*) by (?<artists>.*)/)?.groups?.artists?.split(/[&,]/)?.map(removeNoise) ?? []),
].filter(Boolean);

for (const artist of artists) {
name = name.replace(artist, '');
Expand Down Expand Up @@ -114,34 +85,15 @@ export const Megalobiz = {

const html = await fetch(`${this.baseUrl}${closestResult.href}`).then((r) => r.text());
const lyricsDoc = this.domParser.parseFromString(html, 'text/html');
const lyrics = lyricsDoc.querySelector(`span[id^="lrc_"][id$="_lyrics"]`)?.textContent?.split('\n');
if (!lyrics?.length) throw new Error('Failed to extract lyrics from page.');

lyrics.unshift('[0:0.0] ');

const syncedLyricList = lyrics.reduce<LineLyrics[]>((acc, line, index) => {
const syncedLine = extractTimeAndText(line, index);
if (syncedLine) {
acc.push(syncedLine);
}
const raw = lyricsDoc.querySelector(`span[id^="lrc_"][id$="_lyrics"]`)?.textContent;
if (!raw) throw new Error('Failed to extract lyrics from page.');

return acc;
}, []).sort((a, b) => a.index - b.index);

for (const line of syncedLyricList) {
const next = syncedLyricList[syncedLyricList.indexOf(line) + 1];
if (!next) {
line.duration = Infinity;
break;
}

line.duration = next.timeInMs - line.timeInMs;
}
const lyrics = LRC.parse(raw);

return {
title: closestResult.title,
artists: closestResult.artists,
lines: syncedLyricList,
lines: lyrics.lines.map((l) => ({ ...l, status: 'upcoming' })),
};
},
} as LyricProvider & { domParser: DOMParser };
Expand Down
Loading

0 comments on commit 3d46f07

Please sign in to comment.