diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json
index 763e854b82..c58bbd789c 100644
--- a/src/i18n/resources/en.json
+++ b/src/i18n/resources/en.json
@@ -909,6 +909,19 @@
}
},
"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"
}
},
"name": "Synced Lyrics",
@@ -916,6 +929,9 @@
"fetching": "Fetching...",
"normal": "Refetch lyrics"
},
+ "toast": {
+ "auto-skip": "Auto-skipping song with detected language: {{language}}"
+ },
"warnings": {
"duration-mismatch": "⚠️ - The lyrics may be out of sync due to a duration mismatch.",
"inexact": "⚠️ - The lyrics for this song may not be exact",
diff --git a/src/plugins/synced-lyrics/index.ts b/src/plugins/synced-lyrics/index.ts
index 533f05bfb1..cfd352f784 100644
--- a/src/plugins/synced-lyrics/index.ts
+++ b/src/plugins/synced-lyrics/index.ts
@@ -22,6 +22,8 @@ export default createPlugin({
defaultTextString: '♪',
lineEffect: 'fancy',
romanization: true,
+ autoSkipLanguages: '',
+ autoDislikeSkippedLanguages: false,
} satisfies SyncedLyricsPluginConfig as SyncedLyricsPluginConfig,
menu,
diff --git a/src/plugins/synced-lyrics/menu.ts b/src/plugins/synced-lyrics/menu.ts
index 9f4fcc6d93..8f87f530df 100644
--- a/src/plugins/synced-lyrics/menu.ts
+++ b/src/plugins/synced-lyrics/menu.ts
@@ -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';
@@ -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,
+ });
+ },
+ },
+ {
+ 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,
+ });
+ },
+ },
];
};
diff --git a/src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx b/src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx
index 3e18626bed..19f39cb209 100644
--- a/src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx
+++ b/src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx
@@ -260,10 +260,17 @@ export const LyricsPicker = (props: {
/>
-
+
+
+
+
+ {currentLyrics().data?.language?.toUpperCase()}
+
+
+
{
}
});
+// Auto-skip songs based on detected language
+let skippedVideoId: string | null = null;
+let skipTimer: ReturnType | null = null;
+createEffect(() => {
+ const cfg = config();
+ const lyrics = bestLanguageResult();
+
+ if (!cfg?.enabled || !cfg.autoSkipLanguages || !lyrics?.data?.language) {
+ // lyrics is null while providers are fetching (i.e. a new song just started).
+ // Also reset guards when auto-skip is disabled or configured languages are cleared.
+ if (!lyrics || !cfg?.enabled || !cfg.autoSkipLanguages) {
+ skippedVideoId = null;
+ }
+ if (skipTimer !== null) {
+ clearTimeout(skipTimer);
+ skipTimer = null;
+ }
+ 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 videoId = getSongInfo().videoId;
+ if (videoId === skippedVideoId) return;
+ skippedVideoId = videoId;
+
+ const appApi = document.querySelector('ytmusic-app');
+
+ // Show toast notification
+ appApi?.toastService?.show(
+ t('plugins.synced-lyrics.toast.auto-skip', {
+ language: lyrics.data.language.toUpperCase(),
+ }),
+ );
+
+ // Optionally dislike the song
+ if (cfg.autoDislikeSkippedLanguages) {
+ const dislikeButton = document.querySelector(
+ '#button-shape-dislike > button[aria-pressed="false"]',
+ );
+ if (dislikeButton) {
+ dislikeButton.click();
+ }
+ }
+
+ // Skip to next song — timer lives outside the effect so re-runs don't cancel it.
+ // Capture videoId so the callback can verify the track hasn't changed by the time it fires.
+ const scheduledFor = videoId;
+ if (skipTimer !== null) clearTimeout(skipTimer);
+ skipTimer = setTimeout(() => {
+ skipTimer = null;
+ if (getSongInfo().videoId === scheduledFor) {
+ appApi?.playerApi?.nextVideo();
+ }
+ }, 500);
+ }
+});
+
type LyricsRendererChild =
| { kind: 'LyricsPicker' }
| { kind: 'LoadingKaomoji' }
diff --git a/src/plugins/synced-lyrics/renderer/store.ts b/src/plugins/synced-lyrics/renderer/store.ts
index 8d9df88bc7..36eeb7cbd1 100644
--- a/src/plugins/synced-lyrics/renderer/store.ts
+++ b/src/plugins/synced-lyrics/renderer/store.ts
@@ -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';
@@ -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 = {
@@ -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;
@@ -51,6 +70,44 @@ interface SearchCache {
// TODO: Maybe use localStorage for the cache.
const searchCache = new Map();
+
+/**
+ * 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)!;
@@ -100,8 +157,11 @@ 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) => {
@@ -109,7 +169,7 @@ export const fetchLyrics = (info: SongInfo) => {
...old,
[providerName]: {
state: 'done',
- data: res ? { ...res } : null,
+ data: resultWithLanguage ? { ...resultWithLanguage } : null,
error: null,
},
};
@@ -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 },
};
});
})
diff --git a/src/plugins/synced-lyrics/style.css b/src/plugins/synced-lyrics/style.css
index 19154b4468..9efc7370d5 100644
--- a/src/plugins/synced-lyrics/style.css
+++ b/src/plugins/synced-lyrics/style.css
@@ -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;
@@ -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 {
diff --git a/src/plugins/synced-lyrics/types.ts b/src/plugins/synced-lyrics/types.ts
index a429364406..14b3aa8333 100644
--- a/src/plugins/synced-lyrics/types.ts
+++ b/src/plugins/synced-lyrics/types.ts
@@ -14,6 +14,8 @@ export type SyncedLyricsPluginConfig = {
| 'simplifiedToTraditional'
| 'traditionalToSimplified'
| 'disabled';
+ autoSkipLanguages: string;
+ autoDislikeSkippedLanguages: boolean;
};
export type LineLyricsStatus = 'previous' | 'current' | 'upcoming';
@@ -35,6 +37,7 @@ export interface LyricResult {
lyrics?: string;
lines?: LineLyrics[];
+ language?: string;
}
// prettier-ignore