11package org .schabi .newpipe .streams ;
22
33import static org .schabi .newpipe .MainActivity .DEBUG ;
4+ import static org .schabi .newpipe .extractor .utils .Utils .isNullOrEmpty ;
45
56import android .graphics .Bitmap ;
67import android .util .Log ;
910import androidx .annotation .NonNull ;
1011import androidx .annotation .Nullable ;
1112
13+ import org .schabi .newpipe .extractor .localization .DateWrapper ;
14+ import org .schabi .newpipe .extractor .stream .SongMetadata ;
1215import org .schabi .newpipe .extractor .stream .StreamInfo ;
1316import org .schabi .newpipe .streams .WebMReader .Cluster ;
1417import org .schabi .newpipe .streams .WebMReader .Segment ;
4043 * </p>
4144 * <ul>
4245 * <li>FLAC: <a href="https://www.rfc-editor.org/rfc/rfc9639">RFC 9639</a></li>
46+ * <li>
47+ * Vorbis: <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html">Vorbis I</a>.
48+ * <br>
49+ * Vorbis uses FLAC picture blocks for embedding cover art in the metadata.
50+ * </li>
4351 * <li>Opus: All specs can be found at <a href="https://opus-codec.org/docs/">
4452 * https://opus-codec.org/docs/</a>.
4553 * <a href="https://datatracker.ietf.org/doc/html/rfc7845.html">RFC7845</a>
4654 * defines the Ogg encapsulation for Opus streams, i.e.the container format and metadata.
55+ * <br>
56+ * Opus uses multiple Vorbis I features, e.g. the comment header format for metadata.
4757 * </li>
48- * <li>Vorbis: <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html">Vorbis I</a></li>
4958 * </ul>
5059 *
5160 * @author kapodamy
@@ -352,8 +361,8 @@ private int makePacketHeader(final long granPos, @NonNull final ByteBuffer buffe
352361 * @see <a href="https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2">
353362 * Vorbis I 4.2. Header decode and decode setup</a> and
354363 * <a href="https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-820005">
355- * Vorbis 5. comment field and header specification</a>
356- * for VORBIS metadata header format
364+ * Vorbis I 5. comment field and header specification</a>
365+ * for VORBIS metadata header format. Vorbis I 5. lists all the possible metadata tags.
357366 *
358367 * @return the metadata header as a byte array, or null if the codec is not supported
359368 * for metadata generation
@@ -364,38 +373,62 @@ private byte[] makeCommentHeader() {
364373 Log .d (TAG , "Downloading media with codec ID " + webmTrack .codecId );
365374 }
366375
367- if ("A_OPUS" .equals (webmTrack .codecId )) {
368- final var metadata = new ArrayList <Pair <String , String >>();
369- if (streamInfo != null ) {
370- metadata .add (Pair .create ("COMMENT" , streamInfo .getUrl ()));
371- metadata .add (Pair .create ("GENRE" , streamInfo .getCategory ()));
372- metadata .add (Pair .create ("ARTIST" , streamInfo .getUploaderName ()));
373- metadata .add (Pair .create ("TITLE" , streamInfo .getName ()));
374- metadata .add (Pair .create ("DATE" , streamInfo
375- .getUploadDate ()
376- .getLocalDateTime ()
377- .format (DateTimeFormatter .ISO_DATE )));
378- if (thumbnail != null ) {
379- metadata .add (makeFlacPictureTag (thumbnail ));
380- }
376+ final var metadata = new ArrayList <Pair <String , String >>();
377+ if (streamInfo != null ) {
378+ final SongMetadata songMetadata = streamInfo .getSongMetadata ();
379+ // metadata that can be present in the stream info and the song metadata.
380+ // Use the song metadata if available, otherwise fallback to stream info.
381+ metadata .add (Pair .create ("COMMENT" , streamInfo .getUrl ()));
382+ metadata .add (Pair .create ("GENRE" , streamInfo .getCategory ()));
383+ final String artist = songMetadata != null && !isNullOrEmpty (songMetadata .artist )
384+ ? songMetadata .artist : streamInfo .getUploaderName ();
385+ metadata .add (Pair .create ("ARTIST" , artist ));
386+ final String title = songMetadata != null && !isNullOrEmpty (songMetadata .title )
387+ ? songMetadata .title : streamInfo .getName ();
388+ metadata .add (Pair .create ("TITLE" , title ));
389+ final DateWrapper date = songMetadata != null && songMetadata .releaseDate != null
390+ ? songMetadata .releaseDate : streamInfo .getUploadDate ();
391+ metadata .add (Pair .create ("DATE" , date .getLocalDateTime ()
392+ .format (DateTimeFormatter .ISO_DATE )));
393+ // Additional metadata that is only present in the song metadata
394+ if (songMetadata != null ) {
395+ metadata .add (Pair .create ("ALBUM" , songMetadata .album ));
396+ if (songMetadata .track != SongMetadata .TRACK_UNKNOWN ) {
397+ // TRACKNUMBER is suggested in Vorbis spec,
398+ // but TRACK is more commonly used in practice
399+ metadata .add (Pair .create ("TRACKNUMBER" , String .valueOf (songMetadata .track )));
400+ metadata .add (Pair .create ("TRACK" , String .valueOf (songMetadata .track )));
401+ }
402+ metadata .add (Pair .create ("PERFORMER" , String .join (", " , songMetadata .performer )));
403+ metadata .add (Pair .create ("ORGANIZATION" , songMetadata .label ));
404+ metadata .add (Pair .create ("COPYRIGHT" , songMetadata .copyright ));
381405 }
382-
383- if (DEBUG ) {
384- Log .d (TAG , "Creating metadata header with this data:" );
385- metadata .forEach (p -> Log .d (TAG , p .first + "=" + p .second ));
406+ // Add thumbnail as cover art at the end because it is the largest metadata entry
407+ if (thumbnail != null ) {
408+ metadata .add (makeFlacPictureTag (thumbnail ));
386409 }
410+ }
411+
412+ if (DEBUG ) {
413+ Log .d (TAG , "Creating metadata header with this data:" );
414+ metadata .forEach (p -> Log .d (TAG , p .first + "=" + p .second ));
415+ }
387416
388- return makeOpusTagsHeader (metadata );
417+ if ("A_OPUS" .equals (webmTrack .codecId )) {
418+ // See RFC7845 5.2: https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2
419+ final byte [] identificationHeader = new byte []{
420+ 0x4F , 0x70 , 0x75 , 0x73 , 0x54 , 0x61 , 0x67 , 0x73 , // "OpusTags" binary string
421+ 0x00 , 0x00 , 0x00 , 0x00 , // vendor (aka. Encoder) string of length 0
422+ };
423+ return makeCommentHeader (metadata , identificationHeader );
389424 } else if ("A_VORBIS" .equals (webmTrack .codecId )) {
390425 // See https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2
391- // for the Vorbis comment header format
392- // TODO: add Vorbis metadata: same as Opus, but with the Vorbis comment header format
393- return new byte []{
426+ final byte [] identificationHeader = new byte []{
394427 0x03 , // packet type for Vorbis comment header
395428 0x76 , 0x6f , 0x72 , 0x62 , 0x69 , 0x73 , // "vorbis" binary string
396- 0x00 , 0x00 , 0x00 , 0x00 , // writing application string size (not present)
397- 0x00 , 0x00 , 0x00 , 0x00 // additional tags count (zero means no tags)
429+ 0x00 , 0x00 , 0x00 , 0x00 , // vendor (aka. Encoder) string of length 0
398430 };
431+ return makeCommentHeader (metadata , identificationHeader );
399432 }
400433
401434 // not implemented for the desired codec
@@ -405,12 +438,12 @@ private byte[] makeCommentHeader() {
405438 /**
406439 * This creates a single metadata tag for use in opus metadata headers. It contains the four
407440 * byte string length field and includes the string as-is. This cannot be used independently,
408- * but must follow a proper "OpusTags" header.
441+ * but must follow a proper Comment header.
409442 *
410443 * @param pair A key-value pair in the format "KEY=some value"
411444 * @return The binary data of the encoded metadata tag
412445 */
413- private static byte [] makeOpusMetadataTag (final Pair <String , String > pair ) {
446+ private static byte [] makeVorbisMetadataTag (final Pair <String , String > pair ) {
414447 final var keyValue = pair .first .toUpperCase () + "=" + pair .second .trim ();
415448
416449 final var bytes = keyValue .getBytes ();
@@ -486,24 +519,21 @@ private static Pair<String, String> makeFlacPictureTag(final Bitmap bitmap) {
486519 }
487520
488521 /**
489- * This returns a complete "OpusTags" header, created from the provided metadata tags.
490- * <p>
491- * You probably want to use makeOpusMetadata(), which uses this function to create
492- * a header with sensible metadata filled in.
493- *
494- * @ImplNote See <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
495- * RFC7845 5.2</a>
522+ * This returns a complete Comment header, created from the provided metadata tags.
496523 *
497524 * @param keyValueLines A list of pairs of the tags. This can also be though of as a mapping
498525 * from one key to multiple values.
526+ * @param identificationHeader the identification header for the codec,
527+ * which is required to be prefixed to the comment header.
499528 * @return The binary header
500529 */
501- private static byte [] makeOpusTagsHeader (final List <Pair <String , String >> keyValueLines ) {
530+ private static byte [] makeCommentHeader (final List <Pair <String , String >> keyValueLines ,
531+ final byte [] identificationHeader ) {
502532 final var tags = keyValueLines
503533 .stream ()
504- .filter (p -> !p .second .isBlank ())
505- .map (OggFromWebMWriter ::makeOpusMetadataTag )
506- .collect ( Collectors . toUnmodifiableList () );
534+ .filter (p -> p . second != null && !p .second .isBlank ())
535+ .map (OggFromWebMWriter ::makeVorbisMetadataTag )
536+ .toList ( );
507537
508538 final var tagsBytes = tags .stream ().collect (Collectors .summingInt (arr -> arr .length ));
509539
@@ -512,11 +542,7 @@ private static byte[] makeOpusTagsHeader(final List<Pair<String, String>> keyVal
512542
513543 final var head = ByteBuffer .allocate (byteCount );
514544 head .order (ByteOrder .LITTLE_ENDIAN );
515- // See RFC7845 5.2: https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2
516- head .put (new byte []{
517- 0x4F , 0x70 , 0x75 , 0x73 , 0x54 , 0x61 , 0x67 , 0x73 , // "OpusTags" binary string
518- 0x00 , 0x00 , 0x00 , 0x00 , // vendor (aka. Encoder) string of length 0
519- });
545+ head .put (identificationHeader );
520546 head .putInt (tags .size ()); // 4 bytes for tag count
521547 tags .forEach (head ::put ); // dynamic amount of tag bytes
522548
0 commit comments