Support for podcast 2.0 chapters (#5630)
This commit is contained in:
parent
dad4e405d4
commit
1a1bf02e8a
|
@ -60,6 +60,9 @@ public class MediaDownloadedHandler implements Runnable {
|
||||||
media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context));
|
media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (media.getItem() != null && media.getItem().getPodcastIndexChapterUrl() != null) {
|
||||||
|
ChapterUtils.loadChaptersFromUrl(media.getItem().getPodcastIndexChapterUrl());
|
||||||
|
}
|
||||||
// Get duration
|
// Get duration
|
||||||
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
|
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
|
||||||
String durationStr = null;
|
String durationStr = null;
|
||||||
|
|
|
@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.util;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import de.danoeh.antennapod.model.feed.Chapter;
|
import de.danoeh.antennapod.model.feed.Chapter;
|
||||||
|
@ -11,11 +12,13 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||||
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
|
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
|
||||||
import de.danoeh.antennapod.core.storage.DBReader;
|
import de.danoeh.antennapod.core.storage.DBReader;
|
||||||
import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator;
|
import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator;
|
||||||
|
import de.danoeh.antennapod.parser.feed.PodcastIndexChapterParser;
|
||||||
import de.danoeh.antennapod.parser.media.id3.ChapterReader;
|
import de.danoeh.antennapod.parser.media.id3.ChapterReader;
|
||||||
import de.danoeh.antennapod.parser.media.id3.ID3ReaderException;
|
import de.danoeh.antennapod.parser.media.id3.ID3ReaderException;
|
||||||
import de.danoeh.antennapod.model.playback.Playable;
|
import de.danoeh.antennapod.model.playback.Playable;
|
||||||
import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentChapterReader;
|
import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentChapterReader;
|
||||||
import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException;
|
import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException;
|
||||||
|
import okhttp3.CacheControl;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
import org.apache.commons.io.input.CountingInputStream;
|
import org.apache.commons.io.input.CountingInputStream;
|
||||||
|
@ -57,6 +60,7 @@ public class ChapterUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Chapter> chaptersFromDatabase = null;
|
List<Chapter> chaptersFromDatabase = null;
|
||||||
|
List<Chapter> chaptersFromPodcastIndex = null;
|
||||||
if (playable instanceof FeedMedia) {
|
if (playable instanceof FeedMedia) {
|
||||||
FeedMedia feedMedia = (FeedMedia) playable;
|
FeedMedia feedMedia = (FeedMedia) playable;
|
||||||
if (feedMedia.getItem() == null) {
|
if (feedMedia.getItem() == null) {
|
||||||
|
@ -65,10 +69,17 @@ public class ChapterUtils {
|
||||||
if (feedMedia.getItem().hasChapters()) {
|
if (feedMedia.getItem().hasChapters()) {
|
||||||
chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem());
|
chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(feedMedia.getItem().getPodcastIndexChapterUrl())) {
|
||||||
|
chaptersFromPodcastIndex = ChapterUtils.loadChaptersFromUrl(
|
||||||
|
feedMedia.getItem().getPodcastIndexChapterUrl());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context);
|
List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context);
|
||||||
List<Chapter> chapters = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
|
List<Chapter> chaptersMergePhase1 = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
|
||||||
|
List<Chapter> chapters = ChapterMerger.merge(chaptersMergePhase1, chaptersFromPodcastIndex);
|
||||||
if (chapters == null) {
|
if (chapters == null) {
|
||||||
// Do not try loading again. There are no chapters.
|
// Do not try loading again. There are no chapters.
|
||||||
playable.setChapters(Collections.emptyList());
|
playable.setChapters(Collections.emptyList());
|
||||||
|
@ -123,6 +134,27 @@ public class ChapterUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<Chapter> loadChaptersFromUrl(String url) {
|
||||||
|
try {
|
||||||
|
Request request = new Request.Builder().url(url).cacheControl(CacheControl.FORCE_CACHE).build();
|
||||||
|
Response response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
List<Chapter> chapters = PodcastIndexChapterParser.parse(response.body().string());
|
||||||
|
if (chapters != null && !chapters.isEmpty()) {
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request = new Request.Builder().url(url).build();
|
||||||
|
response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
return PodcastIndexChapterParser.parse(response.body().string());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
|
private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
|
||||||
ChapterReader reader = new ChapterReader(in);
|
ChapterReader reader = new ChapterReader(in);
|
||||||
|
|
|
@ -41,6 +41,7 @@ public class FeedItem extends FeedComponent implements Serializable {
|
||||||
|
|
||||||
private transient Feed feed;
|
private transient Feed feed;
|
||||||
private long feedId;
|
private long feedId;
|
||||||
|
private String podcastIndexChapterUrl;
|
||||||
|
|
||||||
private int state;
|
private int state;
|
||||||
public static final int NEW = -1;
|
public static final int NEW = -1;
|
||||||
|
@ -81,7 +82,7 @@ public class FeedItem extends FeedComponent implements Serializable {
|
||||||
* */
|
* */
|
||||||
public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId,
|
public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId,
|
||||||
boolean hasChapters, String imageUrl, int state,
|
boolean hasChapters, String imageUrl, int state,
|
||||||
String itemIdentifier, long autoDownload) {
|
String itemIdentifier, long autoDownload, String podcastIndexChapterUrl) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.link = link;
|
this.link = link;
|
||||||
|
@ -93,6 +94,7 @@ public class FeedItem extends FeedComponent implements Serializable {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.itemIdentifier = itemIdentifier;
|
this.itemIdentifier = itemIdentifier;
|
||||||
this.autoDownload = autoDownload;
|
this.autoDownload = autoDownload;
|
||||||
|
this.podcastIndexChapterUrl = podcastIndexChapterUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,6 +159,9 @@ public class FeedItem extends FeedComponent implements Serializable {
|
||||||
chapters = other.chapters;
|
chapters = other.chapters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (other.podcastIndexChapterUrl != null) {
|
||||||
|
podcastIndexChapterUrl = other.podcastIndexChapterUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -427,6 +432,14 @@ public class FeedItem extends FeedComponent implements Serializable {
|
||||||
tags.remove(tag);
|
tags.remove(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPodcastIndexChapterUrl() {
|
||||||
|
return podcastIndexChapterUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPodcastIndexChapterUrl(String url) {
|
||||||
|
podcastIndexChapterUrl = url;
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package de.danoeh.antennapod.parser.feed;
|
||||||
|
|
||||||
|
import de.danoeh.antennapod.model.feed.Chapter;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PodcastIndexChapterParser {
|
||||||
|
public static List<Chapter> parse(String jsonStr) {
|
||||||
|
try {
|
||||||
|
List<Chapter> chapters = new ArrayList<>();
|
||||||
|
JSONObject obj = new JSONObject(jsonStr);
|
||||||
|
JSONArray objChapters = obj.getJSONArray("chapters");
|
||||||
|
for (int i = 0; i < objChapters.length(); i++) {
|
||||||
|
JSONObject jsonObject = objChapters.getJSONObject(i);
|
||||||
|
int startTime = jsonObject.optInt("startTime", 0);
|
||||||
|
String title = jsonObject.optString("title");
|
||||||
|
String link = jsonObject.optString("url");
|
||||||
|
String img = jsonObject.optString("img");
|
||||||
|
chapters.add(new Chapter(startTime * 1000L, title, link, img));
|
||||||
|
}
|
||||||
|
return chapters;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
package de.danoeh.antennapod.parser.feed.namespace;
|
package de.danoeh.antennapod.parser.feed.namespace;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
import de.danoeh.antennapod.parser.feed.HandlerState;
|
import de.danoeh.antennapod.parser.feed.HandlerState;
|
||||||
import de.danoeh.antennapod.parser.feed.element.SyndElement;
|
import de.danoeh.antennapod.parser.feed.element.SyndElement;
|
||||||
import org.jsoup.helper.StringUtil;
|
|
||||||
import org.xml.sax.Attributes;
|
import org.xml.sax.Attributes;
|
||||||
import de.danoeh.antennapod.model.feed.FeedFunding;
|
import de.danoeh.antennapod.model.feed.FeedFunding;
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ public class PodcastIndex extends Namespace {
|
||||||
public static final String NSURI2 = "https://podcastindex.org/namespace/1.0";
|
public static final String NSURI2 = "https://podcastindex.org/namespace/1.0";
|
||||||
private static final String URL = "url";
|
private static final String URL = "url";
|
||||||
private static final String FUNDING = "funding";
|
private static final String FUNDING = "funding";
|
||||||
|
private static final String CHAPTERS = "chapters";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SyndElement handleElementStart(String localName, HandlerState state,
|
public SyndElement handleElementStart(String localName, HandlerState state,
|
||||||
|
@ -22,6 +23,11 @@ public class PodcastIndex extends Namespace {
|
||||||
FeedFunding funding = new FeedFunding(href, "");
|
FeedFunding funding = new FeedFunding(href, "");
|
||||||
state.setCurrentFunding(funding);
|
state.setCurrentFunding(funding);
|
||||||
state.getFeed().addPayment(state.getCurrentFunding());
|
state.getFeed().addPayment(state.getCurrentFunding());
|
||||||
|
} else if (CHAPTERS.equals(localName)) {
|
||||||
|
String href = attributes.getValue(URL);
|
||||||
|
if (!TextUtils.isEmpty(href)) {
|
||||||
|
state.getCurrentItem().setPodcastIndexChapterUrl(href);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new SyndElement(localName, this);
|
return new SyndElement(localName, this);
|
||||||
}
|
}
|
||||||
|
@ -32,7 +38,7 @@ public class PodcastIndex extends Namespace {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String content = state.getContentBuf().toString();
|
String content = state.getContentBuf().toString();
|
||||||
if (FUNDING.equals(localName) && state.getCurrentFunding() != null && !StringUtil.isBlank(content)) {
|
if (FUNDING.equals(localName) && state.getCurrentFunding() != null && !TextUtils.isEmpty(content)) {
|
||||||
state.getCurrentFunding().setContent(content);
|
state.getCurrentFunding().setContent(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,6 +326,10 @@ class DBUpgrader {
|
||||||
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
|
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
|
||||||
+ " ADD COLUMN " + PodDBAdapter.KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1");
|
+ " ADD COLUMN " + PodDBAdapter.KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1");
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 2060000) {
|
||||||
|
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
|
||||||
|
+ " ADD COLUMN " + PodDBAdapter.KEY_PODCASTINDEX_CHAPTER_URL + " TEXT");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ public class PodDBAdapter {
|
||||||
|
|
||||||
private static final String TAG = "PodDBAdapter";
|
private static final String TAG = "PodDBAdapter";
|
||||||
public static final String DATABASE_NAME = "Antennapod.db";
|
public static final String DATABASE_NAME = "Antennapod.db";
|
||||||
public static final int VERSION = 2050000;
|
public static final int VERSION = 2060000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum number of arguments for IN-operator.
|
* Maximum number of arguments for IN-operator.
|
||||||
|
@ -116,6 +116,7 @@ public class PodDBAdapter {
|
||||||
public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending";
|
public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending";
|
||||||
public static final String KEY_FEED_TAGS = "tags";
|
public static final String KEY_FEED_TAGS = "tags";
|
||||||
public static final String KEY_EPISODE_NOTIFICATION = "episode_notification";
|
public static final String KEY_EPISODE_NOTIFICATION = "episode_notification";
|
||||||
|
public static final String KEY_PODCASTINDEX_CHAPTER_URL = "podcastindex_chapter_url";
|
||||||
|
|
||||||
// Table names
|
// Table names
|
||||||
public static final String TABLE_NAME_FEEDS = "Feeds";
|
public static final String TABLE_NAME_FEEDS = "Feeds";
|
||||||
|
@ -166,7 +167,8 @@ public class PodDBAdapter {
|
||||||
+ KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER,"
|
+ KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER,"
|
||||||
+ KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT,"
|
+ KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT,"
|
||||||
+ KEY_IMAGE_URL + " TEXT,"
|
+ KEY_IMAGE_URL + " TEXT,"
|
||||||
+ KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER)";
|
+ KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER,"
|
||||||
|
+ KEY_PODCASTINDEX_CHAPTER_URL + " TEXT)";
|
||||||
|
|
||||||
private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE "
|
private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE "
|
||||||
+ TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION
|
+ TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION
|
||||||
|
@ -292,7 +294,8 @@ public class PodDBAdapter {
|
||||||
+ TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS + ", "
|
+ TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS + ", "
|
||||||
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + ", "
|
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + ", "
|
||||||
+ TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE_URL + ", "
|
+ TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE_URL + ", "
|
||||||
+ TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS;
|
+ TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS + ", "
|
||||||
|
+ TABLE_NAME_FEED_ITEMS + "." + KEY_PODCASTINDEX_CHAPTER_URL;
|
||||||
|
|
||||||
private static final String KEYS_FEED_MEDIA =
|
private static final String KEYS_FEED_MEDIA =
|
||||||
TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " AS " + SELECT_KEY_MEDIA_ID + ", "
|
TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " AS " + SELECT_KEY_MEDIA_ID + ", "
|
||||||
|
@ -648,6 +651,7 @@ public class PodDBAdapter {
|
||||||
values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier());
|
values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier());
|
||||||
values.put(KEY_AUTO_DOWNLOAD_ATTEMPTS, item.getAutoDownloadAttemptsAndTime());
|
values.put(KEY_AUTO_DOWNLOAD_ATTEMPTS, item.getAutoDownloadAttemptsAndTime());
|
||||||
values.put(KEY_IMAGE_URL, item.getImageUrl());
|
values.put(KEY_IMAGE_URL, item.getImageUrl());
|
||||||
|
values.put(KEY_PODCASTINDEX_CHAPTER_URL, item.getPodcastIndexChapterUrl());
|
||||||
|
|
||||||
if (item.getId() == 0) {
|
if (item.getId() == 0) {
|
||||||
item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values));
|
item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values));
|
||||||
|
|
|
@ -27,6 +27,7 @@ public abstract class FeedItemCursorMapper {
|
||||||
int indexItemIdentifier = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ITEM_IDENTIFIER);
|
int indexItemIdentifier = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ITEM_IDENTIFIER);
|
||||||
int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS);
|
int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS);
|
||||||
int indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL);
|
int indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL);
|
||||||
|
int indexPodcastIndexChapterUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PODCASTINDEX_CHAPTER_URL);
|
||||||
|
|
||||||
long id = cursor.getInt(indexId);
|
long id = cursor.getInt(indexId);
|
||||||
String title = cursor.getString(indexTitle);
|
String title = cursor.getString(indexTitle);
|
||||||
|
@ -39,8 +40,9 @@ public abstract class FeedItemCursorMapper {
|
||||||
String itemIdentifier = cursor.getString(indexItemIdentifier);
|
String itemIdentifier = cursor.getString(indexItemIdentifier);
|
||||||
long autoDownload = cursor.getLong(indexAutoDownload);
|
long autoDownload = cursor.getLong(indexAutoDownload);
|
||||||
String imageUrl = cursor.getString(indexImageUrl);
|
String imageUrl = cursor.getString(indexImageUrl);
|
||||||
|
String podcastIndexChapterUrl = cursor.getString(indexPodcastIndexChapterUrl);
|
||||||
|
|
||||||
return new FeedItem(id, title, link, pubDate, paymentLink, feedId,
|
return new FeedItem(id, title, link, pubDate, paymentLink, feedId,
|
||||||
hasChapters, imageUrl, state, itemIdentifier, autoDownload);
|
hasChapters, imageUrl, state, itemIdentifier, autoDownload, podcastIndexChapterUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue