Skip to content

Commit 55619ac

Browse files
author
Volodymyr Tymchenko
committed
Added a feature to try to detect the language of lyrics. The language would by default be shown in the Lyrics panel and the songs can be optionally skipped and/or disliked
1 parent b6d2bcd commit 55619ac

File tree

8 files changed

+178
-8
lines changed

8 files changed

+178
-8
lines changed

src/i18n/resources/en.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,18 @@
909909
}
910910
},
911911
"tooltip": "Convert Chinese character to Traditional or Simplified"
912+
},
913+
"auto-skip-languages": {
914+
"label": "Auto-skip languages",
915+
"tooltip": "Automatically skip songs when these languages are detected in lyrics (comma-separated language codes)",
916+
"prompt": {
917+
"title": "Auto-skip languages",
918+
"label": "Enter language codes (comma-separated, e.g., ja,ko,zh):"
919+
}
920+
},
921+
"auto-dislike-skipped-languages": {
922+
"label": "Auto-dislike skipped songs",
923+
"tooltip": "Automatically dislike songs before skipping them when a language from the skip list is detected"
912924
}
913925
},
914926
"name": "Synced Lyrics",

src/plugins/synced-lyrics/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export default createPlugin({
2222
defaultTextString: '♪',
2323
lineEffect: 'fancy',
2424
romanization: true,
25+
autoSkipLanguages: '',
26+
autoDislikeSkippedLanguages: false,
2527
} satisfies SyncedLyricsPluginConfig as SyncedLyricsPluginConfig,
2628

2729
menu,

src/plugins/synced-lyrics/menu.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { t } from '@/i18n';
2+
import prompt from 'custom-electron-prompt';
3+
import promptOptions from '@/providers/prompt-options';
24

35
import { providerNames } from './providers';
46

@@ -233,5 +235,43 @@ export const menu = async (
233235
});
234236
},
235237
},
238+
{
239+
label: t('plugins.synced-lyrics.menu.auto-skip-languages.label'),
240+
toolTip: t('plugins.synced-lyrics.menu.auto-skip-languages.tooltip'),
241+
async click() {
242+
const languages =
243+
(await prompt(
244+
{
245+
title: t('plugins.synced-lyrics.menu.auto-skip-languages.prompt.title'),
246+
label: t('plugins.synced-lyrics.menu.auto-skip-languages.prompt.label'),
247+
value: config.autoSkipLanguages || '',
248+
type: 'input',
249+
inputAttrs: {
250+
type: 'text',
251+
placeholder: 'e.g., ja,ko,zh',
252+
},
253+
...promptOptions(),
254+
},
255+
ctx.window,
256+
)) ?? config.autoSkipLanguages;
257+
258+
ctx.setConfig({
259+
autoSkipLanguages: languages as string,
260+
});
261+
},
262+
},
263+
{
264+
label: t('plugins.synced-lyrics.menu.auto-dislike-skipped-languages.label'),
265+
toolTip: t(
266+
'plugins.synced-lyrics.menu.auto-dislike-skipped-languages.tooltip',
267+
),
268+
type: 'checkbox',
269+
checked: config.autoDislikeSkippedLanguages,
270+
click(item) {
271+
ctx.setConfig({
272+
autoDislikeSkippedLanguages: item.checked,
273+
});
274+
},
275+
},
236276
];
237277
};

src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,17 @@ export const LyricsPicker = (props: {
260260
/>
261261
</Match>
262262
</Switch>
263-
<yt-formatted-string
264-
class="description ytmusic-description-shelf-renderer"
265-
text={{ runs: [{ text: provider() }] }}
266-
/>
263+
<div class="provider-info">
264+
<yt-formatted-string
265+
class="description ytmusic-description-shelf-renderer"
266+
text={{ runs: [{ text: provider() }] }}
267+
/>
268+
<Show when={currentLyrics().data?.language}>
269+
<span class="language-badge">
270+
{currentLyrics().data?.language?.toUpperCase()}
271+
</span>
272+
</Show>
273+
</div>
267274
<mdui-button-icon onClick={toggleStar} tabindex={-1}>
268275
<Show
269276
fallback={

src/plugins/synced-lyrics/renderer/renderer.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,16 @@ import {
2121
} from './components';
2222

2323
import { currentLyrics } from './store';
24+
import { _ytAPI } from './index';
2425

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

28+
interface AppElement extends HTMLElement {
29+
toastService?: {
30+
show: (message: string) => void;
31+
};
32+
}
33+
2734
export const [isVisible, setIsVisible] = createSignal<boolean>(false);
2835
export const [config, setConfig] =
2936
createSignal<SyncedLyricsPluginConfig | null>(null);
@@ -126,6 +133,47 @@ createEffect(() => {
126133
}
127134
});
128135

136+
// Auto-skip songs based on detected language
137+
createEffect(() => {
138+
const cfg = config();
139+
const lyrics = currentLyrics();
140+
141+
if (!cfg?.autoSkipLanguages || !lyrics?.data?.language) return;
142+
143+
const skipLanguages = cfg.autoSkipLanguages
144+
.split(',')
145+
.map(lang => lang.trim().toLowerCase())
146+
.filter(lang => lang.length > 0);
147+
148+
if (skipLanguages.length === 0) return;
149+
150+
const detectedLanguage = lyrics.data.language.toLowerCase();
151+
152+
if (skipLanguages.includes(detectedLanguage)) {
153+
const appApi = document.querySelector<AppElement>('ytmusic-app');
154+
155+
// Show toast notification
156+
appApi?.toastService?.show(
157+
`Auto-skipping song with detected language: ${lyrics.data.language.toUpperCase()}`,
158+
);
159+
160+
// Optionally dislike the song
161+
if (cfg.autoDislikeSkippedLanguages) {
162+
const dislikeButton = document.querySelector<HTMLButtonElement>(
163+
'#button-shape-dislike > button[aria-pressed="false"]',
164+
);
165+
if (dislikeButton) {
166+
dislikeButton.click();
167+
}
168+
}
169+
170+
// Skip to next song
171+
setTimeout(() => {
172+
_ytAPI?.nextVideo();
173+
}, 500);
174+
}
175+
});
176+
129177
type LyricsRendererChild =
130178
| { kind: 'LyricsPicker' }
131179
| { kind: 'LoadingKaomoji' }

src/plugins/synced-lyrics/renderer/store.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createStore } from 'solid-js/store';
22
import { createMemo } from 'solid-js';
3+
import { detect } from 'tinyld';
34

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

@@ -10,7 +11,7 @@ import {
1011
} from '../providers';
1112
import { providers } from '../providers/renderer';
1213

13-
import type { LyricProvider } from '../types';
14+
import type { LyricProvider, LyricResult } from '../types';
1415
import type { SongInfo } from '@/providers/song-info';
1516

1617
type LyricsStore = {
@@ -51,6 +52,39 @@ interface SearchCache {
5152

5253
// TODO: Maybe use localStorage for the cache.
5354
const searchCache = new Map<VideoId, SearchCache>();
55+
56+
/**
57+
* Detects the language of lyrics and adds it to the result.
58+
* Handles edge cases: no lyrics, empty text, detection failure.
59+
*/
60+
const detectLyricsLanguage = (result: LyricResult | null): LyricResult | null => {
61+
if (!result) return null;
62+
63+
try {
64+
// Extract text from either plain lyrics or synced lines
65+
let textToAnalyze = '';
66+
67+
if (result.lyrics) {
68+
textToAnalyze = result.lyrics.trim();
69+
} else if (result.lines && result.lines.length > 0) {
70+
textToAnalyze = result.lines.map(line => line.text).join('\n').trim();
71+
}
72+
73+
// Only attempt detection if we have meaningful text
74+
if (textToAnalyze.length > 0) {
75+
const detectedLang = detect(textToAnalyze);
76+
// Only set language if detection was successful (not empty string)
77+
if (detectedLang) {
78+
result.language = detectedLang;
79+
}
80+
}
81+
} catch (error) {
82+
// Detection failed - log but don't throw, just leave language undefined
83+
console.warn('Language detection failed:', error);
84+
}
85+
86+
return result;
87+
};
5488
export const fetchLyrics = (info: SongInfo) => {
5589
if (searchCache.has(info.videoId)) {
5690
const cache = searchCache.get(info.videoId)!;
@@ -100,16 +134,19 @@ export const fetchLyrics = (info: SongInfo) => {
100134
provider
101135
.search(info)
102136
.then((res) => {
137+
// Detect language from the lyrics result
138+
const resultWithLanguage = detectLyricsLanguage(res);
139+
103140
pCache.state = 'done';
104-
pCache.data = res;
141+
pCache.data = resultWithLanguage;
105142

106143
if (getSongInfo().videoId === info.videoId) {
107144
setLyricsStore('lyrics', (old) => {
108145
return {
109146
...old,
110147
[providerName]: {
111148
state: 'done',
112-
data: res ? { ...res } : null,
149+
data: resultWithLanguage ? { ...resultWithLanguage } : null,
113150
error: null,
114151
},
115152
};
@@ -157,10 +194,13 @@ export const retrySearch = (provider: ProviderName, info: SongInfo) => {
157194
providers[provider]
158195
.search(info)
159196
.then((res) => {
197+
// Detect language from the lyrics result
198+
const resultWithLanguage = detectLyricsLanguage(res);
199+
160200
setLyricsStore('lyrics', (old) => {
161201
return {
162202
...old,
163-
[provider]: { state: 'done', data: res, error: null },
203+
[provider]: { state: 'done', data: resultWithLanguage, error: null },
164204
};
165205
});
166206
})

src/plugins/synced-lyrics/style.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,13 @@
195195
transition: transform 0.25s ease-in-out;
196196
}
197197

198+
.provider-info {
199+
display: flex;
200+
flex-direction: column;
201+
align-items: center;
202+
gap: 2px;
203+
}
204+
198205
.lyrics-picker-dot {
199206
display: inline-block;
200207
cursor: pointer;
@@ -232,6 +239,17 @@ div:has(> .lyrics-picker) {
232239
}
233240
}
234241

242+
.language-badge {
243+
display: inline-block;
244+
padding: 2px 6px;
245+
font-size: 0.7rem;
246+
font-weight: 600;
247+
color: var(--ytmusic-text-primary);
248+
background: rgba(255, 255, 255, 0.15);
249+
border-radius: 4px;
250+
text-transform: uppercase;
251+
}
252+
235253
/* Animations */
236254
@keyframes lyrics-wobble {
237255
from {

src/plugins/synced-lyrics/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type SyncedLyricsPluginConfig = {
1414
| 'simplifiedToTraditional'
1515
| 'traditionalToSimplified'
1616
| 'disabled';
17+
autoSkipLanguages?: string;
18+
autoDislikeSkippedLanguages: boolean;
1719
};
1820

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

3638
lyrics?: string;
3739
lines?: LineLyrics[];
40+
language?: string;
3841
}
3942

4043
// prettier-ignore

0 commit comments

Comments
 (0)