Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -225,13 +231,43 @@ private void syncCurrentVideo(MediaItemMetadata mediaItemMetadata, Video video)
}

video.sync(mediaItemMetadata);
resolveUnlocalizedTitle(video);
getPlayer().setVideo(video);

getPlayer().setNextTitle(getNext());

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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> getVideoIds(VideoGroup videoGroup) {
List<String> 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<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> 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);
}
}