Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
7ed663d
feat(i18n): add strings for enhanced line effect and empty line symbo…
robroid Sep 13, 2025
7e35f88
feat(synced-lyrics): update defaults and add author
robroid Sep 13, 2025
9d9e1f2
feat(synced-lyrics): add enhanced line effect option and toggle for e…
robroid Sep 13, 2025
3107ee2
style(synced-lyrics): refine font family and improve empty line & fad…
robroid Sep 13, 2025
bd2c030
feat(synced-lyrics): add showEmptyLineSymbols config and enhanced lin…
robroid Sep 13, 2025
07eb2ad
fix(synced-lyrics): improve LRC parser to handle variable millisecond…
robroid Sep 13, 2025
e54505a
fix(synced-lyrics): append trailing empty line in synced lyrics when …
robroid Sep 13, 2025
e09bf99
fix(synced-lyrics): ensure final empty line is added to Genius lyrics…
robroid Sep 13, 2025
fc1ffc6
fix(synced-lyrics): merge consecutive empty lines in MusixMatch lyrics
robroid Sep 13, 2025
9bf0fe8
fix(synced-lyrics): ensure final empty line exists in YTMusic synced …
robroid Sep 13, 2025
db660af
feat(synced-lyrics): support enhanced effect, precise timing, and fin…
robroid Sep 13, 2025
6c84a3b
feat(synced-lyrics): add getSeekTime and SFont utils
robroid Sep 13, 2025
082c7a4
feat(synced-lyrics): improve SyncedLine rendering and precise timing
robroid Sep 13, 2025
3e8da09
refactor(synced-lyrics): use ProviderNames enum instead of hardcoded …
robroid Sep 14, 2025
54751ac
feat(synced-lyrics): enable display of empty line symbols by default
robroid Sep 19, 2025
01dca62
feat(synced-lyrics): enhance styling with hover effects, empty line h…
robroid Sep 19, 2025
5ed0062
fix(synced-lyrics): correct millisToTime calculation using modulo for…
robroid Sep 19, 2025
6e68a3f
refactor(synced-lyrics): rename EmptyLine class to synced-emptyline a…
robroid Sep 19, 2025
6448674
feat(synced-lyrics): add enhanced scroll animation and hover styles t…
robroid Sep 19, 2025
e29a72b
feat(synced-lyrics): trigger fast scroll on provider switch and impro…
robroid Sep 19, 2025
fd901b9
fix(synced-lyrics): merge consecutive empty lines and improve time pa…
robroid Sep 19, 2025
64947e2
refactor(synced-lyrics): optimize artist matching and merge consecuti…
robroid Sep 19, 2025
098f778
fix(synced-lyrics): improve scroll stability with fallback index and …
robroid Sep 19, 2025
f797783
Merge branch 'ytmd-devs:master' into master
robroid Oct 2, 2025
9c04c95
feat(i18n): add strings for enhanced line effect and empty line symbo…
robroid Sep 13, 2025
f2a5c81
feat(synced-lyrics): update defaults and add author
robroid Sep 13, 2025
79b3be5
feat(synced-lyrics): add enhanced line effect option and toggle for e…
robroid Sep 13, 2025
402178a
style(synced-lyrics): refine font family and improve empty line & fad…
robroid Sep 13, 2025
b8b8fea
feat(synced-lyrics): add showEmptyLineSymbols config and enhanced lin…
robroid Sep 13, 2025
96a39ab
fix(synced-lyrics): improve LRC parser to handle variable millisecond…
robroid Sep 13, 2025
e2278bf
fix(synced-lyrics): append trailing empty line in synced lyrics when …
robroid Sep 13, 2025
4b352a5
fix(synced-lyrics): ensure final empty line is added to Genius lyrics…
robroid Sep 13, 2025
fae9545
fix(synced-lyrics): merge consecutive empty lines in MusixMatch lyrics
robroid Sep 13, 2025
a1dd9fe
fix(synced-lyrics): ensure final empty line exists in YTMusic synced …
robroid Sep 13, 2025
da20ab9
feat(synced-lyrics): support enhanced effect, precise timing, and fin…
robroid Sep 13, 2025
53b5751
feat(synced-lyrics): add getSeekTime and SFont utils
robroid Sep 13, 2025
097cc93
feat(synced-lyrics): improve SyncedLine rendering and precise timing
robroid Sep 13, 2025
0b5e494
refactor(synced-lyrics): use ProviderNames enum instead of hardcoded …
robroid Sep 14, 2025
5e8a003
feat(synced-lyrics): enable display of empty line symbols by default
robroid Sep 19, 2025
7f0bd97
feat(synced-lyrics): enhance styling with hover effects, empty line h…
robroid Sep 19, 2025
920afd1
fix(synced-lyrics): correct millisToTime calculation using modulo for…
robroid Sep 19, 2025
1c67dfe
refactor(synced-lyrics): rename EmptyLine class to synced-emptyline a…
robroid Sep 19, 2025
c870c94
feat(synced-lyrics): add enhanced scroll animation and hover styles t…
robroid Sep 19, 2025
e17b82b
feat(synced-lyrics): trigger fast scroll on provider switch and impro…
robroid Sep 19, 2025
d917021
fix(synced-lyrics): merge consecutive empty lines and improve time pa…
robroid Sep 19, 2025
aedf1f1
refactor(synced-lyrics): optimize artist matching and merge consecuti…
robroid Sep 19, 2025
5de5cf7
fix(synced-lyrics): improve scroll stability with fallback index and …
robroid Sep 19, 2025
c4ddcdc
Merge branch 'master' of https://github.com/robroid/youtube-music
robroid Oct 2, 2025
a151439
refactor(synced-lyrics): extract empty line handling into shared util…
robroid Oct 2, 2025
8501f03
refactor(synced-lyrics): unify empty line handling
robroid Oct 2, 2025
db0f24c
refactor(synced-lyrics): unify provider switching, add utilities, and…
robroid Oct 2, 2025
d94c96b
Merge branch 'ytmd-devs:master' into master
robroid Oct 3, 2025
749d65a
refactor(synced-lyrics): extract scrolling constants and helpers
robroid Oct 3, 2025
b11c0ab
refactor(synced-lyrics): extract helpers for provider switching and w…
robroid Oct 3, 2025
a455ebf
Merge branch 'ytmd-devs:master' into master
robroid Oct 4, 2025
f22b718
fix(synced-lyrics): migrate old placeholder arrays and unify cumulati…
robroid Oct 4, 2025
ec2ebd9
refactor(synced-lyrics): add helper to simplify end delay logic
robroid Oct 4, 2025
1cf34b7
refactor(synced-lyrics): optimize end delay calculation using memoiza…
robroid Oct 5, 2025
6088531
Merge branch 'pear-devs:master' into master
robroid Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/config/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,42 @@ export type IStore = InstanceType<
>;

const migrations = {
'>=3.12.0'(store: IStore) {
// Synced Lyrics: migrate placeholder defaults
const syncedLyricsConfig = store.get('plugins.synced-lyrics') as
| SyncedLyricsPluginConfig
| undefined;

if (!syncedLyricsConfig) return;

const current = syncedLyricsConfig.defaultTextString as
| string
| string[]
| undefined;
let updatedValue: string | string[] | undefined;

if (Array.isArray(current)) {
const asJson = JSON.stringify(current);
// Migrate progressive sequences to non-cumulative variants
if (asJson === JSON.stringify(['•', '••', '•••'])) {
updatedValue = ['•', '•', '•'];
} else if (asJson === JSON.stringify(['.', '..', '...'])) {
updatedValue = ['.', '.', '.'];
}
} else if (typeof current === 'string') {
// Replace regular space placeholder with NBSP to preserve layout
if (current === ' ') {
updatedValue = '\u00A0';
}
}

if (updatedValue !== undefined) {
store.set('plugins.synced-lyrics', {
...syncedLyricsConfig,
defaultTextString: updatedValue,
} as SyncedLyricsPluginConfig);
}
},
'>=3.10.0'(store: IStore) {
const lyricGeniusConfig = store.get('plugins.lyrics-genius') as
| {
Expand Down
8 changes: 8 additions & 0 deletions src/i18n/resources/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,10 @@
"line-effect": {
"label": "Line effect",
"submenu": {
"enhanced": {
"label": "Enhanced",
"tooltip": "A refined lyric effect for smoother, more enjoyable reading."
},
"fancy": {
"label": "Fancy",
"tooltip": "Use large, app-like effects on the current line"
Expand All @@ -847,6 +851,10 @@
},
"tooltip": "Choose the effect to apply to the current line"
},
"show-empty-line-symbols": {
"label": "Show character between lyrics",
"tooltip": "Choose whether to always display the character between empty lyric lines."
},
"precise-timing": {
"label": "Make the lyrics perfectly synced",
"tooltip": "Calculate to the milisecond the display of the next line (can have a small impact on performance)"
Expand Down
7 changes: 4 additions & 3 deletions src/plugins/synced-lyrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ import type { SyncedLyricsPluginConfig } from './types';
export default createPlugin({
name: () => t('plugins.synced-lyrics.name'),
description: () => t('plugins.synced-lyrics.description'),
authors: ['Non0reo', 'ArjixWasTaken', 'KimJammer', 'Strvm'],
authors: ['Non0reo', 'ArjixWasTaken', 'KimJammer', 'Strvm', 'robroid'],
restartNeeded: true,
addedVersion: '3.5.X',
config: {
enabled: false,
preciseTiming: true,
showLyricsEvenIfInexact: true,
showTimeCodes: false,
defaultTextString: '♪',
lineEffect: 'fancy',
defaultTextString: '•••',
lineEffect: 'enhanced',
showEmptyLineSymbols: true,
romanization: true,
} satisfies SyncedLyricsPluginConfig as SyncedLyricsPluginConfig,

Expand Down
87 changes: 60 additions & 27 deletions src/plugins/synced-lyrics/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,26 @@ export const menu = async (
),
],
},
{
label: t('plugins.synced-lyrics.menu.precise-timing.label'),
toolTip: t('plugins.synced-lyrics.menu.precise-timing.tooltip'),
type: 'checkbox',
checked: config.preciseTiming,
click(item) {
ctx.setConfig({
preciseTiming: item.checked,
});
},
},
{
label: t('plugins.synced-lyrics.menu.line-effect.label'),
toolTip: t('plugins.synced-lyrics.menu.line-effect.tooltip'),
type: 'submenu',
submenu: [
{
label: t(
'plugins.synced-lyrics.menu.line-effect.submenu.enhanced.label',
),
toolTip: t(
'plugins.synced-lyrics.menu.line-effect.submenu.enhanced.tooltip',
),
type: 'radio',
checked: config.lineEffect === 'enhanced',
click() {
ctx.setConfig({
lineEffect: 'enhanced',
});
},
},
{
label: t(
'plugins.synced-lyrics.menu.line-effect.submenu.fancy.label',
Expand Down Expand Up @@ -124,23 +128,52 @@ export const menu = async (
toolTip: t('plugins.synced-lyrics.menu.default-text-string.tooltip'),
type: 'submenu',
submenu: [
{ label: '♪', value: '♪' },
{ label: '" "', value: ' ' },
{ label: '...', value: ['.', '..', '...'] },
{ label: '•••', value: ['•', '••', '•••'] },
{ label: '———', value: '———' },
].map(({ label, value }) => ({
label,
type: 'radio',
checked:
typeof value === 'string'
? config.defaultTextString === value
: JSON.stringify(config.defaultTextString) ===
JSON.stringify(value),
click() {
ctx.setConfig({ defaultTextString: value });
...[
{ label: '•••', value: ['•', '•', '•'] },
{ label: '...', value: ['.', '.', '.'] },
{ label: '♪', value: '♪' },
{ label: '———', value: '———' },
{ label: '(𝑏𝑙𝑎𝑛𝑘)', value: '\u00A0' },
].map(
({ label, value }) =>
({
label,
type: 'radio',
checked:
JSON.stringify(config.defaultTextString) ===
JSON.stringify(value),
enabled: config.showEmptyLineSymbols,
click() {
ctx.setConfig({ defaultTextString: value });
},
}) as const,
),
{ type: 'separator' },
{
label: t('plugins.synced-lyrics.menu.show-empty-line-symbols.label'),
toolTip: t(
'plugins.synced-lyrics.menu.show-empty-line-symbols.tooltip',
),
type: 'checkbox',
checked: config.showEmptyLineSymbols ?? false,
click(item) {
ctx.setConfig({
showEmptyLineSymbols: item.checked,
});
},
},
})),
],
},
{
label: t('plugins.synced-lyrics.menu.precise-timing.label'),
toolTip: t('plugins.synced-lyrics.menu.precise-timing.tooltip'),
type: 'checkbox',
checked: config.preciseTiming,
click(item) {
ctx.setConfig({
preciseTiming: item.checked,
});
},
},
{
label: t('plugins.synced-lyrics.menu.romanization.label'),
Expand Down
36 changes: 21 additions & 15 deletions src/plugins/synced-lyrics/parsers/lrc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
ensureLeadingPaddingEmptyLine,
mergeConsecutiveEmptySyncedLines,
} from '../shared/lines';

interface LRCTag {
tag: string;
value: string;
Expand All @@ -17,7 +22,7 @@ interface LRC {

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

export const LRC = {
parse: (text: string): LRC => {
Expand Down Expand Up @@ -50,13 +55,18 @@ export const LRC = {
}

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

// Normalize: take first 2 digits, pad if only 1 digit
const ms2 = milliseconds.padEnd(2, '0').slice(0, 2);

// Convert to ms (xx → xx0)
const minutesMs = parseInt(minutes) * 60 * 1000;
const secondsMs = parseInt(seconds) * 1000;
const centisMs = parseInt(ms2) * 10;
const timeInMs = minutesMs + secondsMs + centisMs;

const currentLine: LRCLine = {
time: `${minutes}:${seconds}:${milliseconds}`,
time: `${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}.${ms2}`,
timeInMs,
text: text.trim(),
duration: Infinity,
Expand All @@ -74,15 +84,11 @@ export const LRC = {
line.timeInMs += offset;
}

const first = lrc.lines.at(0);
if (first && first.timeInMs > 300) {
lrc.lines.unshift({
time: '0:0:0',
timeInMs: 0,
duration: first.timeInMs,
text: '',
});
}
// leading padding line if the first line starts late
lrc.lines = ensureLeadingPaddingEmptyLine(lrc.lines, 300, 'span');

// Merge consecutive empty lines into a single empty line
lrc.lines = mergeConsecutiveEmptySyncedLines(lrc.lines);

return lrc;
},
Expand Down
Loading