Skip to content

Commit

Permalink
Added full translation support for lyrics.
Browse files Browse the repository at this point in the history
- Updated app deps.
- Updated the underlying lyrics parsing process.
- Fixed a bug where lyric metadata are lost when saving lyrics from lrclib to lrc files.
- Original lyric will now be shown along with the translated lyric.
  • Loading branch information
Sandakan committed Sep 5, 2024
1 parent b6258b0 commit 5806c9c
Show file tree
Hide file tree
Showing 27 changed files with 936 additions and 491 deletions.
36 changes: 10 additions & 26 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,31 +1,6 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
},
{
"name": "Main - attach",
"port": 5858,
Expand All @@ -41,12 +16,21 @@
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/out/**/*.js", "!**/node_modules/**"]
},
{
"name": "Renderer - Attach",
"port": 9223,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}",
"showAsyncStacks": true,
"sourceMaps": true
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"configurations": ["Renderer - Attach", "Main - attach"],
"presentation": {
"order": 1
}
Expand Down
576 changes: 343 additions & 233 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@tanstack/react-query": "^5.52.2",
"@tanstack/react-query-devtools": "^5.52.2",
"@tanstack/react-virtual": "^3.10.4",
"@vitalets/google-translate-api": "^9.2.0",
"didyoumean2": "^7.0.2",
Expand Down Expand Up @@ -136,7 +138,7 @@
"eslint-plugin-react-refresh": "^0.4.6",
"husky": "^9.0.11",
"jest": "^29.7.0",
"material-symbols": "^0.22.0",
"material-symbols": "^0.22.2",
"postcss": "^8.4.35",
"prettier": "^3.2.4",
"prettier-plugin-tailwindcss": "^0.6.1",
Expand Down
73 changes: 41 additions & 32 deletions src/@types/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { DropdownOption } from '../renderer/src/components/Dropdown';
import { api } from '../preload';
import { LastFMSessionData } from './last_fm_api';
import { SimilarArtist, Tag } from './last_fm_artist_info_api';
import { ElectronAPI } from '@electron-toolkit/preload';
import { resources } from 'src/renderer/src/i18n';
import { Presence } from 'discord-rpc-revamp';

Expand Down Expand Up @@ -87,7 +86,7 @@ declare global {
albumArtists?: { artistId: string; name: string }[];
bitrate?: number;
trackNo?: number;
diskNo?: number;
discNo?: number;
noOfChannels?: number;
year?: number;
sampleRate?: number;
Expand Down Expand Up @@ -239,51 +238,61 @@ declare global {

type AutomaticallySaveLyricsTypes = 'SYNCED' | 'SYNCED_OR_UN_SYNCED' | 'NONE';

type LyricsTypes = 'SYNCED' | 'UN_SYNCED' | 'ANY';
type LyricsTypes = 'ENHANCED_SYNCED' | 'SYNCED' | 'UN_SYNCED' | 'ANY';

type LyricsRequestTypes = 'ONLINE_ONLY' | 'OFFLINE_ONLY' | 'ANY';

type LyricsSource = 'IN_SONG_LYRICS' | 'MUSIXMATCH' | string;

interface LyricsRequestTrackInfo {
songTitle: string;
songArtists?: string[];
album?: string;
songPath: string;
duration: number;
}

interface SongLyrics {
title: string;
source: LyricsSource;
lyricsType: LyricsTypes;
link?: string;
lyrics: LyricsData;
lang?: string;
copyright?: string;
isOfflineLyricsAvailable: boolean;
isTranslated: boolean;
}

export type SyncedLyricsLineText = {
export type SyncedLyricsLineWord = {
text: string;
start: number;
end: number;
unparsedText: string;
}[];
interface SyncedLyricLine {
text: string | SyncedLyricsLineText;
start: number;
end: number;
};

// Represents a single translation of a lyric line in a specific language
interface TranslatedLyricLine {
lang: string; // Language code of the translation
text: string | SyncedLyricsLineWord[]; // Translated text or synced lyrics
}

// Represents a single line of lyrics, either synced or unsynced
interface LyricLine {
originalText: string | SyncedLyricsLineWord[]; // Original text of the lyric line
translatedTexts: TranslatedLyricLine[]; // Array of translations in different languages
start?: number; // Timing start (for synced lyrics only)
end?: number; // Timing end (for synced lyrics only)
isEnhancedSynced: boolean; // Indicates if the original text is enhanced synced lyrics
}

// Holds all the lyrics data, whether synced or unsynced
interface LyricsData {
isSynced: boolean;
lyrics: string[];
syncedLyrics?: SyncedLyricLine[];
isTranslated: boolean;
parsedLyrics: LyricLine[]; // Array of original lyric lines (both synced and unsynced
unparsedLyrics: string;
offset?: number;
originalLanguage?: string; // Language of the original lyrics (optional)
translatedLanguages?: string[]; // Array of language codes of the translated lyrics (optional)
copyright?: string;
offset: number;
}

interface SongLyrics {
title: string;
source: LyricsSource;
lyricsType: LyricsTypes;
link?: string;
lyrics: LyricsData; // original and translated lyrics data
isOfflineLyricsAvailable: boolean;
}

interface LyricsRequestTrackInfo {
songTitle: string;
songArtists?: string[];
album?: string;
songPath: string;
duration: number;
}

// node-id3 synchronisedLyrics types.
Expand Down
2 changes: 1 addition & 1 deletion src/common/isLyricsSynced.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const syncedLyricsRegex =
/^(\[(?:la:)?(?<lang>\w{2,3})])?(?<timestamp>\[\d+:\d{1,2}\.\d{1,3}])(\[(?:la:)?(?:\w{2,3})])?(?=(?<lyric>.+$))/gm;
/^(\[(?:lang:)?(?<lang>\w{2,3})])?(?<timestamp>\[\d+:\d{1,2}\.\d{1,3}])(\[(?:lang:)?(?:\w{2,3})])?(?=(?<lyric>.+$))/gm;
export const extendedSyncedLyricsLineRegex =
// eslint-disable-next-line no-useless-escape
/(?<extSyncTimeStamp>[\[<]\d+:\d{1,2}\.\d{1,3}[\]>]) ?(?=(?<lyric>[^<>\n]+))/gm;
Expand Down
14 changes: 8 additions & 6 deletions src/main/core/convertParsedLyricsToNodeID3Format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ const convertParsedLyricsToNodeID3Format = (
prevSyncedLyrics: SynchronisedLyrics = []
): SynchronisedLyrics => {
try {
if (parsedLyrics && parsedLyrics.isSynced && parsedLyrics.syncedLyrics) {
const { syncedLyrics, copyright } = parsedLyrics;
if (parsedLyrics && parsedLyrics.isSynced) {
const { parsedLyrics: syncedLyrics, copyright } = parsedLyrics;
const synchronisedText = syncedLyrics.map((line) => {
const { originalText, start = 0 } = line;

const text =
typeof line.text === 'string'
? line.text
: line.text.map((x) => x.unparsedText).join(' ');
typeof originalText === 'string'
? originalText
: originalText.map((x) => x.unparsedText).join(' ');

return {
text,
// to convert seconds to milliseconds
timeStamp: Math.round(line.start * 1000)
timeStamp: Math.round(start * 1000)
};
});
// lyrics metadata like copyright info is stored on the shortText.
Expand Down
8 changes: 3 additions & 5 deletions src/main/core/getSongLyrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,7 @@ const getSongLyrics = async (
source: 'IN_SONG_LYRICS',
lyricsType: type,
lyrics: offlineLyrics,
isOfflineLyricsAvailable,
isTranslated: false
isOfflineLyricsAvailable
};
return cachedLyrics;
}
Expand All @@ -332,8 +331,7 @@ const getSongLyrics = async (
if (onlineLyrics) {
cachedLyrics = {
...onlineLyrics,
isOfflineLyricsAvailable,
isTranslated: false
isOfflineLyricsAvailable
};

if (saveLyricsAutomatically !== 'NONE')
Expand All @@ -349,7 +347,7 @@ const getSongLyrics = async (
if (lyricsType !== 'SYNCED') {
const unsyncedLyrics = await fetchUnsyncedLyrics(songTitle, songArtists);
if (unsyncedLyrics) {
cachedLyrics = { ...unsyncedLyrics, isOfflineLyricsAvailable, isTranslated: false };
cachedLyrics = { ...unsyncedLyrics, isOfflineLyricsAvailable };

if (saveLyricsAutomatically === 'SYNCED_OR_UN_SYNCED')
await saveLyricsAutomaticallyIfAsked(
Expand Down
117 changes: 106 additions & 11 deletions src/main/core/saveLyricsToLrcFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,118 @@ import fs from 'fs/promises';

import log from '../log';
import { getUserData } from '../filesystem';
import { version } from '../../../package.json';
import {
getAlbumFromLyricsString,
getArtistFromLyricsString,
getCopyrightInfoFromLyricsString,
getDurationFromLyricsString,
getLanguageFromLyricsString,
getOffsetFromLyricsString,
getTitleFromLyricsString
} from '../utils/parseLyrics';

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

const title = getTitleFromLyricsString(unparsedLyrics) || songLyrics.title;
const artist = getArtistFromLyricsString(unparsedLyrics);
const album = getAlbumFromLyricsString(unparsedLyrics);
const lang = getLanguageFromLyricsString(unparsedLyrics) || songLyrics.lyrics.originalLanguage;
const length = getDurationFromLyricsString(unparsedLyrics);
const offset = getOffsetFromLyricsString(unparsedLyrics) || songLyrics.lyrics.offset;
const copyright = getCopyrightInfoFromLyricsString(unparsedLyrics) || songLyrics.lyrics.copyright;

return {
title,
artist,
album,
lang,
length,
offset,
copyright
};
};

const convertSecondsToLrcTime = (seconds: number) => {
const time = {
minutes: Math.floor(seconds / 60),
seconds: Math.floor(seconds % 60),
hundredths: Math.floor((seconds % 1) * 100)
};
const lrcTime = `${time.minutes >= 10 ? time.minutes : `0${time.minutes}`}:${
time.seconds.toString().length > 1 ? time.seconds : `0${time.seconds}`
}.${time.hundredths}`;

return { time, lrcTime };
};

const generateLrcLyricLine = (text: string | SyncedLyricsLineWord[], start = 0, lang?: string) => {
const language = lang ? `[lang:${lang}]` : '';

if (typeof text === 'string') {
// lyrics is either synced or unsynced
if (typeof start === 'number') {
// lyrics is synced
const { lrcTime } = convertSecondsToLrcTime(start);

return `[${lrcTime}]${language} ${text || '♪'}`;
}
// lyrics is unsynced
return `${language} text` || '♪';
}

// lyrics is enhanced synced
const words: string[] = [];

const { lrcTime } = convertSecondsToLrcTime(start);
words.push(`[${lrcTime}]${language}`);

for (const word of text) {
const { lrcTime } = convertSecondsToLrcTime(word.start);
words.push(`<${lrcTime}> ${word.text}`);
}

return words.join(' ');
};

export const getLrcLyricLinesFromParsedLyrics = (parsedLyrics: LyricLine[]) => {
const lines: string[] = [];

for (const lyric of parsedLyrics) {
const { originalText, translatedTexts, start = 0 } = lyric;

lines.push(generateLrcLyricLine(originalText, start));
for (const translatedText of translatedTexts) {
lines.push(generateLrcLyricLine(translatedText.text, start, translatedText.lang));
}
}

return lines;
};

const convertLyricsToLrcFormat = (songLyrics: SongLyrics) => {
// const { title, lyrics } = songLyrics;
// const output: string[] = [];
const { lyrics } = songLyrics;
const { parsedLyrics } = lyrics;

const lyricsArr: string[] = [];

const { title, artist, album, length, offset, copyright } = getLrcLyricsMetadata(songLyrics);

// output.push(`[ti:${title}]`);
// output.push(`[length:${min}:${sec}]`);
lyricsArr.push(`[re:Nora (https://github.com/Sandakan/Nora)]`);
lyricsArr.push(`[ve:${version}]`);
lyricsArr.push(`[ti:${title}]`);

// if (artists && artists?.length > 0)
// metadataLines.push(`[ar:${artists.map((x) => x.name).join(', ')}]`);
// if (album) metadataLines.push(`[al:${album.name}]`);
// metadataLines.push('');
if (artist) lyricsArr.push(`[ar:${artist}]`);
if (album) lyricsArr.push(`[al:${album}]`);
if (typeof length === 'number')
lyricsArr.push(`[length:${Math.floor(length / 60)}:${length % 60}]`);
if (typeof offset === 'number') lyricsArr.push(`[offset:${offset}]`);
if (copyright) lyricsArr.push(`[copyright:${copyright}]`);

// const unparsedLines = lyrics.unparsedLyrics.split('\n');
// output.push(...unparsedLines);
lyricsArr.push(...getLrcLyricLinesFromParsedLyrics(parsedLyrics));

return songLyrics.lyrics.unparsedLyrics;
return lyricsArr.join('\n');
};

const getLrcFileSaveDirectory = (songPathWithoutProtocol: string, lrcFileName: string) => {
Expand Down
Loading

0 comments on commit 5806c9c

Please sign in to comment.