mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-25 02:23:11 +00:00
Merge pull request #673 from paregi12/Mal
feat(mal): robust unmarking logic and enhanced skip-intro accuracy wi…
This commit is contained in:
commit
77ce6568d7
9 changed files with 254 additions and 16 deletions
|
|
@ -206,7 +206,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (item.type === 'movie') {
|
if (item.type === 'movie') {
|
||||||
watchedService.unmarkMovieAsWatched(item.id, item.imdb_id ?? undefined);
|
watchedService.unmarkMovieAsWatched(item.id, undefined, undefined, item.name, item.imdb_id ?? undefined);
|
||||||
} else {
|
} else {
|
||||||
// Unmarking a series from the top level is tricky as we don't know the exact episodes.
|
// Unmarking a series from the top level is tricky as we don't know the exact episodes.
|
||||||
// For safety and consistency with old behavior, we just clear the legacy flag.
|
// For safety and consistency with old behavior, we just clear the legacy flag.
|
||||||
|
|
|
||||||
|
|
@ -650,12 +650,30 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
|
|
||||||
// 3. Background Async Operation
|
// 3. Background Async Operation
|
||||||
const showImdbId = imdbId || metadata.id;
|
const showImdbId = imdbId || metadata.id;
|
||||||
|
const malId = (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id;
|
||||||
|
const tmdbId = (metadata as any)?.tmdbId || (metadata as any)?.external_ids?.tmdb_id;
|
||||||
|
|
||||||
|
// Calculate dayIndex for same-day releases
|
||||||
|
let dayIndex = 0;
|
||||||
|
if (episode.air_date) {
|
||||||
|
const sameDayEpisodes = episodes
|
||||||
|
.filter(ep => ep.air_date === episode.air_date)
|
||||||
|
.sort((a, b) => a.episode_number - b.episode_number);
|
||||||
|
dayIndex = sameDayEpisodes.findIndex(ep => ep.episode_number === episode.episode_number);
|
||||||
|
if (dayIndex < 0) dayIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await watchedService.unmarkEpisodeAsWatched(
|
const result = await watchedService.unmarkEpisodeAsWatched(
|
||||||
showImdbId,
|
showImdbId || '',
|
||||||
metadata.id,
|
metadata.id || '',
|
||||||
episode.season_number,
|
episode.season_number,
|
||||||
episode.episode_number
|
episode.episode_number,
|
||||||
|
episode.air_date,
|
||||||
|
metadata?.name,
|
||||||
|
malId,
|
||||||
|
dayIndex,
|
||||||
|
tmdbId
|
||||||
);
|
);
|
||||||
|
|
||||||
loadEpisodesProgress(); // Sync with source of truth
|
loadEpisodesProgress(); // Sync with source of truth
|
||||||
|
|
@ -768,12 +786,23 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
|
|
||||||
// 3. Background Async Operation
|
// 3. Background Async Operation
|
||||||
const showImdbId = imdbId || metadata.id;
|
const showImdbId = imdbId || metadata.id;
|
||||||
|
const malId = (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id;
|
||||||
|
const tmdbId = (metadata as any)?.tmdbId || (metadata as any)?.external_ids?.tmdb_id;
|
||||||
|
|
||||||
|
const lastEp = Math.max(...episodeNumbers);
|
||||||
|
const lastEpisodeData = seasonEpisodes.find(e => e.episode_number === lastEp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await watchedService.unmarkSeasonAsWatched(
|
const result = await watchedService.unmarkSeasonAsWatched(
|
||||||
showImdbId,
|
showImdbId || '',
|
||||||
metadata.id,
|
metadata.id || '',
|
||||||
currentSeason,
|
currentSeason,
|
||||||
episodeNumbers
|
episodeNumbers,
|
||||||
|
lastEpisodeData?.air_date,
|
||||||
|
metadata?.name,
|
||||||
|
malId,
|
||||||
|
0, // dayIndex (assuming 0 for season batch unmarking)
|
||||||
|
tmdbId
|
||||||
);
|
);
|
||||||
|
|
||||||
// Re-sync
|
// Re-sync
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
const nextEpisodeHook = useNextEpisode(type, season, episode, groupedEpisodes, (metadataResult as any)?.groupedEpisodes, episodeId);
|
const nextEpisodeHook = useNextEpisode(type, season, episode, groupedEpisodes, (metadataResult as any)?.groupedEpisodes, episodeId);
|
||||||
|
|
||||||
|
const currentTmdbId = (metadata as any)?.tmdbId || (metadata as any)?.external_ids?.tmdb_id;
|
||||||
|
|
||||||
const { segments: skipIntervals, outroSegment } = useSkipSegments({
|
const { segments: skipIntervals, outroSegment } = useSkipSegments({
|
||||||
imdbId: resolvedImdbId || (id?.startsWith('tt') ? id : undefined),
|
imdbId: resolvedImdbId || (id?.startsWith('tt') ? id : undefined),
|
||||||
type,
|
type,
|
||||||
|
|
@ -278,6 +280,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
episode,
|
episode,
|
||||||
malId: (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id,
|
malId: (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id,
|
||||||
kitsuId: id?.startsWith('kitsu:') ? id.split(':')[1] : undefined,
|
kitsuId: id?.startsWith('kitsu:') ? id.split(':')[1] : undefined,
|
||||||
|
tmdbId: currentTmdbId,
|
||||||
enabled: settings.skipIntroEnabled
|
enabled: settings.skipIntroEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,8 @@ const KSPlayerCore: React.FC = () => {
|
||||||
episodeId
|
episodeId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const currentTmdbId = (metadata as any)?.tmdbId || (metadata as any)?.external_ids?.tmdb_id;
|
||||||
|
|
||||||
const { segments: skipIntervals, outroSegment } = useSkipSegments({
|
const { segments: skipIntervals, outroSegment } = useSkipSegments({
|
||||||
imdbId: imdbId || (id?.startsWith('tt') ? id : undefined),
|
imdbId: imdbId || (id?.startsWith('tt') ? id : undefined),
|
||||||
type,
|
type,
|
||||||
|
|
@ -221,6 +223,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
episode,
|
episode,
|
||||||
malId: (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id,
|
malId: (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id,
|
||||||
kitsuId: id?.startsWith('kitsu:') ? id.split(':')[1] : undefined,
|
kitsuId: id?.startsWith('kitsu:') ? id.split(':')[1] : undefined,
|
||||||
|
tmdbId: currentTmdbId,
|
||||||
enabled: settings.skipIntroEnabled
|
enabled: settings.skipIntroEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -243,7 +246,6 @@ const KSPlayerCore: React.FC = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentMalId = (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id;
|
const currentMalId = (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id;
|
||||||
const currentTmdbId = (metadata as any)?.tmdbId || (metadata as any)?.external_ids?.tmdb_id;
|
|
||||||
|
|
||||||
// Calculate dayIndex for same-day releases
|
// Calculate dayIndex for same-day releases
|
||||||
const currentDayIndex = useMemo(() => {
|
const currentDayIndex = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ interface UseSkipSegmentsProps {
|
||||||
malId?: string;
|
malId?: string;
|
||||||
kitsuId?: string;
|
kitsuId?: string;
|
||||||
releaseDate?: string;
|
releaseDate?: string;
|
||||||
|
tmdbId?: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,6 +22,7 @@ export const useSkipSegments = ({
|
||||||
malId,
|
malId,
|
||||||
kitsuId,
|
kitsuId,
|
||||||
releaseDate,
|
releaseDate,
|
||||||
|
tmdbId,
|
||||||
enabled
|
enabled
|
||||||
}: UseSkipSegmentsProps) => {
|
}: UseSkipSegmentsProps) => {
|
||||||
const [segments, setSegments] = useState<SkipInterval[]>([]);
|
const [segments, setSegments] = useState<SkipInterval[]>([]);
|
||||||
|
|
@ -29,9 +31,9 @@ export const useSkipSegments = ({
|
||||||
const lastKeyRef = useRef('');
|
const lastKeyRef = useRef('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const key = `${imdbId}-${season}-${episode}-${malId}-${kitsuId}-${releaseDate}`;
|
const key = `${imdbId}-${season}-${episode}-${malId}-${kitsuId}-${releaseDate}-${tmdbId}`;
|
||||||
|
|
||||||
if (!enabled || type !== 'series' || (!imdbId && !malId && !kitsuId) || !season || !episode) {
|
if (!enabled || type !== 'series' || (!imdbId && !malId && !kitsuId && !tmdbId) || !season || !episode) {
|
||||||
setSegments([]);
|
setSegments([]);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
fetchedRef.current = false;
|
fetchedRef.current = false;
|
||||||
|
|
@ -55,7 +57,7 @@ export const useSkipSegments = ({
|
||||||
|
|
||||||
const fetchSegments = async () => {
|
const fetchSegments = async () => {
|
||||||
try {
|
try {
|
||||||
const intervals = await introService.getSkipTimes(imdbId, season, episode, malId, kitsuId, releaseDate);
|
const intervals = await introService.getSkipTimes(imdbId, season, episode, malId, kitsuId, releaseDate, tmdbId);
|
||||||
|
|
||||||
// Ignore stale responses from old requests.
|
// Ignore stale responses from old requests.
|
||||||
if (cancelled || lastKeyRef.current !== key) return;
|
if (cancelled || lastKeyRef.current !== key) return;
|
||||||
|
|
@ -78,7 +80,7 @@ export const useSkipSegments = ({
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [imdbId, type, season, episode, malId, kitsuId, releaseDate, enabled]);
|
}, [imdbId, type, season, episode, malId, kitsuId, releaseDate, tmdbId, enabled]);
|
||||||
|
|
||||||
const getActiveSegment = (currentTime: number) => {
|
const getActiveSegment = (currentTime: number) => {
|
||||||
return segments.find(
|
return segments.find(
|
||||||
|
|
|
||||||
|
|
@ -962,6 +962,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
name: localized.title || finalMetadata.name,
|
name: localized.title || finalMetadata.name,
|
||||||
description: localized.overview || finalMetadata.description,
|
description: localized.overview || finalMetadata.description,
|
||||||
movieDetails: movieDetailsObj,
|
movieDetails: movieDetailsObj,
|
||||||
|
tmdbId: finalTmdbId,
|
||||||
...(productionInfo.length > 0 && { networks: productionInfo }),
|
...(productionInfo.length > 0 && { networks: productionInfo }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -999,6 +1000,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
name: localized.name || finalMetadata.name,
|
name: localized.name || finalMetadata.name,
|
||||||
description: localized.overview || finalMetadata.description,
|
description: localized.overview || finalMetadata.description,
|
||||||
tvDetails,
|
tvDetails,
|
||||||
|
tmdbId: finalTmdbId,
|
||||||
...(productionInfo.length > 0 && { networks: productionInfo }),
|
...(productionInfo.length > 0 && { networks: productionInfo }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,8 @@ export async function getSkipTimes(
|
||||||
episode: number,
|
episode: number,
|
||||||
malId?: string,
|
malId?: string,
|
||||||
kitsuId?: string,
|
kitsuId?: string,
|
||||||
releaseDate?: string
|
releaseDate?: string,
|
||||||
|
tmdbId?: number
|
||||||
): Promise<SkipInterval[]> {
|
): Promise<SkipInterval[]> {
|
||||||
// 1. Try IntroDB (TV Shows) first
|
// 1. Try IntroDB (TV Shows) first
|
||||||
if (imdbId) {
|
if (imdbId) {
|
||||||
|
|
@ -320,7 +321,21 @@ export async function getSkipTimes(
|
||||||
let finalMalId = malId;
|
let finalMalId = malId;
|
||||||
let finalEpisode = episode;
|
let finalEpisode = episode;
|
||||||
|
|
||||||
// If we have IMDb ID and Release Date, try ArmSyncService to resolve exact MAL ID and Episode
|
// Priority 1: TMDB-based Resolution (Highest Accuracy)
|
||||||
|
if (!finalMalId && tmdbId && releaseDate) {
|
||||||
|
try {
|
||||||
|
const tmdbResult = await ArmSyncService.resolveByTmdb(tmdbId, releaseDate);
|
||||||
|
if (tmdbResult) {
|
||||||
|
finalMalId = tmdbResult.malId.toString();
|
||||||
|
finalEpisode = tmdbResult.episode;
|
||||||
|
logger.log(`[IntroService] TMDB resolved: MAL ${finalMalId} Ep ${finalEpisode}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[IntroService] TMDB resolve failed', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: IMDb-based ARM Sync (Fallback)
|
||||||
if (!finalMalId && imdbId && releaseDate) {
|
if (!finalMalId && imdbId && releaseDate) {
|
||||||
try {
|
try {
|
||||||
const armResult = await ArmSyncService.resolveByDate(imdbId, releaseDate);
|
const armResult = await ArmSyncService.resolveByDate(imdbId, releaseDate);
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,71 @@ export const MalSync = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
unscrobbleEpisode: async (
|
||||||
|
animeTitle: string,
|
||||||
|
episodeNumber: number,
|
||||||
|
type: 'movie' | 'series' = 'series',
|
||||||
|
season?: number,
|
||||||
|
imdbId?: string,
|
||||||
|
releaseDate?: string,
|
||||||
|
providedMalId?: number,
|
||||||
|
dayIndex?: number,
|
||||||
|
tmdbId?: number
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!MalAuth.isAuthenticated()) return;
|
||||||
|
|
||||||
|
let malId: number | null = providedMalId || null;
|
||||||
|
let finalEpisodeNumber = episodeNumber;
|
||||||
|
|
||||||
|
// Resolve ID using same strategies as scrobbling
|
||||||
|
if (!malId && tmdbId && releaseDate) {
|
||||||
|
const tmdbResult = await ArmSyncService.resolveByTmdb(tmdbId, releaseDate, dayIndex);
|
||||||
|
if (tmdbResult) {
|
||||||
|
malId = tmdbResult.malId;
|
||||||
|
finalEpisodeNumber = tmdbResult.episode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!malId && imdbId && type === 'series' && releaseDate) {
|
||||||
|
const armResult = await ArmSyncService.resolveByDate(imdbId, releaseDate, dayIndex);
|
||||||
|
if (armResult) {
|
||||||
|
malId = armResult.malId;
|
||||||
|
finalEpisodeNumber = armResult.episode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!malId) {
|
||||||
|
malId = await MalSync.getMalId(animeTitle, type, undefined, season, imdbId, episodeNumber, releaseDate, dayIndex, tmdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!malId) return;
|
||||||
|
|
||||||
|
// Get current count
|
||||||
|
const currentInfo = await MalApiService.getMyListStatus(malId);
|
||||||
|
if (!currentInfo.my_list_status) return;
|
||||||
|
|
||||||
|
// Decrement logic: Only if the episode we are unmarking is the LAST one watched or current
|
||||||
|
const currentlyWatched = currentInfo.my_list_status.num_episodes_watched;
|
||||||
|
if (finalEpisodeNumber === currentlyWatched) {
|
||||||
|
const newCount = Math.max(0, finalEpisodeNumber - 1);
|
||||||
|
let newStatus = currentInfo.my_list_status.status;
|
||||||
|
|
||||||
|
// If we unmark everything, maybe move back to 'plan_to_watch' or keep 'watching'
|
||||||
|
if (newCount === 0 && newStatus === 'watching') {
|
||||||
|
// Optional: Move back to plan to watch if desired
|
||||||
|
} else if (newStatus === 'completed') {
|
||||||
|
newStatus = 'watching';
|
||||||
|
}
|
||||||
|
|
||||||
|
await MalApiService.updateStatus(malId, newStatus, newCount);
|
||||||
|
console.log(`[MalSync] Unscrobbled MAL ID ${malId} to Ep ${newCount}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[MalSync] Unscrobble failed:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct scrobble with known MAL ID and Episode
|
* Direct scrobble with known MAL ID and Episode
|
||||||
* Used when ArmSync has already resolved the exact details.
|
* Used when ArmSync has already resolved the exact details.
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { logger } from '../utils/logger';
|
||||||
import { MalSync } from './mal/MalSync';
|
import { MalSync } from './mal/MalSync';
|
||||||
import { MalAuth } from './mal/MalAuth';
|
import { MalAuth } from './mal/MalAuth';
|
||||||
import { ArmSyncService } from './mal/ArmSyncService';
|
import { ArmSyncService } from './mal/ArmSyncService';
|
||||||
|
import { MalApiService } from './mal/MalApi';
|
||||||
|
|
||||||
export interface LocalWatchedItem {
|
export interface LocalWatchedItem {
|
||||||
content_id: string;
|
content_id: string;
|
||||||
|
|
@ -577,10 +578,16 @@ class WatchedService {
|
||||||
/**
|
/**
|
||||||
* Unmark a movie as watched (remove from history).
|
* Unmark a movie as watched (remove from history).
|
||||||
* @param imdbId - The primary content ID (may be a provider ID like "kitsu:123")
|
* @param imdbId - The primary content ID (may be a provider ID like "kitsu:123")
|
||||||
|
* @param malId - Optional MAL ID
|
||||||
|
* @param tmdbId - Optional TMDB ID
|
||||||
|
* @param title - Optional title
|
||||||
* @param fallbackImdbId - The resolved IMDb ID from metadata (used when imdbId isn't IMDb format)
|
* @param fallbackImdbId - The resolved IMDb ID from metadata (used when imdbId isn't IMDb format)
|
||||||
*/
|
*/
|
||||||
public async unmarkMovieAsWatched(
|
public async unmarkMovieAsWatched(
|
||||||
imdbId: string,
|
imdbId: string,
|
||||||
|
malId?: number,
|
||||||
|
tmdbId?: number,
|
||||||
|
title?: string,
|
||||||
fallbackImdbId?: string
|
fallbackImdbId?: string
|
||||||
): Promise<{ success: boolean; syncedToTrakt: boolean }> {
|
): Promise<{ success: boolean; syncedToTrakt: boolean }> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -594,6 +601,21 @@ class WatchedService {
|
||||||
logger.log(`[WatchedService] Trakt remove result for movie: ${syncedToTrakt}`);
|
logger.log(`[WatchedService] Trakt remove result for movie: ${syncedToTrakt}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync to MAL
|
||||||
|
if (MalAuth.isAuthenticated()) {
|
||||||
|
MalSync.unscrobbleEpisode(
|
||||||
|
title || 'Movie',
|
||||||
|
1,
|
||||||
|
'movie',
|
||||||
|
undefined,
|
||||||
|
imdbId,
|
||||||
|
undefined,
|
||||||
|
malId,
|
||||||
|
undefined,
|
||||||
|
tmdbId
|
||||||
|
).catch(err => logger.error('[WatchedService] MAL movie unsync failed:', err));
|
||||||
|
}
|
||||||
|
|
||||||
// Simkl Unmark — try both IDs
|
// Simkl Unmark — try both IDs
|
||||||
const isSimklAuth = await this.simklService.isAuthenticated();
|
const isSimklAuth = await this.simklService.isAuthenticated();
|
||||||
if (isSimklAuth) {
|
if (isSimklAuth) {
|
||||||
|
|
@ -627,7 +649,12 @@ class WatchedService {
|
||||||
showImdbId: string,
|
showImdbId: string,
|
||||||
showId: string,
|
showId: string,
|
||||||
season: number,
|
season: number,
|
||||||
episode: number
|
episode: number,
|
||||||
|
releaseDate?: string,
|
||||||
|
showTitle?: string,
|
||||||
|
malId?: number,
|
||||||
|
dayIndex?: number,
|
||||||
|
tmdbId?: number
|
||||||
): Promise<{ success: boolean; syncedToTrakt: boolean }> {
|
): Promise<{ success: boolean; syncedToTrakt: boolean }> {
|
||||||
try {
|
try {
|
||||||
logger.log(`[WatchedService] Unmarking episode as watched: ${showImdbId} S${season}E${episode}`);
|
logger.log(`[WatchedService] Unmarking episode as watched: ${showImdbId} S${season}E${episode}`);
|
||||||
|
|
@ -647,6 +674,21 @@ class WatchedService {
|
||||||
logger.log(`[WatchedService] Trakt remove result for episode: ${syncedToTrakt}`);
|
logger.log(`[WatchedService] Trakt remove result for episode: ${syncedToTrakt}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync to MAL
|
||||||
|
if (MalAuth.isAuthenticated()) {
|
||||||
|
MalSync.unscrobbleEpisode(
|
||||||
|
showTitle || 'Anime',
|
||||||
|
episode,
|
||||||
|
'series',
|
||||||
|
season,
|
||||||
|
showImdbId,
|
||||||
|
releaseDate,
|
||||||
|
malId,
|
||||||
|
dayIndex,
|
||||||
|
tmdbId
|
||||||
|
).catch(err => logger.error('[WatchedService] MAL unsync failed:', err));
|
||||||
|
}
|
||||||
|
|
||||||
// Simkl Unmark — use best available ID
|
// Simkl Unmark — use best available ID
|
||||||
const isSimklAuth = await this.simklService.isAuthenticated();
|
const isSimklAuth = await this.simklService.isAuthenticated();
|
||||||
if (isSimklAuth) {
|
if (isSimklAuth) {
|
||||||
|
|
@ -688,7 +730,12 @@ class WatchedService {
|
||||||
showImdbId: string,
|
showImdbId: string,
|
||||||
showId: string,
|
showId: string,
|
||||||
season: number,
|
season: number,
|
||||||
episodeNumbers: number[]
|
episodeNumbers: number[],
|
||||||
|
releaseDate?: string,
|
||||||
|
showTitle?: string,
|
||||||
|
malId?: number,
|
||||||
|
dayIndex?: number,
|
||||||
|
tmdbId?: number
|
||||||
): Promise<{ success: boolean; syncedToTrakt: boolean; count: number }> {
|
): Promise<{ success: boolean; syncedToTrakt: boolean; count: number }> {
|
||||||
try {
|
try {
|
||||||
logger.log(`[WatchedService] Unmarking season ${season} as watched for ${showImdbId}`);
|
logger.log(`[WatchedService] Unmarking season ${season} as watched for ${showImdbId}`);
|
||||||
|
|
@ -708,6 +755,79 @@ class WatchedService {
|
||||||
logger.log(`[WatchedService] Trakt season removal result: ${syncedToTrakt}`);
|
logger.log(`[WatchedService] Trakt season removal result: ${syncedToTrakt}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync to MAL (Unscrobble the latest episode in this season ONLY if it's the one we're currently on)
|
||||||
|
if (MalAuth.isAuthenticated() && episodeNumbers.length > 0) {
|
||||||
|
const maxEpisodeInSeason = Math.max(...episodeNumbers);
|
||||||
|
|
||||||
|
const resolveAndUnscrobble = async () => {
|
||||||
|
try {
|
||||||
|
// Use the robust resolution logic from MalSync.unscrobbleEpisode
|
||||||
|
// to find the ACTUAL malId and absolute episode number
|
||||||
|
let finalMalId = malId;
|
||||||
|
let resolvedEpisode = maxEpisodeInSeason;
|
||||||
|
|
||||||
|
// 1. Try TMDB Resolution
|
||||||
|
if (!finalMalId && tmdbId && releaseDate) {
|
||||||
|
const tmdbResult = await ArmSyncService.resolveByTmdb(tmdbId, releaseDate, dayIndex);
|
||||||
|
if (tmdbResult) {
|
||||||
|
finalMalId = tmdbResult.malId;
|
||||||
|
resolvedEpisode = tmdbResult.episode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try IMDb/ARM Fallback
|
||||||
|
if (!finalMalId && showImdbId && releaseDate) {
|
||||||
|
const armResult = await ArmSyncService.resolveByDate(showImdbId, releaseDate, dayIndex);
|
||||||
|
if (armResult) {
|
||||||
|
finalMalId = armResult.malId;
|
||||||
|
resolvedEpisode = armResult.episode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Last resort: Standard lookup
|
||||||
|
if (!finalMalId) {
|
||||||
|
finalMalId = (await MalSync.getMalId(
|
||||||
|
showTitle || 'Anime',
|
||||||
|
'series',
|
||||||
|
undefined,
|
||||||
|
season,
|
||||||
|
showImdbId,
|
||||||
|
maxEpisodeInSeason,
|
||||||
|
releaseDate,
|
||||||
|
dayIndex,
|
||||||
|
tmdbId
|
||||||
|
)) || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalMalId) {
|
||||||
|
const currentInfo = await MalApiService.getMyListStatus(finalMalId);
|
||||||
|
const currentlyWatched = currentInfo.my_list_status?.num_episodes_watched || 0;
|
||||||
|
|
||||||
|
// Only unscrobble if the season's end matches our current progress
|
||||||
|
if (currentlyWatched === resolvedEpisode) {
|
||||||
|
// Calculate the episode count BEFORE this season started
|
||||||
|
const minEpisodeInSeason = Math.min(...episodeNumbers);
|
||||||
|
const newCount = Math.max(0, minEpisodeInSeason - 1);
|
||||||
|
|
||||||
|
let newStatus: any = currentInfo.my_list_status?.status || 'watching';
|
||||||
|
if (newCount === 0 && newStatus === 'watching') {
|
||||||
|
// Optional: could move to plan_to_watch
|
||||||
|
} else if (newStatus === 'completed') {
|
||||||
|
newStatus = 'watching';
|
||||||
|
}
|
||||||
|
|
||||||
|
await MalApiService.updateStatus(finalMalId, newStatus, newCount);
|
||||||
|
logger.log(`[WatchedService] Unmarked season: MAL ID ${finalMalId} reverted to Ep ${newCount}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[WatchedService] MAL season unsync resolution failed:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
resolveAndUnscrobble();
|
||||||
|
}
|
||||||
|
|
||||||
// Sync to Simkl — use best available ID
|
// Sync to Simkl — use best available ID
|
||||||
const isSimklAuth = await this.simklService.isAuthenticated();
|
const isSimklAuth = await this.simklService.isAuthenticated();
|
||||||
if (isSimklAuth) {
|
if (isSimklAuth) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue