diff --git a/app/build.gradle.kts b/app/build.gradle.kts index df8c278c1..1aa5297c5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -42,9 +42,9 @@ android { minSdk = 21 targetSdk = 35 - versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1005 + versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1006 - versionName = "0.28.0" + versionName = "0.28.1" System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it } testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0cdffbe2e..8bcef3fbd 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -16,6 +16,11 @@ -dontwarn javax.script.** -keep class jdk.dynalink.** { *; } -dontwarn jdk.dynalink.** +# Rules for jsoup +# Ignore intended-to-be-optional re2j classes - only needed if using re2j for jsoup regex +# jsoup safely falls back to JDK regex if re2j not on classpath, but has concrete re2j refs +# See https://github.com/jhy/jsoup/issues/2459 - may be resolved in future, then this may be removed +-dontwarn com.google.re2j.** ## Rules for ExoPlayer -keep class com.google.android.exoplayer2.** { *; } diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 0857fa339..741bda246 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -1133,7 +1133,7 @@ public class DownloadDialog extends DialogFragment } DownloadManagerService.startMission(context, urls, storage, kind, threads, - currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo)); + currentInfo, psName, psArgs, nearLength, new ArrayList<>(recoveryInfo)); Toast.makeText(context, getString(R.string.download_has_started), Toast.LENGTH_SHORT).show(); diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index d15db84c7..1f3772dd5 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.local.bookmark; import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists; +import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout; import android.content.DialogInterface; import android.os.Bundle; @@ -417,10 +418,11 @@ public final class BookmarkFragment extends BaseLocalListFragment backup; private List streams; - private transient BehaviorSubject eventBroadcast; + private transient PublishSubject eventBroadcast; private transient Flowable broadcastReceiver; private transient boolean disposed = false; @@ -70,7 +70,7 @@ public abstract class PlayQueue implements Serializable { *

*/ public void init() { - eventBroadcast = BehaviorSubject.create(); + eventBroadcast = PublishSubject.create(); broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java index 266cec24a..7cdc84e22 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java @@ -1,8 +1,14 @@ package org.schabi.newpipe.streams; +import static org.schabi.newpipe.MainActivity.DEBUG; + +import android.util.Log; +import android.util.Pair; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.streams.WebMReader.Cluster; import org.schabi.newpipe.streams.WebMReader.Segment; import org.schabi.newpipe.streams.WebMReader.SimpleBlock; @@ -13,6 +19,10 @@ import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; /** * @author kapodamy @@ -52,8 +62,10 @@ public class OggFromWebMWriter implements Closeable { private long segmentTableNextTimestamp = TIME_SCALE_NS; private final int[] crc32Table = new int[256]; + private final StreamInfo streamInfo; - public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) { + public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target, + @Nullable final StreamInfo streamInfo) { if (!source.canRead() || !source.canRewind()) { throw new IllegalArgumentException("source stream must be readable and allows seeking"); } @@ -63,6 +75,7 @@ public class OggFromWebMWriter implements Closeable { this.source = source; this.output = target; + this.streamInfo = streamInfo; this.streamId = (int) System.currentTimeMillis(); @@ -271,12 +284,31 @@ public class OggFromWebMWriter implements Closeable { @Nullable private byte[] makeMetadata() { + if (DEBUG) { + Log.d("OggFromWebMWriter", "Downloading media with codec ID " + webmTrack.codecId); + } + if ("A_OPUS".equals(webmTrack.codecId)) { - return new byte[]{ - 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string - 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) - 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) - }; + final var metadata = new ArrayList>(); + if (streamInfo != null) { + metadata.add(Pair.create("COMMENT", streamInfo.getUrl())); + metadata.add(Pair.create("GENRE", streamInfo.getCategory())); + metadata.add(Pair.create("ARTIST", streamInfo.getUploaderName())); + metadata.add(Pair.create("TITLE", streamInfo.getName())); + metadata.add(Pair.create("DATE", streamInfo + .getUploadDate() + .getLocalDateTime() + .format(DateTimeFormatter.ISO_DATE))); + } + + if (DEBUG) { + Log.d("OggFromWebMWriter", "Creating metadata header with this data:"); + metadata.forEach(p -> { + Log.d("OggFromWebMWriter", p.first + "=" + p.second); + }); + } + + return makeOpusTagsHeader(metadata); } else if ("A_VORBIS".equals(webmTrack.codecId)) { return new byte[]{ 0x03, // ¿¿¿??? @@ -290,6 +322,59 @@ public class OggFromWebMWriter implements Closeable { return null; } + /** + * This creates a single metadata tag for use in opus metadata headers. It contains the four + * byte string length field and includes the string as-is. This cannot be used independently, + * but must follow a proper "OpusTags" header. + * + * @param pair A key-value pair in the format "KEY=some value" + * @return The binary data of the encoded metadata tag + */ + private static byte[] makeOpusMetadataTag(final Pair pair) { + final var keyValue = pair.first.toUpperCase() + "=" + pair.second.trim(); + + final var bytes = keyValue.getBytes(); + final var buf = ByteBuffer.allocate(4 + bytes.length); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(bytes.length); + buf.put(bytes); + return buf.array(); + } + + /** + * This returns a complete "OpusTags" header, created from the provided metadata tags. + *

+ * You probably want to use makeOpusMetadata(), which uses this function to create + * a header with sensible metadata filled in. + * + * @param keyValueLines A list of pairs of the tags. This can also be though of as a mapping + * from one key to multiple values. + * @return The binary header + */ + private static byte[] makeOpusTagsHeader(final List> keyValueLines) { + final var tags = keyValueLines + .stream() + .filter(p -> !p.second.isBlank()) + .map(OggFromWebMWriter::makeOpusMetadataTag) + .collect(Collectors.toUnmodifiableList()); + + final var tagsBytes = tags.stream().collect(Collectors.summingInt(arr -> arr.length)); + + // Fixed header fields + dynamic fields + final var byteCount = 16 + tagsBytes; + + final var head = ByteBuffer.allocate(byteCount); + head.order(ByteOrder.LITTLE_ENDIAN); + head.put(new byte[]{ + 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string + 0x00, 0x00, 0x00, 0x00, // vendor (aka. Encoder) string of length 0 + }); + head.putInt(tags.size()); // 4 bytes for tag count + tags.forEach(head::put); // dynamic amount of tag bytes + + return head.array(); + } + private void write(final ByteBuffer buffer) throws IOException { output.write(buffer.array(), 0, buffer.position()); buffer.position(0); diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index ea41f3e81..409fcb30c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -806,7 +806,7 @@ public final class ListHelper { final Locale preferredLanguage = Localization.getPreferredLocale(context); final boolean preferOriginalAudio = preferences.getBoolean(context.getString(R.string.prefer_original_audio_key), - false); + true); final boolean preferDescriptiveAudio = preferences.getBoolean(context.getString(R.string.prefer_descriptive_audio_key), false); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java index dc46ced5d..badb5f7ed 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java @@ -34,7 +34,7 @@ class OggFromWebmDemuxer extends Postprocessing { @Override int process(SharpStream out, @NonNull SharpStream... sources) throws IOException { - OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out); + OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out, streamInfo); demuxer.parseSource(); demuxer.selectTrack(0); demuxer.build(); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 7f5c85d27..1c9143252 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -4,6 +4,7 @@ import android.util.Log; import androidx.annotation.NonNull; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.streams.io.SharpStream; import java.io.File; @@ -30,7 +31,8 @@ public abstract class Postprocessing implements Serializable { public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; public transient static final String ALGORITHM_OGG_FROM_WEBM_DEMUXER = "webm-ogg-d"; - public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) { + public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args, + StreamInfo streamInfo) { Postprocessing instance; switch (algorithmName) { @@ -56,6 +58,7 @@ public abstract class Postprocessing implements Serializable { } instance.args = args; + instance.streamInfo = streamInfo; return instance; } @@ -75,8 +78,8 @@ public abstract class Postprocessing implements Serializable { */ private final String name; - private String[] args; + protected StreamInfo streamInfo; private transient DownloadMission mission; diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index 45211211f..76da18b2d 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -40,6 +40,7 @@ import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import org.schabi.newpipe.download.DownloadActivity; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.streams.io.StoredDirectoryHelper; import org.schabi.newpipe.streams.io.StoredFileHelper; @@ -74,12 +75,12 @@ public class DownloadManagerService extends Service { private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; - private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath"; private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath"; private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag"; private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo"; + private static final String EXTRA_STREAM_INFO = "DownloadManagerService.extra.streamInfo"; private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; @@ -353,13 +354,13 @@ public class DownloadManagerService extends Service { * @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined) * @param threads the number of threads maximal used to download chunks of the file. * @param psName the name of the required post-processing algorithm, or {@code null} to ignore. - * @param source source url of the resource + * @param streamInfo stream metadata that may be written into the downloaded file. * @param psArgs the arguments for the post-processing algorithm. * @param nearLength the approximated final length of the file * @param recoveryInfo array of MissionRecoveryInfo, in case is required recover the download */ public static void startMission(Context context, String[] urls, StoredFileHelper storage, - char kind, int threads, String source, String psName, + char kind, int threads, StreamInfo streamInfo, String psName, String[] psArgs, long nearLength, ArrayList recoveryInfo) { final Intent intent = new Intent(context, DownloadManagerService.class) @@ -367,14 +368,14 @@ public class DownloadManagerService extends Service { .putExtra(EXTRA_URLS, urls) .putExtra(EXTRA_KIND, kind) .putExtra(EXTRA_THREADS, threads) - .putExtra(EXTRA_SOURCE, source) .putExtra(EXTRA_POSTPROCESSING_NAME, psName) .putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs) .putExtra(EXTRA_NEAR_LENGTH, nearLength) .putExtra(EXTRA_RECOVERY_INFO, recoveryInfo) .putExtra(EXTRA_PARENT_PATH, storage.getParentUri()) .putExtra(EXTRA_PATH, storage.getUri()) - .putExtra(EXTRA_STORAGE_TAG, storage.getTag()); + .putExtra(EXTRA_STORAGE_TAG, storage.getTag()) + .putExtra(EXTRA_STREAM_INFO, streamInfo); context.startService(intent); } @@ -387,9 +388,9 @@ public class DownloadManagerService extends Service { char kind = intent.getCharExtra(EXTRA_KIND, '?'); String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); - String source = intent.getStringExtra(EXTRA_SOURCE); long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); String tag = intent.getStringExtra(EXTRA_STORAGE_TAG); + StreamInfo streamInfo = (StreamInfo)intent.getSerializableExtra(EXTRA_STREAM_INFO); final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO, MissionRecoveryInfo.class); Objects.requireNonNull(recovery); @@ -405,11 +406,11 @@ public class DownloadManagerService extends Service { if (psName == null) ps = null; else - ps = Postprocessing.getAlgorithm(psName, psArgs); + ps = Postprocessing.getAlgorithm(psName, psArgs, streamInfo); final DownloadMission mission = new DownloadMission(urls, storage, kind, ps); mission.threadCount = threads; - mission.source = source; + mission.source = streamInfo.getUrl(); mission.nearLength = nearLength; mission.recoveryInfo = recovery.toArray(new MissionRecoveryInfo[0]); diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml index 727ce4df4..6e8e2979b 100644 --- a/app/src/main/res/xml/video_audio_settings.xml +++ b/app/src/main/res/xml/video_audio_settings.xml @@ -62,7 +62,7 @@ app:useSimpleSummaryProvider="true" /> = 23