From 78af5e353befd4608c8e765d4af8f310c22c3511 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Wed, 8 Jul 2026 07:53:13 +0200 Subject: [PATCH 01/23] Fix possible memory leaks in new playback service (#8578) ### Description Fix possible memory leaks in new playback service ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../playback/service/PlaybackController.java | 8 +++- .../internal/MediaLibrarySessionCallback.java | 38 +++++++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackController.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackController.java index 4e3512617..e30e52b6b 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackController.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackController.java @@ -521,12 +521,16 @@ public abstract class PlaybackController { ListenableFuture controllerFuture = new MediaController.Builder(context, sessionToken).buildAsync(); controllerFuture.addListener(() -> { + MediaController controller = null; try { - MediaController controller = controllerFuture.get(); + controller = controllerFuture.get(); consumer.accept(controller); - controller.release(); } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); + } finally { + if (controller != null) { + controller.release(); + } } }, MoreExecutors.directExecutor()); diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java index bfa696009..769127aed 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java @@ -41,7 +41,6 @@ import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; import org.greenrobot.eventbus.EventBus; import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.schedulers.Schedulers; import java.util.Collections; @@ -102,7 +101,6 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib } private final Context context; - private final CompositeDisposable disposables = new CompositeDisposable(); public MediaLibrarySessionCallback(Context context) { this.context = context; @@ -264,7 +262,7 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib mediaItems, index, startPositionMs)); } SettableFuture future = SettableFuture.create(); - disposables.add(Single.fromCallable( + Single.fromCallable( () -> { List updatedItems = onAddMediaItems(mediaSession, controller, mediaItems).get(); long mediaId = Long.parseLong(updatedItems.get(index).mediaId); @@ -280,7 +278,7 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib }, error -> { Log.e(TAG, "Failed to load media", error); future.set(new MediaSession.MediaItemsWithStartPosition(mediaItems, index, startPositionMs)); - })); + }); return future; } @@ -294,7 +292,7 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib } SettableFuture> future = SettableFuture.create(); - disposables.add(Single.fromCallable(() -> enrichMediaItems(mediaItems)) + Single.fromCallable(() -> enrichMediaItems(mediaItems)) .subscribeOn(Schedulers.io()) .subscribe( items -> future.set(items.isEmpty() ? Collections.emptyList() : items), @@ -302,7 +300,7 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib Log.e(TAG, "Failed to load media items", error); future.set(Collections.emptyList()); } - )); + ); return future; } @@ -312,7 +310,7 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib public ListenableFuture onPlaybackResumption( @NonNull MediaSession mediaSession, @NonNull MediaSession.ControllerInfo controller) { SettableFuture future = SettableFuture.create(); - disposables.add(Single.fromCallable(() -> { + Single.fromCallable(() -> { FeedMedia media = DBReader.getFeedMedia(PlaybackPreferences.getCurrentlyPlayingFeedMediaId()); // If there is no media to resume, media3 crashes. So instead of crashing, just play something random. if (media == null) { @@ -343,7 +341,7 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib future.set(result); }, future::setException - )); + ); return future; } @@ -373,10 +371,10 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib @NonNull MediaSession.ControllerInfo browser, @NonNull String mediaId) { if (BROWSABLE_MEDIA_IDS.contains(mediaId) || mediaId.startsWith(MediaItemAdapter.MEDIA_ID_FEED_PREFIX)) { SettableFuture> future = SettableFuture.create(); - disposables.add(Single.fromCallable(() -> createBrowsableMediaItem(mediaId)) + Single.fromCallable(() -> createBrowsableMediaItem(mediaId)) .subscribeOn(Schedulers.io()) .subscribe(item -> future.set(LibraryResult.ofItem(item, null)), - future::setException)); + future::setException); return future; } return MediaLibraryService.MediaLibrarySession.Callback.super.onGetItem(session, browser, mediaId); @@ -393,7 +391,7 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib switch (parentId) { case MEDIA_ID_ROOT: - disposables.add(Single.fromCallable(() -> ImmutableList.of( + Single.fromCallable(() -> ImmutableList.of( createBrowsableMediaItem(MEDIA_ID_CONTINUE_LISTENING), createBrowsableMediaItem(MEDIA_ID_QUEUE), createBrowsableMediaItem(MEDIA_ID_DOWNLOADS), @@ -401,10 +399,10 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib createBrowsableMediaItem(MEDIA_ID_SUBSCRIPTIONS))) .subscribeOn(Schedulers.io()) .subscribe(items -> future.set(LibraryResult.ofItemList(items, params)), - future::setException)); + future::setException); return future; case MEDIA_ID_SUBSCRIPTIONS: - disposables.add(Single.fromCallable(DBReader::getFeedList) + Single.fromCallable(DBReader::getFeedList) .subscribeOn(Schedulers.io()) .subscribe( items -> { @@ -416,10 +414,10 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib } future.set(LibraryResult.ofItemList(builder.build(), params)); }, - future::setException)); + future::setException); return future; case MEDIA_ID_CONTINUE_LISTENING: - disposables.add(Single.fromCallable( + Single.fromCallable( () -> DBReader.getPausedQueue(CONTINUE_LISTENING_NUM_EPISODES)) .subscribeOn(Schedulers.io()) .subscribe( @@ -429,10 +427,10 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib Log.e(TAG, "Failed to load continue listening", error); future.set(LibraryResult.ofItemList(ImmutableList.of(), params)); } - )); + ); return future; default: // Episodes lists - disposables.add(Single.fromCallable(() -> { + Single.fromCallable(() -> { if (parentId.startsWith(MediaItemAdapter.MEDIA_ID_FEED_PREFIX)) { long feedId = Long.parseLong(parentId.split(":")[1]); return DBReader.getFeed(feedId, true, page * pageSize, pageSize).getItems(); @@ -451,7 +449,7 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib .subscribeOn(Schedulers.io()) .subscribe(items -> future.set(LibraryResult.ofItemList( MediaItemAdapter.fromItemList(context, items), params)), - future::setException)); + future::setException); return future; } } @@ -462,12 +460,12 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib @NonNull MediaLibraryService.MediaLibrarySession session, @NonNull MediaSession.ControllerInfo browser, @NonNull String query, int page, int pageSize, @Nullable MediaLibraryService.LibraryParams params) { SettableFuture>> future = SettableFuture.create(); - disposables.add(Single.fromCallable(() -> + Single.fromCallable(() -> DBReader.searchFeedItems(0, query, FeedItemFilter.unfiltered())) .subscribeOn(Schedulers.io()) .subscribe(items -> future.set(LibraryResult.ofItemList( MediaItemAdapter.fromItemList(context, items), params)), - future::setException)); + future::setException); return future; } From e32c9e90ff8d30ae533be11df09e93602dd9e893 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Thu, 9 Jul 2026 08:10:35 +0200 Subject: [PATCH 02/23] Fix local folder playback not showing cover (#8580) ### Description Fix local folder playback not showing cover Closes #8562 ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../danoeh/antennapod/playback/base/MediaItemAdapter.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java index 603dd47d7..7782d9491 100644 --- a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java +++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java @@ -116,10 +116,11 @@ public class MediaItemAdapter { private static Bitmap loadArtworkBitmap(Context context, Playable playable, int iconSize) { try { + String imageLocation = playable.getImageLocation(); return Glide.with(context) .asBitmap() - .onlyRetrieveFromCache(true) - .load(playable.getImageLocation()) + .onlyRetrieveFromCache(imageLocation != null && imageLocation.startsWith("http")) + .load(imageLocation) .submit(iconSize, iconSize) .get(500, TimeUnit.MILLISECONDS); } catch (Exception tr1) { @@ -139,7 +140,7 @@ public class MediaItemAdapter { try { return Glide.with(context) .asBitmap() - .onlyRetrieveFromCache(true) + .onlyRetrieveFromCache(fallback.startsWith("http")) .load(fallback) .submit(iconSize, iconSize) .get(500, TimeUnit.MILLISECONDS); From df29c2fe27337a2cba8d9583ac92a726eb1991b2 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Thu, 9 Jul 2026 08:21:33 +0200 Subject: [PATCH 03/23] Fix miniplayer disappearing when switching episode during cast (#8582) ### Description Fix miniplayer disappearing when switching episode during cast. The player comes back after a while (probably because of other service changes since that issue was opened) but it disappearing is the wrong behavior. With this change, the player stays active until actually transitioning to "playing". Closes #8504 ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- playback/cast/README.md | 16 +++++++++++++++- .../playback/service/Media3PlaybackService.java | 2 -- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/playback/cast/README.md b/playback/cast/README.md index a2e07d38c..22ac3ecb7 100644 --- a/playback/cast/README.md +++ b/playback/cast/README.md @@ -1,4 +1,18 @@ # :playback:cast This module provides Chromecast support for the Google Play version of the app. -Implements `PlaybackServiceMediaPlayer` using the Cast SDK; compiled only in the `play` build flavor. +Compiled only in the `play` build flavor (the `free` flavor contains no-op stubs). + +## Active path (Media3) + +`CastPlayerWrapper.wrap(localPlayer, context)` builds a Media3 `CastPlayer` that automatically +routes playback to the Cast device when a session is active and falls back to the local player +otherwise. It uses `ApMediaItemConverter` (inner class) to translate between Media3 `MediaItem` +and Cast `MediaQueueItem`. The media database ID is round-tripped through Cast metadata under +the key `"media_id"`, which `Media3PlaybackService.ensureCurrentMediaLoaded()` reads back to +look up the `FeedMedia` from the local database. + +## Legacy path (CastPsmp) + +`CastPsmp` implements `PlaybackServiceMediaPlayer` for the old `PlaybackService`. It is still +compiled but is effectively dead code — `PlaybackService` throws an exception if started. diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index f75884a64..78271a4f8 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -310,8 +310,6 @@ public class Media3PlaybackService extends MediaLibraryService { public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { if (mediaItem == null) { currentPlayable = null; - PlaybackPreferences.writeNoMediaPlaying(); - EventBus.getDefault().post(new PlayerStatusEvent()); } else { ensureCurrentMediaLoaded(); } From 3d1c1a79bad2e3e6c6f4cd107587ace28051e004 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Thu, 9 Jul 2026 08:48:05 +0200 Subject: [PATCH 04/23] Fix some crashes reported through google play (#8579) ### Description Possible fixes for some crashes reported through google play ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../java/de/danoeh/antennapod/activity/MainActivity.java | 9 +++------ .../danoeh/antennapod/ui/screen/home/HomeFragment.java | 3 +++ .../service/internal/MediaLibrarySessionCallback.java | 7 ++++++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index dbc2d7888..bba5d519c 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -492,20 +492,17 @@ public class MainActivity extends CastEnabledActivity implements NavigationToolb public void loadFragment(Fragment fragment) { FragmentManager fragmentManager = getSupportFragmentManager(); - // clear back stack - for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) { - fragmentManager.popBackStack(); - } + // Clear all synchronously to avoid conflicting with predictive back gesture cancellation + fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); FragmentTransaction t = fragmentManager.beginTransaction(); t.replace(R.id.main_content_view, fragment, MAIN_FRAGMENT_TAG); - fragmentManager.popBackStack(); // TODO: we have to allow state loss here // since this function can get called from an AsyncTask which // could be finishing after our app has already committed state // and is about to get shutdown. What we *should* do is // not commit anything in an AsyncTask, but that's a bigger // change than we want now. - t.commitAllowingStateLoss(); + t.commitNowAllowingStateLoss(); if (drawerLayout != null) { // Tablet layout does not have a drawer drawerLayout.closeDrawer(navDrawer); diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeFragment.java index 77af87ce2..e85d42ff3 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/home/HomeFragment.java @@ -93,6 +93,9 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis } private void addSection(Fragment section) { + if (section == null) { // Can happen when stored settings reference a section that no longer exists + return; + } FragmentContainerView containerView = new FragmentContainerView(getContext()); containerView.setId(View.generateViewId()); viewBinding.homeContainer.addView(containerView); diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java index 769127aed..42baf3f04 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java @@ -277,7 +277,8 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib future.set(new MediaSession.MediaItemsWithStartPosition(result.first, index, startPosition)); }, error -> { Log.e(TAG, "Failed to load media", error); - future.set(new MediaSession.MediaItemsWithStartPosition(mediaItems, index, startPositionMs)); + future.set(new MediaSession.MediaItemsWithStartPosition( + Collections.emptyList(), index, startPositionMs)); }); return future; } @@ -484,6 +485,10 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib try { long mediaId = Long.parseLong(item.mediaId); FeedMedia media = DBReader.getFeedMedia(mediaId); + if (media == null) { + Log.e(TAG, "Media not found for ID: " + mediaId); + continue; + } builder.add(MediaItemAdapter.fromPlayable(context, media, false)); } catch (NumberFormatException e) { Log.e(TAG, "Invalid media ID: " + item.mediaId, e); From e3b8b0d01b11bdbd20f42a8ecf269e0fd9a2fba0 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Thu, 9 Jul 2026 20:39:46 +0200 Subject: [PATCH 05/23] Fix stale position broadcast in new playback service (#8584) ### Description Fix stale position broadcast in new playback service. super.seekTo() is asynchronous. player.getCurrentPosition() called immediately after still returns the old position. ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../antennapod/playback/service/Media3PlaybackService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index 78271a4f8..a3aa1cdce 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -172,7 +172,7 @@ public class Media3PlaybackService extends MediaLibraryService { public void seekTo(long positionMs) { super.seekTo(positionMs); EventBus.getDefault().post( - new PlaybackPositionEvent((int) player.getCurrentPosition(), (int) player.getDuration())); + new PlaybackPositionEvent((int) positionMs, (int) player.getDuration())); } }; player.addListener(playerListener); From 4d4684525c350ae825a7b1fec9dbc56a8fd01c65 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Thu, 9 Jul 2026 20:42:37 +0200 Subject: [PATCH 06/23] Fix playback position getting reset on continuous playback on new playback service (#8588) ### Description Fix playback position getting reset on continuous playback on new playback service Closes #8468 ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../antennapod/playback/service/Media3PlaybackService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index a3aa1cdce..c09b97480 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -662,8 +662,8 @@ public class Media3PlaybackService extends MediaLibraryService { applyVolumeAdaption(1.0f); } player.setPlayWhenReady(UserPreferences.isFollowQueue()); - player.setMediaItem(nextMediaItem); - player.seekTo(SkipUtils.skipIntroIfNecessary(this, nextMedia)); + player.setMediaItem(nextMediaItem, + SkipUtils.skipIntroIfNecessary(this, nextMedia)); player.prepare(); }, error -> Log.e(TAG, "Failed to load next queue item", error), From af577fef0848fe853d1ff960284e9c792f050712 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Thu, 9 Jul 2026 20:57:16 +0200 Subject: [PATCH 07/23] Stop playback when currently playing file gets deleted (#8583) ### Description Stop playback when currently playing file gets deleted Closes #8515 ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../service/Media3PlaybackService.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index c09b97480..8cbf5446c 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -22,6 +22,7 @@ import androidx.media3.session.SessionCommand; import androidx.media3.session.SessionResult; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.PlayerErrorEvent; import de.danoeh.antennapod.event.StreamingConfirmationEvent; import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent; @@ -746,6 +747,23 @@ public class Media3PlaybackService extends MediaLibraryService { } } + @Subscribe(threadMode = ThreadMode.MAIN) + @SuppressWarnings("unused") + public void feedItemsUpdated(FeedItemEvent event) { + if (currentPlayable == null || !currentPlayable.localFileAvailable()) { + return; + } + int index = FeedItemEvent.indexOfItemWithId(event.items, currentPlayable.getItemId()); + if (index >= 0 && event.items.get(index).getMedia() != null + && !event.items.get(index).getMedia().localFileAvailable()) { + player.stop(); + player.clearMediaItems(); + currentPlayable = null; + PlaybackPreferences.writeNoMediaPlaying(); + EventBus.getDefault().post(new PlayerStatusEvent()); + } + } + private void initLoudnessEnhancer(int audioSessionId) { if (!VolumeAdaptionSetting.isBoostSupported()) { return; From 42b0ef5e2760c47325ac54456395b1ae2b9f7f41 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Sat, 11 Jul 2026 22:00:51 +0200 Subject: [PATCH 08/23] Unblock users when sleep timer acts up (#8593) ### Description Unblock users when sleep timer acts up. Release mode only, we want to handle/fix this as a bug if it happens. ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../antennapod/playback/service/Media3PlaybackService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index 8cbf5446c..62984fbb5 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -721,6 +721,9 @@ public class Media3PlaybackService extends MediaLibraryService { if (sleepTimer != null) { sleepTimer.stop(); sleepTimer = null; + } else if (!BuildConfig.DEBUG) { + // Unblock user in case we somehow got into an inconsistent state + EventBus.getDefault().postSticky(SleepTimerUpdatedEvent.cancelled()); } if (player != null) { applyVolumeAdaption(1.0f); From c3c2ab61d74ae82de7b6c6c37fc7086464baf731 Mon Sep 17 00:00:00 2001 From: Geoffrey Sisco Date: Sat, 11 Jul 2026 17:22:03 -0400 Subject: [PATCH 09/23] Restore Media3 Next behavior (#8560) ### Description Restore legacy manual Next behavior in the Media3 playback service. This PR advertises the standard Media3 NEXT command and routes it through the Media3 next-in-queue path with legacy-equivalent playback finalization. That restores legacy queue cleanup, episode played-state handling, playback history, auto-delete evaluation, and end-of-queue behavior for manual Next. While implementing this, several related Media3 playback synchronization issues were also addressed: - Route playback state synchronization through `updatePlaybackPreferences()` after switching episodes so `PlayerStatusEvent` is emitted through the centralized helper and the playback UI immediately reflects the new media. - Clear the current playable when skipping the final queue item so the service, player, widget, and notification do not retain stale playback state. - Restore the existing end-of-queue message when manually skipping the final queued episode. - Use Media3 media button preferences instead of the legacy custom layout API so the standard NEXT command can be exposed while preserving the intended notification/SystemUI button arrangement. Closes #8528 ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [ ] If it is a core feature, I have added automated tests --- .../service/Media3PlaybackService.java | 49 ++++++++++++------- .../internal/MediaLibrarySessionCallback.java | 12 ++--- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index 62984fbb5..f940b4cf7 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -22,6 +22,8 @@ import androidx.media3.session.SessionCommand; import androidx.media3.session.SessionResult; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; + +import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.PlayerErrorEvent; import de.danoeh.antennapod.event.StreamingConfirmationEvent; @@ -120,6 +122,7 @@ public class Media3PlaybackService extends MediaLibraryService { public Player.Commands getAvailableCommands() { return super.getAvailableCommands() .buildUpon() + .add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .remove(Player.COMMAND_SEEK_TO_PREVIOUS) .remove(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .build(); @@ -165,7 +168,7 @@ public class Media3PlaybackService extends MediaLibraryService { @Override public void seekToNextMediaItem() { if (currentPlayable != null) { - startNextInQueue(currentPlayable.getItem()); + startNextInQueue(currentPlayable, true, false); } } @@ -193,9 +196,6 @@ public class Media3PlaybackService extends MediaLibraryService { if (customCommand.customAction.equals(SESSION_COMMAND_PLAYBACK_SPEED.customAction)) { setNextPlaybackSpeed(); return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); - } else if (customCommand.customAction.equals(SESSION_COMMAND_SKIP_TO_NEXT.customAction)) { - session.getPlayer().seekToNextMediaItem(); - return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); } else if (customCommand.customAction.equals(SESSION_COMMAND_NEXT_CHAPTER.customAction)) { seekToNextChapter(); return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); @@ -257,10 +257,10 @@ public class Media3PlaybackService extends MediaLibraryService { if (playbackState == Player.STATE_ENDED && currentPlayable != null) { FeedMedia media = currentPlayable; currentPlayable = null; // To avoid position updater saving position after we already reset it - onPlaybackEnd(media); if (sleepTimer != null && sleepTimer.isActive()) { sleepTimer.episodeFinishedPlayback(); if (!sleepTimer.shouldContinueToNextEpisode()) { + finalizePlayback(media, true, false, false); player.stop(); player.clearMediaItems(); PlaybackPreferences.writeNoMediaPlaying(); @@ -270,7 +270,7 @@ public class Media3PlaybackService extends MediaLibraryService { return; } } - startNextInQueue(media.getItem()); + startNextInQueue(media, false, true); } } @@ -501,7 +501,7 @@ public class Media3PlaybackService extends MediaLibraryService { PlayableUtils.saveCurrentPosition(currentPlayable, (int) position, timestamp); } - private void onPlaybackEnd(FeedMedia media) { + private void finalizePlayback(FeedMedia media, boolean ended, boolean skipped, boolean playingNext) { if (media == null) { return; } @@ -511,11 +511,13 @@ public class Media3PlaybackService extends MediaLibraryService { boolean almostEnded = media.getDuration() > 0 && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000; - SynchronizationQueue.getInstance().enqueueEpisodePlayed(media, almostEnded); - if (almostEnded) { - if (item != null) { + SynchronizationQueue.getInstance().enqueueEpisodePlayed(media, ended || almostEnded); + if (item != null) { + if (ended || almostEnded) { DBWriter.markItemsPlayed(FeedItem.PLAYED, true, Collections.singletonList(item)); - DBWriter.removeQueueItem(this, true, item); + } + if (ended || almostEnded || (skipped && !UserPreferences.shouldSkipKeepEpisode())) { + DBWriter.removeQueueItem(this, ended, item); FeedPreferences.AutoDeleteAction action = item.getFeed().getPreferences().getCurrentAutoDelete(); boolean autoDeleteEnabledGlobally = UserPreferences.isAutoDelete() && (!item.getFeed().isLocalFeed() || UserPreferences.isAutoDeleteLocal()); @@ -526,6 +528,8 @@ public class Media3PlaybackService extends MediaLibraryService { DBWriter.deleteFeedMediaOfItem(this, media); } } + } + if (ended || skipped || playingNext) { DBWriter.addItemToPlaybackHistory(media); } } @@ -560,7 +564,7 @@ public class Media3PlaybackService extends MediaLibraryService { List chapters = currentPlayable.getChapters(); if (chapters == null) { if (currentPlayable.getItem() != null) { - startNextInQueue(currentPlayable.getItem()); + startNextInQueue(currentPlayable, true, false); } return; } @@ -569,7 +573,7 @@ public class Media3PlaybackService extends MediaLibraryService { if (chapters.size() < nextChapter + 1) { if (currentPlayable.getItem() != null) { - startNextInQueue(currentPlayable.getItem()); + startNextInQueue(currentPlayable, true, false); } return; } @@ -627,17 +631,24 @@ public class Media3PlaybackService extends MediaLibraryService { * Loads the next item, and starts it if continuous playback is enabled. */ @UnstableApi - private void startNextInQueue(FeedItem item) { + private void startNextInQueue(FeedMedia media, boolean wasSkipped, boolean ended) { if (queueLoaderDisposable != null) { queueLoaderDisposable.dispose(); } + if (media == null) { + return; + } + FeedItem item = media.getItem(); if (item == null) { return; } queueLoaderDisposable = Maybe.fromCallable(() -> { FeedItem nextItem = DBReader.getNextInQueue(item); - if (nextItem != null && nextItem.getMedia() != null) { - return new Pair<>(nextItem.getMedia(), MediaItemAdapter.fromPlayable(Media3PlaybackService.this, nextItem.getMedia(), false)); + boolean hasNext = nextItem != null && nextItem.getMedia() != null; + finalizePlayback(media, ended, wasSkipped, hasNext); + if (hasNext) { + return new Pair<>(nextItem.getMedia(), + MediaItemAdapter.fromPlayable(Media3PlaybackService.this, nextItem.getMedia(), false)); } return null; }) @@ -656,7 +667,7 @@ public class Media3PlaybackService extends MediaLibraryService { currentPlayable = nextMedia; currentPlayable.onPlaybackStart(); - PlaybackPreferences.writeMediaPlaying(nextMedia); + updatePlaybackPreferences(); if (nextMedia.getItem() != null && nextMedia.getItem().getFeed() != null) { volumeAdaptionFactor = nextMedia.getItem().getFeed() .getPreferences().getVolumeAdaptionSetting().getAdaptionFactor(); @@ -669,12 +680,16 @@ public class Media3PlaybackService extends MediaLibraryService { }, error -> Log.e(TAG, "Failed to load next queue item", error), () -> { + currentPlayable = null; player.stop(); player.clearMediaItems(); PlaybackPreferences.writeNoMediaPlaying(); EventBus.getDefault().post(new PlayerStatusEvent()); EventBus.getDefault().post( new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)); + if (wasSkipped) { + EventBus.getDefault().post(new MessageEvent(getString(R.string.no_following_in_queue))); + } }); } diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java index 42baf3f04..89f8c44c1 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java @@ -65,8 +65,6 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib = new SessionCommand("fast_forward", Bundle.EMPTY); protected static final SessionCommand SESSION_COMMAND_PLAYBACK_SPEED = new SessionCommand("playback_speed", Bundle.EMPTY); - protected static final SessionCommand SESSION_COMMAND_SKIP_TO_NEXT - = new SessionCommand("skip_to_next", Bundle.EMPTY); protected static final SessionCommand SESSION_COMMAND_NEXT_CHAPTER = new SessionCommand("next_chapter", Bundle.EMPTY); public static final SessionCommand SESSION_COMMAND_SKIP_SILENCE @@ -116,7 +114,6 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib .add(SESSION_COMMAND_REWIND) .add(SESSION_COMMAND_FAST_FORWARD) .add(SESSION_COMMAND_PLAYBACK_SPEED) - .add(SESSION_COMMAND_SKIP_TO_NEXT) .add(SESSION_COMMAND_NEXT_CHAPTER) .add(SESSION_COMMAND_SKIP_SILENCE) .add(SESSION_COMMAND_SET_SLEEP_TIMER) @@ -130,7 +127,7 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib .build(); return new MediaSession.ConnectionResult.AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) - .setCustomLayout(buildCustomLayout()) + .setMediaButtonPreferences(buildCustomLayout()) .setAvailablePlayerCommands(playerCommands) .build(); } @@ -138,12 +135,12 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib @Override @UnstableApi public void onPostConnect(@NonNull MediaSession session, @NonNull MediaSession.ControllerInfo controller) { - session.setCustomLayout(buildCustomLayout()); + session.setMediaButtonPreferences(buildCustomLayout()); } @UnstableApi public void refreshNotification(MediaLibraryService.MediaLibrarySession session) { - session.setCustomLayout(buildCustomLayout()); + session.setMediaButtonPreferences(buildCustomLayout()); } @UnstableApi @@ -178,7 +175,8 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib if (UserPreferences.showSkipOnFullNotification()) { buttons.add(new CommandButton.Builder(CommandButton.ICON_NEXT) - .setSessionCommand(SESSION_COMMAND_SKIP_TO_NEXT) + .setSlots(CommandButton.SLOT_OVERFLOW) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .setDisplayName(context.getString(R.string.skip_episode_label)) .build()); } From 98c7125a56eb807346d43e2e7ffd00dae9e3e133 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Sun, 12 Jul 2026 10:30:22 +0200 Subject: [PATCH 10/23] Fix starting to buffer over mobile data without confirmation (#8585) ### Description Fix starting to buffer over mobile data without confirmation. In the new playback service, when "Follow Queue" is off, the streaming-consent flow is not shown and the app silently starts streaming the next episode over mobile data. The episode won't auto-play, but data is consumed without asking the user. ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../antennapod/playback/service/Media3PlaybackService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index f940b4cf7..da082b1f6 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -620,7 +620,9 @@ public class Media3PlaybackService extends MediaLibraryService { player.setMediaItem(confirmItem); player.setPlayWhenReady(false); player.prepare(); + PlaybackService.isRunning = false; EventBus.getDefault().post(new StreamingConfirmationEvent()); + EventBus.getDefault().post(new PlayerStatusEvent()); } private static boolean needsStreaming(FeedMedia media) { @@ -659,7 +661,7 @@ public class Media3PlaybackService extends MediaLibraryService { final FeedMedia nextMedia = pair.first; final MediaItem nextMediaItem = pair.second; if (needsStreaming(nextMedia) && !NetworkUtils.isStreamingAllowed() - && !allowStreamingThisTime && UserPreferences.isFollowQueue()) { + && !allowStreamingThisTime) { showStreamingConfirmation(nextMedia); return; } From 2d714739f72d305f231a812315c182e51f3ed74d Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Sun, 12 Jul 2026 15:32:11 +0200 Subject: [PATCH 11/23] Fix playing next on chromecast (#8594) ### Description That was a tricky one. When Cast finishes, it unloads the media session, so the Media3 CastPlayer reports IDLE, not ENDED like the local player. Media3 never reads the Cast SDK's idle reason, so "finished naturally" and "stopped by user" look identical. We now manually read the idle reason directly from the Cast SDK to make that distinction. Closes #8508 ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../playback/cast/CastPlayerWrapper.java | 4 ++ .../playback/cast/CastPlayerWrapper.java | 31 ++++++++++++ .../service/Media3PlaybackService.java | 48 +++++++++++-------- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPlayerWrapper.java b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPlayerWrapper.java index ed0710eb8..2f9d26fe9 100644 --- a/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPlayerWrapper.java +++ b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPlayerWrapper.java @@ -7,4 +7,8 @@ public class CastPlayerWrapper { public static Player wrap(Player player, Context context) { return player; } + + public static boolean hasPlaybackJustFinished(Context context) { + return false; + } } diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPlayerWrapper.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPlayerWrapper.java index f1c6a3051..4da2b7aff 100644 --- a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPlayerWrapper.java +++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPlayerWrapper.java @@ -13,6 +13,12 @@ import androidx.media3.common.Player; import androidx.media3.common.util.UnstableApi; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaQueueItem; +import com.google.android.gms.cast.MediaStatus; +import com.google.android.gms.cast.framework.CastContext; +import com.google.android.gms.cast.framework.CastSession; +import com.google.android.gms.cast.framework.media.RemoteMediaClient; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; import com.google.android.gms.common.images.WebImage; import de.danoeh.antennapod.playback.base.MediaItemAdapter; @@ -31,6 +37,31 @@ public class CastPlayerWrapper { .build(); } + public static boolean hasPlaybackJustFinished(Context context) { + // When Cast finishes, it unloads the media session, so the Media3 CastPlayer reports IDLE, not ENDED. + // Media3 never reads the Cast SDK's idle reason, so "finished naturally" and "stopped by user" look identical. + // This method reads the idle reason directly from the Cast SDK to make that distinction. + // It only gives a meaningful result while the idle status is still current, i.e. when called synchronously + // from a Player listener callback reacting to the unload. + if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) { + return false; + } + try { + CastSession castSession = CastContext.getSharedInstance(context) + .getSessionManager().getCurrentCastSession(); + if (castSession == null) { + return false; + } + RemoteMediaClient remoteMediaClient = castSession.getRemoteMediaClient(); + return remoteMediaClient != null + && remoteMediaClient.getPlayerState() == MediaStatus.PLAYER_STATE_IDLE + && remoteMediaClient.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + /** * Alias name to avoid specifying the full package import due to the MediaMetadata name collision */ diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index da082b1f6..11a593490 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -255,22 +255,7 @@ public class Media3PlaybackService extends MediaLibraryService { saveCurrentPosition(); } if (playbackState == Player.STATE_ENDED && currentPlayable != null) { - FeedMedia media = currentPlayable; - currentPlayable = null; // To avoid position updater saving position after we already reset it - if (sleepTimer != null && sleepTimer.isActive()) { - sleepTimer.episodeFinishedPlayback(); - if (!sleepTimer.shouldContinueToNextEpisode()) { - finalizePlayback(media, true, false, false); - player.stop(); - player.clearMediaItems(); - PlaybackPreferences.writeNoMediaPlaying(); - EventBus.getDefault().post( - new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)); - EventBus.getDefault().post(new PlayerStatusEvent()); - return; - } - } - startNextInQueue(media, false, true); + handlePlaybackEnded(); } } @@ -310,7 +295,12 @@ public class Media3PlaybackService extends MediaLibraryService { @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { if (mediaItem == null) { - currentPlayable = null; + if (currentPlayable != null + && CastPlayerWrapper.hasPlaybackJustFinished(Media3PlaybackService.this)) { + handlePlaybackEnded(); + } else { + currentPlayable = null; + } } else { ensureCurrentMediaLoaded(); } @@ -501,7 +491,7 @@ public class Media3PlaybackService extends MediaLibraryService { PlayableUtils.saveCurrentPosition(currentPlayable, (int) position, timestamp); } - private void finalizePlayback(FeedMedia media, boolean ended, boolean skipped, boolean playingNext) { + private void updateDatabaseAfterPlayback(FeedMedia media, boolean ended, boolean skipped, boolean playingNext) { if (media == null) { return; } @@ -629,6 +619,26 @@ public class Media3PlaybackService extends MediaLibraryService { return !media.localFileAvailable() && !URLUtil.isContentUrl(media.getStreamUrl()); } + @UnstableApi + private void handlePlaybackEnded() { + FeedMedia media = currentPlayable; + currentPlayable = null; // To avoid position updater saving position after we already reset it + if (sleepTimer != null && sleepTimer.isActive()) { + sleepTimer.episodeFinishedPlayback(); + if (!sleepTimer.shouldContinueToNextEpisode()) { + updateDatabaseAfterPlayback(media, true, false, false); + player.stop(); + player.clearMediaItems(); + PlaybackPreferences.writeNoMediaPlaying(); + EventBus.getDefault().post( + new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)); + EventBus.getDefault().post(new PlayerStatusEvent()); + return; + } + } + startNextInQueue(media, false, true); + } + /** * Loads the next item, and starts it if continuous playback is enabled. */ @@ -647,7 +657,7 @@ public class Media3PlaybackService extends MediaLibraryService { queueLoaderDisposable = Maybe.fromCallable(() -> { FeedItem nextItem = DBReader.getNextInQueue(item); boolean hasNext = nextItem != null && nextItem.getMedia() != null; - finalizePlayback(media, ended, wasSkipped, hasNext); + updateDatabaseAfterPlayback(media, ended, wasSkipped, hasNext); if (hasNext) { return new Pair<>(nextItem.getMedia(), MediaItemAdapter.fromPlayable(Media3PlaybackService.this, nextItem.getMedia(), false)); From d4eb078a52e27b5c8986c0dbed828aa369f2ae21 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Mon, 13 Jul 2026 00:19:31 +0200 Subject: [PATCH 12/23] Fix state of cast device when connecting while service is stopped (#8596) ### Description When freshly binding to the service to start playback (while chromecast is already connected), the service gets started but casting does not transition to PLAYING fast enough. So when unbinding, the service gets destroyed again and releases the session, which stops casting again. So we manually start the service when connected to Chromecast. We also send the media information (in paused state), so that we do not get stuck on the AntennaPod logo. Closes #8581 ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../playback/base/MediaItemAdapter.java | 8 +-- ...rviceStartingMediaRouteActionProvider.java | 46 ++++++++++++++++ .../cast/src/play/res/menu/cast_button.xml | 2 +- .../service/Media3PlaybackService.java | 54 +++++++++++++++++-- .../service/PlaybackServiceStarter.java | 2 +- 5 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/ServiceStartingMediaRouteActionProvider.java diff --git a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java index 7782d9491..c6489640f 100644 --- a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java +++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/MediaItemAdapter.java @@ -37,16 +37,12 @@ public class MediaItemAdapter { * Create a basic media item without attached metadata. * Should be used when initiating playback from outside the service. */ - public static MediaItem fromPlayableStub(Playable playable) { + public static MediaItem fromMediaIdStub(long mediaId) { MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder(); metadataBuilder.setIsPlayable(true); metadataBuilder.setIsBrowsable(false); - String mediaId = "0"; - if (playable instanceof FeedMedia) { - mediaId = String.valueOf(((FeedMedia) playable).getId()); - } return new MediaItem.Builder() - .setMediaId(mediaId) + .setMediaId(String.valueOf(mediaId)) .setMediaMetadata(metadataBuilder.build()) .build(); } diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/ServiceStartingMediaRouteActionProvider.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/ServiceStartingMediaRouteActionProvider.java new file mode 100644 index 000000000..8c824957d --- /dev/null +++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/ServiceStartingMediaRouteActionProvider.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.playback.cast; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.mediarouter.app.MediaRouteActionProvider; +import androidx.mediarouter.app.MediaRouteButton; + +/** + * Shows the currently playing episode directly after connecting to the cast device + * by starting the playback service. + */ +public class ServiceStartingMediaRouteActionProvider extends MediaRouteActionProvider { + private static final String TAG = "SrvStartMediaRouteBtn"; + + public ServiceStartingMediaRouteActionProvider(@NonNull Context context) { + super(context); + } + + @NonNull + @Override + public MediaRouteButton onCreateMediaRouteButton() { + return new ServiceStartingMediaRouteButton(getContext()); + } + + private static class ServiceStartingMediaRouteButton extends MediaRouteButton { + ServiceStartingMediaRouteButton(@NonNull Context context) { + super(context); + } + + @Override + public boolean performClick() { + Intent intent = new Intent(); + intent.setComponent(new ComponentName(getContext(), + "de.danoeh.antennapod.playback.service.Media3PlaybackService")); + try { + getContext().startService(intent); + } catch (IllegalStateException e) { + Log.e(TAG, "Unable to start playback service", e); + } + return super.performClick(); + } + } +} diff --git a/playback/cast/src/play/res/menu/cast_button.xml b/playback/cast/src/play/res/menu/cast_button.xml index 6e65bce18..9cc2a324d 100644 --- a/playback/cast/src/play/res/menu/cast_button.xml +++ b/playback/cast/src/play/res/menu/cast_button.xml @@ -5,7 +5,7 @@ diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index 11a593490..bd09ea3e5 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.playback.service; +import android.content.Intent; import android.media.audiofx.LoudnessEnhancer; import android.os.Bundle; import android.util.Log; @@ -8,6 +9,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; import androidx.core.util.Pair; +import androidx.media3.common.DeviceInfo; import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackException; @@ -183,6 +185,39 @@ public class Media3PlaybackService extends MediaLibraryService { mediaSession = new MediaLibraryService.MediaLibrarySession.Builder(this, player, sessionCallback) .setSessionActivity(new MainActivityStarter(this).withOpenPlayer().getPendingIntent()) .build(); + if (isCasting()) { + keepServiceRunningWhileCasting(); + loadCurrentMediaWhileCasting(); + } + } + + private void loadCurrentMediaWhileCasting() { + long mediaId = PlaybackPreferences.getCurrentlyPlayingFeedMediaId(); + if (mediaId == PlaybackPreferences.NO_MEDIA_PLAYING) { + return; + } + PlaybackController.bindToMedia3Service(this, controller -> { + if (player.getCurrentMediaItem() != null || !isCasting()) { + return; + } + controller.setPlayWhenReady(false); + controller.setMediaItem(MediaItemAdapter.fromMediaIdStub(mediaId)); + controller.prepare(); + }); + } + + /** + * When freshly binding to the service to start playback (while chromecast is already connected), + * the service gets started but casting does not transition to PLAYING fast enough. So when unbinding, + * the service gets destroyed again and releases the session, which stops casting again. + * This method manually starts the service, to be used when connected to chromecast. + */ + private void keepServiceRunningWhileCasting() { + try { + startService(new Intent(this, Media3PlaybackService.class)); + } catch (IllegalStateException e) { + Log.e(TAG, "Unable to keep service running while casting", e); + } } MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback(this) { @@ -292,6 +327,14 @@ public class Media3PlaybackService extends MediaLibraryService { } } + @Override + public void onDeviceInfoChanged(@NonNull DeviceInfo deviceInfo) { + if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE) { + keepServiceRunningWhileCasting(); + loadCurrentMediaWhileCasting(); + } + } + @Override public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { if (mediaItem == null) { @@ -425,7 +468,7 @@ public class Media3PlaybackService extends MediaLibraryService { return; } if (needsStreaming(media) && !NetworkUtils.isStreamingAllowed() - && !allowStreamingThisTime) { + && !allowStreamingThisTime && !isCasting()) { showStreamingConfirmation(media); return; } @@ -576,7 +619,8 @@ public class Media3PlaybackService extends MediaLibraryService { && (player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING) && needsStreaming(currentPlayable) - && !NetworkUtils.isStreamingAllowed(); + && !NetworkUtils.isStreamingAllowed() + && !isCasting(); } private boolean handleStreamingConfirmation() { @@ -639,6 +683,10 @@ public class Media3PlaybackService extends MediaLibraryService { startNextInQueue(media, false, true); } + private boolean isCasting() { + return player.getDeviceInfo().playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE; + } + /** * Loads the next item, and starts it if continuous playback is enabled. */ @@ -671,7 +719,7 @@ public class Media3PlaybackService extends MediaLibraryService { final FeedMedia nextMedia = pair.first; final MediaItem nextMediaItem = pair.second; if (needsStreaming(nextMedia) && !NetworkUtils.isStreamingAllowed() - && !allowStreamingThisTime) { + && !allowStreamingThisTime && !isCasting()) { showStreamingConfirmation(nextMedia); return; } diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackServiceStarter.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackServiceStarter.java index 0726ab111..d27638a53 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackServiceStarter.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackServiceStarter.java @@ -53,7 +53,7 @@ public class PlaybackServiceStarter { == DeviceInfo.PLAYBACK_TYPE_REMOTE) { controller.play(); // Casting somehow does not play when not quickly starting the old episode } - controller.setMediaItem(MediaItemAdapter.fromPlayableStub(media)); + controller.setMediaItem(MediaItemAdapter.fromMediaIdStub(((FeedMedia) media).getId())); controller.prepare(); controller.play(); }); From 407d23321b53c741b8194d701d43f066bf305c2b Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Mon, 13 Jul 2026 07:51:56 +0200 Subject: [PATCH 13/23] Fix playback speed not set when playing next from queue (#8597) ### Description Fix playback speed not set when playing next from queue. We had two different places that set the current playable. Extract this to a single place and do speed setup there. Closes #8520 ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../service/Media3PlaybackService.java | 79 +++++++++---------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index bd09ea3e5..0ad31755a 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -463,35 +463,14 @@ public class Media3PlaybackService extends MediaLibraryService { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(media -> { - currentPlayable = media; - if (player == null) { + if (player == null || confirmStreamingIfNeeded(media)) { return; } - if (needsStreaming(media) && !NetworkUtils.isStreamingAllowed() - && !allowStreamingThisTime && !isCasting()) { - showStreamingConfirmation(media); - return; + media.setPosition((int) player.getCurrentPosition()); + if (media.getItem() != null && !media.getItem().isTagged(FeedItem.TAG_QUEUE)) { + DBWriter.addQueueItem(this, media.getItem()); } - allowStreamingThisTime = false; - currentPlayable.setPosition((int) player.getCurrentPosition()); - currentPlayable.onPlaybackStart(); - if (currentPlayable.getItem() != null - && !currentPlayable.getItem().isTagged(FeedItem.TAG_QUEUE)) { - DBWriter.addQueueItem(this, currentPlayable.getItem()); - } - float speed = PlaybackSpeedUtils.getCurrentPlaybackSpeed(currentPlayable); - player.setPlaybackSpeed(speed); - boolean enabled = PlaybackSpeedUtils.getCurrentSkipSilencePreference( - currentPlayable) == FeedPreferences.SkipSilence.AGGRESSIVE; - PlaybackPreferences.setCurrentlyPlayingTemporarySkipSilence(enabled); - exoPlayer.setSkipSilenceEnabled(enabled); - if (currentPlayable.getItem() != null - && currentPlayable.getItem().getFeed() != null) { - volumeAdaptionFactor = currentPlayable.getItem().getFeed() - .getPreferences().getVolumeAdaptionSetting().getAdaptionFactor(); - applyVolumeAdaption(1.0f); - } - updatePlaybackPreferences(); + switchToPlayable(media); }, error -> Log.e(TAG, "Failed to load current media", error)); @@ -503,6 +482,36 @@ public class Media3PlaybackService extends MediaLibraryService { } } + @OptIn(markerClass = UnstableApi.class) + private boolean confirmStreamingIfNeeded(FeedMedia media) { + if (needsStreaming(media) && !NetworkUtils.isStreamingAllowed() + && !allowStreamingThisTime && !isCasting()) { + showStreamingConfirmation(media); + return true; + } + allowStreamingThisTime = false; + return false; + } + + @OptIn(markerClass = UnstableApi.class) + private void switchToPlayable(FeedMedia media) { + currentPlayable = media; + currentPlayable.onPlaybackStart(); + + float speed = PlaybackSpeedUtils.getCurrentPlaybackSpeed(currentPlayable); + player.setPlaybackSpeed(speed); + boolean enabled = PlaybackSpeedUtils.getCurrentSkipSilencePreference(currentPlayable) + == FeedPreferences.SkipSilence.AGGRESSIVE; + PlaybackPreferences.setCurrentlyPlayingTemporarySkipSilence(enabled); + exoPlayer.setSkipSilenceEnabled(enabled); + if (currentPlayable.getItem() != null && currentPlayable.getItem().getFeed() != null) { + volumeAdaptionFactor = currentPlayable.getItem().getFeed() + .getPreferences().getVolumeAdaptionSetting().getAdaptionFactor(); + applyVolumeAdaption(1.0f); + } + updatePlaybackPreferences(); + } + private void updatePlaybackPreferences() { int statusBefore = PlaybackPreferences.getCurrentPlayerStatus(); long mediaBefore = PlaybackPreferences.getCurrentlyPlayingFeedMediaId(); @@ -718,24 +727,12 @@ public class Media3PlaybackService extends MediaLibraryService { pair -> { final FeedMedia nextMedia = pair.first; final MediaItem nextMediaItem = pair.second; - if (needsStreaming(nextMedia) && !NetworkUtils.isStreamingAllowed() - && !allowStreamingThisTime && !isCasting()) { - showStreamingConfirmation(nextMedia); + if (player == null || confirmStreamingIfNeeded(nextMedia)) { return; } - allowStreamingThisTime = false; - - currentPlayable = nextMedia; - currentPlayable.onPlaybackStart(); - updatePlaybackPreferences(); - if (nextMedia.getItem() != null && nextMedia.getItem().getFeed() != null) { - volumeAdaptionFactor = nextMedia.getItem().getFeed() - .getPreferences().getVolumeAdaptionSetting().getAdaptionFactor(); - applyVolumeAdaption(1.0f); - } + switchToPlayable(nextMedia); player.setPlayWhenReady(UserPreferences.isFollowQueue()); - player.setMediaItem(nextMediaItem, - SkipUtils.skipIntroIfNecessary(this, nextMedia)); + player.setMediaItem(nextMediaItem, SkipUtils.skipIntroIfNecessary(this, nextMedia)); player.prepare(); }, error -> Log.e(TAG, "Failed to load next queue item", error), From 9126d98d4adbfc2ea59da02c3f3c98bf7ec28f29 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 13 Jul 2026 20:30:33 +0200 Subject: [PATCH 14/23] Translations update from Hosted Weblate (#8598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translator: sk by Martin Translated using Weblate Translator: sl by nilo mind Translated using Weblate Translator: ru by hyperflint Translated using Weblate Translator: el by "Λευτέρης Τ." Translated using Weblate Translator: de by Kachelkaiser Translated using Weblate Translator: nb by EdoAug Translated using Weblate Translator: ar by Muhammed Al-Basha Translated using Weblate Translator: bg by ҍӀąղҟ Translated using Weblate Translator: ar by Muhammed Al-Basha Translated using Weblate Translator: es by Iago Translated using Weblate Translator: fr by Matth78 <3684177+Matth7878@users.noreply.github.com> Translated using Weblate Translator: vi by Thu Trang Pham Translated using Weblate Translator: ar by عمار Translated using Weblate Translator: fr by Matth78 <3684177+Matth7878@users.noreply.github.com> Co-authored-by: EdoAug Co-authored-by: Hosted Weblate Co-authored-by: Iago Co-authored-by: Kachelkaiser Co-authored-by: Martin Co-authored-by: Matth78 <3684177+Matth7878@users.noreply.github.com> Co-authored-by: Muhammed Al-Basha Co-authored-by: Thu Trang Pham Co-authored-by: hyperflint Co-authored-by: nilo mind Co-authored-by: Λευτέρης Τ. Co-authored-by: ҍӀąղҟ Co-authored-by: عمار --- ui/i18n/src/main/res/values-ar/strings.xml | 123 ++++++++++++++++++--- ui/i18n/src/main/res/values-bg/strings.xml | 62 ++++++++++- ui/i18n/src/main/res/values-de/strings.xml | 6 +- ui/i18n/src/main/res/values-el/strings.xml | 19 +++- ui/i18n/src/main/res/values-es/strings.xml | 2 +- ui/i18n/src/main/res/values-fr/strings.xml | 10 +- ui/i18n/src/main/res/values-nb/strings.xml | 10 +- ui/i18n/src/main/res/values-ru/strings.xml | 17 +++ ui/i18n/src/main/res/values-sk/strings.xml | 24 ++++ ui/i18n/src/main/res/values-sl/strings.xml | 79 +++++++++++-- ui/i18n/src/main/res/values-vi/strings.xml | 2 + 11 files changed, 314 insertions(+), 40 deletions(-) diff --git a/ui/i18n/src/main/res/values-ar/strings.xml b/ui/i18n/src/main/res/values-ar/strings.xml index a694e6576..063c44692 100644 --- a/ui/i18n/src/main/res/values-ar/strings.xml +++ b/ui/i18n/src/main/res/values-ar/strings.xml @@ -179,7 +179,7 @@ أظهِر إعدادات المسموعيَّة إعدادات المسموعيَّة غيّر الاسم - احذِف + ألغِ الاشتراك مشاركة مشاركة ملف حدِّث كامل المسموعيَّة @@ -367,7 +367,7 @@ الحذف التلقائي احذف الحلقات بعد تشغيلها أو عند احتياج التنزيل التلقائي لمساحة احذف من المجلدات المحلية تلقائيًا - ضمّن المجلدات المحلية في خاصية الحذف التلقائي + أيضًا احذف الملفات المضافة عبر «أضف مجلد محلي». لاحظ أن تلك الملفات لم يتم تنزيلها من AntennaPod ولا يمكن إعادة تنزيلها فيما يخص المجلدات المحلية، يرجى ملاحظة أن الحلقات ستزال من أنْتِنَابُد وستحذف ملفاتها من ذاكرة تخزين جهازك، ولن تتمكن من تنزيلها عن طريق أنْتِنَابُد مرة أخرى. هل تود بأن تفعل الحذف التلقائي؟ علم على الحلقات على أنها إنتهت حتى لو بقي أقل من مقدار ثوانٍ معين من وقت التشغيل علم بذكاء أنها انتهت @@ -599,7 +599,7 @@ امنح الوصول باستخدام المتصفح المفتوح ثم ارجع إلى أنتنابد. تسجيل الدخول الرجاء إضافة بيانات الدخول لحسابك على خادم المزامنة. - كلمة السر والبيانات غير مشفرة! + كلمة المرور والبيانات غير مشفرة! اسم المستخدم كلمة المرور سجّل الدخول @@ -755,7 +755,7 @@ أخطاء الأخبار مطلوب اتخاذ إجراء - يظهر إذا كان الإجراء المطلوب منك ، على سبيل المثال إذا كنت بحاجة إلى إدخال كلمة مرور. + يظهر إذا كان الإجراء المطلوب منك، على سبيل المثال إذا كنت بحاجة إلى إدخال كلمة مرور. جارٍ تنزيل الحلقات تظهر أثناء تنزيل الحلقات حالياً. يشغل حاليا @@ -890,8 +890,8 @@ أرشِف الأرشيف استعِد - احذف / أرشِف - يؤدي الحذف إلى إزالة الاشتراك وجميع حلقاته، وتنزيلاته، وسجل تشغيله، وإحصائياته. + ألغِ الاشتراك / أرشِف + يؤدي إلغاء الاشتراك إلى إزالته وجميع حلقاته، وتنزيلاته، وسجل تشغيله، وإحصائياته. تؤدي الأرشفة إلى إخفاء الاشتراك من القائمة وتجنب تحديثه. ستبقى التنزيلات، والإحصائيات، وسجل التشغيل. انتظر قبل أن تنعش الاشتراكات مرة أخرى. @@ -905,7 +905,7 @@ وسّع صغّر جارٍ أرشفة المسموعية %1$d من %2$d… - جارٍ حذف المسموعية %1$d من %2$d… + جارٍ إلغاء اشتراك المسموعية %1$d من %2$d… لم تحذف أي حلقات منزلة. حذفت حلقة منزلة واحدة. @@ -935,12 +935,12 @@ %d حلقة عُلّمت كغير مُشغّلة. - %d حلقة أُضيفت لقائمة الانتظار. - حلقة واحدة أُضيفت لقائمة الانتظار. - حلقتان أُضيفتان لقائمة الانتظار. - %d حلقات أُضيفت لقائمة الانتظار. - %d حلقة أُضيفت لقائمة الانتظار. - %d حلقة أُضيفت لقائمة الانتظار. + أُضيفت %d حلقة لقائمة الانتظار. + أُضيفت حلقة واحدة لقائمة الانتظار. + أُضيفت حلقتان لقائمة الانتظار. + أُضيفت %d حلقات لقائمة الانتظار. + أُضيفت %d حلقة لقائمة الانتظار. + أُضيفت %d حلقة لقائمة الانتظار. حُذفت %d حلقة من قائمة الانتظار. @@ -995,4 +995,101 @@ ترتيب الفرز الافتراضي اختر الترتيب الافتراضي للحلقات في صفحة المسموعية أبلغ عن علة + إحصائيات + مفضلات + لا يمكن تغيره والتنزيل التلقائي مفعل. + ألغِ الاشتراك / استعِد + يستعيد المسموعية %1$d من %2$d… + + تمت إضافة %d حلقة إلى المفضلات. + تمت إضافة %d حلقة إلى المفضلات. + تمت إضافة حلقتان إلى المفضلات. + تمت إضافة %d حلقات إلى المفضلات. + تمت إضافة %d حلقة إلى المفضلات. + تمت إضافة %d حلقة إلى المفضلات. + + + تم حذف %d حلقة من المفضلات. + تم حذف %d حلقة من المفضلات. + تم حذف حلقتان من المفضلات. + تم حذف %d حلقات من المفضلات. + تم حذف %d حلقة من المفضلات. + تم حذف %d حلقة من المفضلات. + + %1$s • %2$s متبقي + لا توجد مفضلات + علّم الحلقات كمفضلة عبر النقر على أيقونة النجمة. + للأسف نحن مضطرون إلى حذف التنقل الجانبي في تحديث مستقبلي. يتم تطوير AntennaPod من قبل فريق صغير من المتطوعين، وبعد الحفاظ على كلا أسلوبي التنقل لسنوات، اصبح الحفاظ على كلاهما حملًا زائدًا.\n\nإذا كنت تعتمد على التنقل الجانبي بشكل كبير، عطّل التحديثات التلقائية لـAntennaPod في متجر التطبيقات خاصتك. + نعتذر لعدم عمل وظيفة على نحوها المطلوب. نود مساعدتك لتحسين AntennaPod. بالرجاء زيارة منتدياتنا أو صفحتنا على GitHub للتحقق ما إذا تم الإبلاغ عن مشكلتك بالفعل. إذا لم يكن كذلك، اطلعنا وسنتحقق. + الجهاز + إصدار التطبيق + إصدار Android + اسم الجهاز + سجل الأعطال + تم أنشأه: %1$s. + المنتدى + GitHub + متقدم + العرض + استيراد الخطأ + حلقات + تعطيل التشغيل المستمر + تغيير عدد الساعات + بدلًا من التفعيل التلقائي الدائم لمؤقت النوم مع الحلقة الواحدة، عطّل «تشغيل مستمر» في الإعدادات. + + التشغيل المستمر معطّل وتبقى على الحلقة الحالية %1$d دقيقة متبقية + التشغيل المستمر معطّل وتبقى على الحلقة الحالية %1$d دقيقة فقط + التشغيل المستمر معطّل وتبقى على الحلقة الحالية دقيقتان فقط + التشغيل المستمر معطّل وتبقى على الحلقة الحالية %1$d دقائق فقط + التشغيل المستمر معطّل وتبقى على الحلقة الحالية %1$d دقيقة فقط + التشغيل المستمر معطّل وتبقى على الحلقة الحالية %1$d دقيقة فقط + + التشغيل المستمر معطّل في الإعدادات. يتوقف التشغيل دائمًا عند انتهاء الحلقة. + + تبقت %1$d حلقة فقط في طابورك + تبقت %1$d حلقة فقط في طابورك + تبقت حلقتان فقط في طابورك + تبقت %1$d حلقات فقط في طابورك + تبقت %1$d حلقة فقط في طابورك + تبقت %1$d حلقة فقط في طابورك + + عرض كلمة المرور + الكل + غير موسوم + + بدأتها + بدأتها + بدأتها + بدأتها + بدأتها + بدأتها + + + إجمالي + إجمالي + إجمالي + إجمالي + إجمالي + إجمالي + + + تم تنزيلها + تم تنزيلها + تم تنزيلهما + تم تنزيلهم + تم تنزيلها + تم تنزيلها + + في أي وقت الآن + أوقات متنوعة كل يوم + عنوان RSS الذي أدخلته غير صالح. + الغلاف كخلفية + متصل بـ%s + إصدار %s + الهاتف غير مُتوافَق. تأكد من استخدام احدث إصدار لـAntennaPod من Google Play على هاتفك. + لا توجد عناصر + تعذّر الوصول للهاتف. تأكد من أنه هاتفك قريب وأن AntennaPod مثبّت. + تشغيل على الهاتف + تصفح الهاتف + فتح على الهاتف diff --git a/ui/i18n/src/main/res/values-bg/strings.xml b/ui/i18n/src/main/res/values-bg/strings.xml index f1bee2191..cd87fe274 100644 --- a/ui/i18n/src/main/res/values-bg/strings.xml +++ b/ui/i18n/src/main/res/values-bg/strings.xml @@ -126,7 +126,7 @@ %dепизода Известия за епизоди - Покажи известие, когато нов епизод е издаден. + Покажи известие, когато нов епизод е издаден %2$s има нов епизод %2$s има %1$d нови епизода @@ -145,8 +145,8 @@ Покажи информация Покажи настройки за подкаста Настройки за подкаста - Преименувай подкаста - Премахни подкаста + Преименувай + Отписване Сподели Сподели файл Обнови целия подкаст @@ -160,7 +160,7 @@ Редактирай етикетите Добави етикет Преименувай етикета - Презареждането на подкасти докато използвате мобилен интернет е изключено в настройките. Искаш ли да презаредиш все пак? + Презареждането на подкасти докато използвате мобилен интернет е изключено в настройките.\n\nИскаш ли да презаредиш все пак? Твоето VPN приложение се преструва на мобилен интернет (ограничена връзка). Обновяването на подкасти докато използваш мобилен интернет е изключено в настройките. \n\nИскаш ли да обновиш все пак? Ако искаш този проблем да се разреши, свържи се със създателя на VPN приложението ти. Изтегли @@ -196,7 +196,7 @@ Неизветно заглавие Емисия Медия файл - Необходимо удостоверяване + Необходима е автентификация Изтегли по-късно Изтегли все пак Винаги @@ -320,4 +320,56 @@ Пълен размер на %d епизод на устройството Пълен размер на %d епизоди на устройството + Разшири + Съкрати + Статистики + Любими + Няма нищо + Не може да бъде променено докато автоматичното изтегляне е включено. + Архивирай + Архив + Реставриране + Отписване / Архивиране + Отписване / Реставрация + Моля потвърдете, че искате да изтриете тага \"%1$s\". + Изтрий гледани + Моля потвърдете, че искате да изтриете всички от гледаните изтеглени. + + Теглене на %d епизод. + Теглене на %d епизоди. + + + %d изтеглен епизод изтрит. + %d изтеглени епизоди изтрити. + + Премахнато от входящи + Маркиране като изслушано + За да преминете към позиции, трябва да пуснете епизода + + %d епизод маркиран като изслушан. + %d епизоди маркирани като изслушани. + + Маркирай като неизслушан + + %d епизод маркиран като неизслушан. + %d епизода маркирани като неизслушани. + + + %d епизод премахнат от опашката. + %d епизоди премахнати от опашката. + + + %d епизод премахнат от входящи. + %d епизоди премахнати от входящи. + + + %d епизод добавен към любими. + %d епизоди добавени към любими. + + Този линк няма валиден подкаст URL. Моля потвърди линка и пробвай пак, или потърси подкаста ръчно. + Предложения подкаст няма RSS линк, но АнтенаПод намери един, който може да съвпада + Потвърди мобилно изтегляне + Изтегляне на мобилно данни е спряно в настройките. АнтенаПод може да изтегли епизода после автоматично, когато има достъп до Wi-Fi. + - Използвате VPN? Вашето VPN приложение се преструва на мобилна мрежа (връзка с измерване). Ако искате този проблем да бъде решен, свържете се със създателите на вашето VPN приложение.\n- Не използвате VPN? Отворете настройките за Wi-Fi на Android и променете типа на вашата Wi-Fi мрежа на „без измерване“. + Потвърди мобилен стрийминг diff --git a/ui/i18n/src/main/res/values-de/strings.xml b/ui/i18n/src/main/res/values-de/strings.xml index c17fa7815..a9522d4da 100644 --- a/ui/i18n/src/main/res/values-de/strings.xml +++ b/ui/i18n/src/main/res/values-de/strings.xml @@ -44,9 +44,9 @@ Lass dich überraschen Genieße deine Klassiker - Höre weiter + Weiterhören Was gibt\'s Neues - Verwalte Downloads + Downloads verwalten Willkommen bei AntennaPod! Du hast noch keinen Podcast abonniert. Öffne das Menü, um einen Podcast hinzuzufügen. Du hast die Berechtigung abgelehnt. @@ -100,7 +100,7 @@ Wische nach oben, um die Shownotes zu lesen Schließen Erneut versuchen - Automatisch herunterladen + In automatisches Herunterladen einbeziehen Lautstärkeanpassung Lautstärke für Episoden dieses Podcasts lauter oder leiser stellen: %1$s Keine Anpassung diff --git a/ui/i18n/src/main/res/values-el/strings.xml b/ui/i18n/src/main/res/values-el/strings.xml index 259344661..eb11a6e39 100644 --- a/ui/i18n/src/main/res/values-el/strings.xml +++ b/ui/i18n/src/main/res/values-el/strings.xml @@ -50,7 +50,7 @@ Καλωσήρθατε στο Antennapod! Δεν έχετε εγγραφεί ακόμα σε κανένα podcast. Ανοίξτε το μενού για να προσθέσετε ένα podcast. Αρνηθήκατε τη πρόσβαση. - Διαμόρφωση Αρχικής Οθόνης + Διαμόρφωση αρχικής οθόνης Κρυμμένο Άνοιγμα μενού @@ -154,8 +154,8 @@ Εμφάνιση πληροφοριών Εμφάνιση ρυθμίσεων podcast Ρυθμίσεις podcast - Μετονομασία podcast - Αφαίρεση podcast + Μετονομασία + Απεγγραφή Κοινοποίηση Κοινοποίηση αρχείου Ανανέωση όλου του podcast @@ -839,4 +839,17 @@ Εξάπλωση Κλείσιμο Κανένα δεν έχει επιλεχθεί + Στατ + Αγαπημένα + Δεν μπορεί να αλλαχθεί όταν είναι ενεργή η αυτόματη λήψη. + + %d εγγραφή + %d εγγραφές + + Αρχειοθέτηση + Αρχείο + Επαναφορά + Απεγγραφή / αρχειοθέτηση + Απεγγραφή / επαναφορά + Η απεγγραφή αφαιρεί την εγγραφή μαζί με όλα τα επεισόδια, λήψεις, ιστορικό αναπαραγωγής και στατιστικά. diff --git a/ui/i18n/src/main/res/values-es/strings.xml b/ui/i18n/src/main/res/values-es/strings.xml index d83310777..ed6514d42 100644 --- a/ui/i18n/src/main/res/values-es/strings.xml +++ b/ui/i18n/src/main/res/values-es/strings.xml @@ -750,7 +750,7 @@ calendario de lanzamiento Eliminar los reproducidos Descubrir más → - Los episodios nuvaos aparecerán aquí. Luego podrás decidir si te interesan. + Los episodios nuevos aparecerán aquí. Luego podrás decidir si te interesan. Puedes descargar cualquier episodio para escucharlo sin conexión. Visible Navegación inferior diff --git a/ui/i18n/src/main/res/values-fr/strings.xml b/ui/i18n/src/main/res/values-fr/strings.xml index cf28ebc3b..eadecba5c 100644 --- a/ui/i18n/src/main/res/values-fr/strings.xml +++ b/ui/i18n/src/main/res/values-fr/strings.xml @@ -340,9 +340,9 @@ Suppression après lecture Suppression automatique Supprimer les épisodes après la lecture ou si le téléchargement automatique a besoin de plus de place - Suppression automatique pour dossiers locaux - Supprimer aussi les fichiers des dossiers locaux ajoutés ; attention, ces fichiers ne sont pas téléchargés par AntennaPod et ne peuvent pas être retéléchargés - Attention la suppression des épisodes des dossiers locaux dans AntennaPod les suppriment aussi de votre appareil. Ils seront définitivement perdus et ne pourront pas être \"retélécharger\" à partir d\'AntennaPod. Activer quand même l\'option ? + Suppression automatique des dossiers locaux + Supprimer les fichiers des dossiers locaux ajoutés. Ces fichiers ne sont pas téléchargés par AntennaPod et ne pourront pas être retéléchargés + Attention la suppression des épisodes des dossiers locaux dans AntennaPod les suppriment de votre appareil. Ils seront définitivement perdus et ne pourront pas être \"retéléchargés\" avec AntennaPod. Activer quand même l\'option ? En dessous de la durée choisie les épisodes seront marqués comme lus même s\'ils n\'ont pas été terminés Marquer comme lu intelligemment Garder les épisodes quand ils sont passés @@ -981,10 +981,10 @@ %d positions d\'épisodes réinitialisées. %d positions d\'épisodes réinitialisées. - Reste : %1$s • %2$s + %1$s • %2$s restant Aucun favori Marquer des épisodes comme favori à l\'aide de l’icône en forme d\'étoile. - Malheureusement la navigation latérale va être retirée dans une prochaine version. AntennaPod est développé par un petit nombre de volontaires et, après plusieurs années de support pour les 2 types de navigations, la maintenance des 2 est devenu trop lourde.\n\nSi vous avez absolument besoin de la navigation latérale, désactivez les mises à jour d\'AntennaPod. + Malheureusement la navigation latérale va être retirée dans une prochaine version. AntennaPod est développé par un petit nombre de volontaires et, après plusieurs années de support pour les 2 types de navigations, la maintenance des 2 est trop lourde.\n\nSi vous avez absolument besoin de la navigation latérale, désactivez les mises à jour d\'AntennaPod. Avancé Affichage Erreur d\'import diff --git a/ui/i18n/src/main/res/values-nb/strings.xml b/ui/i18n/src/main/res/values-nb/strings.xml index 77d19e95a..5c4120e05 100644 --- a/ui/i18n/src/main/res/values-nb/strings.xml +++ b/ui/i18n/src/main/res/values-nb/strings.xml @@ -147,7 +147,7 @@ Vis podkastinnstillinger Podkastinnstillinger Gi nytt navn - Slett + Avslutt abonnement Del Del fil Oppdater hele podkasten @@ -888,8 +888,8 @@ Arkiver Arkiv Gjenopprett - Slett/arkiver - Sletting fjerner abonnementet i tillegg til alle episoder, nedlastinger, avspillingshistorikk og statistikk. + Avslutt abonnement / arkiver + Abonnementsavslutting fjerner abonnementet i tillegg til alle episoder, nedlastinger, avspillingshistorikk og statistikk. Arkivering skjuler den fra abonnementslisten og slutter å se etter nye episoder. Nedlastinger, statistikk og avspillingsstatus bevares. Ikke merket Utvid @@ -930,4 +930,8 @@ Det er kun %1$d episoder igjen i køen din Åpne + Statistikk + Favoritter + Kan ikke endres når automatisk nedlastning er aktivert. + Avslutt abonnement / gjenopprett diff --git a/ui/i18n/src/main/res/values-ru/strings.xml b/ui/i18n/src/main/res/values-ru/strings.xml index 6ed2caec0..1be98adf2 100644 --- a/ui/i18n/src/main/res/values-ru/strings.xml +++ b/ui/i18n/src/main/res/values-ru/strings.xml @@ -1015,4 +1015,21 @@ Сброшена позиция для %d выпусков. Сброшена позиция для %d выпусков. + %1$s • %2$s влево + Нет избранных + Добавьте выпуски в избранное нажатием на иконку с звездой. + К сожалению, мы удалим навигацию по сторонам в следующем обновлении. AntennaPod разрабатывается небольшой группой волонтёров и поддержка обеих способов навигации на протяжении стала затруднительной.\n\nЕсли вам абсолютно необходима навигация по сторонам, отключите обновления AntennaPod в вашем магазине приложений. + Продвинутые + Дисплей + Ошибка импорта + Показать пароль + Предоставленный адрес RSS не является действительным. + Подключено к %s + Версия %s + Смартфон не совместим. Убедитесь, что вы используете самую актуальную версию AntennaPod из Google Play на вашем смартфоне. + Нет элементов + Невозможно подключиться к телефону. Убедитесь, что телефон находится поблизости и AntennaPod установлен. + Воспроизвести на телефоне + Данные на телефоне + Открыть на телефоне diff --git a/ui/i18n/src/main/res/values-sk/strings.xml b/ui/i18n/src/main/res/values-sk/strings.xml index 235ca24ef..c181d2055 100644 --- a/ui/i18n/src/main/res/values-sk/strings.xml +++ b/ui/i18n/src/main/res/values-sk/strings.xml @@ -937,4 +937,28 @@ Štatistiky Obľúbené Nedá sa zmeniť, keď je povolené automatické sťahovanie. + Odhlásiť / obnoviť + Obnovuje sa podcast %1$d z %2$d… + Odhlasovanie z podcastu %1$d z %2$d… + + %d epizóda pridaná do obľúbených. + %d epizódy pridané do obľúbených. + %d epizód pridaných do obľúbených. + %d epizód pridaných do obľúbených. + + + %d epizóda odstránená z obľúbených. + %d epizódy odstránené z obľúbených. + %d epizód odstránených z obľúbených. + %d epizód odstránených z obľúbených. + + Stav + Technické detaily + URL adresa súboru + Otvoriť + Toto bola posledná epizóda v poradí + Zariadenie + Verzia aplikácie + Verzia systému + Názov zariadenia diff --git a/ui/i18n/src/main/res/values-sl/strings.xml b/ui/i18n/src/main/res/values-sl/strings.xml index 325d15186..ae4e50ac0 100644 --- a/ui/i18n/src/main/res/values-sl/strings.xml +++ b/ui/i18n/src/main/res/values-sl/strings.xml @@ -67,7 +67,7 @@ Ni bilo najdenih združljivih aplikacij Izvozi podrobne dnevnike - POdrobni dnevniki lahko vsebujejo občutljive podatke, naprimer vaše sezname naročnin + Podrobni dnevniki lahko vsebujejo občutljive podatke, naprimer vaše sezname naročnin Odpri v brskalniku Kopiraj URL naslov @@ -152,7 +152,7 @@ Prikaži nastavitve poddaje Nastavitve poddaje Preimenuj poddajo - Izbriši + Prekliči naročnino Deli Deli datoteko Osveži poddajo v celoti @@ -296,8 +296,8 @@ Sinhroniziraj z drugimi napravami Avtomatizacija Podrobnosti - Uvozi/Izvozi - varnostno kopiranje, varnostna obnovitev + Varnostne kopije + Izdelava in obnova varnostnih kopij Zunanji elementi Prekinitve Predvajalne kontrole @@ -306,7 +306,7 @@ Ni rezultatov Pobriši zgodovino Epizode, ki bi morale biti primerne za odstranitev, če samodejni prenos potrebuje prostor za nove epizode - Začasno ustavite predvajanje, ko se slušalke ali bluetooth odklopi + Začasno ustavite predvajanje, ko se povezava bluetooth naprave prekine Nadaljujte s predvajanjem, ko se slušalke znova priključijo Nadaljujte s predvajanjem, ko se bluetooth znova poveže Gumb za naprej @@ -797,8 +797,8 @@ Razširi Strni Arhiviranje poddaje %1$d od %2$d… - Brisanje poddaje %1$d od %2$d… - Brisanje odstrani naročnino vključno z epizodami, prenosi, zgovodino predvajanj in statistiko. + Preklic naročnine poddaje %1$d od %2$d… + Preklic naročnine odstrani naročnino vključno z epizodami, prenosi, zgovodino predvajanj in statistiko. Arhiviranje skrije naročnino iz seznama in preneha slediti novim epizodam. Prenosi, statistika in predvajanje se ne spremeni. Možnost izbire večih elementov je prikazana na dnu Prosim počakajte trenutek preden ponovno osvežite poddajo. @@ -811,4 +811,69 @@ Tehnične podrobnosti URL datoteke Odpri + Statistike + Priljubljene + Ne more biti spreminjano dokler so avtomatski prenosi omogočeni. + Odnaroči / obnovi + Obnavljam podcast %1$d od %2$d… + + %d epizoda označena kot predvajana. + %d epizodi označeni kot predvajani. + %d epizode označene kot predvajane. + %d epizod označenih kot predvajanih. + + + %d epizoda označena kot nepredvajana. + %d epizodi označeni kot nepredvajani. + %d epizode označene kot nepredvajane. + %d epizod označenih kot nepredvajanih. + + + %d epizoda dodana v čakalno vrsto. + %d epizodi dodani v čakalno vrsto. + %d epizode dodane v čakalno vrsto. + %d epizod dodanih v čakalno vrsto. + + + %d epizoda odstranjena iz čakalne vrste. + %d epizodi odstranjeni iz čakalne vrste. + %d epizode odstranjene iz čakalne vrste. + %d epizod odstranjenih iz čakalne vrste. + + + %d epizoda dodana k priljubljenim. + %d epizodi dodani k priljubljenim. + %d epizode dodane k priljubljenim. + %d epizod dodanih k priljubljenim. + + + %d epizoda odstranjena iz prljubljenih. + %d epizodi odstranjeni iz prljubljenih. + %d epizode odstranjene iz prljubljenih. + %d epizod odstranjenih iz prljubljenih. + + Prosim preveri, da zaupaš tej spletni strani \"%1$s\". + Epizoda je že prenešena + + %d epizoda premaknjen na vrh. + %d epizodi premaknjeni na vrh. + %d epizode premaknjene na vrh. + %d epizod premaknjenih na vrh. + + + %d epizoda premaknjen na dno. + %d epizode premaknjeni na dno. + %d epizode premaknjene na dno. + %d epizod premaknjenih na dno. + + Epizoda + Podcast + %1$s • %2$s preostane + Ni epizod v čakalni vrsti, vendar so nove epizode, ki čakajo nate! + Pojdi v nabiralnik + Ni priljubljenih + Označi epizode kot priljubljen, tako da se dotakneš ikone zvezdice. + Nič ni arhivirano + Podkaste lahko arhiviraš iz glavnega zaslona naročnin. + Preobleke diff --git a/ui/i18n/src/main/res/values-vi/strings.xml b/ui/i18n/src/main/res/values-vi/strings.xml index 9d140eaa6..397919841 100644 --- a/ui/i18n/src/main/res/values-vi/strings.xml +++ b/ui/i18n/src/main/res/values-vi/strings.xml @@ -189,4 +189,6 @@ tiếng giây phút + Th.kê + Thích From e886e7979c5a765143c793e2f4f452092380f25b Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Mon, 13 Jul 2026 20:31:55 +0200 Subject: [PATCH 15/23] Bump version to 3.12.0-beta5 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a71978e16..7d6288e8f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { // Version code schema: // "1.2.3-beta4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 3120004 - versionName "3.12.0-beta4" + versionCode 3120005 + versionName "3.12.0-beta5" } signingConfigs { From be80551645c23b41bf0e8c582e68f9cb01d3fd34 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Wed, 15 Jul 2026 07:58:01 +0200 Subject: [PATCH 16/23] Move store metadata out of source tree (#8601) ### Description Move store metadata out of source tree. Running Triple-T publisher always complains about our custom folders (screenshot creator script). Move everything out of the actual Triple-T metadata folder and symlink only the necessary ones instead. ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .gitmodules | 4 ++-- app/src/main/play | 1 - app/src/main/play/contact-email.txt | 1 + app/src/main/play/contact-website.txt | 1 + app/src/main/play/default-language.txt | 1 + app/src/main/play/listings | 1 + app/src/main/play/release-notes | 1 + store-metadata | 1 + 8 files changed, 8 insertions(+), 3 deletions(-) delete mode 160000 app/src/main/play create mode 120000 app/src/main/play/contact-email.txt create mode 120000 app/src/main/play/contact-website.txt create mode 120000 app/src/main/play/default-language.txt create mode 120000 app/src/main/play/listings create mode 120000 app/src/main/play/release-notes create mode 160000 store-metadata diff --git a/.gitmodules b/.gitmodules index a3d4c3cf2..d82ef6310 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,5 +1,5 @@ -[submodule "app/src/main/play"] - path = app/src/main/play +[submodule "store-metadata"] + path = store-metadata url = https://github.com/AntennaPod/StoreMetadata.git branch = main shallow = true diff --git a/app/src/main/play b/app/src/main/play deleted file mode 160000 index 122c14a36..000000000 --- a/app/src/main/play +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 122c14a36d50f2fd804ea2d9ed780efd2dba9b06 diff --git a/app/src/main/play/contact-email.txt b/app/src/main/play/contact-email.txt new file mode 120000 index 000000000..c352c26e5 --- /dev/null +++ b/app/src/main/play/contact-email.txt @@ -0,0 +1 @@ +../../../../store-metadata/contact-email.txt \ No newline at end of file diff --git a/app/src/main/play/contact-website.txt b/app/src/main/play/contact-website.txt new file mode 120000 index 000000000..e151d8a6a --- /dev/null +++ b/app/src/main/play/contact-website.txt @@ -0,0 +1 @@ +../../../../store-metadata/contact-website.txt \ No newline at end of file diff --git a/app/src/main/play/default-language.txt b/app/src/main/play/default-language.txt new file mode 120000 index 000000000..254efecb1 --- /dev/null +++ b/app/src/main/play/default-language.txt @@ -0,0 +1 @@ +../../../../store-metadata/default-language.txt \ No newline at end of file diff --git a/app/src/main/play/listings b/app/src/main/play/listings new file mode 120000 index 000000000..724dc506f --- /dev/null +++ b/app/src/main/play/listings @@ -0,0 +1 @@ +../../../../store-metadata/listings \ No newline at end of file diff --git a/app/src/main/play/release-notes b/app/src/main/play/release-notes new file mode 120000 index 000000000..17bb5e67e --- /dev/null +++ b/app/src/main/play/release-notes @@ -0,0 +1 @@ +../../../../store-metadata/release-notes \ No newline at end of file diff --git a/store-metadata b/store-metadata new file mode 160000 index 000000000..337d64c87 --- /dev/null +++ b/store-metadata @@ -0,0 +1 @@ +Subproject commit 337d64c872cee8b91251c43822410c528558b2a3 From bf5dcb96b0d14f972e2d142655dc1a643c21e46a Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Wed, 15 Jul 2026 08:32:06 +0200 Subject: [PATCH 17/23] Add a simple test for the media3 library session callback (#8604) ### Description Add a simple test for the media3 library session callback. This prepares adding more tests later. ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- mockitoAgent.gradle | 13 +++ model/build.gradle | 1 + net/download/service/build.gradle | 1 + playback/service/build.gradle | 3 + .../MediaLibrarySessionCallbackTest.java | 88 +++++++++++++++++++ 5 files changed, 106 insertions(+) create mode 100644 mockitoAgent.gradle create mode 100644 playback/service/src/test/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallbackTest.java diff --git a/mockitoAgent.gradle b/mockitoAgent.gradle new file mode 100644 index 000000000..3d3c91313 --- /dev/null +++ b/mockitoAgent.gradle @@ -0,0 +1,13 @@ +configurations { + mockitoAgent +} + +dependencies { + mockitoAgent("org.mockito:mockito-core:$mockitoVersion") { + transitive = false + } +} + +tasks.withType(Test).configureEach { + jvmArgs "-javaagent:${configurations.mockitoAgent.singleFile}" +} diff --git a/model/build.gradle b/model/build.gradle index 14b79c6ae..0f64756a2 100644 --- a/model/build.gradle +++ b/model/build.gradle @@ -2,6 +2,7 @@ plugins { id("com.android.library") } apply from: "../common.gradle" +apply from: "../mockitoAgent.gradle" android { namespace "de.danoeh.antennapod.model" diff --git a/net/download/service/build.gradle b/net/download/service/build.gradle index d957e6112..713f45c36 100644 --- a/net/download/service/build.gradle +++ b/net/download/service/build.gradle @@ -4,6 +4,7 @@ plugins { } apply from: "../../../common.gradle" apply from: "../../../playFlavor.gradle" +apply from: "../../../mockitoAgent.gradle" android { namespace "de.danoeh.antennapod.net.download.service" diff --git a/playback/service/build.gradle b/playback/service/build.gradle index 6d71dd525..bbd645213 100644 --- a/playback/service/build.gradle +++ b/playback/service/build.gradle @@ -3,6 +3,7 @@ plugins { } apply from: "../../common.gradle" apply from: "../../playFlavor.gradle" +apply from: "../../mockitoAgent.gradle" android { namespace "de.danoeh.antennapod.playback.service" @@ -48,4 +49,6 @@ dependencies { testImplementation "junit:junit:$junitVersion" testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation "androidx.test:core:$testCoreVersion" + testImplementation "org.robolectric:robolectric:$robolectricVersion" } diff --git a/playback/service/src/test/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallbackTest.java b/playback/service/src/test/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallbackTest.java new file mode 100644 index 000000000..7d907c464 --- /dev/null +++ b/playback/service/src/test/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallbackTest.java @@ -0,0 +1,88 @@ +package de.danoeh.antennapod.playback.service.internal; + +import android.content.Context; +import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.session.MediaSession; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueue; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueStub; +import de.danoeh.antennapod.playback.base.MediaItemAdapter; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; +import de.danoeh.antennapod.storage.database.PodDBAdapter; +import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +@RunWith(RobolectricTestRunner.class) +public class MediaLibrarySessionCallbackTest { + private static final String EPISODE_TITLE = "Episode Title"; + private Context context; + private MediaLibrarySessionCallback callback; + private final MediaSession session = mock(MediaSession.class); + private final MediaSession.ControllerInfo controllerInfo = mock(MediaSession.ControllerInfo.class); + + @Before + public void setUp() { + context = RuntimeEnvironment.getApplication(); + UserPreferences.init(context); + PlaybackPreferences.init(context); + PodDBAdapter.init(context); + PodDBAdapter.deleteDatabase(); + SynchronizationQueue.setInstance(new SynchronizationQueueStub()); + callback = new MediaLibrarySessionCallback(context); + } + + @After + public void tearDown() { + PodDBAdapter.tearDownTests(); + } + + @Test + public void onSetMediaItemsStub() throws Exception { + long mediaId = seedEpisode().getId(); + MediaItem browseItem = MediaItemAdapter.fromMediaIdStub(mediaId); + MediaSession.MediaItemsWithStartPosition result = callback.onSetMediaItems( + session, controllerInfo, Collections.singletonList(browseItem), C.INDEX_UNSET, C.TIME_UNSET) + .get(5, TimeUnit.SECONDS); + assertEquals(1, result.mediaItems.size()); + assertEquals(String.valueOf(mediaId), result.mediaItems.get(0).mediaId); + assertEquals(EPISODE_TITLE, result.mediaItems.get(0).mediaMetadata.title); + } + + @Test + public void onPlaybackResumption() throws Exception { + FeedMedia media = seedEpisode(); + PlaybackPreferences.writeMediaPlaying(media); + MediaSession.MediaItemsWithStartPosition result = callback.onPlaybackResumption(session, controllerInfo) + .get(5, TimeUnit.SECONDS); + assertEquals(1, result.mediaItems.size()); + assertEquals(String.valueOf(media.getId()), result.mediaItems.get(0).mediaId); + } + + private FeedMedia seedEpisode() { + Feed feed = new Feed("url", null, null); + feed.setItems(new ArrayList<>()); + FeedItem item = new FeedItem(); + item.setItemIdentifier("id"); + item.setTitle(EPISODE_TITLE); + item.setMedia(new FeedMedia(item, "http://example.com", 2, "mime")); + item.setFeed(feed); + feed.getItems().add(item); + return FeedDatabaseWriter.updateFeed(context, feed, false).getItems().get(0).getMedia(); + } +} From f267812d29bd7e483bb1b93d47b968267c01edb9 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Wed, 15 Jul 2026 18:32:28 +0200 Subject: [PATCH 18/23] Fix android auto voice commands in media3 playback service (#8605) ### Description Fix android auto voice commands in media3 playback service. I hope this solves the Google Play rejection of new beta versions. ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../internal/MediaLibrarySessionCallback.java | 45 +++++++++++++++++++ .../MediaLibrarySessionCallbackTest.java | 30 +++++++++++++ 2 files changed, 75 insertions(+) diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java index 89f8c44c1..ec8751f43 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallback.java @@ -40,6 +40,7 @@ import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; import org.greenrobot.eventbus.EventBus; +import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -259,6 +260,38 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib return Futures.immediateFuture(new MediaSession.MediaItemsWithStartPosition( mediaItems, index, startPositionMs)); } + String searchQuery = mediaItems.get(index).requestMetadata.searchQuery; + if (searchQuery != null) { + if ("".equals(searchQuery)) { + return onPlaybackResumption(mediaSession, controller); // "Play something" voice action + } + SettableFuture future = SettableFuture.create(); + Maybe.fromCallable(() -> { + List results = DBReader.searchFeedItems(0, searchQuery, FeedItemFilter.unfiltered()); + for (FeedItem result : results) { + if (result.getMedia() != null) { + return result.getMedia(); + } + } + return null; + }) + .subscribeOn(Schedulers.io()) + .subscribe(media -> { + long startPosition = SkipUtils.skipIntroIfNecessary(context, media); + startPosition = RewindAfterPauseUtils.calculatePositionWithRewind( + (int) startPosition, media.getLastPlayedTimeStatistics()); + future.set(new MediaSession.MediaItemsWithStartPosition( + Collections.singletonList(MediaItemAdapter.fromPlayable(context, media, false)), + 0, startPosition)); + }, error -> { + Log.e(TAG, "Voice search failed", error); + future.set(new MediaSession.MediaItemsWithStartPosition( + Collections.emptyList(), index, startPositionMs)); + }, + () -> future.set(new MediaSession.MediaItemsWithStartPosition( + Collections.emptyList(), index, startPositionMs))); + return future; + } SettableFuture future = SettableFuture.create(); Single.fromCallable( () -> { @@ -473,6 +506,18 @@ public class MediaLibrarySessionCallback implements MediaLibraryService.MediaLib public ListenableFuture> onSearch(@NonNull MediaLibraryService.MediaLibrarySession session, @NonNull MediaSession.ControllerInfo browser, @NonNull String query, @Nullable MediaLibraryService.LibraryParams params) { + if (query.isEmpty()) { + session.notifySearchResultChanged(browser, query, 0, params); + return Futures.immediateFuture(LibraryResult.ofVoid()); + } + Single.fromCallable(() -> DBReader.searchFeedItems(0, query, FeedItemFilter.unfiltered())) + .subscribeOn(Schedulers.io()) + .subscribe( + items -> session.notifySearchResultChanged(browser, query, items.size(), params), + error -> { + Log.e(TAG, "Search failed", error); + session.notifySearchResultChanged(browser, query, 0, params); + }); return Futures.immediateFuture(LibraryResult.ofVoid()); } diff --git a/playback/service/src/test/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallbackTest.java b/playback/service/src/test/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallbackTest.java index 7d907c464..e5395b838 100644 --- a/playback/service/src/test/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallbackTest.java +++ b/playback/service/src/test/java/de/danoeh/antennapod/playback/service/internal/MediaLibrarySessionCallbackTest.java @@ -74,6 +74,36 @@ public class MediaLibrarySessionCallbackTest { assertEquals(String.valueOf(media.getId()), result.mediaItems.get(0).mediaId); } + @Test + public void onAndroidAutoVoiceSearchQuery() throws Exception { + long mediaId = seedEpisode().getId(); + MediaItem searchItem = MediaItem.EMPTY.buildUpon() + .setRequestMetadata(new MediaItem.RequestMetadata.Builder().setSearchQuery(EPISODE_TITLE).build()) + .build(); + MediaSession.MediaItemsWithStartPosition result = callback.onSetMediaItems(session, controllerInfo, + Collections.singletonList(searchItem), C.INDEX_UNSET, C.TIME_UNSET).get(5, TimeUnit.SECONDS); + assertEquals(1, result.mediaItems.size()); + assertEquals(String.valueOf(mediaId), result.mediaItems.get(0).mediaId); + + // No match: nothing to play + searchItem = MediaItem.EMPTY.buildUpon() + .setRequestMetadata(new MediaItem.RequestMetadata.Builder().setSearchQuery("Unrelated").build()) + .build(); + result = callback.onSetMediaItems(session, controllerInfo, + Collections.singletonList(searchItem), C.INDEX_UNSET, C.TIME_UNSET).get(5, TimeUnit.SECONDS); + assertEquals(0, result.mediaItems.size()); + + // Empty query ("play something"): fall back to playing something rather than nothing, per + // Android Auto/Assistant voice action guidelines. + searchItem = MediaItem.EMPTY.buildUpon() + .setRequestMetadata(new MediaItem.RequestMetadata.Builder().setSearchQuery("").build()) + .build(); + result = callback.onSetMediaItems(session, controllerInfo, + Collections.singletonList(searchItem), C.INDEX_UNSET, C.TIME_UNSET).get(5, TimeUnit.SECONDS); + assertEquals(1, result.mediaItems.size()); + assertEquals(String.valueOf(mediaId), result.mediaItems.get(0).mediaId); + } + private FeedMedia seedEpisode() { Feed feed = new Feed("url", null, null); feed.setItems(new ArrayList<>()); From 7c53324ef02a5570a748f4de274ce63f8eba7509 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Wed, 15 Jul 2026 18:33:29 +0200 Subject: [PATCH 19/23] Bump version to 3.12.0-beta6 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7d6288e8f..1d1aee97f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { // Version code schema: // "1.2.3-beta4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 3120005 - versionName "3.12.0-beta5" + versionCode 3120006 + versionName "3.12.0-beta6" } signingConfigs { From b25adc2e825e71eac5f6ab8e94e7aa643167de65 Mon Sep 17 00:00:00 2001 From: Geoffrey Sisco Date: Mon, 20 Jul 2026 16:23:45 -0400 Subject: [PATCH 20/23] Treat fast-forward beyond episode end as playback completion (#8592) ### Description When fast-forwarding reaches or crosses the known end of an episode, treat it as logical playback completion rather than seeking past the known end and relying on Media3 to report playback completion. This routes the boundary through the existing `handlePlaybackEnded()` flow, preserving the established end-of-playback behavior for continuous playback, queue progression, playback history, database updates, and episode-based sleep timers. Closes #8591 ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [ ] If it is a core feature, I have added automated tests --- .../playback/service/Media3PlaybackService.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index 0ad31755a..6e8057392 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -164,7 +164,15 @@ public class Media3PlaybackService extends MediaLibraryService { @Override public void seekForward() { - seekTo(Math.min(getDuration(), getCurrentPosition() + UserPreferences.getFastForwardSecs() * 1000L)); + long duration = getDuration(); + long target = getCurrentPosition() + UserPreferences.getFastForwardSecs() * 1000L; + + if (duration > 0 && target >= duration) { + handlePlaybackEnded(); + return; + } + + seekTo(target); } @Override From 87b2eb903854c2f303eee3a8915c42ff9b0e2a18 Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Sun, 2 Aug 2026 09:47:31 +0200 Subject: [PATCH 21/23] Fix smart mark as played from paused (#8639) ### Description Fix smart mark as played from paused state. Reported on the forum here: https://forum.antennapod.org/t/3-12-0-beta-bugs-in-playback-service-rewrite/8588/68?u=bytehamster While we are at it, this also fixes #6567 ### Checklist - [x] I have read the contribution guidelines: https://github.com/AntennaPod/AntennaPod/blob/develop/CONTRIBUTING.md#submit-a-pull-request - [x] I have performed a self-review of my code, going through my changes line by line and carefully considering why this line change is necessary - [x] I have run the automated code checks using `./gradlew checkstyle lint` - [x] My code follows the style guidelines of the AntennaPod project: https://antennapod.org/contribute/develop/app/code-style - [x] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) - [x] If it is a core feature, I have added automated tests --- .../antennapod/playback/service/Media3PlaybackService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java index 6e8057392..71fc5b6a5 100644 --- a/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java +++ b/playback/service/src/main/java/de/danoeh/antennapod/playback/service/Media3PlaybackService.java @@ -464,6 +464,10 @@ public class Media3PlaybackService extends MediaLibraryService { mediaLoaderDisposable.dispose(); } mediaLoaderDisposable = Single.fromCallable(() -> { + long previousMediaId = PlaybackPreferences.getCurrentlyPlayingFeedMediaId(); + if (previousMediaId != PlaybackPreferences.NO_MEDIA_PLAYING && previousMediaId != mediaId) { + updateDatabaseAfterPlayback(DBReader.getFeedMedia(previousMediaId), false, false, true); + } FeedMedia media = DBReader.getFeedMedia(mediaId); ChapterUtils.loadChapters(media, this, false); return media; From f7361a79ebdd6713798d8e37cca36180696af33f Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sun, 2 Aug 2026 14:03:15 +0200 Subject: [PATCH 22/23] Translations update from Hosted Weblate (#8640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translator: sv by Jonatan Nyberg Translated using Weblate Translator: el by "Λευτέρης Τ." Translated using Weblate Translator: sk by Tibor Blažko Translated using Weblate Translator: sk by Martin Translated using Weblate Translator: lt by Marija Grineviciute Translated using Weblate Translator: fr by Matth78 <3684177+Matth7878@users.noreply.github.com> Translated using Weblate Translator: fi by Lilli Co-authored-by: Hosted Weblate Co-authored-by: Jonatan Nyberg Co-authored-by: Lilli Co-authored-by: Marija Grineviciute Co-authored-by: Martin Co-authored-by: Matth78 <3684177+Matth7878@users.noreply.github.com> Co-authored-by: Tibor Blažko Co-authored-by: Λευτέρης Τ. --- ui/i18n/src/main/res/values-el/strings.xml | 16 ++++ ui/i18n/src/main/res/values-fi/strings.xml | 58 +++++++++++--- ui/i18n/src/main/res/values-fr/strings.xml | 2 +- ui/i18n/src/main/res/values-lt/strings.xml | 90 ++++++++++++++++++++-- ui/i18n/src/main/res/values-sk/strings.xml | 29 +++++++ ui/i18n/src/main/res/values-sv/strings.xml | 74 +++++++++--------- 6 files changed, 214 insertions(+), 55 deletions(-) diff --git a/ui/i18n/src/main/res/values-el/strings.xml b/ui/i18n/src/main/res/values-el/strings.xml index eb11a6e39..5282c6d38 100644 --- a/ui/i18n/src/main/res/values-el/strings.xml +++ b/ui/i18n/src/main/res/values-el/strings.xml @@ -852,4 +852,20 @@ Απεγγραφή / αρχειοθέτηση Απεγγραφή / επαναφορά Η απεγγραφή αφαιρεί την εγγραφή μαζί με όλα τα επεισόδια, λήψεις, ιστορικό αναπαραγωγής και στατιστικά. + Η αρχειοθέτηση κρύβει τις συνδρομές από τη λίστα και σταματάει να ψάχνει για επεισόδια. Λήψεις, στατιστικά και κατάσταση αναπαραγωγής διατηρούνται. + Αρχειοθέτηση podcast %1$d από %2$d… + Επαναφορά podcast %1$d από %2$d… + Απεγγραφή podcast %1$d από %2$d… + Παρακαλώ περίμενε λίγη ώρα πριν ανανεώσεις τα podcasts ξανά. + + Γίνεται λήψη %d επεισοδίου. + Γίνεται λήψη %d επεισοδίων. + + + %d κατεβασμένο επεισόδιο διαγράφηκε. + %d κατεβασμένα επεισόδια διαγράφηκαν. + + Αφαιρέθηκε από τα εισερχόμενα + Επισήμανση ως παιγμένο + Για να μεταπιδήσετε σε συγκεκριμένες θέσεις, πρέπει να αναπαράγετε το επεισόδιο diff --git a/ui/i18n/src/main/res/values-fi/strings.xml b/ui/i18n/src/main/res/values-fi/strings.xml index 3ef92e8d4..0cbd80b30 100644 --- a/ui/i18n/src/main/res/values-fi/strings.xml +++ b/ui/i18n/src/main/res/values-fi/strings.xml @@ -48,7 +48,7 @@ Hallinnoi latauksia Tervetuloa AntennaPodiin! Et ole vielä tilannut yhtään podcastia. Avaa valikko lisätäksesi podcastin. - Et antanut lupaa. + Et ole antanut lupaa. Määritä kotinäyttö Piilotettu @@ -56,7 +56,7 @@ Sulje menu Hyllyasetukset Järjestä kertojen mukaan - Järjestä aakkosellisesti + Järjestä aakkojärjestykseen Järjestä julkaisupäivän mukaan Järjestä toistettujen jaksojen mukaan Jaksoja saapuneissa @@ -142,7 +142,7 @@ Näytä podcastin asetukset Podcastin asetukset Nimeä uudelleen - Poista + Peruuta tilaus Jaa Jaa tietodosto Päivitä podcast @@ -684,7 +684,7 @@ Poista jaksot toistamisen jälkeen tai kun automaattinen lataus vaatii tilaa Poista toistamisen jälkeen Poista automaattisesti paikallisista kansioista - Sisällytä paikalliset kansiot Poista automaattisesti -toimintoon + Poista myös \"lisää paikallinen tiedosto\":n kautta lisätyt tiedostot. Huomaa, että nämä eivät ole AntennaPodin lataamia ja niitä ei voi ladata uudelleen Huomaa, että tämä poistaa paikallisten kansioiden jaksot AntennaPodista ja niiden mediatiedostot laitteen tallennustilasta. Niitä ei voi ladata uudelleen AntennaPodin kautta. Otetaanko automaattinen poisto käyttöön? Päivitysväli, Mobiilidata, Automaattinen lataus, Automaattinen poisto Täysmusta @@ -712,7 +712,7 @@ Tutustu lisää » Voit lisätä jaksoja lataamalla ne tai painamalla niitä pitkään ja valitsemalla \"Lisää jonoon\". Uudet jaksot näkyvät täällä. Voit sitten päättää, oletko kiinnostunut niistä. - Voit ladata minkä tahansa jakson kuunnellaksesi sen ei-verkkotilassa. + Voit ladata minkä tahansa jakson kuunnellaksesi sen ilman verkkoyhteyttä. Poista toistetut Vahvista, että haluat poistaa kaikki toistetut lataukset. Alareunanavigointi @@ -777,7 +777,7 @@ tuntikausia jaksoja\n%1$d podcastista tuntikausia jaksoja\n%1$d eri podcastista - Numerot eivät valehtele, ne sanovat. Ja koska vain %1$d%% %2$d aktiivisesta tilauksestasi on toistettu tänä vuonna, olemme luultavasti oikeassa. + Numerot eivät valehtele, ne kertovat. Ja koska vain %1$d%% %2$d aktiivisesta tilauksestasi on toistettu tänä vuonna, olemme luultavasti oikeassa. Tyhjennä Lisää Lataus jonossa @@ -791,7 +791,7 @@ Sinun vuotesi podcasteissa luotu yksityisesti puhelimellasi - Ja olet valmis aloittamaan vuoden puhtaasti. Sinulla on + Ja olet valmis aloittamaan vuoden puhtaalta pöydältä. Sinulla on Ja sinulla on vielä paljon tehtävää tänä vuonna. Sinulla on Se on noin %1$s joka päivä, kunnes %2$d alkaa. Voit aloittaa vuoden puhtaasti, jos ohitat muutaman jakson. Se on noin %1$s joka päivä, kunnes %2$d alkaa. Odota, mitä? @@ -870,8 +870,8 @@ Arkistoi Arkisto Palauta - Poista/arkistoi - Poistaminen poistaa tilauksen mukaanlukien jaksot sekä niiden lataukset, toistohistorian ja tilastot. + Peruuta tilaus / arkistoi + Tilauksen peruuttaminen poistaa tilauksen mukaanlukien jaksot sekä niiden lataukset, toistohistorian ja tilastot. Arkistointi piilottaa podcastin tilausluettelosta ja lopettaa sen päivitykset. Lataukset, tilastot ja toistotila säilytetään. Ladataan mobiiliyhteydellä seuraava %d minuutti @@ -895,7 +895,7 @@ Laajenna Pienennä Arkistoidaan podcastia %1$d %2$dsta… - Poistetaan podcastia %1$d %2$dsta… + Peruutetaan tilausta podcastilta %1$d %2$dsta… Status Tekniset tiedot Tiedoston URL @@ -906,7 +906,7 @@ Oletusjärjestys Valitse jaksojen oletusjärjestys podcastit-näytöllä Ilmoita ongelmasta - Harmi kuulla, että jokin ei toimi. Me otamme mielellämme vastaan apuasi AntennaPodin kehityksessä. Käy katsomassa meidän foorumissa tai GitHubissa ja katso onko kohtaamastasi virheestä jo ilmoitettu. Jos ei, kerro meille ja me tutkimme asian. + Harmi kuulla, että jokin ei toimi. Me otamme mielellämme vastaan apuasi AntennaPodin kehitykseen. Käy katsomassa meidän foorumissa tai GitHubissa ja katso onko kohtaamastasi virheestä jo ilmoitettu. Jos ei, kerro meille ja me tutkimme asian. Laite Sovelluksen versio Android-versio @@ -923,9 +923,43 @@ Jatkuva toisto on pois käytöstä ja nykyistä jaksoa on vain %1$d minuutti jäljellä Jatkuva toisto on pois käytöstä ja nykyistä jaksoa on vain %1$d minuuttia jäljellä - Jatkuva toisto on poistettu käytöstä asetuksissa. Toisto lopetetaan aina jakson loppuessa. + Jatkuva soitto on poistettu käytöstä asetuksissa. Toisto lopetetaan aina jakson loppuessa. Jonossasi on vain %1$d jakso jäljellä Jonossasi on vain %1$d jaksoa jäljellä + Tilastot + Suosikit + Ei voida muuttaa kun automaattiset lataukset ovat käytössä. + Peruuta tilaus / palauta + Palautetaan podcastia %1$d %2$dsta… + + %d jakso lisättiin suosikkeihin. + %d jaksoa lisättiin suosikkeihin. + + + %d jakso poistettu suosikeista. + %d jaksoa poistettu suosikeista. + + + %d jakson paikat palautettiin. + %d jaksojen paikat palautettiin. + + %1$s•%2$s jäljellä + Ei suosikkeja + Merkkaa jaksoja suosikiksi klikkaamalla tähti-ikonia. + Valitettavasti meidän on poistettava sivunavigaatio tulevaisuudessa. AntennaPodia kehittää pieni vapaaehtoistiimi ja molempien navigointikeinojen tukemisen vuosien jälkeen se on alkanut aiheuttamaan liikaa työtä. \n\nJos sivunavigaatio on sinulle erittäin tärkeä, poista AntennaPodin päivitykset käytöstä sovelluskaupassasi. + Edistynyt + Näyttö + Tuontivirhe + Näytä salasana + Syöttämäsi RSS osoite on virheellinen. + Yhdistetty %s:n + Versio %s + Yhteensopimaton puhelin. Varmista, että käytät puhelimellasi AntennaPodin uusinta versiota Google Playsta. + Ei kohteita + Puhelin ei saavutettavissa. Varmista, että puhelimesi on lähellä ja, että AntennaPod on asennettu. + Soita puhelimella + Selaa puhelinta + Avaa puhelimella diff --git a/ui/i18n/src/main/res/values-fr/strings.xml b/ui/i18n/src/main/res/values-fr/strings.xml index eadecba5c..e33206d28 100644 --- a/ui/i18n/src/main/res/values-fr/strings.xml +++ b/ui/i18n/src/main/res/values-fr/strings.xml @@ -342,7 +342,7 @@ Supprimer les épisodes après la lecture ou si le téléchargement automatique a besoin de plus de place Suppression automatique des dossiers locaux Supprimer les fichiers des dossiers locaux ajoutés. Ces fichiers ne sont pas téléchargés par AntennaPod et ne pourront pas être retéléchargés - Attention la suppression des épisodes des dossiers locaux dans AntennaPod les suppriment de votre appareil. Ils seront définitivement perdus et ne pourront pas être \"retéléchargés\" avec AntennaPod. Activer quand même l\'option ? + Attention la suppression des épisodes des dossiers locaux dans AntennaPod les supprime de votre appareil. Ils seront définitivement perdus et ne pourront pas être \"retéléchargés\" avec AntennaPod. Activer quand même l\'option ? En dessous de la durée choisie les épisodes seront marqués comme lus même s\'ils n\'ont pas été terminés Marquer comme lu intelligemment Garder les épisodes quand ils sont passés diff --git a/ui/i18n/src/main/res/values-lt/strings.xml b/ui/i18n/src/main/res/values-lt/strings.xml index 0ce239a60..8b5f237aa 100644 --- a/ui/i18n/src/main/res/values-lt/strings.xml +++ b/ui/i18n/src/main/res/values-lt/strings.xml @@ -88,7 +88,7 @@ Rodyti tinklalaidės nustatymus Tinklalaidės nustatymai Pervadinti - Ištrinti + Nebeprenumeruoti Dalintis Atnaujinti visą tinklalaidę Pažymėti keletą @@ -701,7 +701,7 @@ Atkreipkite dėmesį, kad vietinių aplankų atveju tai pašalins epizodus iš „AntennaPod“ ir ištrins jų medijos failus iš įrenginio saugyklos. Jų nebus galima vėl atsisiųsti per „AntennaPod“. įjungti automatinį ištrynimą? Ištrinkite epizodus po atkūrimo arba kai reikia vietos automatiškai atsisiunčiant Vienu bakstelėjimu pasiekite svarbiausius ekranus bet kur - Į automatinio ištrynimo funkciją įtraukite vietinius aplankus + Taip pat ištrina per funkciją „pridėti vietinį aplanką“ įtrauktus failus. Atkreipiame dėmesį, kad jie nėra atsiųsti AntennaPod ir negali būti atsisiųsti iš naujo Patvirtinkite, kad norite pažymėti visus pasirinktus elementus kaip neatkurtus. Patvirtinkite, kad norite pažymėti visus pasirinktus elementus kaip atkurtus. Pašalinta visa iš gautųjų @@ -889,9 +889,9 @@ Archyvuoti Archyvas Atstatyti - Ištrinti / archyvuoti - Ištrynus bus pašalinti VISI jos epizodai, įskaitant atsisiuntimus, atkūrimo istoriją ir statistiką. - Archyvavus ji neberodoma prenumeratų sąraše ir negaunami atnaujinimai. Išsaugojami atsisiuntimai, statistika ir atkūrimo būsena. + Nebeprenumeruoti / archyvuoti + Nutraukus prenumeratą pašalinami visi tos tinklalaidės epizodai, įskaitant atsisiuntimus, atkūrimo istoriją ir statistiką. + Archyvavus ji neberodoma prenumeratų sąraše ir negaunami nauji epizodai. Išsaugojami atsisiuntimai, statistika ir atkūrimo būsena. Atsiunčiama naudojant mobiliųjų duomenų ryšį dar %d minutę Atsiunčiama naudojant mobiliųjų duomenų ryšį dar %d minutes @@ -919,4 +919,84 @@ atsisiųsta atsisiųsta + Išskleisti + Suskleisti + Statistika + Mėgstami + Negalima keisti, kai įjungtas automatinis atsiuntimas. + Nebeprenumeruoti / atstatyti + Archyvuojama tinklalaidė, %1$d iš %2$d… + Atstatoma tinklalaidė, %1$d iš %2$d… + Nutraukiama tinklalaidžių prenumerata, %1$d iš %2$d… + + %d epizodas pridėtas į mėgstamiausius. + %d epizodai pridėti į mėgstamiausius. + %d epizodų pridėta į mėgstamiausius. + %d epizodų pridėta į mėgstamiausius. + + + %d epizodas pašalintas iš mėgstamiausių. + %d epizodai pašalinti iš mėgstamiausių. + %d epizodų pašalinta iš mėgstamiausių. + %d epizodų pašalinta iš mėgstamiausių. + + + Atkurta %d epizodo pozicija. + Atkurtos %d epizodų pozicijos. + Atkurtos %d epizodų pozicijos. + Atkurtos %d epizodų pozicijos. + + Būsena + Techniniai duomenys + Failo URL + Atverti + Tai buvo paskutinis epizodas eilėje + Epizodas + Tinklalaidė + %1$s • liko %2$s + Nėra mėgstamiausių + Pažymėkite epizodus kaip mėgstamiausius paliesdami žvaigždutės piktogramą. + Deja, artimiausiu metu pašalinsime šoninės navigacijos funkciją. AntennaPod yra mažos grupės savanorių kūrinys, todėl dviejų navigacijos stilių palaikymas tapo per didele našta. \n\nJei norite išvengti šio pakeitimo, išjunkite AntennaPod atnaujinimus savo naudojamoje programėlių parduotuvėje. + Numatytoji rūšiavimo tvarka + Pasirinkite numatytąją tvarką tinklalaidės ekrane esančioms tinklalaidėms + Pranešti apie klaidą + Apgailestaujame, kad kažkas neveikia. Mums praverstų jūsų pagalba, kad patobulintume AntennaPod. Apsilankykite mūsų forume arba GitHub ir sužinokite, ar jau pranešta apie klaidą. Jei ne, praneškite, o mes patikrinsime. + Įrenginys + Programos versija + Android versija + Įrenginio pavadinimas + Trikčių žurnalas + Sukurta: %1$s. + Forumas + GitHub + Išplėstinis + Ekranas + Importavimo klaida + epizodai + Išjungti nepertraukiamą atkūrimą + Keisti valandas + Nebenaudokite automatiškai įsijungiančio miego laikmačio po vieno epizodo – nustatymuose išjunkite funkciją „nepertraukiamas atkūrimas“. + + Nepertraukiamas atkūrimas išjungtas ir dabar grojančiame epizode liko tik %1$d minutė + Nepertraukiamas atkūrimas išjungtas ir dabar grojančiame epizode liko tik %1$d minutės + Nepertraukiamas atkūrimas išjungtas ir dabar grojančiame epizode liko tik %1$d minučių + Nepertraukiamas atkūrimas išjungtas ir dabar grojančiame epizode liko tik %1$d minučių + + Nepertraukiamas atkūrimas yra išjungtas nustatymuose. Visada sustojama pasibaigus epizodui. + + Teliko %1$d epizodas eilėje + Teliko %1$d epizodai eilėje + Teliko %1$d epizodų eilėje + Teliko %1$d epizodų eilėje + + Rodyti slaptažodį + Jūsų įvestas RSS adresas negalioja. + Prisijungta prie %s + Versija %s + Telefonas nesuderinamas. Įsitikinkite, kad telefone turite naujausią AntennaPod versiją iš „Google Play“ parduotuvės. + Nėra elementų + Nepavyko pasiekti telefono. Įsitikinkite, kad telefonas netoliese ir AntennaPod yra įdiegta. + Groti telefone + Naršyti telefone + Atverti telefone diff --git a/ui/i18n/src/main/res/values-sk/strings.xml b/ui/i18n/src/main/res/values-sk/strings.xml index c181d2055..79d69b656 100644 --- a/ui/i18n/src/main/res/values-sk/strings.xml +++ b/ui/i18n/src/main/res/values-sk/strings.xml @@ -961,4 +961,33 @@ Verzia aplikácie Verzia systému Názov zariadenia + Pokročilé + Obrazovka + epizódy + Zakázať nepretržité prehrávanie + Zmeniť hodiny + Namiesto automatického zapínania časovača spánku pri jednej epizóde radšej vypnite v nastaveniach možnosť „nepretržité prehrávanie\". + Nepretržité prehrávanie je zakázané v nastaveniach. Prehrávanie sa vždy zastaví na konci epizódy. + Chyba importu + Zobraziť heslo + RSS adresa, ktorú ste zadali, nie je platná. + Pripojené k %s + Verzia %s + Zariadenie nie je kompatibilné. Uistite sa, že máte v telefóne poslednú verziu aplikácie AntennaPod z obchodu Google Play. + Žiadne položky + Telefón nie je v dosahu. Uistite sa, že váš telefón je v blízkosti a má nainštalovanú aplikáciu AntennaPod. + Prehrať na telefóne + Prehliadať telefón + Otvoriť s telefónom + Epizóda + Podcast + %1$s • %2$s zostáva + Žiadne obľúbené + Označte epizódy ako obľúbené kliknutím na ikonu hviezdy. + Predvolené poradie + Zvoľte si predvolené poradie pre epizód na obrazovke s podcastmi + Nahlásiť chybu + Záznam chýb + Vytvorené: %1$s. + Fórum diff --git a/ui/i18n/src/main/res/values-sv/strings.xml b/ui/i18n/src/main/res/values-sv/strings.xml index e3ef3c864..aaf93b15f 100644 --- a/ui/i18n/src/main/res/values-sv/strings.xml +++ b/ui/i18n/src/main/res/values-sv/strings.xml @@ -48,7 +48,7 @@ Se vad som är nytt Hantera nedladdningar Välkommen till AntennaPod! - Du prenumererar inte på några poddar än. Öppna menyn för att lägga till en podcast. + Du prenumererar inte på några poddar än. Öppna menyn för att lägga till en podd. Du nekade behörigheten. Konfigurera hemskärm Döljs @@ -88,7 +88,7 @@ Global standardinställning URL Stöd - Stöd denna podcast + Stöd den här podden Fel Ett fel inträffade: Uppdatera @@ -107,7 +107,7 @@ Liten minskning Stor minskning Liten höjning - Medium höjning + Medelhög höjning Stor höjning Alltid Aldrig @@ -115,7 +115,7 @@ Lägg till i kön Ingenting Aldrig - När ej favorit + När avsnittet inte är favoritmarkerat Om inte köad Efter uppspelning @@ -165,7 +165,7 @@ Flervalsåtgärder visas i nederkant Filtrerad Senaste uppdateringen misslyckades. Tryck för att visa detaljer. - Öppna podcast + Öppna podd Vänta tills data har lästs in Uppdateringar avstängda @@ -213,14 +213,14 @@ Det finns inte tillräckligt med utrymme kvar på enheten. HTTP-datafel Okänt fel - Podcastens server svarade med ett trasigt flöde. Vi rekommenderar att du kontrollerar med en podcastvalidator som castfeedvalidator.com och kontaktar poddskaparen för att meddela dem. + Poddvärdens server skickade ett trasigt poddflöde. Vi rekommenderar att du kontrollerar flödet med en poddvalidator, till exempel castfeedvalidator.com, och meddelar poddskaparen. Flödestypen stöds inte Podcastens server svarade med en webbplats, inte en podcast. Podcastens server kunde inte hitta filen. Den kan ha tagits bort. Anslutningsfel Ingen nätverksanslutning Kunde inte hitta servern. Kontrollera att adressen är korrekt och att nätverksanslutningen fungerar. - Autentiseringsfel. Se till att användarnamn och lösenord är korrekt. + Autentiseringsfel. Kontrollera att användarnamnet och lösenordet är korrekta. Filtypsfel Podcastens server vägrar svara. Nedladdning avbruten @@ -239,7 +239,7 @@ Okänd titel Flöde Mediafil - Den föreslagna podcasten hade inte en RSS-länk, AntennaPod hittade en podcast som kunde matcha + Den föreslagna podden saknade en RSS-länk. AntennaPod hittade en podd som kan stämma Autentisering krävs Bekräfta mobil nedladdning Nedladdning via mobil dataanslutning är inaktiverat i inställningarna. AntennaPod kan ladda ner avsnittet senare automatiskt när Wi-Fi är tillgängligt. @@ -247,11 +247,11 @@ Ladda ner senare Ladda ner ändå Bekräfta mobil strömning - Strömning över mobilanslutningar är avaktiverat i inställningarna. Tryck för att strömma ändå. + Strömning via mobildata är inaktiverad i inställningarna. Tryck för att strömma ändå. Alltid En gång - Mediafilen kunde inte spelas upp.\n\n- Testa att ta bort och ladda ner avsnittet igen.\n- Kontrollera din nätverksanslutning och se till att ingen VPN eller inloggningssida blockerar åtkomst.\n- Försök att trycka länge och dela \"Mediaadressen\" till din webbläsare för att se om den kan spelas där. Om inte, kontakta poddskaparna. + Mediafilen kunde inte spelas upp.\n\n- Försök att ta bort avsnittet och ladda ned det igen.\n- Kontrollera nätverksanslutningen och se till att ingen VPN-tjänst eller inloggningssida blockerar åtkomsten.\n- Tryck länge, dela ”Medieadress” till webbläsaren och kontrollera om filen kan spelas upp där. Kontakta annars poddskaparen. Ingen media spelas AntennaPod - Okänd mediaknapp: %1$d Filen hittades inte @@ -275,7 +275,7 @@ Slumpa Smart blandning Storlek - Var god bekräfta att du vill radera ALLA avsnitt från kön. + Bekräfta att du vill ta bort ALLA avsnitt från kön. Förhandsinställningar %1$.2fx är redan sparad som en förhandsinställning. @@ -293,7 +293,7 @@ När du lagt till ett avsnitt visas det här. Testa att nollställa filtret för att se fler avsnitt. Inga avsnitt i inkorgen - När nya avsnitt hämtas visas de här. Du kan sedan avgöra ifall de är intressanta eller ej. + När nya avsnitt kommer visas de här. Du kan sedan avgöra om de är intressanta. Inga prenumerationer Tryck på plus-ikonen nedan för att prenumerera på en podcast. @@ -303,7 +303,7 @@ Automatisering Detaljer Säkerhetskopiera & återställ - importera, exportera, backup, återställning + importera, exportera Teman Externa element Avbrott @@ -331,8 +331,8 @@ Automatisk radering Radera avsnitt efter uppspelning eller när automatisk nedladdning behöver utrymme Radera automatiskt från lokala mappar - Ta också bort filer som lagts till genom \"lägg till lokal mapp\". Notera att dessa inte laddas ner av AntennaPod och inte kan laddas ner igen - Notera att detta kommer att ta bort avsnitt från AntennaPod från lokala mappar och radera deras mediafiler från enhetens lagring. De kan inte laddas ner igen via AntennaPod. Aktivera automatisk radering? + Radera även filer som lagts till via ”Lägg till lokal mapp”. De har inte laddats ned av AntennaPod och kan därför inte laddas ned igen + För lokala mappar innebär detta att avsnitten tas bort från AntennaPod och att mediefilerna raderas från enheten. De kan inte laddas ned igen via AntennaPod. Vill du aktivera automatisk radering? Markera avsnitt som spelade även om mindre än ett visst antal sekunder återstår Smart markera som spelad Ta inte bort avsnitt när de hoppas över @@ -340,7 +340,7 @@ Behåll avsnitt när de är favoritmarkerade Behåll favoritavsnitt Uppspelning - Hörlurskontroller, Överhoppningsintervaller, Kö + Hörlurskontroller, hoppintervall, kö Nedladdningar Uppdateringsintervall, Mobildata, Automatisk nedladdning, Automatisk radering Uppdatera poddar @@ -439,7 +439,7 @@ Manuell uppdatering Avisering aktiverad Avisering avstängt - Denna inställning är unik för varje podcast. Du kan ändra den genom att öppna podcastsidan. + Inställningen är unik för varje podd. Du kan ändra den på poddens sida. Bidra Visa titlar Åtgärd vid nya avsnitt @@ -490,11 +490,11 @@ HTML-export Databasexport Automatisk databasexport - Skapa en backup av AntennaPods databas var 3:e dag. Behåll bara de 5 senaste backuperna. - Fel under automatisk databasbackup + Skapa en säkerhetskopia av AntennaPods databas var tredje dag. Behåll endast de fem senaste säkerhetskopiorna. + Fel vid automatisk säkerhetskopiering av databasen Databasimport - Importering av en databas kommer att ersätta alla dina befintliga prenumerationer och spelhistoriken. Du bör exportera din nuvarande databas som en backup. Vill du ersätta den? - Var god vänta… + När en databas importeras ersätts alla befintliga prenumerationer och hela uppspelningshistoriken. Exportera gärna den nuvarande databasen som en säkerhetskopia först. Vill du ersätta den? + Vänta… Exporteringsfel Exporten lyckades Tillgång till extern lagring krävs för att läsa OPML-filen @@ -546,7 +546,7 @@ Fortsätt till inloggning Ge tillåtelse med webbläsaren som öppnades och kom tillbaka till AntennaPod. Logga in - Ange inloggningsuppgifter för ditt konto på synkroniseringservern. + Ange inloggningsuppgifterna för kontot på synkroniseringsservern. Lösenord och data är inte krypterade! Användarnamn Lösenord @@ -610,23 +610,23 @@ Nedladdning klar för %1$s Autentisering - Byt ditt användarnamn och lösenord för den här podcasten och dess avsnitt + Ändra användarnamn och lösenord för den här podden och dess avsnitt Etiketter - Ändra etiketterna för denna podcast för att organisera prenumerationerna + Ändra etiketterna för den här podden för att ordna prenumerationerna Visa ovanför etiketter (endast i sidomeny) Visar endast gemensamma etiketter bland valda prenumerationer. Andra etiketter påverkas inte. Avsnittsfilter - Lista av termer som används för att avgöra om ett avsnitt ingår eller ej vid automatisk nedladdning + Lista över termer som avgör om ett avsnitt ska tas med eller uteslutas vid automatisk nedladdning Lägg till term Exkludera avsnitt som innehåller någon av termerna nedan Inkludera endast avsnitt som innehåller någon av termerna nedan Exkludera avsnitt kortare än Håll uppdaterad - Inkludera detta flöde vid (auto-)uppdatering av alla podcaster + Ta med den här podden när alla poddar uppdateras automatiskt eller manuellt Vilken dag som helst Okänt Redigera flödes-URL - Att byta RSS adressen kan påverka podcastens uppspelningsstatus och avstnittslistning negativt. Vi rekommenderar att du INTE ändrar den och kommer INTE att tillhandahålla support om något går fel. Detta kan inte ångras. Den trasiga prenumerationen GÅR INTE at reparera genom att bara ändra tillbaka adressen. vI rekommenderar att skapa en backup innan du fortsätter. + Om du ändrar RSS-adressen kan poddens uppspelningsstatus och avsnittslista sluta fungera. Vi rekommenderar INTE att du ändrar den och kan INTE ge support om något går fel. Åtgärden kan inte ångras. En trasig prenumeration kan INTE repareras genom att bara ändra tillbaka adressen. Skapa en säkerhetskopia innan du fortsätter. dagligen på veckodagar @@ -641,7 +641,7 @@ Lör Sön - Sök podcast… + Sök efter podd… Sök i Apple Podcasts Sök i Podcast Index Sök i fyyd @@ -885,12 +885,12 @@ När som helst nu flera gånger om dagen Omslagsbild som bakgrund - Att avprenemurera raderar alla dess avsnitt, nedladdningar, uppspelningshistorik och statistik. - Arkivering döljer prenumerationen från prenumerationslistan och uppdatering av nya avsnitt upphör. Nedladdningar, statistik och uppspelningsposition behålls. + När du avprenumererar tas prenumerationen bort tillsammans med alla avsnitt, nedladdningar, uppspelningshistorik och statistik. + När du arkiverar döljs prenumerationen i listan och AntennaPod slutar söka efter nya avsnitt. Nedladdningar, statistik och uppspelningsstatus behålls. Vänligen vänta en stund innan du uppdaterar poddarna igen. - Laddar ned via mobil dataanslutning under den kommande minuten - Laddar ned via mobil dataanslutning under de kommande %d minuterna + Laddar ned via mobildata under den kommande %d minuten + Laddar ned via mobildata under de kommande %d minuterna Du kan arkivera poddar från huvudskärmen för prenumerationer. Utan etikett @@ -919,7 +919,7 @@ Avaktivera oavbruten uppspelning Ändra klockslag Expandera - Podcast + Podd Istället för att alltid automatiskt aktivera insomningstimern med ett avsnitt, vänligen inaktivera \"kontinuerlig uppspelning\" i inställningarna. Kontinuerlig uppspelning är inaktiverad och det aktuella avsnittet har bara %1$d minut kvar @@ -927,8 +927,8 @@ Kontinuerlig uppspelning är inaktiverad i inställningarna. Stoppar alltid på slutet av ett avsnitt. - Det finns bara %1$d avsnitt kvar i din kö - Det finns bara %1$d flera avsnitt kvar i din kö + Det finns bara %1$d avsnitt kvar i kön + Det finns bara %1$d avsnitt kvar i kön Statistik Favoriter @@ -950,7 +950,7 @@ %1$s • %2$s kvar Inga favoriter Markera avsnitt som favoriter genom att trycka på stjärnikonen. - Tyvärr måste vi ta bort sidonavigering i en framtida uppdatering. AntennaPod utvecklas av en liten grupp volontärer och efter att ha underhållit båda navigerinsstilarna i flera år har det blivit för mycket omkostnader.\n\nOm du är beroende av sidonavigering bör du inaktivera uppdateringar fö AntennaPod i din app store. + Tyvärr måste vi ta bort sidonavigeringen i en framtida uppdatering. AntennaPod utvecklas av en liten grupp volontärer, och efter flera års underhåll av båda navigeringssätten har arbetsbördan blivit för stor.\n\nOm du är beroende av sidonavigeringen bör du inaktivera uppdateringar för AntennaPod i din appbutik. Avancerat Importeringsfel Visa lösenord @@ -963,5 +963,5 @@ Spela upp på telefon Bläddra i telefonen Öppna på telefonen - Display + Visning From 980b2f32d9ded0bb65ca9e9d84a44790c2d0eab5 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 2 Aug 2026 14:05:43 +0200 Subject: [PATCH 23/23] Bump version to 3.12.0 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1d1aee97f..43c2a8139 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { // Version code schema: // "1.2.3-beta4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 3120006 - versionName "3.12.0-beta6" + versionCode 3120095 + versionName "3.12.0" } signingConfigs {