Skip to content

Commit bd96158

Browse files
committed
Fix YouTube caption translation track selection
1 parent 583ee6b commit bd96158

4 files changed

Lines changed: 154 additions & 3 deletions

File tree

app/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCaptionTranslationHelper.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import javax.annotation.Nonnull;
1414
import javax.annotation.Nullable;
1515

16-
final class YoutubeCaptionTranslationHelper {
16+
public final class YoutubeCaptionTranslationHelper {
1717
private static final Pattern LANGUAGE_CODE_PATTERN = Pattern.compile(
1818
"^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$");
1919

@@ -58,6 +58,41 @@ static void addTranslatedSubtitle(@Nonnull final List<SubtitlesStream> subtitles
5858
.build());
5959
}
6060

61+
/**
62+
* Adds the currently requested YouTube translation from already extracted subtitle tracks.
63+
*
64+
* <p>This is intentionally also used at player-resolution time. StreamInfo instances are
65+
* cached, while the user can change the caption translation target later. Building the one
66+
* requested translated track from the cached native timed-text URL keeps the player in sync
67+
* with the current preference without forcing a network re-extraction.</p>
68+
*/
69+
public static void addTranslatedSubtitleFromExtractedStreams(
70+
@Nonnull final List<SubtitlesStream> subtitles,
71+
@Nullable final String requestedLanguage) {
72+
final String targetLanguage = normalizeLanguageCode(requestedLanguage);
73+
if (targetLanguage == null || hasSubtitleLanguage(subtitles, targetLanguage)) {
74+
return;
75+
}
76+
77+
final SubtitlesStream source = selectTranslatableSource(subtitles);
78+
if (source == null || source.getFormat() == null) {
79+
return;
80+
}
81+
82+
final String url = buildCaptionUrl(
83+
source.getContent(), source.getFormat().getSuffix(), targetLanguage);
84+
if (url == null) {
85+
return;
86+
}
87+
88+
subtitles.add(new SubtitlesStream.Builder()
89+
.setContent(url, true)
90+
.setMediaFormat(source.getFormat())
91+
.setLanguageCode(targetLanguage)
92+
.setAutoGenerated(true)
93+
.build());
94+
}
95+
6196
@Nullable
6297
static String normalizeLanguageCode(@Nullable final String languageCode) {
6398
if (languageCode == null) {
@@ -112,6 +147,17 @@ static boolean hasNativeCaption(@Nonnull final JsonArray captions,
112147
return false;
113148
}
114149

150+
static boolean hasSubtitleLanguage(@Nonnull final List<SubtitlesStream> subtitles,
151+
@Nonnull final String targetLanguage) {
152+
for (final SubtitlesStream subtitle : subtitles) {
153+
final String captionLanguage = normalizeLanguageCode(subtitle.getLanguageTag());
154+
if (captionLanguage != null && languagesMatch(captionLanguage, targetLanguage)) {
155+
return true;
156+
}
157+
}
158+
return false;
159+
}
160+
115161
@Nullable
116162
static JsonObject selectTranslatableSource(@Nonnull final JsonArray captions) {
117163
JsonObject generatedFallback = null;
@@ -134,6 +180,26 @@ static JsonObject selectTranslatableSource(@Nonnull final JsonArray captions) {
134180
return generatedFallback;
135181
}
136182

183+
@Nullable
184+
static SubtitlesStream selectTranslatableSource(
185+
@Nonnull final List<SubtitlesStream> subtitles) {
186+
SubtitlesStream generatedFallback = null;
187+
for (final SubtitlesStream subtitle : subtitles) {
188+
if (!subtitle.isUrl() || subtitle.getFormat() == null
189+
|| isBlank(subtitle.getContent())) {
190+
continue;
191+
}
192+
193+
if (!subtitle.isAutoGenerated()) {
194+
return subtitle;
195+
}
196+
if (generatedFallback == null) {
197+
generatedFallback = subtitle;
198+
}
199+
}
200+
return generatedFallback;
201+
}
202+
137203
@Nullable
138204
static String buildCaptionUrl(@Nullable final String baseUrl,
139205
@Nonnull final String formatSuffix,

app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import androidx.media3.exoplayer.source.MergingMediaSource;
1414

1515
import org.schabi.newpipe.extractor.MediaFormat;
16+
import org.schabi.newpipe.extractor.ServiceList;
17+
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeCaptionTranslationHelper;
1618
import org.schabi.newpipe.extractor.stream.AudioStream;
1719
import org.schabi.newpipe.extractor.stream.StreamInfo;
1820
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
@@ -21,6 +23,7 @@
2123
import org.schabi.newpipe.player.helper.PlayerHelper;
2224
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
2325
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
26+
import org.schabi.newpipe.settings.CaptionTranslationPreferences;
2427
import org.schabi.newpipe.util.ListHelper;
2528

2629
import java.util.ArrayList;
@@ -158,8 +161,16 @@ public MediaSource resolve(@NonNull final StreamInfo info,
158161

159162
// Below are auxiliary media sources
160163

161-
// Create subtitle sources
162-
final List<SubtitlesStream> subtitlesStreams = info.getSubtitles();
164+
// Create subtitle sources. StreamInfo is cached, but the translation preference can change
165+
// afterwards, so synthesize the one requested YouTube translation at resolution time too.
166+
final List<SubtitlesStream> subtitlesStreams = info.getSubtitles() == null
167+
? null : new ArrayList<>(info.getSubtitles());
168+
if (subtitlesStreams != null
169+
&& info.getServiceId() == ServiceList.YouTube.getServiceId()) {
170+
YoutubeCaptionTranslationHelper.addTranslatedSubtitleFromExtractedStreams(
171+
subtitlesStreams,
172+
CaptionTranslationPreferences.getTargetLanguage(context));
173+
}
163174
if (subtitlesStreams != null) {
164175
// Torrent and non URL subtitles are not supported by ExoPlayer
165176
final List<SubtitlesStream> nonTorrentAndUrlStreams = getUrlAndNonTorrentStreams(

app/src/test/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeCaptionTranslationHelperTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
import com.grack.nanojson.JsonParserException;
1313

1414
import org.junit.Test;
15+
import org.schabi.newpipe.extractor.MediaFormat;
16+
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
1520

1621
public class YoutubeCaptionTranslationHelperTest {
1722
@Test
@@ -94,4 +99,46 @@ public void translatedUrlReplacesExistingFormatAndTarget() {
9499
"https://www.youtube.com/api/timedtext?lang=en&v=abc&fmt=ttml&tlang=ar#track",
95100
translated);
96101
}
102+
103+
@Test
104+
public void cachedNativeSubtitleCanProduceCurrentTranslationTarget() {
105+
final List<SubtitlesStream> subtitles = new ArrayList<>();
106+
subtitles.add(subtitle("ar", true,
107+
"https://www.youtube.com/api/timedtext?lang=ar&v=abc"));
108+
109+
YoutubeCaptionTranslationHelper.addTranslatedSubtitleFromExtractedStreams(
110+
subtitles, "en");
111+
112+
assertEquals(2, subtitles.size());
113+
final SubtitlesStream translated = subtitles.get(1);
114+
assertEquals("en", translated.getLanguageTag());
115+
assertTrue(translated.isAutoGenerated());
116+
assertTrue(translated.getContent().contains("fmt=ttml"));
117+
assertTrue(translated.getContent().contains("tlang=en"));
118+
}
119+
120+
@Test
121+
public void cachedTargetSubtitleIsNotDuplicated() {
122+
final List<SubtitlesStream> subtitles = new ArrayList<>();
123+
subtitles.add(subtitle("ar", true,
124+
"https://www.youtube.com/api/timedtext?lang=ar&v=abc"));
125+
subtitles.add(subtitle("en", false,
126+
"https://www.youtube.com/api/timedtext?lang=en&v=abc"));
127+
128+
YoutubeCaptionTranslationHelper.addTranslatedSubtitleFromExtractedStreams(
129+
subtitles, "en");
130+
131+
assertEquals(2, subtitles.size());
132+
}
133+
134+
private static SubtitlesStream subtitle(final String language,
135+
final boolean autoGenerated,
136+
final String url) {
137+
return new SubtitlesStream.Builder()
138+
.setContent(url, true)
139+
.setMediaFormat(MediaFormat.TTML)
140+
.setLanguageCode(language)
141+
.setAutoGenerated(autoGenerated)
142+
.build();
143+
}
97144
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.schabi.newpipe.player.resolver;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import org.junit.Test;
6+
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
10+
public class VideoPlaybackResolverCaptionTranslationTest {
11+
private final Path sourceDirectory = Files.exists(Path.of("src/main/java"))
12+
? Path.of("src/main/java") : Path.of("app/src/main/java");
13+
14+
@Test
15+
public void currentCaptionTranslationIsAppliedWhenResolvingCachedStreamInfo()
16+
throws Exception {
17+
final String source = Files.readString(sourceDirectory.resolve(
18+
"org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java"));
19+
20+
assertTrue(source.contains("new ArrayList<>(info.getSubtitles())"));
21+
assertTrue(source.contains(
22+
"YoutubeCaptionTranslationHelper.addTranslatedSubtitleFromExtractedStreams"));
23+
assertTrue(source.contains("CaptionTranslationPreferences.getTargetLanguage(context)"));
24+
assertTrue(source.indexOf("addTranslatedSubtitleFromExtractedStreams")
25+
< source.indexOf("getUrlAndNonTorrentStreams"));
26+
}
27+
}

0 commit comments

Comments
 (0)