mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-18 16:01:44 +00:00
feat(mal): added unmarking logic, enhanced skip-intro resolution, and fixed TypeScript stability issues
This commit is contained in:
parent
fb0805324d
commit
c287c01d72
8 changed files with 156 additions and 14 deletions
|
|
@ -642,12 +642,30 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
|
||||
// 3. Background Async Operation
|
||||
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 {
|
||||
const result = await watchedService.unmarkEpisodeAsWatched(
|
||||
showImdbId,
|
||||
metadata.id,
|
||||
showImdbId || 'Anime',
|
||||
metadata.id || '',
|
||||
episode.season_number,
|
||||
episode.episode_number
|
||||
episode.episode_number,
|
||||
episode.air_date,
|
||||
metadata?.name,
|
||||
malId,
|
||||
dayIndex,
|
||||
tmdbId
|
||||
);
|
||||
|
||||
loadEpisodesProgress(); // Sync with source of truth
|
||||
|
|
|
|||
|
|
@ -273,6 +273,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
episode,
|
||||
malId: (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id,
|
||||
kitsuId: id?.startsWith('kitsu:') ? id.split(':')[1] : undefined,
|
||||
tmdbId: currentTmdbId,
|
||||
enabled: settings.skipIntroEnabled
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -214,6 +214,9 @@ const KSPlayerCore: React.FC = () => {
|
|||
episodeId
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
const { segments: skipIntervals, outroSegment } = useSkipSegments({
|
||||
imdbId: imdbId || (id?.startsWith('tt') ? id : undefined),
|
||||
type,
|
||||
|
|
@ -221,6 +224,7 @@ const KSPlayerCore: React.FC = () => {
|
|||
episode,
|
||||
malId: (metadata as any)?.mal_id || (metadata as any)?.external_ids?.mal_id,
|
||||
kitsuId: id?.startsWith('kitsu:') ? id.split(':')[1] : undefined,
|
||||
tmdbId: currentTmdbId,
|
||||
enabled: settings.skipIntroEnabled
|
||||
});
|
||||
|
||||
|
|
@ -242,9 +246,6 @@ const KSPlayerCore: React.FC = () => {
|
|||
}
|
||||
});
|
||||
|
||||
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
|
||||
const currentDayIndex = useMemo(() => {
|
||||
if (!releaseDate || !groupedEpisodes) return 0;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ interface UseSkipSegmentsProps {
|
|||
malId?: string;
|
||||
kitsuId?: string;
|
||||
releaseDate?: string;
|
||||
tmdbId?: number;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ export const useSkipSegments = ({
|
|||
malId,
|
||||
kitsuId,
|
||||
releaseDate,
|
||||
tmdbId,
|
||||
enabled
|
||||
}: UseSkipSegmentsProps) => {
|
||||
const [segments, setSegments] = useState<SkipInterval[]>([]);
|
||||
|
|
@ -29,9 +31,9 @@ export const useSkipSegments = ({
|
|||
const lastKeyRef = useRef('');
|
||||
|
||||
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([]);
|
||||
setIsLoading(false);
|
||||
fetchedRef.current = false;
|
||||
|
|
@ -55,7 +57,7 @@ export const useSkipSegments = ({
|
|||
|
||||
const fetchSegments = async () => {
|
||||
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.
|
||||
if (cancelled || lastKeyRef.current !== key) return;
|
||||
|
|
@ -78,7 +80,7 @@ export const useSkipSegments = ({
|
|||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [imdbId, type, season, episode, malId, kitsuId, releaseDate, enabled]);
|
||||
}, [imdbId, type, season, episode, malId, kitsuId, releaseDate, tmdbId, enabled]);
|
||||
|
||||
const getActiveSegment = (currentTime: number) => {
|
||||
return segments.find(
|
||||
|
|
|
|||
|
|
@ -958,6 +958,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
name: localized.title || finalMetadata.name,
|
||||
description: localized.overview || finalMetadata.description,
|
||||
movieDetails: movieDetailsObj,
|
||||
tmdbId: finalTmdbId,
|
||||
...(productionInfo.length > 0 && { networks: productionInfo }),
|
||||
};
|
||||
}
|
||||
|
|
@ -995,6 +996,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
name: localized.name || finalMetadata.name,
|
||||
description: localized.overview || finalMetadata.description,
|
||||
tvDetails,
|
||||
tmdbId: finalTmdbId,
|
||||
...(productionInfo.length > 0 && { networks: productionInfo }),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -306,7 +306,8 @@ export async function getSkipTimes(
|
|||
episode: number,
|
||||
malId?: string,
|
||||
kitsuId?: string,
|
||||
releaseDate?: string
|
||||
releaseDate?: string,
|
||||
tmdbId?: number
|
||||
): Promise<SkipInterval[]> {
|
||||
// 1. Try IntroDB (TV Shows) first
|
||||
if (imdbId) {
|
||||
|
|
@ -320,7 +321,21 @@ export async function getSkipTimes(
|
|||
let finalMalId = malId;
|
||||
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) {
|
||||
try {
|
||||
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
|
||||
* Used when ArmSync has already resolved the exact details.
|
||||
|
|
|
|||
|
|
@ -543,7 +543,10 @@ class WatchedService {
|
|||
* Unmark a movie as watched (remove from history)
|
||||
*/
|
||||
public async unmarkMovieAsWatched(
|
||||
imdbId: string
|
||||
imdbId: string,
|
||||
malId?: number,
|
||||
tmdbId?: number,
|
||||
title?: string
|
||||
): Promise<{ success: boolean; syncedToTrakt: boolean }> {
|
||||
try {
|
||||
logger.log(`[WatchedService] Unmarking movie as watched: ${imdbId}`);
|
||||
|
|
@ -556,6 +559,21 @@ class WatchedService {
|
|||
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
|
||||
const isSimklAuth = await this.simklService.isAuthenticated();
|
||||
if (isSimklAuth) {
|
||||
|
|
@ -584,7 +602,12 @@ class WatchedService {
|
|||
showImdbId: string,
|
||||
showId: string,
|
||||
season: number,
|
||||
episode: number
|
||||
episode: number,
|
||||
releaseDate?: string,
|
||||
showTitle?: string,
|
||||
malId?: number,
|
||||
dayIndex?: number,
|
||||
tmdbId?: number
|
||||
): Promise<{ success: boolean; syncedToTrakt: boolean }> {
|
||||
try {
|
||||
logger.log(`[WatchedService] Unmarking episode as watched: ${showImdbId} S${season}E${episode}`);
|
||||
|
|
@ -601,6 +624,21 @@ class WatchedService {
|
|||
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
|
||||
const isSimklAuth = await this.simklService.isAuthenticated();
|
||||
if (isSimklAuth) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue