diff --git a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/SuggestionsController.java b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/SuggestionsController.java index fbee02de43..3c7df16fbb 100644 --- a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/SuggestionsController.java +++ b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/SuggestionsController.java @@ -29,6 +29,8 @@ import com.liskovsoft.smartyoutubetv2.common.misc.BrowseProcessorManager; import com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager; import com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData; +import com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData; +import com.liskovsoft.smartyoutubetv2.common.prefs.UnlocalizedTitleData; import com.liskovsoft.smartyoutubetv2.common.utils.Utils; import com.liskovsoft.youtubeapi.service.YouTubeServiceManager; import io.reactivex.Observable; @@ -43,6 +45,8 @@ public class SuggestionsController extends BasePlayerController { private MediaItemService mMediaItemService; private ContentService mContentService; private BrowseProcessorManager mBrowseProcessor; + private MainUIData mMainUIData; + private UnlocalizedTitleData mTitleCache; private Video mNextSectionVideo; private int mFocusCount; private int mNextRetryCount; @@ -64,6 +68,8 @@ public void onInit() { mBrowseProcessor = new BrowseProcessorManager(getContext(), PlaybackPresenter.instance(getContext())::syncItem); mMediaItemService = YouTubeServiceManager.instance().getMediaItemService(); mContentService = YouTubeServiceManager.instance().getContentService(); + mMainUIData = MainUIData.instance(getContext()); + mTitleCache = UnlocalizedTitleData.instance(getContext()); } @Override @@ -225,6 +231,7 @@ private void syncCurrentVideo(MediaItemMetadata mediaItemMetadata, Video video) } video.sync(mediaItemMetadata); + resolveUnlocalizedTitle(video); getPlayer().setVideo(video); getPlayer().setNextTitle(getNext()); @@ -232,6 +239,35 @@ private void syncCurrentVideo(MediaItemMetadata mediaItemMetadata, Video video) appendDislikes(video); } + private void resolveUnlocalizedTitle(Video video) { + if (!mMainUIData.isUnlocalizedTitlesEnabled() || video == null || video.videoId == null) { + return; + } + + // Already resolved (e.g. from browse card processor) + if (video.deArrowTitle != null) { + return; + } + + // Check persistent cache + String cachedTitle = mTitleCache.getTitle(video.videoId); + if (cachedTitle != null) { + video.deArrowTitle = cachedTitle; + return; + } + + // Cache miss — fetch asynchronously, refresh player title on arrival + Disposable action = mMediaItemService.getUnlocalizedTitleObserve(video.videoId) + .subscribe(title -> { + mTitleCache.putTitle(video.videoId, title); + video.deArrowTitle = title; + if (getPlayer() != null && !Helpers.equals(video.title, title)) { + getPlayer().setVideo(video); + } + }, error -> Log.d(TAG, "Unlocalized title: fetch failed for player video")); + mActions.add(action); + } + public void loadSuggestions(Video video) { if (isEmbedPlayer()) { return; diff --git a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/UnlocalizedTitleProcessor.java b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/UnlocalizedTitleProcessor.java index 2d5c332bc6..8c23520a90 100644 --- a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/UnlocalizedTitleProcessor.java +++ b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/UnlocalizedTitleProcessor.java @@ -1,92 +1,109 @@ -package com.liskovsoft.smartyoutubetv2.common.misc; - -import android.content.Context; -import android.util.Pair; - -import com.liskovsoft.mediaserviceinterfaces.MediaItemService; -import com.liskovsoft.mediaserviceinterfaces.ServiceManager; -import com.liskovsoft.sharedutils.helpers.Helpers; -import com.liskovsoft.sharedutils.mylogger.Log; -import com.liskovsoft.sharedutils.rx.RxHelper; -import com.liskovsoft.smartyoutubetv2.common.app.models.data.Video; -import com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup; -import com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData; -import com.liskovsoft.smartyoutubetv2.common.prefs.common.DataChangeBase.OnDataChange; -import com.liskovsoft.youtubeapi.service.YouTubeServiceManager; - -import java.util.ArrayList; -import java.util.List; - -import io.reactivex.Observable; -import io.reactivex.disposables.Disposable; - -public class UnlocalizedTitleProcessor implements OnDataChange, BrowseProcessor { - private static final String TAG = UnlocalizedTitleProcessor.class.getSimpleName(); - private final OnItemReady mOnItemReady; - private final MediaItemService mItemService; - private final MainUIData mMainUIData; - private boolean mIsUnlocalizedTitlesEnabled; - private Disposable mResult; - - public UnlocalizedTitleProcessor(Context context, OnItemReady onItemReady) { - mOnItemReady = onItemReady; - ServiceManager service = YouTubeServiceManager.instance(); - mItemService = service.getMediaItemService(); - mMainUIData = MainUIData.instance(context); - mMainUIData.setOnChange(this); - initData(); - } - - @Override - public void onDataChange() { - initData(); - } - - private void initData() { - mIsUnlocalizedTitlesEnabled = mMainUIData.isUnlocalizedTitlesEnabled(); - } - - @Override - public void process(VideoGroup videoGroup) { - if (!mIsUnlocalizedTitlesEnabled || videoGroup == null || videoGroup.isEmpty()) { - return; - } - - List videoIds = getVideoIds(videoGroup); - mResult = Observable.fromIterable(videoIds) - .flatMap(videoId -> mItemService.getUnlocalizedTitleObserve(videoId) - .map(newTitle -> new Pair<>(videoId, newTitle))) - .subscribe(title -> { - Video video = videoGroup.findVideoById(title.first); - if (video == null) { - return; - } - video.deArrowTitle = title.second; // sometimes player titles localized while cards not - if (!Helpers.equals(video.title, video.deArrowTitle)) { - mOnItemReady.onItemReady(video); - } - }, - error -> { - Log.d(TAG, "Unlocalized title: Cannot process the video"); - }); - } - - @Override - public void dispose() { - RxHelper.disposeActions(mResult); - } - - private List getVideoIds(VideoGroup videoGroup) { - List result = new ArrayList<>(); - - for (Video video : videoGroup.getVideos()) { - if (video.deArrowProcessed) { - continue; - } - video.deArrowProcessed = true; - result.add(video.videoId); - } - - return result; - } -} +package com.liskovsoft.smartyoutubetv2.common.misc; + +import android.content.Context; +import android.util.Pair; + +import com.liskovsoft.mediaserviceinterfaces.MediaItemService; +import com.liskovsoft.mediaserviceinterfaces.ServiceManager; +import com.liskovsoft.sharedutils.helpers.Helpers; +import com.liskovsoft.sharedutils.mylogger.Log; +import com.liskovsoft.sharedutils.rx.RxHelper; +import com.liskovsoft.smartyoutubetv2.common.app.models.data.Video; +import com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup; +import com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData; +import com.liskovsoft.smartyoutubetv2.common.prefs.UnlocalizedTitleData; +import com.liskovsoft.smartyoutubetv2.common.prefs.common.DataChangeBase.OnDataChange; +import com.liskovsoft.youtubeapi.service.YouTubeServiceManager; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.Observable; +import io.reactivex.disposables.Disposable; + +public class UnlocalizedTitleProcessor implements OnDataChange, BrowseProcessor { + private static final String TAG = UnlocalizedTitleProcessor.class.getSimpleName(); + private static final int CONCURRENT_REQUESTS = 5; + private final OnItemReady mOnItemReady; + private final MediaItemService mItemService; + private final MainUIData mMainUIData; + private final UnlocalizedTitleData mTitleCache; + private boolean mIsUnlocalizedTitlesEnabled; + private Disposable mResult; + + public UnlocalizedTitleProcessor(Context context, OnItemReady onItemReady) { + mOnItemReady = onItemReady; + ServiceManager service = YouTubeServiceManager.instance(); + mItemService = service.getMediaItemService(); + mMainUIData = MainUIData.instance(context); + mTitleCache = UnlocalizedTitleData.instance(context); + mMainUIData.setOnChange(this); + initData(); + } + + @Override + public void onDataChange() { + initData(); + } + + private void initData() { + mIsUnlocalizedTitlesEnabled = mMainUIData.isUnlocalizedTitlesEnabled(); + } + + @Override + public void process(VideoGroup videoGroup) { + if (!mIsUnlocalizedTitlesEnabled || videoGroup == null || videoGroup.isEmpty()) { + return; + } + + List videoIds = new ArrayList<>(); + + for (Video video : videoGroup.getVideos()) { + if (video.deArrowProcessed) { + continue; + } + video.deArrowProcessed = true; + + // Check cache first — apply immediately if hit + String cachedTitle = mTitleCache.getTitle(video.videoId); + if (cachedTitle != null) { + video.deArrowTitle = cachedTitle; + if (!Helpers.equals(video.title, video.deArrowTitle)) { + mOnItemReady.onItemReady(video); + } + } else { + videoIds.add(video.videoId); + } + } + + // Only fetch titles we don't have cached + if (videoIds.isEmpty()) { + return; + } + + mResult = Observable.fromIterable(videoIds) + .flatMap(videoId -> mItemService.getUnlocalizedTitleObserve(videoId) + .map(newTitle -> new Pair<>(videoId, newTitle)), CONCURRENT_REQUESTS) + .subscribe(title -> { + // Store in cache + mTitleCache.putTitle(title.first, title.second); + + Video video = videoGroup.findVideoById(title.first); + if (video == null) { + return; + } + video.deArrowTitle = title.second; + if (!Helpers.equals(video.title, video.deArrowTitle)) { + mOnItemReady.onItemReady(video); + } + }, + error -> { + Log.d(TAG, "Unlocalized title: Cannot process the video"); + }); + } + + @Override + public void dispose() { + RxHelper.disposeActions(mResult); + } +} diff --git a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/UnlocalizedTitleData.java b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/UnlocalizedTitleData.java new file mode 100644 index 0000000000..b4f0553fa6 --- /dev/null +++ b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/UnlocalizedTitleData.java @@ -0,0 +1,99 @@ +package com.liskovsoft.smartyoutubetv2.common.prefs; + +import android.annotation.SuppressLint; +import android.content.Context; + +import com.liskovsoft.sharedutils.helpers.Helpers; +import com.liskovsoft.smartyoutubetv2.common.utils.Utils; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class UnlocalizedTitleData { + private static final String UNLOCALIZED_TITLE_DATA = "unlocalized_title_data"; + private static final int MAX_ENTRIES = 5000; + @SuppressLint("StaticFieldLeak") + private static UnlocalizedTitleData sInstance; + private final AppPrefs mPrefs; + private LinkedHashMap mTitleCache; // videoId -> original title + private final Runnable mPersistStateInt = this::persistStateInt; + private boolean mIsDirty; + + private UnlocalizedTitleData(Context context) { + mPrefs = AppPrefs.instance(context); + restoreState(); + } + + public static UnlocalizedTitleData instance(Context context) { + if (sInstance == null) { + sInstance = new UnlocalizedTitleData(context.getApplicationContext()); + } + + return sInstance; + } + + /** + * Get a cached original title for the given video ID. + * @return the original title, or null if not cached + */ + public String getTitle(String videoId) { + if (videoId == null || mTitleCache == null) { + return null; + } + + return mTitleCache.get(videoId); + } + + /** + * Cache an original title for the given video ID. + * Evicts oldest entries (FIFO) when the cache exceeds MAX_ENTRIES. + */ + public void putTitle(String videoId, String title) { + if (videoId == null || title == null) { + return; + } + + mTitleCache.put(videoId, title); + + // FIFO eviction: remove oldest entries when exceeding limit + while (mTitleCache.size() > MAX_ENTRIES) { + String oldestKey = mTitleCache.keySet().iterator().next(); + mTitleCache.remove(oldestKey); + } + + mIsDirty = true; + persistState(); + } + + private synchronized void restoreState() { + String data = mPrefs.getData(UNLOCALIZED_TITLE_DATA); + + mTitleCache = new LinkedHashMap<>(); + + Map parsed = Helpers.parseMap(data, Helpers::parseStr, Helpers::parseStr); + + if (!parsed.isEmpty()) { + mTitleCache.putAll(parsed); + } + } + + private void persistState() { + Utils.postDelayed(mPersistStateInt, 10_000); + } + + private void persistStateInt() { + if (!mIsDirty) { + return; + } + + mPrefs.setData(UNLOCALIZED_TITLE_DATA, Helpers.mergeMap(mTitleCache)); + mIsDirty = false; + } + + /** + * Force immediate persistence (e.g. before app exit). + */ + public void persistNow() { + Utils.post(mPersistStateInt); + } +}