Skip to content

Commit

Permalink
Fixed test fail due to parseLyrics function errors
Browse files Browse the repository at this point in the history
- Moved isLyricsSynced to src/common
- Updated parseLyrics.test.ts file to be compatible with the newer functionality.
  • Loading branch information
Sandakan committed Sep 7, 2024
1 parent 5806c9c commit 2757dc3
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 360 deletions.
24 changes: 14 additions & 10 deletions src/common/isLyricsSynced.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
export const syncedLyricsRegex =
/^(\[(?:lang:)?(?<lang>\w{2,3})])?(?<timestamp>\[\d+:\d{1,2}\.\d{1,3}])(\[(?:lang:)?(?:\w{2,3})])?(?=(?<lyric>.+$))/gm;
export const extendedSyncedLyricsLineRegex =
// matches unsynced, synced, or an enhanced synced lyrics line
export const LYRICS_LINE_REGEX =
/^(?<timestamp>\[\d+:\d{1,2}\.\d{1,3}])?(\[(?:lang:)?(?<lang>\w{2,3})])?(?=(?<lyric>.+$))/gm;

export const SYNCED_LYRICS_REGEX =
/^(\[(?:lang:)?(?<lang>\w{2,3})])?(?<timestamp>\[(?<sec>\d+):(?<ms>\d{1,2}\.\d{1,3})])(\[(?:lang:)?(?:\w{2,3})])?(?=(?<lyric>.+$))/gm;
export const EXTENDED_SYNCED_LYRICS_LINE_REGEX =
// eslint-disable-next-line no-useless-escape
/(?<extSyncTimeStamp>[\[<]\d+:\d{1,2}\.\d{1,3}[\]>]) ?(?=(?<lyric>[^<>\n]+))/gm;
export const extendedSyncedLyricsRegex =
export const EXTENDED_SYNCED_LYRICS_REGEX =
/(?<extSyncTimeStamp><\d+:\d{1,2}\.\d{1,3}>) ?(?=(?<lyric>[^<>\n]+))/gm;

const isLyricsSynced = (lyrics: string) => {
const bool = syncedLyricsRegex.test(lyrics);
syncedLyricsRegex.lastIndex = 0;
const bool = SYNCED_LYRICS_REGEX.test(lyrics);
SYNCED_LYRICS_REGEX.lastIndex = 0;

return bool;
};

export const isLyricsEnhancedSynced = (syncedLyricsString: string) => {
const isEnhancedSynced = extendedSyncedLyricsRegex.test(syncedLyricsString);
extendedSyncedLyricsRegex.lastIndex = 0;
const isEnhancedSynced = EXTENDED_SYNCED_LYRICS_REGEX.test(syncedLyricsString);
EXTENDED_SYNCED_LYRICS_REGEX.lastIndex = 0;

return isEnhancedSynced;
};

export const isAnExtendedSyncedLyricsLine = (line: string) => {
const bool = extendedSyncedLyricsLineRegex.test(line);
extendedSyncedLyricsLineRegex.lastIndex = 0;
const bool = EXTENDED_SYNCED_LYRICS_LINE_REGEX.test(line);
EXTENDED_SYNCED_LYRICS_LINE_REGEX.lastIndex = 0;

return bool;
};
Expand Down
206 changes: 133 additions & 73 deletions src/main/utils/parseLyrics.ts → src/common/parseLyrics.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,82 @@
import { TagConstants } from 'node-id3';
// import log from '../log';
import isLyricsSynced, {
extendedSyncedLyricsLineRegex,
isAnExtendedSyncedLyricsLine,
syncedLyricsRegex
} from '../../common/isLyricsSynced';
EXTENDED_SYNCED_LYRICS_LINE_REGEX,
LYRICS_LINE_REGEX,
SYNCED_LYRICS_REGEX,
isAnExtendedSyncedLyricsLine
} from './isLyricsSynced';

export type SyncedLyricsInput = NonNullable<NodeID3Tags['synchronisedLyrics']>[number];

export const INSTRUMENTAL_LYRIC_IDENTIFIER = '♪';
const titleMatchRegex = /^\[ti:(?<title>.+)\]$/gm;
const artistMatchRegex = /^\[ar:(?<artist>.+)\]$/gm;
const albumMatchRegex = /^\[al:(?<album>.+)\]$/gm;
const durationMatchRegex = /^\[length:(?<duration>.+)\]$/gm;
const langMatchRegex = /^\[lang:(?<lang>.+)\]$/gm;
const copyrightMatchRegex = /^\[copyright:(?<copyright>.+)\]$/gm;
const lyricLinelangMatchRegex = /\[lang:(?<lang>.+)\](.+)$/gm;
const lyricsOffsetRegex = /^\[offset:(?<lyricsOffset>[+-]?\d+)\]$/gm;

const startTimestampMatchRegex = /(?<extSyncTimeStamp>\[\d+:\d{1,2}\.\d{1,3}\])/gm;
const TITLE_MATCH_REGEX = /^\[ti:(?<title>.+)\]$/gm;
const ARTIST_MATCH_REGEX = /^\[ar:(?<artist>.+)\]$/gm;
const ALBUM_MATCH_REGEX = /^\[al:(?<album>.+)\]$/gm;
const DURATION_MATCH_REGEX = /^\[length:(?<duration>.+)\]$/gm;
const LANG_MATCH_REGEX = /^\[lang:(?<lang>.+)\]$/gm;
const COPYRIGHT_MATCH_REGEX = /^\[copyright:(?<copyright>.+)\]$/gm;
const LYRIC_LINE_LANG_MATCH_REGEX = /\[lang:(?<lang>.+)\](.+)$/gm;
const LYRIC_OFFSET_MATCH_REGEX = /^\[offset:(?<lyricsOffset>[+-]?\d+)\]$/gm;
const START_TIMESTAMP_MATCH_REGEX = /(?<extSyncTimeStamp>\[\d+:\d{1,2}\.\d{1,3}\])/gm;

export const getTitleFromLyricsString = (lyricsString: string) => {
const titleMatch = titleMatchRegex.exec(lyricsString);
titleMatchRegex.lastIndex = 0;
const titleMatch = TITLE_MATCH_REGEX.exec(lyricsString);
TITLE_MATCH_REGEX.lastIndex = 0;

return titleMatch?.groups?.title;
};

export const getArtistFromLyricsString = (lyricsString: string) => {
const artistMatch = artistMatchRegex.exec(lyricsString);
artistMatchRegex.lastIndex = 0;
const artistMatch = ARTIST_MATCH_REGEX.exec(lyricsString);
ARTIST_MATCH_REGEX.lastIndex = 0;

return artistMatch?.groups?.artist;
};

export const getAlbumFromLyricsString = (lyricsString: string) => {
const albumMatch = albumMatchRegex.exec(lyricsString);
albumMatchRegex.lastIndex = 0;
const albumMatch = ALBUM_MATCH_REGEX.exec(lyricsString);
ALBUM_MATCH_REGEX.lastIndex = 0;

return albumMatch?.groups?.album;
};

export const getDurationFromLyricsString = (lyricsString: string) => {
const durationMatch = durationMatchRegex.exec(lyricsString);
durationMatchRegex.lastIndex = 0;
const durationMatch = DURATION_MATCH_REGEX.exec(lyricsString);
DURATION_MATCH_REGEX.lastIndex = 0;

return durationMatch?.groups?.duration;
};

export const getCopyrightInfoFromLyricsString = (lyricsString: string) => {
const copyrightMatch = copyrightMatchRegex.exec(lyricsString);
copyrightMatchRegex.lastIndex = 0;
const copyrightMatch = COPYRIGHT_MATCH_REGEX.exec(lyricsString);
COPYRIGHT_MATCH_REGEX.lastIndex = 0;

return copyrightMatch?.groups?.copyright;
};

export const getLanguageFromLyricsString = (lyricsString: string, regex = langMatchRegex) => {
export const getLanguageFromLyricsString = (lyricsString: string, regex = LANG_MATCH_REGEX) => {
const langMatch = regex.exec(lyricsString);
regex.lastIndex = 0;

return langMatch?.groups?.lang?.toLowerCase();
};

export const getOffsetFromLyricsString = (lyricsString: string) => {
const offsetMatch = lyricsOffsetRegex.exec(lyricsString);
lyricsOffsetRegex.lastIndex = 0;
const offsetMatch = LYRIC_OFFSET_MATCH_REGEX.exec(lyricsString);
LYRIC_OFFSET_MATCH_REGEX.lastIndex = 0;

return Number(offsetMatch?.groups?.lyricsOffset || 0) / 1000;
};

const getSecondsFromLyricsLine = (lyric: string) => {
const lyricsStartMatch = lyric.match(syncedLyricsRegex);
syncedLyricsRegex.lastIndex = 0;
const replaceRegex = /[[\]]/gm;
if (Array.isArray(lyricsStartMatch)) {
const [sec, ms] = lyricsStartMatch[0].replaceAll(replaceRegex, '').split(':');
replaceRegex.lastIndex = 0;

return parseInt(sec) * 60 + parseFloat(ms);
const lyricsStartMatch = SYNCED_LYRICS_REGEX.exec(lyric);
SYNCED_LYRICS_REGEX.lastIndex = 0;

if (lyricsStartMatch && lyricsStartMatch?.groups) {
const { sec, ms } = lyricsStartMatch.groups;

return parseInt(sec.trim()) * 60 + parseFloat(ms.trim());
}
return 0;
};
Expand All @@ -92,14 +91,11 @@ const getSecondsFromExtendedTimeStamp = (text: string) => {
return parseInt(sec) * 60 + parseFloat(ms);
};

const getLyricEndTime = (lyricsArr: string[], index: number, start: number) => {
const getLyricEndTime = (lyricsArr: string[], index: number) => {
if (lyricsArr.length - 1 === index) return Number.POSITIVE_INFINITY;

if (lyricsArr[index + 1]) {
const end = getSecondsFromLyricsLine(lyricsArr[index + 1]);

if (start === end && lyricsArr[index + 2])
return getSecondsFromLyricsLine(lyricsArr[index + 2]);
return end;
}

Expand Down Expand Up @@ -133,8 +129,8 @@ const getExtendedSyncedLineInfo = (
lineEndTime: number
): string | SyncedLyricsLineWord[] => {
try {
const matches = [...line.matchAll(extendedSyncedLyricsLineRegex)];
extendedSyncedLyricsLineRegex.lastIndex = 0;
const matches = [...line.matchAll(EXTENDED_SYNCED_LYRICS_LINE_REGEX)];
EXTENDED_SYNCED_LYRICS_LINE_REGEX.lastIndex = 0;

if (matches.length > 0) {
const extendedSyncLines: SyncedLyricsLineWord[] = matches.map((match, index, arr) => {
Expand Down Expand Up @@ -167,8 +163,8 @@ const getExtendedSyncedLineInfo = (
};

const parseLyricsText = (line: string, lineEndTime: number): string | SyncedLyricsLineWord[] => {
const textLine = line.replaceAll(syncedLyricsRegex, '').trim();
syncedLyricsRegex.lastIndex = 0;
const textLine = line.replaceAll(SYNCED_LYRICS_REGEX, '').trim();
SYNCED_LYRICS_REGEX.lastIndex = 0;

const isAnExtendedSyncedLine = isAnExtendedSyncedLyricsLine(textLine);

Expand All @@ -178,30 +174,73 @@ const parseLyricsText = (line: string, lineEndTime: number): string | SyncedLyri
};

const isNotALyricsMetadataLine = (line: string) => !/^\[\w+:.{1,}\]$/gm.test(line);
const groupLyricsByTime = (lyricsLines: string[]) => {
const groupedLines: { [key: string]: string[] } = {};
lyricsLines.forEach((line) => {
const time = getSecondsFromLyricsLine(line).toString();
if (!groupedLines[time]) groupedLines[time] = [];
groupedLines[time].push(line);
});

return Object.entries(groupedLines).map(([start, lines]) => {
const partiallyParseLrcLyricLine = (line: string) => {
const match = LYRICS_LINE_REGEX.exec(line);
LYRICS_LINE_REGEX.lastIndex = 0;

if (match?.groups) {
const { timestamp, lang, lyric } = match.groups;

return {
start,
originalText: lines[0],
translatedText: lines.length > 1 ? lines.slice(1) : []
input: line,
timestamp,
lang,
lyric
};
});
}

return undefined;
};

const groupOriginalAndTranslatedLyricLines = (lyricsLines: string[], isSynced: boolean) => {
const partiallyParsedLines = lyricsLines
.map((line) => partiallyParseLrcLyricLine(line))
.filter((line) => line !== undefined);

if (isSynced) {
const groupedLines: { [key: string]: typeof partiallyParsedLines } = {};

partiallyParsedLines.forEach((line) => {
const time = getSecondsFromLyricsLine(line.input).toString();

if (!groupedLines[time]) groupedLines[time] = [];
groupedLines[time].push(line);
});

return Object.entries(groupedLines).map(([, lines]) => {
return {
original: lines[0],
translated: lines.length > 1 ? lines.slice(1) : []
};
});
} else {
const groupedLines: { [key: string]: typeof partiallyParsedLines } = {};
let lineCount = -1;

partiallyParsedLines.forEach((line) => {
if (line.lang === undefined) lineCount++;

if (!groupedLines[lineCount]) groupedLines[lineCount] = [];
groupedLines[lineCount].push(line);
});

return Object.entries(groupedLines).map(([, lines]) => {
return {
original: lines[0],
translated: lines.length > 1 ? lines.slice(1) : []
};
});
}
};

const parseTranslatedLyricsText = (lines: string[]): TranslatedLyricLine[] => {
return lines.map((line, i): TranslatedLyricLine => {
const start = getSecondsFromLyricsLine(line);
const end = getLyricEndTime(lines, i, start);
// const start = getSecondsFromLyricsLine(line);
const end = getLyricEndTime(lines, i);

return {
lang: getLanguageFromLyricsString(line, lyricLinelangMatchRegex) || 'en',
lang: getLanguageFromLyricsString(line, LYRIC_LINE_LANG_MATCH_REGEX) || 'en',
text: parseLyricsText(line, end)
};
});
Expand All @@ -224,35 +263,54 @@ const parseLyrics = (lrcString: string): LyricsData => {
.split('\n')
.filter((line) => line.trim() !== '' && isNotALyricsMetadataLine(line));

const groupedLines = groupLyricsByTime(lines);
const originalLyricsLines = groupedLines.map((line) => line.originalText);
const groupedLines = groupOriginalAndTranslatedLyricLines(lines, output.isSynced);
const originalLyricsLines = groupedLines.map((line) => line.original.input);

output.parsedLyrics = groupedLines.map((line, index) => {
const start = getSecondsFromLyricsLine(line.originalText);
const end = getLyricEndTime(originalLyricsLines, index, start);
if (output.isSynced) {
const start = getSecondsFromLyricsLine(line.original.input);
const end = getLyricEndTime(originalLyricsLines, index);

const parsedLine = parseLyricsText(line.originalText, end);
const isEnhancedSynced = typeof parsedLine !== 'string';
const parsedLine = parseLyricsText(line.original.input, end);
const isEnhancedSynced = typeof parsedLine !== 'string';

const translatedTexts = parseTranslatedLyricsText(line.translatedText);
if (translatedTexts.length > 0) output.isTranslated = true;
const translatedTexts = parseTranslatedLyricsText(line.translated.map((t) => t.input));
if (translatedTexts.length > 0) output.isTranslated = true;

translatedTexts.forEach((t) => {
translatedTexts.forEach((t) => {
if (output.translatedLanguages && !output.translatedLanguages.includes(t.lang))
output.translatedLanguages.push(t.lang);
});

return {
originalText: parsedLine,
translatedTexts,
isEnhancedSynced,
start,
end
};
}

const translatedTexts = line.translated.map((t) => {
if (output.translatedLanguages && !output.translatedLanguages.includes(t.lang))
output.translatedLanguages.push(t.lang);

return { lang: t.lang, text: t.lyric.trim() };
});
if (translatedTexts.length > 0) output.isTranslated = true;

return {
originalText: parsedLine,
originalText: line.original.lyric.trim(),
translatedTexts,
isEnhancedSynced,
start,
end
isEnhancedSynced: false,
start: undefined,
end: undefined
};
});

return output;
};

const parseMetadataFromShortText = (shortText?: string) => {
const metadata: LyricsMetadataFromShortText = { copyright: undefined };

Expand Down Expand Up @@ -326,8 +384,8 @@ export const parseSyncedLyricsFromAudioDataSource = (
? text
: text
.map((x) => {
startTimestampMatchRegex.lastIndex = 0;
if (startTimestampMatchRegex.test(x.unparsedText)) return x.text;
START_TIMESTAMP_MATCH_REGEX.lastIndex = 0;
if (START_TIMESTAMP_MATCH_REGEX.test(x.unparsedText)) return x.text;
return x.unparsedText;
})
.join(' ');
Expand All @@ -349,6 +407,8 @@ export const parseSyncedLyricsFromAudioDataSource = (
copyright: metadata.copyright,
parsedLyrics: lyrics,
unparsedLyrics,
originalLanguage: undefined,
translatedLanguages: [],
offset: 0
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/core/getSongLyrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import log from '../log';
import { checkIfConnectedToInternet, sendMessageToRenderer } from '../main';
import fetchLyricsFromMusixmatch from '../utils/fetchLyricsFromMusixmatch';
import { appPreferences } from '../../../package.json';
import parseLyrics, { parseSyncedLyricsFromAudioDataSource } from '../utils/parseLyrics';
import parseLyrics, { parseSyncedLyricsFromAudioDataSource } from '../../common/parseLyrics';
import saveLyricsToSong from '../saveLyricsToSong';
import { decrypt } from '../utils/safeStorage';
import fetchLyricsFromLrclib from '../utils/fetchLyricsFromLrclib';
Expand Down
2 changes: 1 addition & 1 deletion src/main/core/saveLyricsToLrcFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
getLanguageFromLyricsString,
getOffsetFromLyricsString,
getTitleFromLyricsString
} from '../utils/parseLyrics';
} from '../../common/parseLyrics';

export const getLrcLyricsMetadata = (songLyrics: SongLyrics) => {
const { unparsedLyrics } = songLyrics.lyrics;
Expand Down
Loading

0 comments on commit 2757dc3

Please sign in to comment.