Read vorbis description of local files

This commit is contained in:
ByteHamster 2022-03-11 19:58:41 +01:00
parent 36a36e4f85
commit 933fde839e
8 changed files with 171 additions and 76 deletions

View File

@ -38,6 +38,8 @@ import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.parser.feed.util.MimeTypeUtils;
import de.danoeh.antennapod.parser.media.id3.ID3ReaderException;
import de.danoeh.antennapod.parser.media.id3.Id3MetadataReader;
import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentMetadataReader;
import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException;
import org.apache.commons.io.input.CountingInputStream;
public class LocalFeedUpdater {
@ -209,8 +211,15 @@ public class LocalFeedUpdater {
reader.readInputStream();
item.setDescriptionIfLonger(reader.getComment());
} catch (IOException | ID3ReaderException e) {
// Do not flood Logcat with full stack traces
Log.d(TAG, "Unable to parse ID3 of " + file.getUri() + ": " + e.getMessage());
try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
VorbisCommentMetadataReader reader = new VorbisCommentMetadataReader(inputStream);
reader.readInputStream();
item.setDescriptionIfLonger(reader.getDescription());
} catch (IOException | VorbisCommentReaderException e2) {
Log.d(TAG, "Unable to parse vorbis comments of " + file.getUri() + ": " + e2.getMessage());
}
}
}

View File

@ -171,8 +171,8 @@ public class ChapterUtils {
@NonNull
private static List<Chapter> readOggChaptersFromInputStream(InputStream input) throws VorbisCommentReaderException {
VorbisCommentChapterReader reader = new VorbisCommentChapterReader();
reader.readInputStream(input);
VorbisCommentChapterReader reader = new VorbisCommentChapterReader(input);
reader.readInputStream();
List<Chapter> chapters = reader.getChapters();
if (chapters == null) {
return Collections.emptyList();

View File

@ -2,6 +2,7 @@ package de.danoeh.antennapod.parser.media.vorbis;
import android.util.Log;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -17,25 +18,15 @@ public class VorbisCommentChapterReader extends VorbisCommentReader {
private static final String CHAPTER_ATTRIBUTE_LINK = "url";
private static final int CHAPTERXXX_LENGTH = "chapterxxx".length();
private List<Chapter> chapters;
private final List<Chapter> chapters = new ArrayList<>();
public VorbisCommentChapterReader() {
public VorbisCommentChapterReader(InputStream input) {
super(input);
}
@Override
public void onVorbisCommentFound() {
System.out.println("Vorbis comment found");
}
@Override
public void onVorbisCommentHeaderFound(VorbisCommentHeader header) {
chapters = new ArrayList<>();
System.out.println(header.toString());
}
@Override
public boolean onContentVectorKey(String content) {
return content.matches(CHAPTER_KEY);
public boolean handles(String key) {
return key.matches(CHAPTER_KEY);
}
@Override
@ -68,19 +59,6 @@ public class VorbisCommentChapterReader extends VorbisCommentReader {
}
}
@Override
public void onEndOfComment() {
System.out.println("End of comment");
for (Chapter c : chapters) {
System.out.println(c.toString());
}
}
@Override
public void onError(VorbisCommentReaderException exception) {
exception.printStackTrace();
}
private Chapter getChapterById(long id) {
for (Chapter c : chapters) {
if (("" + id).equals(c.getChapterId())) {

View File

@ -0,0 +1,32 @@
package de.danoeh.antennapod.parser.media.vorbis;
import java.io.InputStream;
public class VorbisCommentMetadataReader extends VorbisCommentReader {
private static final String KEY_DESCRIPTION = "description";
private static final String KEY_COMMENT = "comment";
private String description = null;
public VorbisCommentMetadataReader(InputStream input) {
super(input);
}
@Override
public boolean handles(String key) {
return KEY_DESCRIPTION.equals(key) || KEY_COMMENT.equals(key);
}
@Override
public void onContentVectorValue(String key, String value) {
if (KEY_DESCRIPTION.equals(key) || KEY_COMMENT.equals(key)) {
if (description == null || value.length() > description.length()) {
description = value;
}
}
}
public String getDescription() {
return description;
}
}

View File

@ -3,6 +3,7 @@ package de.danoeh.antennapod.parser.media.vorbis;
import androidx.annotation.NonNull;
import org.apache.commons.io.EndianUtils;
import org.apache.commons.io.IOUtils;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
@ -12,51 +13,35 @@ import java.nio.charset.Charset;
import java.util.Locale;
public abstract class VorbisCommentReader {
/** Length of first page in an ogg file in bytes. */
private static final String TAG = "VorbisCommentReader";
private static final int FIRST_OGG_PAGE_LENGTH = 58;
private static final int FIRST_OPUS_PAGE_LENGTH = 47;
private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024;
private static final int PACKET_TYPE_IDENTIFICATION = 1;
private static final int PACKET_TYPE_COMMENT = 3;
/** Called when Reader finds identification header. */
protected abstract void onVorbisCommentFound();
private final InputStream input;
protected abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header);
VorbisCommentReader(InputStream input) {
this.input = input;
}
/**
* Is called every time the Reader finds a content vector. The handler
* should return true if it wants to handle the content vector.
*/
protected abstract boolean onContentVectorKey(String content);
/**
* Is called if onContentVectorKey returned true for the key.
*/
protected abstract void onContentVectorValue(String key, String value) throws VorbisCommentReaderException;
protected abstract void onEndOfComment();
protected abstract void onError(VorbisCommentReaderException exception);
public void readInputStream(InputStream input) throws VorbisCommentReaderException {
public void readInputStream() throws VorbisCommentReaderException {
try {
findIdentificationHeader(input);
onVorbisCommentFound();
findOggPage(input);
findCommentHeader(input);
VorbisCommentHeader commentHeader = readCommentHeader(input);
onVorbisCommentHeaderFound(commentHeader);
findIdentificationHeader();
findOggPage();
findCommentHeader();
VorbisCommentHeader commentHeader = readCommentHeader();
Log.d(TAG, commentHeader.toString());
for (int i = 0; i < commentHeader.getUserCommentLength(); i++) {
readUserComment(input);
readUserComment();
}
onEndOfComment();
} catch (IOException e) {
onError(new VorbisCommentReaderException(e));
e.printStackTrace();
}
}
private void findOggPage(InputStream input) throws IOException {
private void findOggPage() throws IOException {
// find OggS
byte[] buffer = new byte[4];
final byte[] oggPageHeader = {'O', 'g', 'g', 'S'};
@ -76,17 +61,19 @@ public abstract class VorbisCommentReader {
IOUtils.skipFully(input, numSegments);
}
private void readUserComment(InputStream input) throws VorbisCommentReaderException {
private void readUserComment() throws VorbisCommentReaderException {
try {
long vectorLength = EndianUtils.readSwappedUnsignedInteger(input);
if (vectorLength > 20 * 1024 * 1024) {
// Avoid reading entire file if it is encoded incorrectly
throw new VorbisCommentReaderException("User comment unrealistically long: " + vectorLength);
String keyPart = readUtf8String(10);
throw new VorbisCommentReaderException("User comment unrealistically long. "
+ "key=" + keyPart + ", length=" + vectorLength);
}
String key = readContentVectorKey(input, vectorLength).toLowerCase(Locale.US);
boolean readValue = onContentVectorKey(key);
if (readValue) {
String value = readUtf8String(input, vectorLength - key.length() - 1);
String key = readContentVectorKey(vectorLength).toLowerCase(Locale.US);
boolean shouldReadValue = handles(key);
Log.d(TAG, "key=" + key + ", length=" + vectorLength + ", handles=" + shouldReadValue);
if (shouldReadValue) {
String value = readUtf8String(vectorLength - key.length() - 1);
onContentVectorValue(key, value);
} else {
IOUtils.skipFully(input, vectorLength - key.length() - 1);
@ -96,7 +83,7 @@ public abstract class VorbisCommentReader {
}
}
private String readUtf8String(InputStream input, long length) throws IOException {
private String readUtf8String(long length) throws IOException {
byte[] buffer = new byte[(int) length];
IOUtils.readFully(input, buffer);
Charset charset = Charset.forName("UTF-8");
@ -107,7 +94,7 @@ public abstract class VorbisCommentReader {
* Looks for an identification header in the first page of the file. If an
* identification header is found, it will be skipped completely
*/
private void findIdentificationHeader(InputStream input) throws IOException {
private void findIdentificationHeader() throws IOException {
byte[] buffer = new byte[FIRST_OPUS_PAGE_LENGTH];
IOUtils.readFully(input, buffer);
final byte[] oggIdentificationHeader = new byte[]{ PACKET_TYPE_IDENTIFICATION, 'v', 'o', 'r', 'b', 'i', 's' };
@ -122,7 +109,7 @@ public abstract class VorbisCommentReader {
throw new IOException("No vorbis identification header found");
}
private void findCommentHeader(InputStream input) throws IOException {
private void findCommentHeader() throws IOException {
byte[] buffer = new byte[64]; // Enough space for some bytes. Used circularly.
final byte[] oggCommentHeader = new byte[]{ PACKET_TYPE_COMMENT, 'v', 'o', 'r', 'b', 'i', 's' };
for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) {
@ -155,10 +142,10 @@ public abstract class VorbisCommentReader {
}
@NonNull
private VorbisCommentHeader readCommentHeader(InputStream input) throws IOException, VorbisCommentReaderException {
private VorbisCommentHeader readCommentHeader() throws IOException, VorbisCommentReaderException {
try {
long vendorLength = EndianUtils.readSwappedUnsignedInteger(input);
String vendorName = readUtf8String(input, vendorLength);
String vendorName = readUtf8String(vendorLength);
long userCommentLength = EndianUtils.readSwappedUnsignedInteger(input);
return new VorbisCommentHeader(vendorName, userCommentLength);
} catch (UnsupportedEncodingException e) {
@ -166,7 +153,7 @@ public abstract class VorbisCommentReader {
}
}
private String readContentVectorKey(InputStream input, long vectorLength) throws IOException {
private String readContentVectorKey(long vectorLength) throws IOException {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < vectorLength; i++) {
char c = (char) input.read();
@ -178,4 +165,15 @@ public abstract class VorbisCommentReader {
}
return null; // no key found
}
/**
* Is called every time the Reader finds a content vector. The handler
* should return true if it wants to handle the content vector.
*/
protected abstract boolean handles(String key);
/**
* Is called if onContentVectorKey returned true for the key.
*/
protected abstract void onContentVectorValue(String key, String value) throws VorbisCommentReaderException;
}

View File

@ -0,0 +1,50 @@
package de.danoeh.antennapod.parser.media.id3;
import org.apache.commons.io.input.CountingInputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
public class MetadataReaderTest {
@Test
public void testRealFileUltraschall() throws IOException, ID3ReaderException {
CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader()
.getResource("ultraschall5.mp3").openStream());
Id3MetadataReader reader = new Id3MetadataReader(inputStream);
reader.readInputStream();
assertEquals("Description", reader.getComment());
}
@Test
public void testRealFileAuphonic() throws IOException, ID3ReaderException {
CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader()
.getResource("auphonic.mp3").openStream());
Id3MetadataReader reader = new Id3MetadataReader(inputStream);
reader.readInputStream();
assertEquals("Summary", reader.getComment());
}
@Test
public void testRealFileHindenburgJournalistPro() throws IOException, ID3ReaderException {
CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader()
.getResource("hindenburg-journalist-pro.mp3").openStream());
Id3MetadataReader reader = new Id3MetadataReader(inputStream);
reader.readInputStream();
assertEquals("This is the summary of this podcast episode. This file was made with"
+ " Hindenburg Journalist Pro version 1.85, build number 2360.", reader.getComment());
}
@Test
public void testRealFileMp3chapsPy() throws IOException, ID3ReaderException {
CountingInputStream inputStream = new CountingInputStream(getClass().getClassLoader()
.getResource("mp3chaps-py.mp3").openStream());
Id3MetadataReader reader = new Id3MetadataReader(inputStream);
reader.readInputStream();
assertEquals("2021.08.13", reader.getComment());
}
}

View File

@ -23,8 +23,8 @@ public class VorbisCommentChapterReaderTest {
public void testRealFileAuphonic(String filename) throws IOException, VorbisCommentReaderException {
InputStream inputStream = getClass().getClassLoader()
.getResource(filename).openStream();
VorbisCommentChapterReader reader = new VorbisCommentChapterReader();
reader.readInputStream(inputStream);
VorbisCommentChapterReader reader = new VorbisCommentChapterReader(inputStream);
reader.readInputStream();
List<Chapter> chapters = reader.getChapters();
assertEquals(4, chapters.size());

View File

@ -0,0 +1,28 @@
package de.danoeh.antennapod.parser.media.vorbis;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
public class VorbisCommentMetadataReaderTest {
@Test
public void testRealFilesAuphonic() throws IOException, VorbisCommentReaderException {
testRealFileAuphonic("auphonic.ogg");
testRealFileAuphonic("auphonic.opus");
}
public void testRealFileAuphonic(String filename) throws IOException, VorbisCommentReaderException {
InputStream inputStream = getClass().getClassLoader()
.getResource(filename).openStream();
VorbisCommentMetadataReader reader = new VorbisCommentMetadataReader(inputStream);
reader.readInputStream();
assertEquals("Summary", reader.getDescription());
}
}