From f99d53a94e88aa5a58f8dae739c31c2c816aaa58 Mon Sep 17 00:00:00 2001 From: TransZAllen Date: Thu, 29 Jan 2026 12:00:00 +0800 Subject: [PATCH 1/2] [duplicated subtitle] Initialize subtitle cache directory for `SubtitleDeduplicator` - Configure cache directory as `/storage/emulated/0/Android/data//cache/subtitle_cache`, otherwise it will be 'null', and subtitle deduplication will be skipped. - Ensures this initialization is called before `checkAndDeduplicate()` in `SubtitleDeduplicator.java` (NewPipeExtractor). --- app/src/main/java/org/schabi/newpipe/util/StateSaver.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java index 61fdb602f..08175e7b3 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java +++ b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java @@ -31,6 +31,7 @@ import androidx.core.os.BundleCompat; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.extractor.utils.SubtitleDeduplicator; import java.io.File; import java.io.FileInputStream; @@ -70,6 +71,8 @@ public final class StateSaver { if (TextUtils.isEmpty(cacheDirPath)) { cacheDirPath = context.getCacheDir().getAbsolutePath(); } + + SubtitleDeduplicator.setCacheDirPath(cacheDirPath); } /** From 4b86db0e8bdd324dc08f92698ccb4d0449f7b676 Mon Sep 17 00:00:00 2001 From: TransZAllen Date: Thu, 29 Jan 2026 12:00:01 +0800 Subject: [PATCH 2/2] [duplicated subtitle] Support downloading local subtitles after deduplication (`SubtitleDeduplicator`) - Introduce `SubtitleDeduplicator` into NewPipeExtractor; subtitles are now cached locally. - Add support for `file://` protocol to process local subtitle files (e.g., file:///storage/emulated/0/Android/data//cache/subtitle_cache/) instead of remote URLs. - Separate handling of local and remote subtitle in `run()` for better clarity. Note: After calling `SubtitleDeduplicator.checkAndDeduplicate()`, all YouTube subtitles are local. - Clarify the early return logic for single-URL subtitle download. --- .../giga/get/DownloadInitializer.java | 52 ++++++++ .../giga/get/LocalSubtitleConverter.java | 115 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 app/src/main/java/us/shandian/giga/get/LocalSubtitleConverter.java diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index 84e968b43..6e20f0e06 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -11,11 +11,15 @@ import java.io.IOException; import java.io.InterruptedIOException; import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN; +import org.schabi.newpipe.extractor.utils.SubtitleDeduplicator; public class DownloadInitializer extends Thread { private static final String TAG = "DownloadInitializer"; @@ -46,6 +50,23 @@ public class DownloadInitializer extends Thread { int retryCount = 0; int httpCode = 204; + //process local file URI (file://) + for (int i = 0; i < mMission.urls.length && mMission.running; i++) { + String currentUrl = mMission.urls[i]; + + if (true == islocalSubtitleUri(currentUrl)) { + LocalSubtitleConverter.convertLocalTtmlToVtt(currentUrl, mMission); + + // Subtitle download missions always contain exactly one URL. + // Once the local subtitle is converted, the mission is + // considered finished. + // Do not replace this with `continue` unless subtitle missions + // support multiple URLs in the future. + return; + } + } + + // process remote, for example: http:// or https:// while (true) { try { if (mMission.blocks == null && mMission.current == 0) { @@ -54,6 +75,7 @@ public class DownloadInitializer extends Thread { long lowestSize = Long.MAX_VALUE; for (int i = 0; i < mMission.urls.length && mMission.running; i++) { + mConn = mMission.openConnection(mMission.urls[i], true, 0, 0); mMission.establishConnection(mId, mConn); dispose(); @@ -205,4 +227,34 @@ public class DownloadInitializer extends Thread { super.interrupt(); if (mConn != null) dispose(); } + + private boolean isLocalUri(String url) { + String URL_PREFIX = SubtitleDeduplicator.LOCAL_SUBTITLE_URL_PREFIX; + + if (url.startsWith(URL_PREFIX)) { + return true; + } else { + return false; + } + } + + private boolean isSubtitleDownloadMission() { + char downloadKind = mMission.kind; + if ('s' == downloadKind) { + return true; + } + + return false; + } + + private boolean islocalSubtitleUri(String url) { + if (true == isSubtitleDownloadMission()) { + if (true == isLocalUri(url)) { + return true; + } + } + + return false; + } + } diff --git a/app/src/main/java/us/shandian/giga/get/LocalSubtitleConverter.java b/app/src/main/java/us/shandian/giga/get/LocalSubtitleConverter.java new file mode 100644 index 000000000..cff63a820 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/get/LocalSubtitleConverter.java @@ -0,0 +1,115 @@ +package us.shandian.giga.get; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import android.util.Log; + +import org.schabi.newpipe.streams.io.SharpStream; + +import org.schabi.newpipe.extractor.utils.SubtitleDeduplicator; + +final class LocalSubtitleConverter { + + private static final String TAG = "LocalSubtitleConverter"; + + private LocalSubtitleConverter() { + // no instance + } + + /** + * Converts a local(file://) TTML subtitle file into VTT format + * and stores it in the user-defined/chosen directory. + * + * @param localSubtitleUri file:// URI of the local TTML subtitle + * @param subtitleMission current download mission, and it is a command + * initiated manually by the user (via a button). + * @return 0 if success, non-zero error code otherwise + */ + public static int convertLocalTtmlToVtt(String localSubtitleUri, + DownloadMission subtitleMission) { + + if (!isValidLocalUri(localSubtitleUri)) { + return 1; + } + + File ttmlFile = new File( + getAbsolutePathFromLocalUri(localSubtitleUri) + ); + + if (!ttmlFile.exists()) { + subtitleMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null); + return 2; + } + + if (!subtitleMission.storage.canWrite()) { + subtitleMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, null); + return 3; + } + + writeLocalTtmlAsVtt(ttmlFile, subtitleMission); + + printLocalSubtitleConvertedOk(subtitleMission); + + return 0; + } + + + private static boolean isValidLocalUri(String localUri) { + String URL_PREFIX = SubtitleDeduplicator.LOCAL_SUBTITLE_URL_PREFIX; + + if (localUri.length() <= URL_PREFIX.length()) { + return false; + } + + return true; + } + + private static String getAbsolutePathFromLocalUri(String localSubtitleUri) { + String URL_PREFIX = SubtitleDeduplicator.LOCAL_SUBTITLE_URL_PREFIX; + int prefixLength = URL_PREFIX.length(); + // Remove URL_PREFIX + String absolutePath = localSubtitleUri.substring(prefixLength); + return absolutePath; + } + + private static void writeLocalTtmlAsVtt(File localTtmlFile, + DownloadMission mission) { + try (FileInputStream inputTtmlStream = new FileInputStream(localTtmlFile); + SharpStream outputVttStream = mission.storage.getStream()) { + + byte[] buffer = new byte[DownloadMission.BUFFER_SIZE]; + int bytesRead; + long totalBytes = 0; + + while ((bytesRead = inputTtmlStream.read(buffer)) != -1) { + outputVttStream.write(buffer, 0, bytesRead); + totalBytes += bytesRead; + mission.notifyProgress(bytesRead); + } + + mission.length = totalBytes; + mission.unknownLength = false; + mission.notifyFinished(); + + } catch (IOException e) { + String logMessage = "Error extracting subtitle paragraphs from " + + localTtmlFile.getAbsolutePath() + ", error:" + + e.getMessage(); + Log.e(TAG, logMessage); + mission.notifyError(DownloadMission.ERROR_FILE_CREATION, e); + } + } + + private static void printLocalSubtitleConvertedOk(DownloadMission mission) { + try { + String logMessage = "Local subtitle uri is extracted to:" + + mission.storage.getName(); + Log.i(TAG, logMessage); + } catch (NullPointerException e) { + Log.w(TAG, "Fail to convert ttml subtitle to vtt.", e); + } + } +}