Identify episodes by guid (#5326)
This commit is contained in:
parent
7ebaa9f619
commit
db39186760
@ -567,15 +567,16 @@ public final class DBReader {
|
|||||||
/**
|
/**
|
||||||
* Loads a specific FeedItem from the database.
|
* Loads a specific FeedItem from the database.
|
||||||
*
|
*
|
||||||
* @param podcastUrl the corresponding feed's url
|
* @param guid feed item guid
|
||||||
* @param episodeUrl the feed item's url
|
* @param episodeUrl the feed item's url
|
||||||
* @return The FeedItem or null if the FeedItem could not be found.
|
* @return The FeedItem or null if the FeedItem could not be found.
|
||||||
* Does NOT load additional attributes like feed or queue state.
|
* Does NOT load additional attributes like feed or queue state.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
private static FeedItem getFeedItemByUrl(final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) {
|
private static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl,
|
||||||
Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl);
|
PodDBAdapter adapter) {
|
||||||
try (Cursor cursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl)) {
|
Log.d(TAG, "Loading feeditem with guid " + guid + " or episode url " + episodeUrl);
|
||||||
|
try (Cursor cursor = adapter.getFeedItemCursor(guid, episodeUrl)) {
|
||||||
if (!cursor.moveToNext()) {
|
if (!cursor.moveToNext()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -626,18 +627,18 @@ public final class DBReader {
|
|||||||
/**
|
/**
|
||||||
* Loads a specific FeedItem from the database.
|
* Loads a specific FeedItem from the database.
|
||||||
*
|
*
|
||||||
* @param podcastUrl the corresponding feed's url
|
* @param guid feed item guid
|
||||||
* @param episodeUrl the feed item's url
|
* @param episodeUrl the feed item's url
|
||||||
* @return The FeedItem or null if the FeedItem could not be found.
|
* @return The FeedItem or null if the FeedItem could not be found.
|
||||||
* Does NOT load additional attributes like feed or queue state.
|
* Does NOT load additional attributes like feed or queue state.
|
||||||
*/
|
*/
|
||||||
public static FeedItem getFeedItemByUrl(final String podcastUrl, final String episodeUrl) {
|
public static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl) {
|
||||||
Log.d(TAG, "getFeedItem() called with: " + "podcastUrl = [" + podcastUrl + "], episodeUrl = [" + episodeUrl + "]");
|
Log.d(TAG, "getFeedItem() called with: " + "guid = [" + guid + "], episodeUrl = [" + episodeUrl + "]");
|
||||||
|
|
||||||
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||||
adapter.open();
|
adapter.open();
|
||||||
try {
|
try {
|
||||||
return getFeedItemByUrl(podcastUrl, episodeUrl, adapter);
|
return getFeedItemByGuidOrEpisodeUrl(guid, episodeUrl, adapter);
|
||||||
} finally {
|
} finally {
|
||||||
adapter.close();
|
adapter.close();
|
||||||
}
|
}
|
||||||
|
@ -803,7 +803,7 @@ public class DBWriter {
|
|||||||
return dbExec.submit(() -> {
|
return dbExec.submit(() -> {
|
||||||
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||||
adapter.open();
|
adapter.open();
|
||||||
adapter.setFeedItemlist(items);
|
adapter.storeFeedItemlist(items);
|
||||||
adapter.close();
|
adapter.close();
|
||||||
EventBus.getDefault().post(FeedItemEvent.updated(items));
|
EventBus.getDefault().post(FeedItemEvent.updated(items));
|
||||||
});
|
});
|
||||||
|
@ -558,7 +558,7 @@ public class PodDBAdapter {
|
|||||||
setFeed(feed);
|
setFeed(feed);
|
||||||
if (feed.getItems() != null) {
|
if (feed.getItems() != null) {
|
||||||
for (FeedItem item : feed.getItems()) {
|
for (FeedItem item : feed.getItems()) {
|
||||||
setFeedItem(item, false);
|
updateOrInsertFeedItem(item, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (feed.getPreferences() != null) {
|
if (feed.getPreferences() != null) {
|
||||||
@ -582,11 +582,11 @@ public class PodDBAdapter {
|
|||||||
db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original});
|
db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFeedItemlist(List<FeedItem> items) {
|
public void storeFeedItemlist(List<FeedItem> items) {
|
||||||
try {
|
try {
|
||||||
db.beginTransactionNonExclusive();
|
db.beginTransactionNonExclusive();
|
||||||
for (FeedItem item : items) {
|
for (FeedItem item : items) {
|
||||||
setFeedItem(item, true);
|
updateOrInsertFeedItem(item, true);
|
||||||
}
|
}
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@ -600,7 +600,7 @@ public class PodDBAdapter {
|
|||||||
long result = 0;
|
long result = 0;
|
||||||
try {
|
try {
|
||||||
db.beginTransactionNonExclusive();
|
db.beginTransactionNonExclusive();
|
||||||
result = setFeedItem(item, true);
|
result = updateOrInsertFeedItem(item, true);
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
Log.e(TAG, Log.getStackTraceString(e));
|
Log.e(TAG, Log.getStackTraceString(e));
|
||||||
@ -618,7 +618,7 @@ public class PodDBAdapter {
|
|||||||
* false if the method is executed on a list of FeedItems of the same Feed.
|
* false if the method is executed on a list of FeedItems of the same Feed.
|
||||||
* @return the id of the entry
|
* @return the id of the entry
|
||||||
*/
|
*/
|
||||||
private long setFeedItem(FeedItem item, boolean saveFeed) {
|
private long updateOrInsertFeedItem(FeedItem item, boolean saveFeed) {
|
||||||
if (item.getId() == 0 && item.getPubDate() == null) {
|
if (item.getId() == 0 && item.getPubDate() == null) {
|
||||||
Log.e(TAG, "Newly saved item has no pubDate. Using current date as pubDate");
|
Log.e(TAG, "Newly saved item has no pubDate. Using current date as pubDate");
|
||||||
item.setPubDate(new Date());
|
item.setPubDate(new Date());
|
||||||
@ -1110,14 +1110,19 @@ public class PodDBAdapter {
|
|||||||
return db.rawQuery(query, null);
|
return db.rawQuery(query, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public final Cursor getFeedItemCursor(final String podcastUrl, final String episodeUrl) {
|
public final Cursor getFeedItemCursor(final String guid, final String episodeUrl) {
|
||||||
String escapedPodcastUrl = DatabaseUtils.sqlEscapeString(podcastUrl);
|
|
||||||
String escapedEpisodeUrl = DatabaseUtils.sqlEscapeString(episodeUrl);
|
String escapedEpisodeUrl = DatabaseUtils.sqlEscapeString(episodeUrl);
|
||||||
|
String whereClauseCondition = TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOAD_URL + "=" + escapedEpisodeUrl;
|
||||||
|
|
||||||
|
if (guid != null) {
|
||||||
|
String escapedGuid = DatabaseUtils.sqlEscapeString(guid);
|
||||||
|
whereClauseCondition = TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + "=" + escapedGuid;
|
||||||
|
}
|
||||||
|
|
||||||
final String query = SELECT_FEED_ITEMS_AND_MEDIA
|
final String query = SELECT_FEED_ITEMS_AND_MEDIA
|
||||||
+ " INNER JOIN " + TABLE_NAME_FEEDS
|
+ " INNER JOIN " + TABLE_NAME_FEEDS
|
||||||
+ " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
|
+ " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
|
||||||
+ " WHERE " + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOAD_URL + "=" + escapedEpisodeUrl
|
+ " WHERE " + whereClauseCondition;
|
||||||
+ " AND " + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL + "=" + escapedPodcastUrl;
|
|
||||||
Log.d(TAG, "SQL: " + query);
|
Log.d(TAG, "SQL: " + query);
|
||||||
return db.rawQuery(query, null);
|
return db.rawQuery(query, null);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
package de.danoeh.antennapod.core.sync;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.collection.ArrayMap;
|
||||||
|
import androidx.core.util.Pair;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import de.danoeh.antennapod.net.sync.model.EpisodeAction;
|
||||||
|
|
||||||
|
public class EpisodeActionFilter {
|
||||||
|
|
||||||
|
public static final String TAG = "EpisodeActionFilter";
|
||||||
|
|
||||||
|
public static Map<Pair<String, String>, EpisodeAction> getRemoteActionsOverridingLocalActions(
|
||||||
|
List<EpisodeAction> remoteActions,
|
||||||
|
List<EpisodeAction> queuedEpisodeActions) {
|
||||||
|
// make sure more recent local actions are not overwritten by older remote actions
|
||||||
|
Map<Pair<String, String>, EpisodeAction> remoteActionsThatOverrideLocalActions = new ArrayMap<>();
|
||||||
|
Map<Pair<String, String>, EpisodeAction> localMostRecentPlayActions =
|
||||||
|
createUniqueLocalMostRecentPlayActions(queuedEpisodeActions);
|
||||||
|
for (EpisodeAction remoteAction : remoteActions) {
|
||||||
|
Log.d(TAG, "Processing remoteAction: " + remoteAction.toString());
|
||||||
|
Pair<String, String> key = new Pair<>(remoteAction.getPodcast(), remoteAction.getEpisode());
|
||||||
|
switch (remoteAction.getAction()) {
|
||||||
|
case NEW:
|
||||||
|
remoteActionsThatOverrideLocalActions.put(key, remoteAction);
|
||||||
|
break;
|
||||||
|
case DOWNLOAD:
|
||||||
|
break;
|
||||||
|
case PLAY:
|
||||||
|
EpisodeAction localMostRecent = localMostRecentPlayActions.get(key);
|
||||||
|
if (secondActionOverridesFirstAction(remoteAction, localMostRecent)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
EpisodeAction remoteMostRecentAction = remoteActionsThatOverrideLocalActions.get(key);
|
||||||
|
if (secondActionOverridesFirstAction(remoteAction, remoteMostRecentAction)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
remoteActionsThatOverrideLocalActions.put(key, remoteAction);
|
||||||
|
break;
|
||||||
|
case DELETE:
|
||||||
|
// NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.e(TAG, "Unknown remoteAction: " + remoteAction);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return remoteActionsThatOverrideLocalActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<Pair<String, String>, EpisodeAction> createUniqueLocalMostRecentPlayActions(
|
||||||
|
List<EpisodeAction> queuedEpisodeActions) {
|
||||||
|
Map<Pair<String, String>, EpisodeAction> localMostRecentPlayAction;
|
||||||
|
localMostRecentPlayAction = new ArrayMap<>();
|
||||||
|
for (EpisodeAction action : queuedEpisodeActions) {
|
||||||
|
Pair<String, String> key = new Pair<>(action.getPodcast(), action.getEpisode());
|
||||||
|
EpisodeAction mostRecent = localMostRecentPlayAction.get(key);
|
||||||
|
if (mostRecent == null || mostRecent.getTimestamp() == null) {
|
||||||
|
localMostRecentPlayAction.put(key, action);
|
||||||
|
} else if (mostRecent.getTimestamp().before(action.getTimestamp())) {
|
||||||
|
localMostRecentPlayAction.put(key, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return localMostRecentPlayAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean secondActionOverridesFirstAction(EpisodeAction firstAction,
|
||||||
|
EpisodeAction secondAction) {
|
||||||
|
return secondAction != null
|
||||||
|
&& secondAction.getTimestamp() != null
|
||||||
|
&& secondAction.getTimestamp().after(firstAction.getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package de.danoeh.antennapod.core.sync;
|
||||||
|
|
||||||
|
public class GuidValidator {
|
||||||
|
|
||||||
|
public static boolean isValidGuid(String guid) {
|
||||||
|
return guid != null
|
||||||
|
&& !guid.trim().isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,8 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.collection.ArrayMap;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.util.Pair;
|
import androidx.core.util.Pair;
|
||||||
import androidx.work.BackoffPolicy;
|
import androidx.work.BackoffPolicy;
|
||||||
@ -19,6 +19,7 @@ import androidx.work.OneTimeWorkRequest;
|
|||||||
import androidx.work.WorkManager;
|
import androidx.work.WorkManager;
|
||||||
import androidx.work.Worker;
|
import androidx.work.Worker;
|
||||||
import androidx.work.WorkerParameters;
|
import androidx.work.WorkerParameters;
|
||||||
|
|
||||||
import de.danoeh.antennapod.core.R;
|
import de.danoeh.antennapod.core.R;
|
||||||
import de.danoeh.antennapod.core.event.SyncServiceEvent;
|
import de.danoeh.antennapod.core.event.SyncServiceEvent;
|
||||||
import de.danoeh.antennapod.model.feed.Feed;
|
import de.danoeh.antennapod.model.feed.Feed;
|
||||||
@ -45,6 +46,7 @@ import de.danoeh.antennapod.net.sync.model.SyncServiceException;
|
|||||||
import de.danoeh.antennapod.net.sync.model.UploadChangesResponse;
|
import de.danoeh.antennapod.net.sync.model.UploadChangesResponse;
|
||||||
import io.reactivex.Completable;
|
import io.reactivex.Completable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
@ -112,13 +114,13 @@ public class SyncService extends Worker {
|
|||||||
public static void clearQueue(Context context) {
|
public static void clearQueue(Context context) {
|
||||||
executeLockedAsync(() ->
|
executeLockedAsync(() ->
|
||||||
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
|
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit()
|
||||||
.putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0)
|
.putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0)
|
||||||
.putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0)
|
.putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0)
|
||||||
.putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0)
|
.putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0)
|
||||||
.putString(PREF_QUEUED_EPISODE_ACTIONS, "[]")
|
.putString(PREF_QUEUED_EPISODE_ACTIONS, "[]")
|
||||||
.putString(PREF_QUEUED_FEEDS_ADDED, "[]")
|
.putString(PREF_QUEUED_FEEDS_ADDED, "[]")
|
||||||
.putString(PREF_QUEUED_FEEDS_REMOVED, "[]")
|
.putString(PREF_QUEUED_FEEDS_REMOVED, "[]")
|
||||||
.apply());
|
.apply());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void enqueueFeedAdded(Context context, String downloadUrl) {
|
public static void enqueueFeedAdded(Context context, String downloadUrl) {
|
||||||
@ -258,7 +260,7 @@ public class SyncService extends Worker {
|
|||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
}).subscribeOn(Schedulers.io())
|
}).subscribeOn(Schedulers.io())
|
||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -429,71 +431,31 @@ public class SyncService extends Worker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<Pair<String, String>, EpisodeAction> localMostRecentPlayAction = new ArrayMap<>();
|
Map<Pair<String, String>, EpisodeAction> playActionsToUpdate = EpisodeActionFilter
|
||||||
for (EpisodeAction action : getQueuedEpisodeActions()) {
|
.getRemoteActionsOverridingLocalActions(remoteActions, getQueuedEpisodeActions());
|
||||||
Pair<String, String> key = new Pair<>(action.getPodcast(), action.getEpisode());
|
|
||||||
EpisodeAction mostRecent = localMostRecentPlayAction.get(key);
|
|
||||||
if (mostRecent == null || mostRecent.getTimestamp() == null) {
|
|
||||||
localMostRecentPlayAction.put(key, action);
|
|
||||||
} else if (mostRecent.getTimestamp().before(action.getTimestamp())) {
|
|
||||||
localMostRecentPlayAction.put(key, action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure more recent local actions are not overwritten by older remote actions
|
|
||||||
Map<Pair<String, String>, EpisodeAction> mostRecentPlayAction = new ArrayMap<>();
|
|
||||||
for (EpisodeAction action : remoteActions) {
|
|
||||||
Log.d(TAG, "Processing action: " + action.toString());
|
|
||||||
switch (action.getAction()) {
|
|
||||||
case NEW:
|
|
||||||
FeedItem newItem = DBReader.getFeedItemByUrl(action.getPodcast(), action.getEpisode());
|
|
||||||
if (newItem != null) {
|
|
||||||
DBWriter.markItemPlayed(newItem, FeedItem.UNPLAYED, true);
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "Unknown feed item: " + action);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DOWNLOAD:
|
|
||||||
break;
|
|
||||||
case PLAY:
|
|
||||||
Pair<String, String> key = new Pair<>(action.getPodcast(), action.getEpisode());
|
|
||||||
EpisodeAction localMostRecent = localMostRecentPlayAction.get(key);
|
|
||||||
if (localMostRecent == null || localMostRecent.getTimestamp() == null
|
|
||||||
|| localMostRecent.getTimestamp().before(action.getTimestamp())) {
|
|
||||||
EpisodeAction mostRecent = mostRecentPlayAction.get(key);
|
|
||||||
if (mostRecent == null || mostRecent.getTimestamp() == null) {
|
|
||||||
mostRecentPlayAction.put(key, action);
|
|
||||||
} else if (action.getTimestamp() != null
|
|
||||||
&& mostRecent.getTimestamp().before(action.getTimestamp())) {
|
|
||||||
mostRecentPlayAction.put(key, action);
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "No date information in action, skipping it");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DELETE:
|
|
||||||
// NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Log.e(TAG, "Unknown action: " + action);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LongList queueToBeRemoved = new LongList();
|
LongList queueToBeRemoved = new LongList();
|
||||||
List<FeedItem> updatedItems = new ArrayList<>();
|
List<FeedItem> updatedItems = new ArrayList<>();
|
||||||
for (EpisodeAction action : mostRecentPlayAction.values()) {
|
for (EpisodeAction action : playActionsToUpdate.values()) {
|
||||||
FeedItem playItem = DBReader.getFeedItemByUrl(action.getPodcast(), action.getEpisode());
|
String guid = GuidValidator.isValidGuid(action.getGuid()) ? action.getGuid() : null;
|
||||||
Log.d(TAG, "Most recent play action: " + action.toString());
|
FeedItem feedItem = DBReader.getFeedItemByGuidOrEpisodeUrl(guid, action.getEpisode());
|
||||||
if (playItem != null) {
|
if (feedItem == null) {
|
||||||
FeedMedia media = playItem.getMedia();
|
Log.i(TAG, "Unknown feed item: " + action);
|
||||||
media.setPosition(action.getPosition() * 1000);
|
continue;
|
||||||
if (FeedItemUtil.hasAlmostEnded(playItem.getMedia())) {
|
|
||||||
Log.d(TAG, "Marking as played");
|
|
||||||
playItem.setPlayed(true);
|
|
||||||
queueToBeRemoved.add(playItem.getId());
|
|
||||||
}
|
|
||||||
updatedItems.add(playItem);
|
|
||||||
}
|
}
|
||||||
|
if (action.getAction() == EpisodeAction.NEW) {
|
||||||
|
DBWriter.markItemPlayed(feedItem, FeedItem.UNPLAYED, true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Most recent play action: " + action.toString());
|
||||||
|
FeedMedia media = feedItem.getMedia();
|
||||||
|
media.setPosition(action.getPosition() * 1000);
|
||||||
|
if (FeedItemUtil.hasAlmostEnded(feedItem.getMedia())) {
|
||||||
|
Log.d(TAG, "Marking as played");
|
||||||
|
feedItem.setPlayed(true);
|
||||||
|
queueToBeRemoved.add(feedItem.getId());
|
||||||
|
}
|
||||||
|
updatedItems.add(feedItem);
|
||||||
|
|
||||||
}
|
}
|
||||||
DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray());
|
DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray());
|
||||||
DBReader.loadAdditionalFeedItemListData(updatedItems);
|
DBReader.loadAdditionalFeedItemListData(updatedItems);
|
||||||
|
@ -192,7 +192,7 @@ public class CastUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
FeedItem feedItem = DBReader.getFeedItemByUrl(metadata.getString(KEY_FEED_URL),
|
FeedItem feedItem = DBReader.getFeedItemByGuidOrEpisodeUrl(null,
|
||||||
metadata.getString(KEY_EPISODE_IDENTIFIER));
|
metadata.getString(KEY_EPISODE_IDENTIFIER));
|
||||||
if (feedItem != null) {
|
if (feedItem != null) {
|
||||||
result = feedItem.getMedia();
|
result = feedItem.getMedia();
|
||||||
|
@ -218,7 +218,7 @@ public class DbReaderTest {
|
|||||||
}
|
}
|
||||||
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||||
adapter.open();
|
adapter.open();
|
||||||
adapter.setFeedItemlist(downloaded);
|
adapter.storeFeedItemlist(downloaded);
|
||||||
adapter.close();
|
adapter.close();
|
||||||
return downloaded;
|
return downloaded;
|
||||||
}
|
}
|
||||||
@ -257,7 +257,7 @@ public class DbReaderTest {
|
|||||||
}
|
}
|
||||||
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||||
adapter.open();
|
adapter.open();
|
||||||
adapter.setFeedItemlist(newItems);
|
adapter.storeFeedItemlist(newItems);
|
||||||
adapter.close();
|
adapter.close();
|
||||||
return newItems;
|
return newItems;
|
||||||
}
|
}
|
||||||
@ -417,4 +417,23 @@ public class DbReaderTest {
|
|||||||
assertTrue(item2.hasChapters());
|
assertTrue(item2.hasChapters());
|
||||||
assertEquals(item1.getChapters(), item2.getChapters());
|
assertEquals(item1.getChapters(), item2.getChapters());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetItemByEpisodeUrl() {
|
||||||
|
List<Feed> feeds = saveFeedlist(1, 1, true);
|
||||||
|
FeedItem item1 = feeds.get(0).getItems().get(0);
|
||||||
|
FeedItem feedItemByEpisodeUrl = DBReader.getFeedItemByGuidOrEpisodeUrl(null,
|
||||||
|
item1.getMedia().getDownload_url());
|
||||||
|
assertEquals(item1.getItemIdentifier(), feedItemByEpisodeUrl.getItemIdentifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetItemByGuid() {
|
||||||
|
List<Feed> feeds = saveFeedlist(1, 1, true);
|
||||||
|
FeedItem item1 = feeds.get(0).getItems().get(0);
|
||||||
|
|
||||||
|
FeedItem feedItemByGuid = DBReader.getFeedItemByGuidOrEpisodeUrl(item1.getItemIdentifier(),
|
||||||
|
item1.getMedia().getDownload_url());
|
||||||
|
assertEquals(item1.getItemIdentifier(), feedItemByGuid.getItemIdentifier());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,187 @@
|
|||||||
|
package de.danoeh.antennapod.core.sync;
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.core.util.Pair;
|
||||||
|
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import de.danoeh.antennapod.net.sync.model.EpisodeAction;
|
||||||
|
|
||||||
|
|
||||||
|
public class EpisodeActionFilterTest extends TestCase {
|
||||||
|
|
||||||
|
EpisodeActionFilter episodeActionFilter = new EpisodeActionFilter();
|
||||||
|
|
||||||
|
public void testGetRemoteActionsHappeningAfterLocalActions() throws ParseException {
|
||||||
|
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
Date morning = format.parse("2021-01-01 08:00:00");
|
||||||
|
Date lateMorning = format.parse("2021-01-01 09:00:00");
|
||||||
|
|
||||||
|
List<EpisodeAction> episodeActions = new ArrayList<>();
|
||||||
|
episodeActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morning)
|
||||||
|
.position(10)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
episodeActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(lateMorning)
|
||||||
|
.position(20)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
episodeActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morning)
|
||||||
|
.position(5)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
Date morningFiveMinutesLater = format.parse("2021-01-01 08:05:00");
|
||||||
|
List<EpisodeAction> remoteActions = new ArrayList<>();
|
||||||
|
remoteActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morningFiveMinutesLater)
|
||||||
|
.position(10)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
remoteActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morningFiveMinutesLater)
|
||||||
|
.position(5)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<Pair<String, String>, EpisodeAction> uniqueList = episodeActionFilter
|
||||||
|
.getRemoteActionsOverridingLocalActions(remoteActions, episodeActions);
|
||||||
|
assertSame(1, uniqueList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testGetRemoteActionsHappeningBeforeLocalActions() throws ParseException {
|
||||||
|
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
Date morning = format.parse("2021-01-01 08:00:00");
|
||||||
|
Date lateMorning = format.parse("2021-01-01 09:00:00");
|
||||||
|
|
||||||
|
List<EpisodeAction> episodeActions = new ArrayList<>();
|
||||||
|
episodeActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morning)
|
||||||
|
.position(10)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
episodeActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(lateMorning)
|
||||||
|
.position(20)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
episodeActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morning)
|
||||||
|
.position(5)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
Date morningFiveMinutesEarlier = format.parse("2021-01-01 07:55:00");
|
||||||
|
List<EpisodeAction> remoteActions = new ArrayList<>();
|
||||||
|
remoteActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morningFiveMinutesEarlier)
|
||||||
|
.position(10)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
remoteActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morningFiveMinutesEarlier)
|
||||||
|
.position(5)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<Pair<String, String>, EpisodeAction> uniqueList = episodeActionFilter
|
||||||
|
.getRemoteActionsOverridingLocalActions(remoteActions, episodeActions);
|
||||||
|
assertSame(0, uniqueList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testGetMultipleRemoteActionsHappeningAfterLocalActions() throws ParseException {
|
||||||
|
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
Date morning = format.parse("2021-01-01 08:00:00");
|
||||||
|
|
||||||
|
List<EpisodeAction> episodeActions = new ArrayList<>();
|
||||||
|
episodeActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morning)
|
||||||
|
.position(10)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
episodeActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morning)
|
||||||
|
.position(5)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
Date morningFiveMinutesLater = format.parse("2021-01-01 08:05:00");
|
||||||
|
List<EpisodeAction> remoteActions = new ArrayList<>();
|
||||||
|
remoteActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morningFiveMinutesLater)
|
||||||
|
.position(10)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
remoteActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morningFiveMinutesLater)
|
||||||
|
.position(5)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<Pair<String, String>, EpisodeAction> uniqueList = episodeActionFilter
|
||||||
|
.getRemoteActionsOverridingLocalActions(remoteActions, episodeActions);
|
||||||
|
assertEquals(2, uniqueList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testGetMultipleRemoteActionsHappeningBeforeLocalActions() throws ParseException {
|
||||||
|
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
Date morning = format.parse("2021-01-01 08:00:00");
|
||||||
|
|
||||||
|
List<EpisodeAction> episodeActions = new ArrayList<>();
|
||||||
|
episodeActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morning)
|
||||||
|
.position(10)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
episodeActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morning)
|
||||||
|
.position(5)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
Date morningFiveMinutesEarlier = format.parse("2021-01-01 07:55:00");
|
||||||
|
List<EpisodeAction> remoteActions = new ArrayList<>();
|
||||||
|
remoteActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morningFiveMinutesEarlier)
|
||||||
|
.position(10)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
remoteActions.add(new EpisodeAction
|
||||||
|
.Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY)
|
||||||
|
.timestamp(morningFiveMinutesEarlier)
|
||||||
|
.position(5)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<Pair<String, String>, EpisodeAction> uniqueList = episodeActionFilter
|
||||||
|
.getRemoteActionsOverridingLocalActions(remoteActions, episodeActions);
|
||||||
|
assertEquals(0, uniqueList.size());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package de.danoeh.antennapod.core.sync;
|
||||||
|
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
public class GuidValidatorTest extends TestCase {
|
||||||
|
|
||||||
|
public void testIsValidGuid() {
|
||||||
|
assertTrue(GuidValidator.isValidGuid("skfjsdvgsd"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testIsInvalidGuid() {
|
||||||
|
assertFalse(GuidValidator.isValidGuid(""));
|
||||||
|
assertFalse(GuidValidator.isValidGuid(" "));
|
||||||
|
assertFalse(GuidValidator.isValidGuid("\n"));
|
||||||
|
assertFalse(GuidValidator.isValidGuid(" \n"));
|
||||||
|
assertFalse(GuidValidator.isValidGuid(null));
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,7 @@ public class EpisodeAction {
|
|||||||
|
|
||||||
private final String podcast;
|
private final String podcast;
|
||||||
private final String episode;
|
private final String episode;
|
||||||
|
private final String guid;
|
||||||
private final Action action;
|
private final Action action;
|
||||||
private final Date timestamp;
|
private final Date timestamp;
|
||||||
private final int started;
|
private final int started;
|
||||||
@ -34,6 +35,7 @@ public class EpisodeAction {
|
|||||||
private EpisodeAction(Builder builder) {
|
private EpisodeAction(Builder builder) {
|
||||||
this.podcast = builder.podcast;
|
this.podcast = builder.podcast;
|
||||||
this.episode = builder.episode;
|
this.episode = builder.episode;
|
||||||
|
this.guid = builder.guid;
|
||||||
this.action = builder.action;
|
this.action = builder.action;
|
||||||
this.timestamp = builder.timestamp;
|
this.timestamp = builder.timestamp;
|
||||||
this.started = builder.started;
|
this.started = builder.started;
|
||||||
@ -72,6 +74,10 @@ public class EpisodeAction {
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
String guid = object.optString("guid", null);
|
||||||
|
if (!TextUtils.isEmpty(guid)) {
|
||||||
|
builder.guid(guid);
|
||||||
|
}
|
||||||
if (action == EpisodeAction.Action.PLAY) {
|
if (action == EpisodeAction.Action.PLAY) {
|
||||||
int started = object.optInt("started", -1);
|
int started = object.optInt("started", -1);
|
||||||
int position = object.optInt("position", -1);
|
int position = object.optInt("position", -1);
|
||||||
@ -94,6 +100,10 @@ public class EpisodeAction {
|
|||||||
return this.episode;
|
return this.episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getGuid() {
|
||||||
|
return this.guid;
|
||||||
|
}
|
||||||
|
|
||||||
public Action getAction() {
|
public Action getAction() {
|
||||||
return this.action;
|
return this.action;
|
||||||
}
|
}
|
||||||
@ -143,16 +153,21 @@ public class EpisodeAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EpisodeAction that = (EpisodeAction) o;
|
EpisodeAction that = (EpisodeAction) o;
|
||||||
return started == that.started && position == that.position && total == that.total && action != that.action
|
return started == that.started
|
||||||
|
&& position == that.position
|
||||||
|
&& total == that.total
|
||||||
|
&& action != that.action
|
||||||
&& ObjectsCompat.equals(podcast, that.podcast)
|
&& ObjectsCompat.equals(podcast, that.podcast)
|
||||||
&& ObjectsCompat.equals(episode, that.episode)
|
&& ObjectsCompat.equals(episode, that.episode)
|
||||||
&& ObjectsCompat.equals(timestamp, that.timestamp);
|
&& ObjectsCompat.equals(timestamp, that.timestamp)
|
||||||
|
&& ObjectsCompat.equals(guid, that.guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int result = podcast != null ? podcast.hashCode() : 0;
|
int result = podcast != null ? podcast.hashCode() : 0;
|
||||||
result = 31 * result + (episode != null ? episode.hashCode() : 0);
|
result = 31 * result + (episode != null ? episode.hashCode() : 0);
|
||||||
|
result = 31 * result + (guid != null ? guid.hashCode() : 0);
|
||||||
result = 31 * result + (action != null ? action.hashCode() : 0);
|
result = 31 * result + (action != null ? action.hashCode() : 0);
|
||||||
result = 31 * result + (timestamp != null ? timestamp.hashCode() : 0);
|
result = 31 * result + (timestamp != null ? timestamp.hashCode() : 0);
|
||||||
result = 31 * result + started;
|
result = 31 * result + started;
|
||||||
@ -171,6 +186,7 @@ public class EpisodeAction {
|
|||||||
try {
|
try {
|
||||||
obj.putOpt("podcast", this.podcast);
|
obj.putOpt("podcast", this.podcast);
|
||||||
obj.putOpt("episode", this.episode);
|
obj.putOpt("episode", this.episode);
|
||||||
|
obj.putOpt("guid", this.guid);
|
||||||
obj.put("action", this.getActionString());
|
obj.put("action", this.getActionString());
|
||||||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
|
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
|
||||||
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
@ -193,6 +209,7 @@ public class EpisodeAction {
|
|||||||
return "EpisodeAction{"
|
return "EpisodeAction{"
|
||||||
+ "podcast='" + podcast + '\''
|
+ "podcast='" + podcast + '\''
|
||||||
+ ", episode='" + episode + '\''
|
+ ", episode='" + episode + '\''
|
||||||
|
+ ", guid='" + guid + '\''
|
||||||
+ ", action=" + action
|
+ ", action=" + action
|
||||||
+ ", timestamp=" + timestamp
|
+ ", timestamp=" + timestamp
|
||||||
+ ", started=" + started
|
+ ", started=" + started
|
||||||
@ -217,6 +234,7 @@ public class EpisodeAction {
|
|||||||
private int started = -1;
|
private int started = -1;
|
||||||
private int position = -1;
|
private int position = -1;
|
||||||
private int total = -1;
|
private int total = -1;
|
||||||
|
private String guid;
|
||||||
|
|
||||||
public Builder(FeedItem item, Action action) {
|
public Builder(FeedItem item, Action action) {
|
||||||
this(item.getFeed().getDownload_url(), item.getMedia().getDownload_url(), action);
|
this(item.getFeed().getDownload_url(), item.getMedia().getDownload_url(), action);
|
||||||
@ -233,6 +251,11 @@ public class EpisodeAction {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder guid(String guid) {
|
||||||
|
this.guid = guid;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder currentTimestamp() {
|
public Builder currentTimestamp() {
|
||||||
return timestamp(new Date());
|
return timestamp(new Date());
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user