Add option to DownloadDialog to enable metadata embedding

Metadata embedding is disabled by default across all post-processing algorithms.

A new parameter and variable is introduced although there is the parameter 'args' already. The information on whether the metadata is going to be embedded needs to be parsed by every audio or video post-processing algorithm anyway. Getting it from the list is more difficult and less error-prone than creating a new param.
This commit is contained in:
tobigr 2026-02-09 22:59:36 +01:00
parent 41c6bb0fac
commit ee5f52ebc2
12 changed files with 68 additions and 25 deletions

View File

@ -1049,6 +1049,7 @@ public class DownloadDialog extends DialogFragment
final Stream selectedStream;
Stream secondaryStream = null;
final char kind;
final boolean embedMetadata = dialogBinding.metadataSwitch.isChecked();
int threads = dialogBinding.threads.getProgress() + 1;
final String[] urls;
final List<MissionRecoveryInfo> recoveryInfo;
@ -1136,8 +1137,8 @@ public class DownloadDialog extends DialogFragment
);
}
DownloadManagerService.startMission(context, urls, storage, kind, threads,
currentInfo, psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo,
psName, embedMetadata, psArgs, nearLength, new ArrayList<>(recoveryInfo));
Toast.makeText(context, getString(R.string.download_has_started),
Toast.LENGTH_SHORT).show();

View File

@ -64,9 +64,11 @@ public class Mp4FromDashWriter {
private final ArrayList<Integer> compatibleBrands = new ArrayList<>(5);
private final boolean embedMetadata;
private final Mp4MetadataHelper metadataHelper;
public Mp4FromDashWriter(final StreamInfo streamInfo,
public Mp4FromDashWriter(final boolean embedMetadata,
final StreamInfo streamInfo,
final Bitmap thumbnail,
final SharpStream... sources) throws IOException {
for (final SharpStream src : sources) {
@ -75,6 +77,7 @@ public class Mp4FromDashWriter {
}
}
this.embedMetadata = embedMetadata;
this.metadataHelper = new Mp4MetadataHelper(
this::auxOffset,
buffer -> {
@ -750,7 +753,9 @@ public class Mp4FromDashWriter {
makeMvhd(longestTrack);
metadataHelper.makeUdta();
if (embedMetadata) {
metadataHelper.makeUdta();
}
for (int i = 0; i < tracks.length; i++) {
if (tracks[i].trak.tkhd.matrix.length != 36) {

View File

@ -121,6 +121,7 @@ public class OggFromWebMWriter implements Closeable {
private long segmentTableNextTimestamp = TIME_SCALE_NS;
private final int[] crc32Table = new int[256];
private final boolean embedMetadata;
private final StreamInfo streamInfo;
private final Bitmap thumbnail;
@ -128,11 +129,13 @@ public class OggFromWebMWriter implements Closeable {
* Constructor of OggFromWebMWriter.
* @param source
* @param target
* @param embedMetadata whether to embed metadata in the output Ogg stream
* @param streamInfo the stream info
* @param thumbnail the thumbnail bitmap used as cover art
*/
public OggFromWebMWriter(@NonNull final SharpStream source,
@NonNull final SharpStream target,
final boolean embedMetadata,
@Nullable final StreamInfo streamInfo,
@Nullable final Bitmap thumbnail) {
if (!source.canRead() || !source.canRewind()) {
@ -144,6 +147,7 @@ public class OggFromWebMWriter implements Closeable {
this.source = source;
this.output = target;
this.embedMetadata = embedMetadata;
this.streamInfo = streamInfo;
this.thumbnail = thumbnail;
@ -264,9 +268,11 @@ public class OggFromWebMWriter implements Closeable {
}
/* step 3: create packet with metadata */
final byte[] buffer = makeCommentHeader();
if (buffer != null) {
addPacketSegmentMultiPage(buffer, header);
if (embedMetadata) {
final byte[] buffer = makeCommentHeader();
if (buffer != null) {
addPacketSegmentMultiPage(buffer, header);
}
}
/* step 4: calculate amount of packets */

View File

@ -31,7 +31,7 @@ class M4aNoDash extends Postprocessing {
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4FromDashWriter muxer = new Mp4FromDashWriter(
this.streamInfo, this.thumbnail, sources[0]);
this.embedMetadata, this.streamInfo, this.thumbnail, sources[0]);
muxer.setMainBrand(0x4D344120);// binary string "M4A "
muxer.parseSources();
muxer.selectTracks(0);

View File

@ -31,6 +31,11 @@ public class Mp3Metadata extends Postprocessing {
super(true, true, ALGORITHM_MP3_METADATA);
}
@Override
boolean test(SharpStream... sources) {
return this.embedMetadata;
}
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
if (sources == null || sources.length == 0 || sources[0] == null) {

View File

@ -16,7 +16,8 @@ class Mp4FromDashMuxer extends Postprocessing {
@Override
int process(SharpStream out, SharpStream... sources) throws IOException {
Mp4FromDashWriter muxer = new Mp4FromDashWriter(this.streamInfo, this.thumbnail, sources);
Mp4FromDashWriter muxer = new Mp4FromDashWriter(
this.embedMetadata, this.streamInfo, this.thumbnail, sources);
muxer.parseSources();
muxer.selectTracks(0, 0);
muxer.build(out);

View File

@ -27,6 +27,9 @@ public class Mp4Metadata extends Postprocessing {
@Override
boolean test(SharpStream... sources) throws IOException {
// nothing to do if metadata should not be embedded
if (!embedMetadata) return false;
// quick check: ensure there's at least one source and it looks like an MP4,
// i.e. the file has a 'moov' box near the beginning.
// THe 'udta' box is inserted inside 'moov', so if there's no 'moov' we can't do anything.

View File

@ -35,7 +35,7 @@ class OggFromWebmDemuxer extends Postprocessing {
@Override
int process(SharpStream out, @NonNull SharpStream... sources) throws IOException {
OggFromWebMWriter demuxer = new OggFromWebMWriter(
sources[0], out, streamInfo, thumbnail);
sources[0], out, embedMetadata, streamInfo, thumbnail);
demuxer.parseSource();
demuxer.selectTrack(0);
demuxer.build();

View File

@ -35,7 +35,9 @@ public abstract class Postprocessing implements Serializable {
public static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
public 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,
boolean embedMetadata,
String[] args,
@NonNull StreamInfo streamInfo) {
Postprocessing instance;
@ -67,6 +69,7 @@ public abstract class Postprocessing implements Serializable {
instance.args = args;
instance.streamInfo = streamInfo;
instance.embedMetadata = embedMetadata;
return instance;
}
@ -88,6 +91,11 @@ public abstract class Postprocessing implements Serializable {
private String[] args;
/**
* Indicates whether the metadata should be embedded in the file or not.
*/
boolean embedMetadata;
/**
* StreamInfo object related to the current download
*/

View File

@ -40,7 +40,6 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
@ -75,6 +74,7 @@ public class DownloadManagerService extends Service {
private static final String EXTRA_KIND = "DownloadManagerService.extra.kind";
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_METADATA = "DownloadManagerService.extra.postprocessingMetadata";
private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs";
private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength";
private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath";
@ -349,20 +349,21 @@ public class DownloadManagerService extends Service {
/**
* Start a new download mission
*
* @param context the activity context
* @param urls array of urls to download
* @param storage where the file is saved
* @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 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
* @param context the activity context
* @param urls array of urls to download
* @param storage where the file is saved
* @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 streamInfo stream metadata that may be written into the downloaded file.
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
* @param embedMetadata whether the metadata should be embedded 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, StreamInfo streamInfo, String psName,
String[] psArgs, long nearLength,
boolean embedMetadata, String[] psArgs, long nearLength,
ArrayList<MissionRecoveryInfo> recoveryInfo) {
final Intent intent = new Intent(context, DownloadManagerService.class)
.setAction(Intent.ACTION_RUN)
@ -370,6 +371,7 @@ public class DownloadManagerService extends Service {
.putExtra(EXTRA_KIND, kind)
.putExtra(EXTRA_THREADS, threads)
.putExtra(EXTRA_POSTPROCESSING_NAME, psName)
.putExtra(EXTRA_POSTPROCESSING_METADATA, embedMetadata)
.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs)
.putExtra(EXTRA_NEAR_LENGTH, nearLength)
.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo)
@ -388,6 +390,7 @@ public class DownloadManagerService extends Service {
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
char kind = intent.getCharExtra(EXTRA_KIND, '?');
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
boolean embedMetadata = intent.getBooleanExtra(EXTRA_POSTPROCESSING_METADATA, false);
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
@ -407,7 +410,7 @@ public class DownloadManagerService extends Service {
if (psName == null)
ps = null;
else
ps = Postprocessing.getAlgorithm(psName, psArgs, streamInfo);
ps = Postprocessing.getAlgorithm(psName, embedMetadata, psArgs, streamInfo);
final DownloadMission mission = new DownloadMission(
urls, storage, kind, ps, streamInfo, getApplicationContext());

View File

@ -105,11 +105,21 @@
android:text="@string/audio_track_present_in_video"
android:textSize="12sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/metadata_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/audio_track_present_in_video_text"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="12dp"
android:text="@string/download_embed_metadata" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/threads_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/audio_track_present_in_video_text"
android:layout_below="@+id/metadata_switch"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="6dp"

View File

@ -354,6 +354,7 @@
<string name="no_dir_yet">No download folder set yet, choose the default download folder now</string>
<string name="msg_popup_permission">This permission is needed to\nopen in popup mode</string>
<string name="one_item_deleted">1 item deleted.</string>
<string name="download_embed_metadata">Embed metadata such as title, author, thumbnail</string>
<!-- reCAPTCHA -->
<string name="title_activity_recaptcha">reCAPTCHA challenge</string>
<string name="subtitle_activity_recaptcha">Press \"Done\" when solved</string>