Skip to content

Commit 3cda47b

Browse files
committed
Opus metadata download: Add dynamic tags
This adds content-aware metadata tags to downloaded opus files. Previously, only a static string with a timestamp was added.
1 parent e3f75f5 commit 3cda47b

5 files changed

Lines changed: 106 additions & 28 deletions

File tree

app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1133,7 +1133,7 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
11331133
}
11341134

11351135
DownloadManagerService.startMission(context, urls, storage, kind, threads,
1136-
currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));
1136+
currentInfo, psName, psArgs, nearLength, new ArrayList<>(recoveryInfo));
11371137

11381138
Toast.makeText(context, getString(R.string.download_has_started),
11391139
Toast.LENGTH_SHORT).show();

app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package org.schabi.newpipe.streams;
22

3+
import android.util.Log;
4+
35
import androidx.annotation.NonNull;
46
import androidx.annotation.Nullable;
57

8+
import org.schabi.newpipe.BuildConfig;
9+
import org.schabi.newpipe.extractor.stream.StreamInfo;
610
import org.schabi.newpipe.streams.WebMReader.Cluster;
711
import org.schabi.newpipe.streams.WebMReader.Segment;
812
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
@@ -14,6 +18,8 @@
1418
import java.nio.ByteBuffer;
1519
import java.nio.ByteOrder;
1620
import java.time.OffsetDateTime;
21+
import java.time.format.DateTimeFormatter;
22+
import java.util.stream.Collectors;
1723

1824
/**
1925
* @author kapodamy
@@ -53,8 +59,10 @@ public class OggFromWebMWriter implements Closeable {
5359
private long segmentTableNextTimestamp = TIME_SCALE_NS;
5460

5561
private final int[] crc32Table = new int[256];
62+
private final StreamInfo streamInfo;
5663

57-
public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) {
64+
public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target,
65+
@Nullable final StreamInfo streamInfo) {
5866
if (!source.canRead() || !source.canRewind()) {
5967
throw new IllegalArgumentException("source stream must be readable and allows seeking");
6068
}
@@ -64,6 +72,7 @@ public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final Sharp
6472

6573
this.source = source;
6674
this.output = target;
75+
this.streamInfo = streamInfo;
6776

6877
this.streamId = (int) System.currentTimeMillis();
6978

@@ -272,25 +281,29 @@ private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffe
272281

273282
@Nullable
274283
private byte[] makeMetadata() {
284+
Log.d("OggFromWebMWriter", "Downloading media with codec ID " + webmTrack.codecId);
285+
275286
if ("A_OPUS".equals(webmTrack.codecId)) {
276-
final var commentFormat = "COMMENT=Downloaded using NewPipe on %s";
277-
final var commentStr = String.format(commentFormat, OffsetDateTime.now().toString());
278-
final var comment = commentStr.getBytes();
279-
final var head = ByteBuffer.allocate(20 + comment.length);
280-
head.order(ByteOrder.LITTLE_ENDIAN);
281-
head.put(new byte[]{
282-
// Byte order is LE, i.e. LSB first
283-
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string
284-
0x00, 0x00, 0x00, 0x00, // vendor string of length 0
285-
0x01, 0x00, 0x00, 0x00, // additional tags count
286-
287-
// + 4 bytes for the comment string length
288-
// + N bytes for the comment string itself
289-
});
290-
head.putInt(comment.length);
291-
head.put(comment);
292-
293-
return head.array();
287+
var metadata = "";
288+
metadata += String.format("COMMENT=Downloaded using NewPipe %s on %s\n",
289+
BuildConfig.VERSION_NAME,
290+
OffsetDateTime.now().toString());
291+
if (streamInfo != null) {
292+
metadata += String.format("COMMENT=URL: %s\n", streamInfo.getUrl());
293+
metadata += String.format("GENRE=%s\n", streamInfo.getCategory());
294+
metadata += String.format("ARTIST=%s\n", streamInfo.getUploaderName());
295+
metadata += String.format("TITLE=%s\n", streamInfo.getName());
296+
metadata += String.format("DATE=%s\n",
297+
streamInfo
298+
.getUploadDate()
299+
.getLocalDateTime()
300+
.format(DateTimeFormatter.ISO_DATE));
301+
}
302+
303+
Log.d("OggFromWebMWriter", "Creating metadata header with this data:");
304+
Log.d("OggFromWebMWriter", metadata);
305+
306+
return makeOpusTagsHeader(metadata);
294307
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
295308
return new byte[]{
296309
0x03, // ¿¿¿???
@@ -304,6 +317,64 @@ private byte[] makeMetadata() {
304317
return null;
305318
}
306319

320+
/**
321+
* This creates a single metadata tag for use in opus metadata headers. It contains the four
322+
* byte string length field and includes the string as-is. This cannot be used independently,
323+
* but must follow a proper "OpusTags" header.
324+
*
325+
* @param keyValue A key-value pair in the format "KEY=some value"
326+
* @return The binary data of the encoded metadata tag
327+
*/
328+
private static byte[] makeOpusMetadataTag(final String keyValue) {
329+
// Ensure the key is uppercase
330+
final var delimiterIndex = keyValue.indexOf('=');
331+
final var key = keyValue.substring(0, delimiterIndex).toUpperCase();
332+
final var value = keyValue.substring(delimiterIndex + 1);
333+
final var reconstructedKeyValue = key + "=" + value;
334+
335+
final var bytes = reconstructedKeyValue.getBytes();
336+
final var buf = ByteBuffer.allocate(4 + bytes.length);
337+
buf.order(ByteOrder.LITTLE_ENDIAN);
338+
buf.putInt(bytes.length);
339+
buf.put(bytes);
340+
return buf.array();
341+
}
342+
343+
/**
344+
* This returns a complete "OpusTags" header, created from the provided tags string.
345+
* <p>
346+
* You probably want to use makeOpusMetadata(), which uses this function to create
347+
* a header with sensible metadata filled in.
348+
*
349+
* @param keyValueLines A multiline string with each line containing a key-value pair
350+
* in the format "KEY=some value". This may also be a blank string.
351+
* @return The binary header
352+
*/
353+
private static byte[] makeOpusTagsHeader(@NonNull final String keyValueLines) {
354+
final var tags = keyValueLines
355+
.lines()
356+
.map(String::trim)
357+
.filter(s -> !s.isBlank())
358+
.map(OggFromWebMWriter::makeOpusMetadataTag)
359+
.collect(Collectors.toUnmodifiableList());
360+
361+
final var tagsBytes = tags.stream().collect(Collectors.summingInt(arr -> arr.length));
362+
363+
// Fixed header fields + dynamic fields
364+
final var byteCount = 16 + tagsBytes;
365+
366+
final var head = ByteBuffer.allocate(byteCount);
367+
head.order(ByteOrder.LITTLE_ENDIAN);
368+
head.put(new byte[]{
369+
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string
370+
0x00, 0x00, 0x00, 0x00, // vendor (aka. Encoder) string of length 0
371+
});
372+
head.putInt(tags.size()); // 4 bytes for tag count
373+
tags.forEach(head::put); // dynamic amount of tag bytes
374+
375+
return head.array();
376+
}
377+
307378
private void write(final ByteBuffer buffer) throws IOException {
308379
output.write(buffer.array(), 0, buffer.position());
309380
buffer.position(0);

app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ boolean test(SharpStream... sources) throws IOException {
3434

3535
@Override
3636
int process(SharpStream out, @NonNull SharpStream... sources) throws IOException {
37-
OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out);
37+
OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out, streamInfo);
3838
demuxer.parseSource();
3939
demuxer.selectTrack(0);
4040
demuxer.build();

app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import androidx.annotation.NonNull;
66

7+
import org.schabi.newpipe.extractor.stream.StreamInfo;
78
import org.schabi.newpipe.streams.io.SharpStream;
89

910
import java.io.File;
@@ -30,7 +31,8 @@ public abstract class Postprocessing implements Serializable {
3031
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
3132
public transient static final String ALGORITHM_OGG_FROM_WEBM_DEMUXER = "webm-ogg-d";
3233

33-
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) {
34+
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args,
35+
StreamInfo streamInfo) {
3436
Postprocessing instance;
3537

3638
switch (algorithmName) {
@@ -56,6 +58,7 @@ public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[
5658
}
5759

5860
instance.args = args;
61+
instance.streamInfo = streamInfo;
5962
return instance;
6063
}
6164

@@ -75,8 +78,8 @@ public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[
7578
*/
7679
private final String name;
7780

78-
7981
private String[] args;
82+
protected StreamInfo streamInfo;
8083

8184
private transient DownloadMission mission;
8285

app/src/main/java/us/shandian/giga/service/DownloadManagerService.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
import org.schabi.newpipe.R;
4242
import org.schabi.newpipe.download.DownloadActivity;
43+
import org.schabi.newpipe.extractor.stream.StreamInfo;
4344
import org.schabi.newpipe.player.helper.LockManager;
4445
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
4546
import org.schabi.newpipe.streams.io.StoredFileHelper;
@@ -80,6 +81,7 @@ public class DownloadManagerService extends Service {
8081
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
8182
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
8283
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
84+
private static final String EXTRA_STREAM_INFO = "DownloadManagerService.extra.streamInfo";
8385

8486
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
8587
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
@@ -353,28 +355,29 @@ public void updateForegroundState(boolean state) {
353355
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
354356
* @param threads the number of threads maximal used to download chunks of the file.
355357
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
356-
* @param source source url of the resource
358+
* @param streamInfo stream metadata that may be written into the downloaded file.
357359
* @param psArgs the arguments for the post-processing algorithm.
358360
* @param nearLength the approximated final length of the file
359361
* @param recoveryInfo array of MissionRecoveryInfo, in case is required recover the download
360362
*/
361363
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
362-
char kind, int threads, String source, String psName,
364+
char kind, int threads, StreamInfo streamInfo, String psName,
363365
String[] psArgs, long nearLength,
364366
ArrayList<MissionRecoveryInfo> recoveryInfo) {
365367
final Intent intent = new Intent(context, DownloadManagerService.class)
366368
.setAction(Intent.ACTION_RUN)
367369
.putExtra(EXTRA_URLS, urls)
368370
.putExtra(EXTRA_KIND, kind)
369371
.putExtra(EXTRA_THREADS, threads)
370-
.putExtra(EXTRA_SOURCE, source)
372+
.putExtra(EXTRA_SOURCE, streamInfo.getUrl())
371373
.putExtra(EXTRA_POSTPROCESSING_NAME, psName)
372374
.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs)
373375
.putExtra(EXTRA_NEAR_LENGTH, nearLength)
374376
.putExtra(EXTRA_RECOVERY_INFO, recoveryInfo)
375377
.putExtra(EXTRA_PARENT_PATH, storage.getParentUri())
376378
.putExtra(EXTRA_PATH, storage.getUri())
377-
.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
379+
.putExtra(EXTRA_STORAGE_TAG, storage.getTag())
380+
.putExtra(EXTRA_STREAM_INFO, streamInfo);
378381

379382
context.startService(intent);
380383
}
@@ -390,6 +393,7 @@ private void startMission(Intent intent) {
390393
String source = intent.getStringExtra(EXTRA_SOURCE);
391394
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
392395
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
396+
StreamInfo streamInfo = (StreamInfo)intent.getSerializableExtra(EXTRA_STREAM_INFO);
393397
final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO,
394398
MissionRecoveryInfo.class);
395399
Objects.requireNonNull(recovery);
@@ -405,7 +409,7 @@ private void startMission(Intent intent) {
405409
if (psName == null)
406410
ps = null;
407411
else
408-
ps = Postprocessing.getAlgorithm(psName, psArgs);
412+
ps = Postprocessing.getAlgorithm(psName, psArgs, streamInfo);
409413

410414
final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
411415
mission.threadCount = threads;

0 commit comments

Comments
 (0)