mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-20 09:47:31 +00:00
fix(mal): production-ready improvements for MAL sync and mapping logic
This commit is contained in:
parent
dad5beee67
commit
433d3a21b9
5 changed files with 270 additions and 503 deletions
|
|
@ -110,21 +110,23 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
|||
}
|
||||
|
||||
fun setPlayer(player: ExoPlayer?) {
|
||||
val currentPlayer = playerView.player
|
||||
|
||||
if (currentPlayer != null) {
|
||||
currentPlayer.removeListener(playerListener)
|
||||
}
|
||||
|
||||
playerView.player = player
|
||||
player?.addListener(playerListener)
|
||||
}
|
||||
|
||||
if (player != null) {
|
||||
player.addListener(playerListener)
|
||||
|
||||
// Apply pending resize mode if we have one
|
||||
pendingResizeMode?.let { resizeMode ->
|
||||
playerView.resizeMode = resizeMode
|
||||
}
|
||||
fun setResizeMode(@ResizeMode.Mode mode: Int) {
|
||||
val resizeMode = when (mode) {
|
||||
ResizeMode.RESIZE_MODE_FIT -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
ResizeMode.RESIZE_MODE_FIXED_WIDTH -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||
ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
|
||||
ResizeMode.RESIZE_MODE_FILL -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL
|
||||
ResizeMode.RESIZE_MODE_ZOOM -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
||||
else -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
}
|
||||
if (playerView.width > 0 && playerView.height > 0) {
|
||||
playerView.resizeMode = resizeMode
|
||||
} else {
|
||||
pendingResizeMode = resizeMode
|
||||
}
|
||||
|
||||
// Re-assert subtitle rendering mode for the current style.
|
||||
|
|
@ -134,27 +136,34 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
|||
|
||||
fun getPlayerView(): PlayerView = playerView
|
||||
|
||||
fun setResizeMode(@ResizeMode.Mode resizeMode: Int) {
|
||||
val targetResizeMode = when (resizeMode) {
|
||||
ResizeMode.RESIZE_MODE_FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
||||
ResizeMode.RESIZE_MODE_CENTER_CROP -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
||||
ResizeMode.RESIZE_MODE_FIT -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
ResizeMode.RESIZE_MODE_FIXED_WIDTH -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||
ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
|
||||
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
}
|
||||
fun setShowSubtitleButton(show: Boolean) {
|
||||
playerView.setShowSubtitleButton(show)
|
||||
}
|
||||
|
||||
// Apply the resize mode to PlayerView immediately
|
||||
playerView.resizeMode = targetResizeMode
|
||||
fun setUseController(useController: Boolean) {
|
||||
playerView.useController = useController
|
||||
}
|
||||
|
||||
// Store it for reapplication if needed
|
||||
pendingResizeMode = targetResizeMode
|
||||
fun setControllerHideOnTouch(hideOnTouch: Boolean) {
|
||||
playerView.setControllerHideOnTouch(hideOnTouch)
|
||||
}
|
||||
|
||||
// Force PlayerView to recalculate its layout
|
||||
playerView.requestLayout()
|
||||
fun setControllerAutoShow(autoShow: Boolean) {
|
||||
playerView.setControllerAutoShow(autoShow)
|
||||
}
|
||||
|
||||
// Also request layout on the parent to ensure proper sizing
|
||||
requestLayout()
|
||||
fun setControllerShowTimeoutMs(timeoutMs: Int) {
|
||||
playerView.controllerShowTimeoutMs = timeoutMs
|
||||
}
|
||||
|
||||
fun isControllerVisible(): Boolean = playerView.isControllerFullyVisible
|
||||
|
||||
fun hideController() {
|
||||
playerView.hideController()
|
||||
}
|
||||
|
||||
fun showController() {
|
||||
playerView.showController()
|
||||
}
|
||||
|
||||
fun setSubtitleStyle(style: SubtitleStyle) {
|
||||
|
|
@ -281,89 +290,16 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
|||
playerView.setShutterBackgroundColor(color)
|
||||
}
|
||||
|
||||
fun updateSurfaceView(viewType: Int) {
|
||||
// TODO: Implement proper surface type switching if needed
|
||||
fun setShowLiveBadge(show: Boolean) {
|
||||
liveBadge.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
val isPlaying: Boolean
|
||||
get() = playerView.player?.isPlaying ?: false
|
||||
|
||||
fun invalidateAspectRatio() {
|
||||
// PlayerView handles aspect ratio automatically through its internal AspectRatioFrameLayout
|
||||
playerView.requestLayout()
|
||||
|
||||
// Reapply the current resize mode to ensure it's properly set
|
||||
pendingResizeMode?.let { resizeMode ->
|
||||
playerView.resizeMode = resizeMode
|
||||
playerView.post {
|
||||
playerView.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
fun setUseController(useController: Boolean) {
|
||||
playerView.useController = useController
|
||||
if (useController) {
|
||||
// Ensure proper touch handling when controls are enabled
|
||||
playerView.controllerAutoShow = true
|
||||
playerView.controllerHideOnTouch = true
|
||||
// Show controls immediately when enabled
|
||||
playerView.showController()
|
||||
}
|
||||
}
|
||||
|
||||
fun showController() {
|
||||
playerView.showController()
|
||||
}
|
||||
|
||||
fun hideController() {
|
||||
playerView.hideController()
|
||||
}
|
||||
|
||||
fun setControllerShowTimeoutMs(showTimeoutMs: Int) {
|
||||
playerView.controllerShowTimeoutMs = showTimeoutMs
|
||||
}
|
||||
|
||||
fun setControllerAutoShow(autoShow: Boolean) {
|
||||
playerView.controllerAutoShow = autoShow
|
||||
}
|
||||
|
||||
fun setControllerHideOnTouch(hideOnTouch: Boolean) {
|
||||
playerView.controllerHideOnTouch = hideOnTouch
|
||||
}
|
||||
|
||||
fun setFullscreenButtonClickListener(listener: PlayerView.FullscreenButtonClickListener?) {
|
||||
playerView.setFullscreenButtonClickListener(listener)
|
||||
}
|
||||
|
||||
fun setShowSubtitleButton(show: Boolean) {
|
||||
playerView.setShowSubtitleButton(show)
|
||||
}
|
||||
|
||||
fun isControllerVisible(): Boolean = playerView.isControllerFullyVisible
|
||||
|
||||
fun setControllerVisibilityListener(listener: PlayerView.ControllerVisibilityListener?) {
|
||||
playerView.setControllerVisibilityListener(listener)
|
||||
}
|
||||
|
||||
override fun addOnLayoutChangeListener(listener: View.OnLayoutChangeListener) {
|
||||
playerView.addOnLayoutChangeListener(listener)
|
||||
}
|
||||
|
||||
override fun setFocusable(focusable: Boolean) {
|
||||
playerView.isFocusable = focusable
|
||||
}
|
||||
|
||||
private fun updateLiveUi() {
|
||||
val player = playerView.player ?: return
|
||||
val isLive = player.isCurrentMediaItemLive
|
||||
val seekable = player.isCurrentMediaItemSeekable
|
||||
|
||||
// Show/hide badge
|
||||
liveBadge.visibility = if (isLive) View.VISIBLE else View.GONE
|
||||
|
||||
// Disable/enable scrubbing based on seekable
|
||||
val timeBar = playerView.findViewById<DefaultTimeBar?>(androidx.media3.ui.R.id.exo_progress)
|
||||
timeBar?.isEnabled = !isLive || seekable
|
||||
}
|
||||
|
||||
private val playerListener = object : Player.Listener {
|
||||
override fun onCues(cueGroup: CueGroup) {
|
||||
// Keep overlay subtitles in sync. This does NOT interfere with PlayerView's own subtitle rendering.
|
||||
|
|
@ -375,61 +311,15 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
|||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||
playerView.post {
|
||||
playerView.requestLayout()
|
||||
// Reapply resize mode to ensure it's properly set after timeline changes
|
||||
pendingResizeMode?.let { resizeMode ->
|
||||
playerView.resizeMode = resizeMode
|
||||
}
|
||||
}
|
||||
updateLiveUi()
|
||||
}
|
||||
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION) ||
|
||||
events.contains(Player.EVENT_IS_PLAYING_CHANGED)
|
||||
) {
|
||||
updateLiveUi()
|
||||
}
|
||||
|
||||
// Handle video size changes which affect aspect ratio
|
||||
if (events.contains(Player.EVENT_VIDEO_SIZE_CHANGED)) {
|
||||
pendingResizeMode?.let { resizeMode ->
|
||||
playerView.resizeMode = resizeMode
|
||||
}
|
||||
playerView.requestLayout()
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ExoPlayerView"
|
||||
}
|
||||
|
||||
/**
|
||||
* React Native (Yoga) can sometimes defer layout passes that are required by
|
||||
* PlayerView for its child views (controller overlay, surface view, subtitle view, …).
|
||||
* This helper forces a second measure / layout after RN finishes, ensuring the
|
||||
* internal views receive the final size. The same approach is used in the v7
|
||||
* implementation (see VideoView.kt) and in React Native core (Toolbar example [link]).
|
||||
*/
|
||||
private val layoutRunnable = Runnable {
|
||||
measure(
|
||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
||||
)
|
||||
layout(left, top, right, bottom)
|
||||
}
|
||||
|
||||
override fun requestLayout() {
|
||||
super.requestLayout()
|
||||
// Post a second layout pass so the ExoPlayer internal views get correct bounds.
|
||||
post(layoutRunnable)
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
|
||||
if (changed) {
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
val width = MeasureSpec.getSize(widthMeasureSpec)
|
||||
val height = MeasureSpec.getSize(heightMeasureSpec)
|
||||
if (width > 0 && height > 0) {
|
||||
pendingResizeMode?.let { resizeMode ->
|
||||
playerView.resizeMode = resizeMode
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -189,7 +189,7 @@ export const useWatchProgress = (
|
|||
currentTmdbId
|
||||
);
|
||||
} else if (type === 'movie' && currentImdbId) {
|
||||
watchedService.markMovieAsWatched(currentImdbId, new Date(), currentMalId, currentTmdbId);
|
||||
watchedService.markMovieAsWatched(currentImdbId, new Date(), currentMalId, currentTmdbId, title);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -375,28 +375,31 @@ export const MalSync = {
|
|||
const currentLibrary = await catalogService.getLibraryItems();
|
||||
const libraryIds = new Set(currentLibrary.map(l => l.id));
|
||||
|
||||
// Process items in parallel
|
||||
await Promise.all(response.data.map(async (item) => {
|
||||
const malId = item.node.id;
|
||||
const { imdbId } = await MalSync.getIdsFromMalId(malId);
|
||||
|
||||
if (imdbId && !libraryIds.has(imdbId)) {
|
||||
const type = item.node.media_type === 'movie' ? 'movie' : 'series';
|
||||
logger.log(`[MalSync] Auto-adding to library: ${item.node.title} (${imdbId})`);
|
||||
// Process items in small batches to avoid rate limiting
|
||||
for (let i = 0; i < response.data.length; i += 5) {
|
||||
const batch = response.data.slice(i, i + 5);
|
||||
await Promise.all(batch.map(async (item) => {
|
||||
const malId = item.node.id;
|
||||
const { imdbId } = await MalSync.getIdsFromMalId(malId);
|
||||
|
||||
await catalogService.addToLibrary({
|
||||
id: imdbId,
|
||||
type: type,
|
||||
name: item.node.title,
|
||||
poster: item.node.main_picture?.large || item.node.main_picture?.medium || '',
|
||||
posterShape: 'poster',
|
||||
year: item.node.start_season?.year,
|
||||
description: '',
|
||||
genres: [],
|
||||
inLibrary: true,
|
||||
});
|
||||
}
|
||||
}));
|
||||
if (imdbId && !libraryIds.has(imdbId)) {
|
||||
const type = item.node.media_type === 'movie' ? 'movie' : 'series';
|
||||
logger.log(`[MalSync] Auto-adding to library: ${item.node.title} (${imdbId})`);
|
||||
|
||||
await catalogService.addToLibrary({
|
||||
id: imdbId,
|
||||
type: type,
|
||||
name: item.node.title,
|
||||
poster: item.node.main_picture?.large || item.node.main_picture?.medium || '',
|
||||
posterShape: 'poster',
|
||||
year: item.node.start_season?.year,
|
||||
description: '',
|
||||
genres: [],
|
||||
inLibrary: true,
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('[MalSync] syncMalWatchingToLibrary failed:', e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@ class WatchedService {
|
|||
imdbId: string,
|
||||
watchedAt: Date = new Date(),
|
||||
malId?: number,
|
||||
tmdbId?: number
|
||||
tmdbId?: number,
|
||||
title?: string
|
||||
): Promise<{ success: boolean; syncedToTrakt: boolean }> {
|
||||
try {
|
||||
logger.log(`[WatchedService] Marking movie as watched: ${imdbId}`);
|
||||
|
|
@ -221,7 +222,7 @@ class WatchedService {
|
|||
const malToken = MalAuth.getToken();
|
||||
if (malToken) {
|
||||
MalSync.scrobbleEpisode(
|
||||
'', // Don't use IMDb ID as title fallback (unlikely to match)
|
||||
title || '', // Use real title if provided for search fallback
|
||||
1,
|
||||
1,
|
||||
'movie',
|
||||
|
|
|
|||
Loading…
Reference in a new issue