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.
This commit is contained in:
parent
e3f75f5455
commit
3cda47b231
@ -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();
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
package org.schabi.newpipe.streams;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
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;
|
||||
@ -14,6 +18,8 @@ import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
@ -53,8 +59,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");
|
||||
}
|
||||
@ -64,6 +72,7 @@ public class OggFromWebMWriter implements Closeable {
|
||||
|
||||
this.source = source;
|
||||
this.output = target;
|
||||
this.streamInfo = streamInfo;
|
||||
|
||||
this.streamId = (int) System.currentTimeMillis();
|
||||
|
||||
@ -272,25 +281,29 @@ public class OggFromWebMWriter implements Closeable {
|
||||
|
||||
@Nullable
|
||||
private byte[] makeMetadata() {
|
||||
Log.d("OggFromWebMWriter", "Downloading media with codec ID " + webmTrack.codecId);
|
||||
|
||||
if ("A_OPUS".equals(webmTrack.codecId)) {
|
||||
final var commentFormat = "COMMENT=Downloaded using NewPipe on %s";
|
||||
final var commentStr = String.format(commentFormat, OffsetDateTime.now().toString());
|
||||
final var comment = commentStr.getBytes();
|
||||
final var head = ByteBuffer.allocate(20 + comment.length);
|
||||
head.order(ByteOrder.LITTLE_ENDIAN);
|
||||
head.put(new byte[]{
|
||||
// Byte order is LE, i.e. LSB first
|
||||
0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string
|
||||
0x00, 0x00, 0x00, 0x00, // vendor string of length 0
|
||||
0x01, 0x00, 0x00, 0x00, // additional tags count
|
||||
var metadata = "";
|
||||
metadata += String.format("COMMENT=Downloaded using NewPipe %s on %s\n",
|
||||
BuildConfig.VERSION_NAME,
|
||||
OffsetDateTime.now().toString());
|
||||
if (streamInfo != null) {
|
||||
metadata += String.format("COMMENT=URL: %s\n", streamInfo.getUrl());
|
||||
metadata += String.format("GENRE=%s\n", streamInfo.getCategory());
|
||||
metadata += String.format("ARTIST=%s\n", streamInfo.getUploaderName());
|
||||
metadata += String.format("TITLE=%s\n", streamInfo.getName());
|
||||
metadata += String.format("DATE=%s\n",
|
||||
streamInfo
|
||||
.getUploadDate()
|
||||
.getLocalDateTime()
|
||||
.format(DateTimeFormatter.ISO_DATE));
|
||||
}
|
||||
|
||||
// + 4 bytes for the comment string length
|
||||
// + N bytes for the comment string itself
|
||||
});
|
||||
head.putInt(comment.length);
|
||||
head.put(comment);
|
||||
Log.d("OggFromWebMWriter", "Creating metadata header with this data:");
|
||||
Log.d("OggFromWebMWriter", metadata);
|
||||
|
||||
return head.array();
|
||||
return makeOpusTagsHeader(metadata);
|
||||
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
|
||||
return new byte[]{
|
||||
0x03, // ¿¿¿???
|
||||
@ -304,6 +317,64 @@ 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 keyValue A key-value pair in the format "KEY=some value"
|
||||
* @return The binary data of the encoded metadata tag
|
||||
*/
|
||||
private static byte[] makeOpusMetadataTag(final String keyValue) {
|
||||
// Ensure the key is uppercase
|
||||
final var delimiterIndex = keyValue.indexOf('=');
|
||||
final var key = keyValue.substring(0, delimiterIndex).toUpperCase();
|
||||
final var value = keyValue.substring(delimiterIndex + 1);
|
||||
final var reconstructedKeyValue = key + "=" + value;
|
||||
|
||||
final var bytes = reconstructedKeyValue.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 tags string.
|
||||
* <p>
|
||||
* You probably want to use makeOpusMetadata(), which uses this function to create
|
||||
* a header with sensible metadata filled in.
|
||||
*
|
||||
* @param keyValueLines A multiline string with each line containing a key-value pair
|
||||
* in the format "KEY=some value". This may also be a blank string.
|
||||
* @return The binary header
|
||||
*/
|
||||
private static byte[] makeOpusTagsHeader(@NonNull final String keyValueLines) {
|
||||
final var tags = keyValueLines
|
||||
.lines()
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -80,6 +81,7 @@ public class DownloadManagerService extends Service {
|
||||
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 +355,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<MissionRecoveryInfo> recoveryInfo) {
|
||||
final Intent intent = new Intent(context, DownloadManagerService.class)
|
||||
@ -367,14 +369,15 @@ public class DownloadManagerService extends Service {
|
||||
.putExtra(EXTRA_URLS, urls)
|
||||
.putExtra(EXTRA_KIND, kind)
|
||||
.putExtra(EXTRA_THREADS, threads)
|
||||
.putExtra(EXTRA_SOURCE, source)
|
||||
.putExtra(EXTRA_SOURCE, streamInfo.getUrl())
|
||||
.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);
|
||||
}
|
||||
@ -390,6 +393,7 @@ public class DownloadManagerService extends Service {
|
||||
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,7 +409,7 @@ 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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user