mirror of
https://github.com/TeamNewPipe/NewPipe.git
synced 2025-12-05 01:10:43 +00:00
Compare commits
31 Commits
fc5c8baf90
...
f4032e5fdd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4032e5fdd | ||
|
|
e528cbb6ae | ||
|
|
7c23a55d60 | ||
|
|
823f1437ca | ||
|
|
ffd3565477 | ||
|
|
c411556b00 | ||
|
|
35244355cd | ||
|
|
f836f5e75d | ||
|
|
2fadaffb98 | ||
|
|
1314a21f71 | ||
|
|
650b51ffec | ||
|
|
c03f405f8c | ||
|
|
0a89276b7a | ||
|
|
300afde83d | ||
|
|
d311faea58 | ||
|
|
71aa6d52d3 | ||
|
|
88eb32be3a | ||
|
|
2a9c6f0538 | ||
|
|
c81148ae0a | ||
|
|
ecd3e85d49 | ||
|
|
c4e6e4d4c4 | ||
|
|
41981902ab | ||
|
|
56a09220ee | ||
|
|
c4bfc119df | ||
|
|
c49e44443c | ||
|
|
1014dd563f | ||
|
|
3516667671 | ||
|
|
22ee01bcfb | ||
|
|
fd4f4737c2 | ||
|
|
e1888ede87 | ||
|
|
2c35db7a07 |
40
.editorconfig
Normal file
40
.editorconfig
Normal file
@@ -0,0 +1,40 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
|
||||
root = true
|
||||
|
||||
[*.{kt,kts}]
|
||||
ktlint_standard_annotation = disabled
|
||||
ktlint_standard_argument-list-wrapping = disabled
|
||||
ktlint_standard_backing-property-naming = disabled
|
||||
ktlint_standard_blank-line-before-declaration = disabled
|
||||
ktlint_standard_chain-method-continuation = disabled
|
||||
ktlint_standard_class-signature = disabled
|
||||
ktlint_standard_comment-wrapping = disabled
|
||||
ktlint_standard_enum-wrapping = disabled
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
ktlint_standard_function-literal = disabled
|
||||
ktlint_standard_function-signature = disabled
|
||||
ktlint_standard_indent = disabled
|
||||
ktlint_standard_max-line-length = disabled
|
||||
ktlint_standard_multiline-expression-wrapping = disabled
|
||||
ktlint_standard_multiline-if-else = disabled
|
||||
ktlint_standard_no-blank-line-in-list = disabled
|
||||
ktlint_standard_no-consecutive-comments = disabled
|
||||
ktlint_standard_no-empty-first-line-in-class-body = disabled
|
||||
ktlint_standard_no-empty-first-line-in-method-block = disabled
|
||||
ktlint_standard_no-line-break-after-else = disabled
|
||||
ktlint_standard_no-semi = disabled
|
||||
ktlint_standard_no-single-line-block-comment = disabled
|
||||
ktlint_standard_package-name = disabled
|
||||
ktlint_standard_parameter-list-wrapping = disabled
|
||||
ktlint_standard_property-naming = disabled
|
||||
ktlint_standard_spacing-between-declarations-with-annotations = disabled
|
||||
ktlint_standard_spacing-between-declarations-with-comments = disabled
|
||||
ktlint_standard_statement-wrapping = disabled
|
||||
ktlint_standard_string-template-indent = disabled
|
||||
ktlint_standard_trailing-comma-on-call-site = disabled
|
||||
ktlint_standard_trailing-comma-on-declaration-site = disabled
|
||||
ktlint_standard_try-catch-finally-spacing = disabled
|
||||
@@ -12,8 +12,6 @@ plugins {
|
||||
checkstyle
|
||||
}
|
||||
|
||||
apply(from = "check-dependencies.gradle.kts")
|
||||
|
||||
val gitWorkingBranch = providers.exec {
|
||||
commandLine("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
}.standardOutput.asText.map { it.trim() }
|
||||
@@ -24,6 +22,14 @@ java {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xannotation-default-target=param-property"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 36
|
||||
namespace = "org.schabi.newpipe"
|
||||
@@ -159,7 +165,7 @@ tasks.register<JavaExec>("runKtlint") {
|
||||
outputs.dir(outputDir)
|
||||
mainClass.set("com.pinterest.ktlint.Main")
|
||||
classpath = configurations.getByName("ktlint")
|
||||
args = listOf("src/**/*.kt")
|
||||
args = listOf("--editorconfig=../.editorconfig", "src/**/*.kt")
|
||||
jvmArgs = listOf("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
@@ -168,10 +174,14 @@ tasks.register<JavaExec>("formatKtlint") {
|
||||
outputs.dir(outputDir)
|
||||
mainClass.set("com.pinterest.ktlint.Main")
|
||||
classpath = configurations.getByName("ktlint")
|
||||
args = listOf("-F", "src/**/*.kt")
|
||||
args = listOf("--editorconfig=../.editorconfig", "-F", "src/**/*.kt")
|
||||
jvmArgs = listOf("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
tasks.register<CheckDependenciesOrder>("checkDependenciesOrder") {
|
||||
tomlFile = layout.projectDirectory.file("../gradle/libs.versions.toml")
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks.named("preDebugBuild").configure {
|
||||
if (!System.getProperties().containsKey("skipFormatKtlint")) {
|
||||
|
||||
@@ -340,6 +340,7 @@
|
||||
<data android:scheme="https" />
|
||||
<data android:host="soundcloud.com" />
|
||||
<data android:host="m.soundcloud.com" />
|
||||
<data android:host="on.soundcloud.com" />
|
||||
<data android:host="www.soundcloud.com" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -65,6 +65,8 @@ public class App extends Application {
|
||||
private static final String TAG = App.class.toString();
|
||||
|
||||
private boolean isFirstRun = false;
|
||||
private boolean notificationsRequested = false;
|
||||
|
||||
private static App app;
|
||||
|
||||
@NonNull
|
||||
@@ -72,6 +74,14 @@ public class App extends Application {
|
||||
return app;
|
||||
}
|
||||
|
||||
public boolean getNotificationsRequested() {
|
||||
return notificationsRequested;
|
||||
}
|
||||
|
||||
public void setNotificationsRequested() {
|
||||
notificationsRequested = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context base) {
|
||||
super.attachBaseContext(base);
|
||||
|
||||
@@ -9,5 +9,5 @@ package org.schabi.newpipe.database.history.dao
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
|
||||
interface HistoryDAO<T> : BasicDAO<T> {
|
||||
val latestEntry: T
|
||||
val latestEntry: T?
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.schabi.newpipe.database.history.model.SearchHistoryEntry
|
||||
interface SearchHistoryDAO : HistoryDAO<SearchHistoryEntry> {
|
||||
|
||||
@get:Query("SELECT * FROM search_history WHERE id = (SELECT MAX(id) FROM search_history)")
|
||||
override val latestEntry: SearchHistoryEntry
|
||||
override val latestEntry: SearchHistoryEntry?
|
||||
|
||||
@Query("DELETE FROM search_history")
|
||||
override fun deleteAll(): Int
|
||||
|
||||
@@ -290,8 +290,9 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
||||
binding.topControls.setFocusable(true);
|
||||
|
||||
binding.metadataView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
|
||||
// Reset workaround changes from popup player
|
||||
binding.audioTrackTextView.setMaxWidth(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -936,8 +937,6 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
|
||||
fragmentListener.onFullscreenStateChanged(isFullscreen);
|
||||
|
||||
binding.metadataView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
|
||||
binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE);
|
||||
setupScreenRotationButton();
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
|
||||
import org.schabi.newpipe.player.gesture.PopupPlayerGestureListener;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
|
||||
public final class PopupPlayerUi extends VideoPlayerUi {
|
||||
private static final String TAG = PopupPlayerUi.class.getSimpleName();
|
||||
@@ -174,6 +175,8 @@ public final class PopupPlayerUi extends VideoPlayerUi {
|
||||
binding.topControls.setClickable(false);
|
||||
binding.topControls.setFocusable(false);
|
||||
binding.bottomControls.bringToFront();
|
||||
// Workaround that UI elements are pushed off screen
|
||||
binding.audioTrackTextView.setMaxWidth(DeviceUtils.dpToPx(48, context));
|
||||
super.setupElementsVisibility();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,11 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
* Converts TTML subtitles to SRT format.
|
||||
*
|
||||
* References:
|
||||
* - TTML 2.0 (W3C): https://www.w3.org/TR/ttml2/
|
||||
* - SRT format: https://en.wikipedia.org/wiki/SubRip
|
||||
*/
|
||||
public class SrtFromTtmlWriter {
|
||||
private static final String NEW_LINE = "\r\n";
|
||||
@@ -59,6 +63,226 @@ public class SrtFromTtmlWriter {
|
||||
out.write(text.getBytes(charset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode XML or HTML entities into their actual (literal) characters.
|
||||
*
|
||||
* TTML is XML-based, so text nodes may contain escaped entities
|
||||
* instead of direct characters. For example:
|
||||
*
|
||||
* "&" → "&"
|
||||
* "<" → "<"
|
||||
* ">" → ">"
|
||||
* "	" → "\t" (TAB)
|
||||
* "
" ( ) → "\n" (LINE FEED)
|
||||
*
|
||||
* XML files cannot contain characters like "<", ">", "&" directly,
|
||||
* so they must be represented using their entity-encoded forms.
|
||||
*
|
||||
* Jsoup sometimes leaves nested or encoded entities unresolved
|
||||
* (e.g. inside <p> text nodes in TTML files), so this function
|
||||
* acts as a final “safety net” to ensure all entities are decoded
|
||||
* before further normalization.
|
||||
*
|
||||
* Character representation layers for reference:
|
||||
* - Literal characters: <, >, &
|
||||
* → appear in runtime/output text (e.g. final SRT output)
|
||||
* - Escaped entities: <, >, &
|
||||
* → appear in XML/HTML/TTML source files
|
||||
* - Numeric entities:  , 	, 
|
||||
* → appear mainly in XML/TTML files (also valid in HTML)
|
||||
* for non-printable or special characters
|
||||
* - Unicode escapes: \u00A0 (Java/Unicode internal form)
|
||||
* → appear only in Java source code (NOT valid in XML)
|
||||
*
|
||||
* XML entities include both named (&, <) and numeric
|
||||
* ( ,  ) forms.
|
||||
*
|
||||
* @param encodedEntities The raw text fragment possibly containing
|
||||
* encoded XML entities.
|
||||
* @return A decoded string where all entities are replaced by their
|
||||
* actual (literal) characters.
|
||||
*/
|
||||
private String decodeXmlEntities(final String encodedEntities) {
|
||||
return Parser.unescapeEntities(encodedEntities, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rare XML entity characters like LF: 
(`\n`),
|
||||
* CR: 
(`\r`) and CRLF: (`\r\n`).
|
||||
*
|
||||
* These are technically valid in TTML (XML allows them)
|
||||
* but unusual in practice, since most TTML line breaks
|
||||
* are represented as <br/> tags instead.
|
||||
* As a defensive approach, we normalize them:
|
||||
*
|
||||
* - Windows (\r\n), macOS (\r), and Unix (\n) → unified SRT NEW_LINE (\r\n)
|
||||
*
|
||||
* Although well-formed TTML normally encodes line breaks
|
||||
* as <br/> tags, some auto-generated or malformed TTML files
|
||||
* may embed literal newline entities (
, 
). This
|
||||
* normalization ensures these cases render properly in SRT
|
||||
* players instead of breaking the subtitle structure.
|
||||
*
|
||||
* @param text To be normalized text with actual characters.
|
||||
* @return Unified SRT NEW_LINE converted from all kinds of line breaks.
|
||||
*/
|
||||
private String normalizeLineBreakForSrt(final String text) {
|
||||
String cleaned = text;
|
||||
|
||||
// NOTE:
|
||||
// The order of newline replacements must NOT change,
|
||||
// or duplicated line breaks (e.g. \r\n → \n\n) will occur.
|
||||
cleaned = cleaned.replace("\r\n", "\n")
|
||||
.replace("\r", "\n");
|
||||
|
||||
cleaned = cleaned.replace("\n", NEW_LINE);
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private String normalizeForSrt(final String actualText) {
|
||||
String cleaned = actualText;
|
||||
|
||||
// Replace NBSP "non-breaking space" (\u00A0) with regular space ' '(\u0020).
|
||||
//
|
||||
// Why:
|
||||
// - Some viewers render NBSP(\u00A0) incorrectly:
|
||||
// * MPlayer 1.5: shown as “??”
|
||||
// * Linux command `cat -A`: displayed as control-like markers
|
||||
// (M-BM-)
|
||||
// * Acode (Android editor): displayed as visible replacement
|
||||
// glyphs (red dots)
|
||||
// - Other viewers show it as a normal space (e.g., VS Code 1.104.0,
|
||||
// vlc 3.0.20, mpv 0.37.0, Totem 43.0)
|
||||
// → Mixed rendering creates inconsistency and may confuse users.
|
||||
//
|
||||
// Details:
|
||||
// - YouTube TTML subtitles use both regular spaces (\u0020)
|
||||
// and non-breaking spaces (\u00A0).
|
||||
// - SRT subtitles only support regular spaces (\u0020),
|
||||
// so \u00A0 may cause display issues.
|
||||
// - \u00A0 and \u0020 are visually identical (i.e., they both
|
||||
// appear as spaces ' '), but they differ in Unicode encoding,
|
||||
// and NBSP (\u00A0) renders differently in different viewers.
|
||||
// - SRT is a plain-text format and does not interpret
|
||||
// "non-breaking" behavior.
|
||||
//
|
||||
// Conclusion:
|
||||
// - Ensure uniform behavior, so replace it to a regular space
|
||||
// without "non-breaking" behavior.
|
||||
//
|
||||
// References:
|
||||
// - Unicode U+00A0 NBSP (Latin-1 Supplement):
|
||||
// https://unicode.org/charts/PDF/U0080.pdf
|
||||
cleaned = cleaned.replace('\u00A0', ' ') // Non-breaking space
|
||||
.replace('\u202F', ' ') // Narrow no-break space
|
||||
.replace('\u205F', ' ') // Medium mathematical space
|
||||
.replace('\u3000', ' ') // Ideographic space
|
||||
// \u2000 ~ \u200A are whitespace characters (e.g.,
|
||||
// en space, em space), replaced with regular space (\u0020).
|
||||
.replaceAll("[\\u2000-\\u200A]", " "); // Whitespace characters
|
||||
|
||||
// \u200B ~ \u200F are a range of non-spacing characters
|
||||
// (e.g., zero-width space, zero-width non-joiner, etc.),
|
||||
// which have no effect in *.SRT files and may cause
|
||||
// display issues.
|
||||
// These characters are invisible to the human eye, and
|
||||
// they still exist in the encoding, so they need to be
|
||||
// removed.
|
||||
// After removal, the actual content becomes completely
|
||||
// empty "", meaning there are no characters left, just
|
||||
// an empty space, which helps avoid formatting issues
|
||||
// in subtitles.
|
||||
cleaned = cleaned.replaceAll("[\\u200B-\\u200F]", ""); // Non-spacing characters
|
||||
|
||||
// Remove control characters (\u0000 ~ \u001F, except
|
||||
// \n, \r, \t).
|
||||
// - These are ASCII C0 control codes (e.g. \u0001 SOH,
|
||||
// \u0008 BS, \u001F US), invisible and irrelevant in
|
||||
// subtitles, may cause square boxes (?) in players.
|
||||
// - Reference:
|
||||
// Unicode Basic Latin (https://unicode.org/charts/PDF/U0000.pdf)
|
||||
// ASCII Control (https://en.wikipedia.org/wiki/ASCII#Control_characters)
|
||||
cleaned = cleaned.replaceAll("[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]", "");
|
||||
|
||||
// Reasoning:
|
||||
// - subtitle files generally don't require tabs for alignment.
|
||||
// - Tabs can be displayed with varying widths across different
|
||||
// editors or platforms, which may cause display issues.
|
||||
// - Replace it with a single space for consistent display
|
||||
// across different editors or platforms.
|
||||
cleaned = cleaned.replace('\t', ' ');
|
||||
|
||||
cleaned = normalizeLineBreakForSrt(cleaned);
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private String sanitizeFragment(final String raw) {
|
||||
if (null == raw) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final String actualCharacters = decodeXmlEntities(raw);
|
||||
|
||||
final String srtSafeText = normalizeForSrt(actualCharacters);
|
||||
|
||||
return srtSafeText;
|
||||
}
|
||||
|
||||
// Recursively process all child nodes to ensure text inside
|
||||
// nested tags (e.g., <span>) is also extracted.
|
||||
private void traverseChildNodesForNestedTags(final Node parent,
|
||||
final StringBuilder text) {
|
||||
for (final Node child : parent.childNodes()) {
|
||||
extractText(child, text);
|
||||
}
|
||||
}
|
||||
|
||||
// CHECKSTYLE:OFF checkstyle:JavadocStyle
|
||||
// checkstyle does not understand that span tags are inside a code block
|
||||
/**
|
||||
* <p>Recursive method to extract text from all nodes.</p>
|
||||
* <p>
|
||||
* This method processes {@link TextNode}s and {@code <br>} tags,
|
||||
* recursively extracting text from nested tags
|
||||
* (e.g. extracting text from nested {@code <span>} tags).
|
||||
* Newlines are added for {@code <br>} tags.
|
||||
* </p>
|
||||
* @param node the current node to process
|
||||
* @param text the {@link StringBuilder} to append the extracted text to
|
||||
*/
|
||||
// --------------------------------------------------------------------
|
||||
// [INTERNAL NOTE] TTML text layer explanation
|
||||
//
|
||||
// TTML parsing involves multiple text "layers":
|
||||
// 1. Raw XML entities (e.g., <,  ) are decoded by Jsoup.
|
||||
// 2. extractText() works on DOM TextNodes (already parsed strings).
|
||||
// 3. sanitizeFragment() decodes remaining entities and fixes
|
||||
// Unicode quirks.
|
||||
// 4. normalizeForSrt() ensures literal text is safe for SRT output.
|
||||
//
|
||||
// In short:
|
||||
// Jsoup handles XML-level syntax,
|
||||
// our code handles text-level normalization for subtitles.
|
||||
// --------------------------------------------------------------------
|
||||
private void extractText(final Node node, final StringBuilder text) {
|
||||
if (node instanceof TextNode textNode) {
|
||||
String rawTtmlFragment = textNode.getWholeText();
|
||||
String srtContent = sanitizeFragment(rawTtmlFragment);
|
||||
text.append(srtContent);
|
||||
} else if (node instanceof Element element) {
|
||||
// <br> is a self-closing HTML tag used to insert a line break.
|
||||
if (element.tagName().equalsIgnoreCase("br")) {
|
||||
// Add a newline for <br> tags
|
||||
text.append(NEW_LINE);
|
||||
}
|
||||
}
|
||||
|
||||
traverseChildNodesForNestedTags(node, text);
|
||||
}
|
||||
// CHECKSTYLE:ON
|
||||
|
||||
public void build(final SharpStream ttml) throws IOException {
|
||||
/*
|
||||
* TTML parser with BASIC support
|
||||
@@ -79,21 +303,15 @@ public class SrtFromTtmlWriter {
|
||||
final Elements paragraphList = doc.select("body > div > p");
|
||||
|
||||
// check if has frames
|
||||
if (paragraphList.size() < 1) {
|
||||
if (paragraphList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final Element paragraph : paragraphList) {
|
||||
text.setLength(0);
|
||||
|
||||
for (final Node children : paragraph.childNodes()) {
|
||||
if (children instanceof TextNode) {
|
||||
text.append(((TextNode) children).text());
|
||||
} else if (children instanceof Element
|
||||
&& ((Element) children).tagName().equalsIgnoreCase("br")) {
|
||||
text.append(NEW_LINE);
|
||||
}
|
||||
}
|
||||
// Recursively extract text from all child nodes
|
||||
extractText(paragraph, text);
|
||||
|
||||
if (ignoreEmptyFrames && text.length() < 1) {
|
||||
continue;
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
|
||||
@@ -89,9 +90,12 @@ public final class PermissionHelper {
|
||||
&& ContextCompat.checkSelfPermission(activity,
|
||||
Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[] {Manifest.permission.POST_NOTIFICATIONS}, requestCode);
|
||||
return false;
|
||||
if (!App.getApp().getNotificationsRequested()) {
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[]{Manifest.permission.POST_NOTIFICATIONS}, requestCode);
|
||||
App.getApp().setNotificationsRequested();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -109,74 +109,89 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/close"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_close"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white"
|
||||
android:contentDescription="@string/close"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/metadataView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="top"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="RtlHardcoded">
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="match_parent"
|
||||
<LinearLayout
|
||||
android:id="@+id/metadataView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:singleLine="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:text="The Video Title LONG very LONG" />
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="top"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="NestedWeights,RtlHardcoded">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/channelTextView"
|
||||
android:layout_width="match_parent"
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:singleLine="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:text="The Video Title LONG very LONG" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/channelTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:singleLine="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
tools:text="The Video Artist LONG very LONG very Long" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:scrollHorizontally="true"
|
||||
android:singleLine="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
tools:text="The Video Artist LONG very LONG very Long" />
|
||||
android:layout_weight="1">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/audioTrackTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="35dp"
|
||||
android:layout_gravity="right"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:minWidth="0dp"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:singleLine="true"
|
||||
android:textColor="@android:color/white"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
tools:ignore="HardcodedText,RtlHardcoded"
|
||||
tools:text="English (United States) original"
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/audioTrackTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:minWidth="0dp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:textColor="@android:color/white"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
tools:ignore="HardcodedText,RtlHardcoded"
|
||||
tools:visibility="visible"
|
||||
tools:text="English (Original)" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/qualityTextView"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -212,6 +227,7 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/open_play_queue"
|
||||
android:focusable="true"
|
||||
android:paddingStart="3dp"
|
||||
android:paddingTop="5dp"
|
||||
@@ -221,7 +237,6 @@
|
||||
android:src="@drawable/ic_list"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white"
|
||||
android:contentDescription="@string/open_play_queue"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
@@ -231,16 +246,16 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/chapters"
|
||||
android:focusable="true"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:paddingTop="3dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_menu_book"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white"
|
||||
android:contentDescription="@string/chapters"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
@@ -249,12 +264,12 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/more_options"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_expand_more"
|
||||
app:tint="@color/white"
|
||||
android:contentDescription="@string/more_options"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -371,11 +386,11 @@
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/toggle_fullscreen"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_fullscreen"
|
||||
android:contentDescription="@string/toggle_fullscreen"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white"
|
||||
tools:ignore="RtlHardcoded"
|
||||
@@ -495,13 +510,13 @@
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/toggle_screen_orientation"
|
||||
android:focusable="true"
|
||||
android:nextFocusUp="@id/playbackSeekBar"
|
||||
android:padding="@dimen/player_main_buttons_padding"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_fullscreen"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/toggle_screen_orientation"
|
||||
app:tint="@color/white"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:visibility="visible" />
|
||||
@@ -523,10 +538,10 @@
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/previous_stream"
|
||||
android:focusable="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_previous"
|
||||
android:contentDescription="@string/previous_stream"
|
||||
app:tint="@color/white" />
|
||||
|
||||
|
||||
@@ -536,9 +551,9 @@
|
||||
android:layout_height="60dp"
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/pause"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_pause"
|
||||
android:contentDescription="@string/pause"
|
||||
app:tint="@color/white" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
@@ -549,10 +564,10 @@
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/next_stream"
|
||||
android:focusable="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_next"
|
||||
android:contentDescription="@string/next_stream"
|
||||
app:tint="@color/white" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -599,12 +614,12 @@
|
||||
android:layout_marginLeft="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/notification_action_repeat"
|
||||
android:focusable="true"
|
||||
android:padding="10dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/exo_controls_repeat_off"
|
||||
android:tint="?attr/colorAccent"
|
||||
android:contentDescription="@string/notification_action_repeat"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
@@ -615,12 +630,12 @@
|
||||
android:layout_toRightOf="@id/repeatButton"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/notification_action_shuffle"
|
||||
android:focusable="true"
|
||||
android:padding="10dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_shuffle"
|
||||
android:tint="?attr/colorAccent"
|
||||
android:contentDescription="@string/notification_action_shuffle"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
@@ -642,12 +657,12 @@
|
||||
android:layout_toLeftOf="@+id/itemsListClose"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/add_to_playlist"
|
||||
android:focusable="true"
|
||||
android:padding="10dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_playlist_add"
|
||||
android:tint="?attr/colorAccent"
|
||||
android:contentDescription="@string/add_to_playlist"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.parser.Parser;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SrtFromTtmlWriter}.
|
||||
*
|
||||
* Tests focus on {@code extractText()} and its handling of TTML <p> elements.
|
||||
* Note:
|
||||
* - Uses reflection to call the private {@code extractText()} method.
|
||||
* - Update {@code EXTRACT_TEXT_METHOD} if renamed.
|
||||
*
|
||||
* ---
|
||||
* NOTE ABOUT ENTITIES VS UNICODE ESCAPES
|
||||
*
|
||||
* - In short:
|
||||
* * UNICODE ESCAPES → used in Java source (e.g. SrtFromTtmlWriter.java)
|
||||
* * ENTITIES → used in TTML strings (this test file)
|
||||
*
|
||||
* - TTML is an XML-based format. Real TTML subtitles often encode special
|
||||
* characters as XML entities (named or numeric), e.g.:
|
||||
* & → '&' (\u0026)
|
||||
* < → '<' (\u003C)
|
||||
* 	 → tab (\u0009)
|
||||
* 
 → line feed (\u000A)
|
||||
* 
 → carriage return (\u000D)
|
||||
*
|
||||
* - Java source code uses **Unicode escapes** (e.g. "\u00A0") which are resolved
|
||||
* at compile time, so they do not represent real XML entities.
|
||||
*
|
||||
* - Purpose of these tests:
|
||||
* We simulate *real TTML input* as NewPipe receives it — i.e., strings that
|
||||
* still contain encoded XML entities (	, 
, 
, etc.).
|
||||
* The production code (`decodeXmlEntities()`) must convert these into their
|
||||
* actual Unicode characters before normalization.
|
||||
*/
|
||||
public class SrtFromTtmlWriterTest {
|
||||
private static final String TTML_WRAPPER_START = "<tt><body><div>";
|
||||
private static final String TTML_WRAPPER_END = "</div></body></tt>";
|
||||
private static final String EXTRACT_TEXT_METHOD = "extractText";
|
||||
// Please keep the same definition from `SrtFromTtmlWriter` class.
|
||||
private static final String NEW_LINE = "\r\n";
|
||||
|
||||
/*
|
||||
* TTML example for simple paragraph <p> without nested tags.
|
||||
* <p begin="00:00:01.000" end="00:00:03.000" style="s2">Hello World!</p>
|
||||
*/
|
||||
private static final String SIMPLE_TTML = "<p begin=\"00:00:01.000\" end=\"00:00:03.000\" "
|
||||
+ "style=\"s2\">Hello World!</p>";
|
||||
/**
|
||||
* TTML example with nested tags with <br>.
|
||||
* <p begin="00:00:01.000" end="00:00:03.000"><span style="s4">Hello</span><br>World!</p>
|
||||
*/
|
||||
private static final String NESTED_TTML = "<p begin=\"00:00:01.000\" end=\"00:00:03.000\">"
|
||||
+ "<span style=\"s4\">Hello</span><br>World!</p>";
|
||||
|
||||
/**
|
||||
* TTML example with HTML entities.
|
||||
* < → <, > → >, & → &, " → ", ' → '
|
||||
* ' → '
|
||||
*   → ' '
|
||||
*/
|
||||
private static final String ENTITY_TTML = "<p begin=\"00:00:05.000\" "
|
||||
+ "end=\"00:00:07.000\">"
|
||||
+ "<tag> & "text"''''"
|
||||
+ "  "
|
||||
+ "</p>";
|
||||
/**
|
||||
* TTML example with special characters:
|
||||
* - Spaces appear at the beginning and end of the text.
|
||||
* - Spaces are also present within the text (not just at the edges).
|
||||
* - The text includes various HTML entities such as ,
|
||||
* &, <, >, etc.
|
||||
* → non-breaking space (Unicode: '\u00A0', Entity: ' ')
|
||||
*/
|
||||
private static final String SPECIAL_TTML = "<p begin=\"00:00:05.000\" end=\"00:00:07.000\">"
|
||||
+ " ~~-Hello &&<<>>World!! "
|
||||
+ "</p>";
|
||||
|
||||
/**
|
||||
* TTML example with characters: tab.
|
||||
* 	 → \t
|
||||
* They are separated by '+' for clarity.
|
||||
*/
|
||||
private static final String TAB_TTML = "<p begin=\"00:00:05.000\" "
|
||||
+ "end=\"00:00:07.000\">"
|
||||
+ "		+		+		"
|
||||
+ "</p>";
|
||||
|
||||
/**
|
||||
* TTML example with line endings.
|
||||
* 
 → \r
|
||||
*/
|
||||
private static final String LINE_ENDING_0_TTML = "<p begin=\"00:00:05.000\" "
|
||||
+ "end=\"00:00:07.000\">"
|
||||
+ "

+

+

"
|
||||
+ "</p>";
|
||||
// 
 → \n
|
||||
private static final String LINE_ENDING_1_TTML = "<p begin=\"00:00:05.000\" "
|
||||
+ "end=\"00:00:07.000\">"
|
||||
+ "

+

+

"
|
||||
+ "</p>";
|
||||
private static final String LINE_ENDING_2_TTML =
|
||||
"<p begin=\"00:00:05.000\" end=\"00:00:07.000\">"
|
||||
+ "
+
+
"
|
||||
+ "</p>";
|
||||
|
||||
/**
|
||||
* TTML example with control characters.
|
||||
* For example:
|
||||
*  → \u0001
|
||||
*  → \u001F
|
||||
*
|
||||
* These control characters, if included as raw Unicode(e.g. '\u0001'),
|
||||
* are either invalid in XML or rendered as '?' when processed.
|
||||
* To avoid issues, they should be encoded(e.g. '') in TTML file.
|
||||
*
|
||||
* - Reference:
|
||||
* Unicode Basic Latin (https://unicode.org/charts/PDF/U0000.pdf),
|
||||
* ASCII Control (https://en.wikipedia.org/wiki/ASCII#Control_characters).
|
||||
* and the defination of these characters can be known.
|
||||
*/
|
||||
private static final String CONTROL_CHAR_TTML = "<p begin=\"00:00:05.000\" "
|
||||
+ "end=\"00:00:07.000\">"
|
||||
+ "+++++"
|
||||
+ "</p>";
|
||||
|
||||
|
||||
|
||||
private static final String EMPTY_TTML = "<p begin=\"00:00:01.000\" "
|
||||
+ "end=\"00:00:03.000\">"
|
||||
+ ""
|
||||
+ "</p>";
|
||||
|
||||
/**
|
||||
* TTML example with Unicode space characters.
|
||||
* These characters are encoded using character references
|
||||
* (&#xXXXX;).
|
||||
*
|
||||
* Includes:
|
||||
* ( ) '\u202F' → Narrow no-break space
|
||||
* ( ) '\u205F' → Medium mathematical space
|
||||
* ( ) '\u3000' → Ideographic space
|
||||
* '\u2000' ~ '\u200A' are whitespace characters:
|
||||
* ( ) '\u2000' → En quad
|
||||
* ( ) '\u2002' → En space
|
||||
* ( ) '\u200A' → Hair space
|
||||
*
|
||||
* Each character is separated by '+' for clarity.
|
||||
*/
|
||||
private static final String UNICODE_SPACE_TTML = "<p begin=\"00:00:05.000\" "
|
||||
+ "end=\"00:00:07.000\">"
|
||||
+ " + + + + + "
|
||||
+ "</p>";
|
||||
|
||||
/**
|
||||
* TTML example with non-spacing (invisible) characters.
|
||||
* These are encoded using character references (&#xXXXX;).
|
||||
*
|
||||
* Includes:
|
||||
* (​)'\u200B' → Zero-width space (ZWSP)
|
||||
* (‎)'\u200E' → Left-to-right mark (LRM)
|
||||
* (‏)'\u200F' → Right-to-left mark (RLM)
|
||||
*
|
||||
* They don't display any characters to the human eye.
|
||||
* '+' is used between them for clarity in test output.
|
||||
*/
|
||||
private static final String NON_SPACING_TTML = "<p begin=\"00:00:05.000\" "
|
||||
+ "end=\"00:00:07.000\">"
|
||||
+ "​+‎+‏"
|
||||
+ "</p>";
|
||||
|
||||
/**
|
||||
* Parses TTML string into a JSoup Document and selects the first <p> element.
|
||||
*
|
||||
* @param ttmlContent TTML content (e.g., <p>...</p>)
|
||||
* @return the first <p> element
|
||||
* @throws Exception if parsing or reflection fails
|
||||
*/
|
||||
private Element parseTtmlParagraph(final String ttmlContent) throws Exception {
|
||||
final String ttml = TTML_WRAPPER_START + ttmlContent + TTML_WRAPPER_END;
|
||||
final Document doc = Jsoup.parse(
|
||||
new ByteArrayInputStream(ttml.getBytes(StandardCharsets.UTF_8)),
|
||||
"UTF-8", "", Parser.xmlParser());
|
||||
return doc.select("body > div > p").first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes private extractText method via reflection.
|
||||
*
|
||||
* @param writer SrtFromTtmlWriter instance
|
||||
* @param paragraph <p> element to extract text from
|
||||
* @param text StringBuilder to store extracted text
|
||||
* @throws Exception if reflection fails
|
||||
*/
|
||||
private void invokeExtractText(final SrtFromTtmlWriter writer, final Element paragraph,
|
||||
final StringBuilder text) throws Exception {
|
||||
final Method method = writer.getClass()
|
||||
.getDeclaredMethod(EXTRACT_TEXT_METHOD, Node.class, StringBuilder.class);
|
||||
method.setAccessible(true);
|
||||
method.invoke(writer, paragraph, text);
|
||||
}
|
||||
|
||||
private String extractTextFromTtml(final String ttmlInput) throws Exception {
|
||||
final Element paragraph = parseTtmlParagraph(ttmlInput);
|
||||
final StringBuilder text = new StringBuilder();
|
||||
final SrtFromTtmlWriter writer = new SrtFromTtmlWriter(null, false);
|
||||
invokeExtractText(writer, paragraph, text);
|
||||
|
||||
final String actualText = text.toString();
|
||||
return actualText;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractTextSimpleParagraph() throws Exception {
|
||||
final String expected = "Hello World!";
|
||||
final String actual = extractTextFromTtml(SIMPLE_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractTextNestedTags() throws Exception {
|
||||
final String expected = "Hello\r\nWorld!";
|
||||
final String actual = extractTextFromTtml(NESTED_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractTextWithEntity() throws Exception {
|
||||
final String expected = "<tag> & \"text\"'''' ";
|
||||
final String actual = extractTextFromTtml(ENTITY_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractTextWithSpecialCharacters() throws Exception {
|
||||
final String expected = " ~~-Hello &&<<>>World!! ";
|
||||
final String actual = extractTextFromTtml(SPECIAL_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractTextWithTab() throws Exception {
|
||||
final String expected = " + + ";
|
||||
final String actual = extractTextFromTtml(TAB_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractTextWithLineEnding0() throws Exception {
|
||||
final String expected = NEW_LINE + NEW_LINE + "+"
|
||||
+ NEW_LINE + NEW_LINE + "+"
|
||||
+ NEW_LINE + NEW_LINE;
|
||||
final String actual = extractTextFromTtml(LINE_ENDING_0_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractTextWithLineEnding1() throws Exception {
|
||||
final String expected = NEW_LINE + NEW_LINE + "+"
|
||||
+ NEW_LINE + NEW_LINE + "+"
|
||||
+ NEW_LINE + NEW_LINE;
|
||||
final String actual = extractTextFromTtml(LINE_ENDING_1_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractTextWithLineEnding2() throws Exception {
|
||||
final String expected = NEW_LINE + "+"
|
||||
+ NEW_LINE + "+"
|
||||
+ NEW_LINE;
|
||||
final String actual = extractTextFromTtml(LINE_ENDING_2_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractTextWithControlCharacters() throws Exception {
|
||||
final String expected = "+++++";
|
||||
final String actual = extractTextFromTtml(CONTROL_CHAR_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case to ensure that extractText() does not throw an exception
|
||||
* when there are no text in the TTML paragraph (i.e., the paragraph
|
||||
* is empty).
|
||||
*
|
||||
* Note:
|
||||
* In the NewPipe, *.srt files will contain empty text lines by default.
|
||||
*/
|
||||
@Test
|
||||
public void testExtractTextWithEmpty() throws Exception {
|
||||
final String expected = "";
|
||||
final String actual = extractTextFromTtml(EMPTY_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractTextWithUnicodeSpaces() throws Exception {
|
||||
final String expected = " + + + + + ";
|
||||
final String actual = extractTextFromTtml(UNICODE_SPACE_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractTextWithNonSpacingCharacters() throws Exception {
|
||||
final String expected = "++";
|
||||
final String actual = extractTextFromTtml(NON_SPACING_TTML);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
}
|
||||
12
buildSrc/build.gradle.kts
Normal file
12
buildSrc/build.gradle.kts
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
@@ -4,18 +4,27 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
tasks.register("checkDependenciesOrder") {
|
||||
group = "verification"
|
||||
description = "Checks that each section in libs.versions.toml is sorted alphabetically"
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
val tomlFile = file("../gradle/libs.versions.toml")
|
||||
abstract class CheckDependenciesOrder : DefaultTask() {
|
||||
|
||||
doLast {
|
||||
if (!tomlFile.exists()) {
|
||||
throw GradleException("TOML file not found")
|
||||
}
|
||||
@get:InputFile
|
||||
abstract val tomlFile: RegularFileProperty
|
||||
|
||||
val lines = tomlFile.readLines()
|
||||
init {
|
||||
group = "verification"
|
||||
description = "Checks that each section in libs.versions.toml is sorted alphabetically"
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun run() {
|
||||
val file = tomlFile.get().asFile
|
||||
if (!file.exists()) error("TOML file not found")
|
||||
|
||||
val lines = file.readLines()
|
||||
val nonSortedBlocks = mutableListOf<List<String>>()
|
||||
var currentBlock = mutableListOf<String>()
|
||||
var prevLine = ""
|
||||
@@ -50,7 +59,7 @@ tasks.register("checkDependenciesOrder") {
|
||||
}
|
||||
|
||||
if (nonSortedBlocks.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
error(
|
||||
"The following lines were not sorted:\n" +
|
||||
nonSortedBlocks.joinToString("\n\n") { it.joinToString("\n") }
|
||||
)
|
||||
@@ -4,3 +4,6 @@ android.nonTransitiveRClass=true
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
|
||||
systemProp.file.encoding=utf-8
|
||||
|
||||
# https://docs.gradle.org/current/userguide/configuration_cache.html
|
||||
org.gradle.configuration-cache=true
|
||||
|
||||
@@ -4,47 +4,47 @@
|
||||
#
|
||||
|
||||
[versions]
|
||||
acra = "5.11.3"
|
||||
acra = "5.13.1"
|
||||
agp = "8.13.0"
|
||||
appcompat = "1.7.1"
|
||||
assertj = "3.24.2"
|
||||
assertj = "3.27.6"
|
||||
autoservice = "1.1.1"
|
||||
bridge = "v2.0.2"
|
||||
cardview = "1.0.0"
|
||||
checkstyle = "10.26.1"
|
||||
constraintlayout = "2.1.4"
|
||||
core = "1.12.0"
|
||||
desugar = "2.0.4"
|
||||
documentfile = "1.0.1"
|
||||
exoplayer = "2.18.7"
|
||||
fragment = "1.6.2"
|
||||
checkstyle = "12.1.1"
|
||||
constraintlayout = "2.2.1"
|
||||
core = "1.17.0"
|
||||
desugar = "2.1.5"
|
||||
documentfile = "1.1.0"
|
||||
exoplayer = "2.19.1"
|
||||
fragment = "1.8.9"
|
||||
groupie = "2.10.1"
|
||||
jsoup = "1.21.2"
|
||||
junit = "4.13.2"
|
||||
junit-ext = "1.1.5"
|
||||
kotlin = "1.9.25"
|
||||
ksp = "1.9.25-1.0.20"
|
||||
ktlint = "0.45.2"
|
||||
leakcanary = "2.12"
|
||||
lifecycle = "2.6.2"
|
||||
junit-ext = "1.3.0"
|
||||
kotlin = "2.2.21"
|
||||
ksp = "2.3.0"
|
||||
ktlint = "1.7.1"
|
||||
leakcanary = "2.14"
|
||||
lifecycle = "2.9.4"
|
||||
localbroadcastmanager = "1.1.0"
|
||||
markwon = "4.6.2"
|
||||
material = "1.11.0"
|
||||
material = "1.13.0"
|
||||
media = "1.7.1"
|
||||
mockitoCore = "5.6.0"
|
||||
okhttp = "4.12.0"
|
||||
phoenix = "2.1.2"
|
||||
mockitoCore = "5.20.0"
|
||||
okhttp = "5.3.0"
|
||||
phoenix = "3.0.0"
|
||||
#noinspection NewerVersionAvailable,GradleDependency --> 2.8 is the last version, not 2.71828!
|
||||
picasso = "2.8"
|
||||
preference = "1.2.1"
|
||||
prettytime = "5.0.8.Final"
|
||||
recyclerview = "1.3.2"
|
||||
room = "2.6.1"
|
||||
runner = "1.5.2"
|
||||
recyclerview = "1.4.0"
|
||||
room = "2.7.2"
|
||||
runner = "1.7.0"
|
||||
rxandroid = "3.0.2"
|
||||
rxbinding = "4.0.0"
|
||||
rxjava = "3.1.12"
|
||||
sonarqube = "4.0.0.2929"
|
||||
sonarqube = "7.0.1.6134"
|
||||
statesaver = "1.4.1"
|
||||
stetho = "1.6.0"
|
||||
swiperefreshlayout = "1.1.0"
|
||||
@@ -60,8 +60,8 @@ teamnewpipe-nanojson = "e9d656ddb49a412a5a0a5d5ef20ca7ef09549996"
|
||||
# to cause jitpack to regenerate the artifact.
|
||||
teamnewpipe-newpipe-extractor = "3af73262cc60cf555fd5f1d691f6c58e2db38ef5"
|
||||
viewpager2 = "1.1.0"
|
||||
webkit = "1.9.0"
|
||||
work = "2.8.1"
|
||||
webkit = "1.14.0"
|
||||
work = "2.10.5"
|
||||
|
||||
[libraries]
|
||||
acra-core = { module = "ch.acra:acra-core", version.ref = "acra" }
|
||||
@@ -87,7 +87,7 @@ androidx-runner = { module = "androidx.test:runner", version.ref = "runner" }
|
||||
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
|
||||
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" }
|
||||
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
|
||||
androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }
|
||||
androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "work" }
|
||||
androidx-work-rxjava3 = { module = "androidx.work:work-rxjava3", version.ref = "work" }
|
||||
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
|
||||
evernote-statesaver-compiler = { module = "com.evernote:android-state-processor", version.ref = "statesaver" }
|
||||
@@ -119,7 +119,7 @@ newpipe-nanojson = { module = "com.github.TeamNewPipe:nanojson", version.ref = "
|
||||
noties-markwon-core = { module = "io.noties.markwon:core", version.ref = "markwon" }
|
||||
noties-markwon-linkify = { module = "io.noties.markwon:linkify", version.ref = "markwon" }
|
||||
ocpsoft-prettytime = { module = "org.ocpsoft.prettytime:prettytime", version.ref = "prettytime" }
|
||||
pinterest-ktlint = { module = "com.pinterest:ktlint", version.ref = "ktlint" }
|
||||
pinterest-ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
|
||||
puppycrawl-checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" }
|
||||
reactivex-rxandroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroid" }
|
||||
reactivex-rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjava" }
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +1,8 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionSha256Sum=df67a32e86e3276d011735facb1535f64d0d88df84fa87521e90becc2d735444
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
33
gradlew
vendored
33
gradlew
vendored
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -83,7 +85,8 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -111,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -130,10 +132,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
@@ -141,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -149,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -166,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -198,16 +202,15 @@ fi
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
25
gradlew.bat
vendored
25
gradlew.bat
vendored
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -25,8 +25,9 @@ include (":app")
|
||||
// We assume, that NewPipe and NewPipe Extractor have the same parent directory.
|
||||
// If this is not the case, please change the path in includeBuild().
|
||||
|
||||
//includeBuild('../NewPipeExtractor') {
|
||||
//includeBuild("../NewPipeExtractor") {
|
||||
// dependencySubstitution {
|
||||
// substitute module('com.github.TeamNewPipe:NewPipeExtractor') using project(':extractor')
|
||||
// substitute(module("com.github.TeamNewPipe:NewPipeExtractor"))
|
||||
// .using(project(":extractor"))
|
||||
// }
|
||||
//}
|
||||
|
||||
Reference in New Issue
Block a user