Skip to content

Commit

Permalink
[YouTube] Replace link text with accessibility label
Browse files Browse the repository at this point in the history
  • Loading branch information
Stypox committed Apr 7, 2024
1 parent 09732d6 commit f4dcab1
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import java.util.Comparator;
import java.util.List;
import java.util.Stack;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
Expand All @@ -29,6 +32,11 @@ private YoutubeDescriptionHelper() {
public static final String ITALIC_OPEN = "<i>";
public static final String ITALIC_CLOSE = "</i>";

// special link chips (e.g. for YT videos, YT channels or social media accounts):
// (u00a0) u00a0 u00a0 [/•] u00a0 <link content> u00a0 u00a0
private static final Pattern LINK_CONTENT_CLEANER_REGEX
= Pattern.compile("(?s)^\u00a0+[/•]\u00a0+(.*)\u00a0+$");

/**
* Can be a command run, or a style run.
*/
Expand All @@ -37,17 +45,30 @@ static final class Run {
@Nonnull final String close;
final int pos;
final boolean isClose;
@Nullable final Function<String, String> transformContent;
int openPosInOutput = -1;

Run(
@Nonnull final String open,
@Nonnull final String close,
final int pos,
final boolean isClose
) {
this(open, close, pos, isClose, null);
}

Run(
@Nonnull final String open,
@Nonnull final String close,
final int pos,
final boolean isClose,
@Nullable final Function<String, String> transformContent
) {
this.open = open;
this.close = close;
this.pos = pos;
this.isClose = isClose;
this.transformContent = transformContent;
}

public boolean sameOpen(@Nonnull final Run other) {
Expand Down Expand Up @@ -148,12 +169,22 @@ static String runsToHtml(
// condition, because no run will close before being opened, but let's be sure
while (!openRuns.empty()) {
final Run popped = openRuns.pop();
textBuilder.append(popped.close);
if (popped.sameOpen(closer)) {
// before closing the current run, if the run has a transformContent
// function, use it to transform the content of the current run, based on
// the openPosInOutput set when the current run was opened
if (popped.transformContent != null && popped.openPosInOutput >= 0) {
textBuilder.replace(popped.openPosInOutput, textBuilder.length(),
popped.transformContent.apply(
textBuilder.substring(popped.openPosInOutput)));
}
// close the run that we really need to close
textBuilder.append(popped.close);
break;
}
// we keep popping from openRuns, closing all of the runs we find,
// until we find the run that we really need to close ...
textBuilder.append(popped.close);
tempStack.push(popped);
}
while (!tempStack.empty()) {
Expand All @@ -168,8 +199,10 @@ static String runsToHtml(
} else {
// this will never be reached if openersIndex >= openers.size() because of the
// way minPos is calculated
textBuilder.append(openers.get(openersIndex).open);
openRuns.push(openers.get(openersIndex));
final Run opener = openers.get(openersIndex);
textBuilder.append(opener.open);
opener.openPosInOutput = textBuilder.length(); // save for transforming later
openRuns.push(opener);
++openersIndex;
}
}
Expand All @@ -180,11 +213,7 @@ static String runsToHtml(
return textBuilder.toString()
.replace("\n", "<br>")
.replace(" ", " &nbsp;")
// special link chips (e.g. for YT videos, YT channels or social media accounts):
// u00a0 u00a0 [/•] u00a0 <link content> u00a0 u00a0
.replace("\">\u00a0\u00a0/\u00a0", "\">")
.replace("\">\u00a0\u00a0\u00a0", "\">")
.replace("\u00a0\u00a0</a>", "</a>");
.replace('\u00a0', ' ');
}

private static void addAllCommandRuns(
Expand Down Expand Up @@ -212,12 +241,44 @@ private static void addAllCommandRuns(
}

final String open = "<a href=\"" + Entities.escape(url) + "\">";
final Function<String, String> transformContent = getTransformContentFun(run);

openers.add(new Run(open, LINK_CLOSE, startIndex, false));
closers.add(new Run(open, LINK_CLOSE, startIndex + length, true));
openers.add(new Run(open, LINK_CLOSE, startIndex, false,
transformContent));
closers.add(new Run(open, LINK_CLOSE, startIndex + length, true,
transformContent));
});
}

private static Function<String, String> getTransformContentFun(final JsonObject run) {
final String accessibilityLabel = run.getObject("onTapOptions")
.getObject("accessibilityInfo")
.getString("accessibilityLabel", "")
// accessibility labels are e.g. "Instagram Channel Link: instagram_profile_name"
.replaceFirst(" Channel Link", "");

final Function<String, String> transformContent;
if (accessibilityLabel.isEmpty() || accessibilityLabel.startsWith("YouTube: ")) {
// if there is no accessibility label, or the link points to YouTube, cleanup the link
// text, see LINK_CONTENT_CLEANER_REGEX's documentation for more details
transformContent = (content) -> {
final Matcher m = LINK_CONTENT_CLEANER_REGEX.matcher(content);
if (m.find()) {
return m.group(1);
}
return content;
};
} else {
// if there is an accessibility label, replace the link text with it, because on the
// YouTube website an ambiguous link text is next to an icon explaining which service it
// belongs to, but since we can't add icons, we instead use the accessibility label
// which contains information about the service
transformContent = (content) -> accessibilityLabel;
}

return transformContent;
}

private static void addAllStyleRuns(
@Nonnull final JsonObject attributedDescription,
@Nonnull final List<Run> openers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
import org.schabi.newpipe.extractor.stream.StreamSegment;
import org.schabi.newpipe.extractor.stream.StreamType;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
Expand Down Expand Up @@ -162,7 +165,7 @@ public static void setUp() throws Exception {
}

public static class DescriptionTestUnboxing extends DefaultStreamExtractorTest {
private static final String ID = "cV5TjZCJkuA";
private static final String ID = "ZeerrnuLi5E";
private static final String URL = BASE_URL + ID;
private static StreamExtractor extractor;

Expand All @@ -187,6 +190,13 @@ public static void setUp() throws Exception {
@Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UCsTcErHg8oDvUnTzoqsYeNw"; }
@Override public long expectedUploaderSubscriberCountAtLeast() { return 18_000_000; }
@Override public List<String> expectedDescriptionContains() {
try(FileOutputStream a = new FileOutputStream("/home/stypox/Desktop/newpipestream.html")) {
a.write(extractor().getDescription().getContent().getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ParsingException e) {
throw new RuntimeException(e);
}
return Arrays.asList("https://www.youtube.com/watch?v=X7FLCHVXpsA&amp;list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
"https://www.youtube.com/watch?v=Lqv6G0pDNnw&amp;list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
"https://www.youtube.com/watch?v=XxaRBPyrnBU&amp;list=PL7u4lWXQ3wfI_7PgX0C-VTiwLeu0S4v34",
Expand Down

0 comments on commit f4dcab1

Please sign in to comment.