Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
55619ac
Added a feature to try to detect the language of lyrics. The language…
Feb 16, 2026
8a09ba2
Update src/plugins/synced-lyrics/menu.ts
halleck1 Feb 16, 2026
87dbf9b
Update src/plugins/synced-lyrics/menu.ts
halleck1 Feb 16, 2026
8edca83
Update src/plugins/synced-lyrics/menu.ts
halleck1 Feb 16, 2026
17debb1
Update src/plugins/synced-lyrics/menu.ts
halleck1 Feb 16, 2026
74e04f4
Update src/plugins/synced-lyrics/menu.ts
halleck1 Feb 16, 2026
eda4d29
Update src/plugins/synced-lyrics/renderer/renderer.tsx
halleck1 Feb 16, 2026
0a0bc91
Update src/plugins/synced-lyrics/renderer/store.ts
halleck1 Feb 16, 2026
3312453
Update src/plugins/synced-lyrics/renderer/store.ts
halleck1 Feb 16, 2026
07243b4
Update src/plugins/synced-lyrics/renderer/renderer.tsx
halleck1 Feb 16, 2026
5cf97e6
Update src/plugins/synced-lyrics/menu.ts
halleck1 Feb 16, 2026
f51edd4
Fixed Copilot comments on PR - added i18n where needed, added timeout…
Feb 27, 2026
02e5d08
Update src/plugins/synced-lyrics/renderer/renderer.tsx
halleck1 Feb 27, 2026
6d36831
Switched the auto-skip logic to be triggered on selected/any provider…
Mar 2, 2026
f0e257d
Merge branch 'synced-lyrics-skip-languages' of https://github.com/hal…
Mar 2, 2026
ef93a14
Fixed Copilot suggestions about plugin enabling check and reusing toa…
Mar 2, 2026
4a79843
Update src/plugins/synced-lyrics/renderer/renderer.tsx
halleck1 Mar 2, 2026
380461b
Copilot comments fixes
Mar 2, 2026
3039fe2
Merge branch 'synced-lyrics-skip-languages' of https://github.com/hal…
Mar 2, 2026
a789b67
Copilot fixes
Mar 2, 2026
63136e7
Update src/plugins/synced-lyrics/menu.ts
halleck1 Mar 2, 2026
a8649ed
Copilot fixes
Mar 2, 2026
4b7f79a
Update src/plugins/synced-lyrics/renderer/renderer.tsx
halleck1 Mar 2, 2026
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
16 changes: 15 additions & 1 deletion src/i18n/resources/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -909,7 +909,21 @@
}
},
"tooltip": "Convert Chinese character to Traditional or Simplified"
}
},
"auto-skip-languages": {
"label": "Auto-skip languages",
"tooltip": "Automatically skip songs when these languages are detected in lyrics (comma-separated language codes)",
"prompt": {
"title": "Auto-skip languages",
"label": "Enter language codes (comma-separated, e.g., ja,ko,zh):",
"placeholder": "e.g., ja,ko,zh"
}
},
"auto-dislike-skipped-languages": {
"label": "Auto-dislike skipped songs",
"tooltip": "Automatically dislike songs before skipping them when a language from the skip list is detected"
},
"auto-skip-toast": "Auto-skipping song with detected language: {{language}}"
},
"name": "Synced Lyrics",
"refetch-btn": {
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/synced-lyrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export default createPlugin({
defaultTextString: '♪',
lineEffect: 'fancy',
romanization: true,
autoSkipLanguages: '',
autoDislikeSkippedLanguages: false,
} satisfies SyncedLyricsPluginConfig as SyncedLyricsPluginConfig,

menu,
Expand Down
50 changes: 50 additions & 0 deletions src/plugins/synced-lyrics/menu.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import prompt from 'custom-electron-prompt';

import { t } from '@/i18n';

import promptOptions from '@/providers/prompt-options';

import { providerNames } from './providers';

import type { MenuItemConstructorOptions } from 'electron';
Expand Down Expand Up @@ -233,5 +237,51 @@ export const menu = async (
});
},
},
{
label: t('plugins.synced-lyrics.menu.auto-skip-languages.label'),
toolTip: t('plugins.synced-lyrics.menu.auto-skip-languages.tooltip'),
async click() {
const languages =
(await prompt(
{
title: t(
'plugins.synced-lyrics.menu.auto-skip-languages.prompt.title',
),
label: t(
'plugins.synced-lyrics.menu.auto-skip-languages.prompt.label',
),
value: config.autoSkipLanguages || '',
type: 'input',
inputAttrs: {
type: 'text',
placeholder: t(
'plugins.synced-lyrics.menu.auto-skip-languages.prompt.placeholder',
),
},
...promptOptions(),
},
ctx.window,
)) ?? config.autoSkipLanguages;

ctx.setConfig({
autoSkipLanguages: languages as string,
});
},
},
{
label: t(
'plugins.synced-lyrics.menu.auto-dislike-skipped-languages.label',
),
toolTip: t(
'plugins.synced-lyrics.menu.auto-dislike-skipped-languages.tooltip',
),
type: 'checkbox',
checked: config.autoDislikeSkippedLanguages,
click(item) {
ctx.setConfig({
autoDislikeSkippedLanguages: item.checked,
});
},
},
];
};
15 changes: 11 additions & 4 deletions src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,17 @@ export const LyricsPicker = (props: {
/>
</Match>
</Switch>
<yt-formatted-string
class="description ytmusic-description-shelf-renderer"
text={{ runs: [{ text: provider() }] }}
/>
<div class="provider-info">
<yt-formatted-string
class="description ytmusic-description-shelf-renderer"
text={{ runs: [{ text: provider() }] }}
/>
<Show when={currentLyrics().data?.language}>
<span class="language-badge">
{currentLyrics().data?.language?.toUpperCase()}
</span>
</Show>
</div>
<mdui-button-icon onClick={toggleStar} tabindex={-1}>
<Show
fallback={
Expand Down
55 changes: 54 additions & 1 deletion src/plugins/synced-lyrics/renderer/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@ import {
PlainLyrics,
} from './components';

import { currentLyrics } from './store';
import { bestLanguageResult, currentLyrics } from './store';
import { _ytAPI } from './index';

import { t } from '@/i18n';

import type { LineLyrics, SyncedLyricsPluginConfig } from '../types';

interface AppElement extends HTMLElement {
toastService?: {
show: (message: string) => void;
};
}

export const [isVisible, setIsVisible] = createSignal<boolean>(false);
export const [config, setConfig] =
createSignal<SyncedLyricsPluginConfig | null>(null);
Expand Down Expand Up @@ -126,6 +135,50 @@ createEffect(() => {
}
});

// Auto-skip songs based on detected language
createEffect(() => {
const cfg = config();
const lyrics = bestLanguageResult();

if (!cfg?.autoSkipLanguages || !lyrics?.data?.language) return;

const skipLanguages = cfg.autoSkipLanguages
.split(',')
.map((lang) => lang.trim().toLowerCase())
.filter((lang) => lang.length > 0);

if (skipLanguages.length === 0) return;

const detectedLanguage = lyrics.data.language.toLowerCase();

if (skipLanguages.includes(detectedLanguage)) {
const appApi = document.querySelector<AppElement>('ytmusic-app');

// Show toast notification
appApi?.toastService?.show(
t('plugins.synced-lyrics.menu.auto-skip-toast', {
language: lyrics.data.language.toUpperCase(),
}),
);

// Optionally dislike the song
if (cfg.autoDislikeSkippedLanguages) {
const dislikeButton = document.querySelector<HTMLButtonElement>(
'#button-shape-dislike > button[aria-pressed="false"]',
);
if (dislikeButton) {
dislikeButton.click();
}
}

// Skip to next song
const timer = setTimeout(() => {
_ytAPI?.nextVideo();
}, 500);
onCleanup(() => clearTimeout(timer));
}
});

type LyricsRendererChild =
| { kind: 'LyricsPicker' }
| { kind: 'LoadingKaomoji' }
Expand Down
71 changes: 67 additions & 4 deletions src/plugins/synced-lyrics/renderer/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createStore } from 'solid-js/store';
import { createMemo } from 'solid-js';
import { detect } from 'tinyld';

import { getSongInfo } from '@/providers/song-info-front';

Expand All @@ -10,7 +11,7 @@ import {
} from '../providers';
import { providers } from '../providers/renderer';

import type { LyricProvider } from '../types';
import type { LyricProvider, LyricResult } from '../types';
import type { SongInfo } from '@/providers/song-info';

type LyricsStore = {
Expand Down Expand Up @@ -41,6 +42,24 @@ export const currentLyrics = createMemo(() => {
return lyricsStore.lyrics[provider];
});

/**
* Returns the best available provider result that has a detected language,
* without requiring lyricsStore.provider to have been set by LyricsPicker.
* Prefers the currently selected provider, then falls back to any done provider
* with a language so that auto-skip works even when the lyrics panel is hidden.
*/
export const bestLanguageResult = createMemo(() => {
const current = lyricsStore.lyrics[lyricsStore.provider];
if (current.state === 'done' && current.data?.language) return current;

for (const name of providerNames) {
const state = lyricsStore.lyrics[name];
if (state.state === 'done' && state.data?.language) return state;
}

return null;
});

type VideoId = string;

type SearchCacheData = Record<ProviderName, ProviderState>;
Expand All @@ -51,6 +70,44 @@ interface SearchCache {

// TODO: Maybe use localStorage for the cache.
const searchCache = new Map<VideoId, SearchCache>();

/**
* Detects the language of lyrics and adds it to the result.
* Handles edge cases: no lyrics, empty text, detection failure.
*/
const detectLyricsLanguage = (
result: LyricResult | null,
): LyricResult | null => {
if (!result) return null;

try {
// Extract text from either plain lyrics or synced lines
let textToAnalyze = '';

if (result.lyrics) {
textToAnalyze = result.lyrics.trim();
} else if (result.lines && result.lines.length > 0) {
textToAnalyze = result.lines
.map((line) => line.text)
.join('\n')
.trim();
}

// Only attempt detection if we have meaningful text
if (textToAnalyze.length > 0) {
const detectedLang = detect(textToAnalyze);
// Only set language if detection was successful (not empty string)
if (detectedLang) {
return { ...result, language: detectedLang };
}
}
} catch (error) {
// Detection failed - log but don't throw, just leave language undefined
console.warn('Language detection failed:', error);
}

return result;
};
export const fetchLyrics = (info: SongInfo) => {
if (searchCache.has(info.videoId)) {
const cache = searchCache.get(info.videoId)!;
Expand Down Expand Up @@ -100,16 +157,19 @@ export const fetchLyrics = (info: SongInfo) => {
provider
.search(info)
.then((res) => {
// Detect language from the lyrics result
const resultWithLanguage = detectLyricsLanguage(res);

pCache.state = 'done';
pCache.data = res;
pCache.data = resultWithLanguage;

if (getSongInfo().videoId === info.videoId) {
setLyricsStore('lyrics', (old) => {
return {
...old,
[providerName]: {
state: 'done',
data: res ? { ...res } : null,
data: resultWithLanguage ? { ...resultWithLanguage } : null,
error: null,
},
};
Expand Down Expand Up @@ -157,10 +217,13 @@ export const retrySearch = (provider: ProviderName, info: SongInfo) => {
providers[provider]
.search(info)
.then((res) => {
// Detect language from the lyrics result
const resultWithLanguage = detectLyricsLanguage(res);

setLyricsStore('lyrics', (old) => {
return {
...old,
[provider]: { state: 'done', data: res, error: null },
[provider]: { state: 'done', data: resultWithLanguage, error: null },
};
});
})
Expand Down
18 changes: 18 additions & 0 deletions src/plugins/synced-lyrics/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@
transition: transform 0.25s ease-in-out;
}

.provider-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}

.lyrics-picker-dot {
display: inline-block;
cursor: pointer;
Expand Down Expand Up @@ -232,6 +239,17 @@ div:has(> .lyrics-picker) {
}
}

.language-badge {
display: inline-block;
padding: 2px 6px;
font-size: 0.7rem;
font-weight: 600;
color: var(--ytmusic-text-primary);
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
text-transform: uppercase;
}

/* Animations */
@keyframes lyrics-wobble {
from {
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/synced-lyrics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export type SyncedLyricsPluginConfig = {
| 'simplifiedToTraditional'
| 'traditionalToSimplified'
| 'disabled';
autoSkipLanguages?: string;
autoDislikeSkippedLanguages: boolean;
};

export type LineLyricsStatus = 'previous' | 'current' | 'upcoming';
Expand All @@ -35,6 +37,7 @@ export interface LyricResult {

lyrics?: string;
lines?: LineLyrics[];
language?: string;
}

// prettier-ignore
Expand Down
Loading