refactor: remove legacy offline mappings and switch to ArmSync/Jikan

This commit is contained in:
paregi12 2026-01-17 17:35:03 +05:30
parent 548a42ffc2
commit bd1cdeb29e
10 changed files with 48 additions and 133345 deletions

View file

@ -1,47 +0,0 @@
import json
with open('/data/data/com.termux/files/home/.gemini/tmp/plex_ani_bridge_mappings/mappings.json', 'r') as f:
data = json.load(f)
# Find Attack on Titan Season 3 Part 2 (MAL ID 38524)
target_mal_id = 38524
found_entry = None
found_key = None
for key, value in data.items():
mal_ids = value.get('mal_id')
if mal_ids:
if isinstance(mal_ids, list):
if target_mal_id in mal_ids:
found_entry = value
found_key = key
break
elif mal_ids == target_mal_id:
found_entry = value
found_key = key
break
print(f"Entry for MAL ID {target_mal_id}:")
print(json.dumps({found_key: found_entry}, indent=2))
# Check for reverse lookup capability (IMDb -> MAL)
print("\nChecking duplicates for IMDb IDs...")
imdb_map = {}
duplicates = 0
for key, value in data.items():
imdb_ids = value.get('imdb_id')
if not imdb_ids:
continue
if not isinstance(imdb_ids, list):
imdb_ids = [imdb_ids]
for imdb in imdb_ids:
if imdb in imdb_map:
duplicates += 1
# print(f"Duplicate IMDb: {imdb} -> {imdb_map[imdb]} and {key}")
else:
imdb_map[imdb] = key
print(f"Total entries: {len(data)}")
print(f"Unique IMDb IDs mapped: {len(imdb_map)}")
print(f"Duplicate IMDb references: {duplicates}")

View file

@ -1,9 +0,0 @@
import sqlite3
conn = sqlite3.connect('/data/data/com.termux/files/home/.gemini/tmp/otaku_mappings/anime_mappings.db')
cursor = conn.cursor()
print("Shingeki no Kyojin entries:")
cursor.execute("SELECT mal_id, mal_title, thetvdb_season, thetvdb_part, anime_media_episodes FROM anime WHERE mal_title LIKE '%Shingeki no Kyojin%'")
rows = cursor.fetchall()
for row in rows:
print(row)
conn.close()

View file

@ -1,19 +0,0 @@
import sqlite3
conn = sqlite3.connect('/data/data/com.termux/files/home/.gemini/tmp/otaku_mappings/anime_mappings.db')
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = cursor.fetchall()
print(f"Tables: {tables}")
for table in tables:
table_name = table[0]
print(f"\nSchema for {table_name}:")
cursor.execute(f"PRAGMA table_info({table_name})")
columns = cursor.fetchall()
for col in columns:
print(col)
print(f"\nFirst 5 rows of {table_name}:")
cursor.execute(f"SELECT * FROM {table_name} LIMIT 5")
rows = cursor.fetchall()
for row in rows:
print(row)
conn.close()

View file

@ -1,14 +0,0 @@
import sqlite3
conn = sqlite3.connect('/data/data/com.termux/files/home/.gemini/tmp/otaku_mappings/anime_mappings.db')
cursor = conn.cursor()
cursor.execute("SELECT imdb_id, COUNT(*) c FROM anime WHERE imdb_id IS NOT NULL GROUP BY imdb_id HAVING c > 1 LIMIT 5")
rows = cursor.fetchall()
print("Duplicate IMDB IDs:")
for row in rows:
print(row)
# Check details for one
cursor.execute(f"SELECT mal_id, mal_title, thetvdb_season, anime_media_episodes FROM anime WHERE imdb_id = '{row[0]}'")
details = cursor.fetchall()
for d in details:
print(f" - {d}")
conn.close()

View file

@ -1,15 +0,0 @@
import sqlite3
conn = sqlite3.connect('/data/data/com.termux/files/home/.gemini/tmp/otaku_mappings/anime_mappings.db')
cursor = conn.cursor()
print("Shingeki no Kyojin Season 3 details:")
cursor.execute("SELECT mal_id, mal_title, thetvdb_season, thetvdb_part, anime_media_episodes, global_media_episodes FROM anime WHERE mal_title LIKE 'Shingeki no Kyojin Season 3%'")
rows = cursor.fetchall()
for row in rows:
print(row)
print("\nOne Piece details:")
cursor.execute("SELECT mal_id, mal_title, thetvdb_season, thetvdb_part, anime_media_episodes, global_media_episodes FROM anime WHERE mal_title = 'One Piece'")
rows = cursor.fetchall()
for row in rows:
print(row)
conn.close()

File diff suppressed because it is too large Load diff

View file

@ -20,9 +20,9 @@ import { MalApiService } from '../services/mal/MalApi';
import { MalAnimeNode, MalListStatus } from '../types/mal';
import { useTheme } from '../contexts/ThemeContext';
import { useTranslation } from 'react-i18next';
import { mappingService } from '../services/MappingService';
import { logger } from '../utils/logger';
import { MalEditModal } from '../components/mal/MalEditModal';
import { MalSync } from '../services/mal/MalSync';
const { width } = Dimensions.get('window');
const ITEM_WIDTH = width * 0.35;
@ -102,9 +102,8 @@ const MalLibraryScreen: React.FC = () => {
// Requirement 8: Resolve correct Cinemata / TMDB / IMDb ID
const malId = item.node.id;
// Check offline mapping first (reverse lookup)
await mappingService.init();
const imdbId = mappingService.getImdbIdFromMalId(malId);
// Use MalSync API to get external IDs
const { imdbId } = await MalSync.getIdsFromMalId(malId);
if (imdbId) {
navigation.navigate('Metadata', {

View file

@ -1,249 +0,0 @@
import * as FileSystem from 'expo-file-system';
import axios from 'axios';
import { Asset } from 'expo-asset';
// We require the bundled mappings as a fallback.
// This ensures the app works immediately upon install without internet.
const BUNDLED_MAPPINGS = require('../assets/mappings.json');
const MAPPINGS_FILE_URI = FileSystem.documentDirectory + 'mappings.json';
const GITHUB_RAW_URL = 'https://raw.githubusercontent.com/eliasbenb/PlexAniBridge-Mappings/master/mappings.json';
interface MappingEntry {
anidb_id?: number;
imdb_id?: string | string[];
mal_id?: number | number[];
tmdb_show_id?: number;
tmdb_movie_id?: number | number[];
tvdb_id?: number;
tvdb_mappings?: { [key: string]: string };
}
interface Mappings {
[anilist_id: string]: MappingEntry;
}
class MappingService {
private mappings: Mappings = {};
private imdbIndex: { [imdbId: string]: string[] } = {}; // Maps IMDb ID to array of AniList IDs
private malIndex: { [malId: number]: string } = {}; // Maps MAL ID to AniList ID
private isInitialized = false;
/**
* Initialize the service. Loads mappings from local storage if available,
* otherwise falls back to the bundled JSON.
*/
async init() {
if (this.isInitialized) return;
try {
const fileInfo = await FileSystem.getInfoAsync(MAPPINGS_FILE_URI);
if (fileInfo.exists) {
console.log('Loading mappings from local storage...');
const content = await FileSystem.readAsStringAsync(MAPPINGS_FILE_URI);
this.mappings = JSON.parse(content);
} else {
console.log('Loading bundled mappings...');
this.mappings = BUNDLED_MAPPINGS;
}
} catch (error) {
console.error('Failed to load mappings, falling back to bundled:', error);
this.mappings = BUNDLED_MAPPINGS;
}
this.buildIndex();
this.isInitialized = true;
console.log(`MappingService initialized with ${Object.keys(this.mappings).length} entries.`);
// Trigger background update
this.checkForUpdates().catch(err => console.warn('Background mapping update failed:', err));
}
/**
* Build a reverse index for fast IMDb lookups.
*/
private buildIndex() {
this.imdbIndex = {};
this.malIndex = {};
for (const [anilistId, entry] of Object.entries(this.mappings)) {
// IMDb Index
if (entry.imdb_id) {
const imdbIds = Array.isArray(entry.imdb_id) ? entry.imdb_id : [entry.imdb_id];
for (const id of imdbIds) {
if (!this.imdbIndex[id]) {
this.imdbIndex[id] = [];
}
this.imdbIndex[id].push(anilistId);
}
}
// MAL Index
if (entry.mal_id) {
const malIds = Array.isArray(entry.mal_id) ? entry.mal_id : [entry.mal_id];
for (const id of malIds) {
this.malIndex[id] = anilistId;
}
}
}
}
/**
* Convert a MAL ID to an IMDb ID.
* This is a reverse lookup used by Stremio services.
*/
getImdbIdFromMalId(malId: number): string | null {
if (!this.isInitialized) {
console.warn('MappingService not initialized. Call init() first.');
}
const anilistId = this.malIndex[malId];
if (anilistId) {
const entry = this.mappings[anilistId];
if (entry && entry.imdb_id) {
return Array.isArray(entry.imdb_id) ? entry.imdb_id[0] : entry.imdb_id;
}
}
return null;
}
/**
* Check for updates from the GitHub repository and save to local storage.
*/
async checkForUpdates() {
try {
console.log('Checking for mapping updates...');
const response = await axios.get(GITHUB_RAW_URL);
if (response.data && typeof response.data === 'object') {
const newMappings = response.data;
const newCount = Object.keys(newMappings).length;
const currentCount = Object.keys(this.mappings).length;
// Basic sanity check: ensure we didn't download an empty or drastically smaller file
if (newCount > 1000) {
await FileSystem.writeAsStringAsync(MAPPINGS_FILE_URI, JSON.stringify(newMappings));
console.log(`Mappings updated successfully. New count: ${newCount} (Old: ${currentCount})`);
// Optional: Hot-reload the mappings immediately?
// For stability, usually better to wait for next app restart,
// but we can update in memory too.
this.mappings = newMappings;
this.buildIndex();
}
}
} catch (error) {
console.warn('Failed to update mappings:', error);
}
}
/**
* Convert an IMDb ID + Season/Episode to a MAL ID.
* Handles complex mapping logic (split seasons, episode offsets).
*/
getMalId(imdbId: string, season: number, episode: number): number | null {
if (!this.isInitialized) {
console.warn('MappingService not initialized. Call init() first.');
}
const anilistIds = this.imdbIndex[imdbId];
if (!anilistIds || anilistIds.length === 0) return null;
// Iterate through all potential matches (usually just 1, but sometimes splits)
for (const anilistId of anilistIds) {
const entry = this.mappings[anilistId];
if (!entry) continue;
// If there are no specific mappings, assumes 1:1 match if it's the only entry
// But usually, we look for 'tvdb_mappings' (which this repo uses for seasons)
// or 'tmdb_mappings'. This repo uses 'tvdb_mappings' for structure.
if (this.isMatch(entry, season, episode)) {
return this.extractMalId(entry);
}
}
// Fallback: If we have exactly one match and no mapping rules defined, return it.
if (anilistIds.length === 1) {
const entry = this.mappings[anilistIds[0]];
// Only return if it doesn't have restrictive mapping rules that failed above
if (!entry.tvdb_mappings) {
return this.extractMalId(entry);
}
}
return null;
}
private extractMalId(entry: MappingEntry): number | null {
if (!entry.mal_id) return null;
if (Array.isArray(entry.mal_id)) return entry.mal_id[0];
return entry.mal_id;
}
/**
* Logic to check if a specific Season/Episode fits within the entry's mapping rules.
*/
private isMatch(entry: MappingEntry, targetSeason: number, targetEpisode: number): boolean {
const mappings = entry.tvdb_mappings;
if (!mappings) {
// If no mappings exist, we can't be sure, but usually strict matching requires them.
// However, some entries might be simple movies or single seasons.
return true;
}
const seasonKey = `s${targetSeason}`;
const rule = mappings[seasonKey];
if (rule === undefined) return false; // Season not in this entry
// Empty string means "matches whole season 1:1"
if (rule === "") return true;
// Parse rules: "e1-e12|2,e13-"
const parts = rule.split(',');
for (const part of parts) {
if (this.checkRulePart(part, targetEpisode)) {
return true;
}
}
return false;
}
private checkRulePart(part: string, targetEpisode: number): boolean {
// Format: e{start}-e{end}|{ratio}
// Examples: "e1-e12", "e13-", "e1", "e1-e12|2"
let [range, ratioStr] = part.split('|');
// We currently ignore ratio for *matching* purposes (just checking if it's in range)
// Ratio is used for calculating the absolute episode number if we were converting TO absolute.
const [startStr, endStr] = range.split('-');
const start = parseInt(startStr.replace('e', ''), 10);
// Single episode mapping: "e5"
if (!endStr && !range.includes('-')) {
return targetEpisode === start;
}
// Range
if (targetEpisode < start) return false;
// Open ended range: "e13-"
if (endStr === '') {
return true;
}
// Closed range: "e1-e12"
if (endStr) {
const end = parseInt(endStr.replace('e', ''), 10);
if (targetEpisode > end) return false;
}
return true;
}
}
export const mappingService = new MappingService();

View file

@ -2,7 +2,7 @@ import { mmkvStorage } from '../mmkvStorage';
import { MalApiService } from './MalApi';
import { MalListStatus } from '../../types/mal';
import { catalogService } from '../catalogService';
import { mappingService } from '../MappingService';
import { ArmSyncService } from './ArmSyncService';
import axios from 'axios';
const MAPPING_PREFIX = 'mal_map_';
@ -41,7 +41,7 @@ export const MalSync = {
* Tries to find a MAL ID for a given anime title or IMDb ID.
* Caches the result to avoid repeated API calls.
*/
getMalId: async (title: string, type: 'movie' | 'series' = 'series', year?: number, season?: number, imdbId?: string, episode: number = 1): Promise<number | null> => {
getMalId: async (title: string, type: 'movie' | 'series' = 'series', year?: number, season?: number, imdbId?: string, episode: number = 1, releaseDate?: string): Promise<number | null> => {
// Safety check: Never perform a MAL search for generic placeholders or empty strings.
// This prevents "cache poisoning" where a generic term matches a random anime.
const normalizedTitle = title.trim().toLowerCase();
@ -53,12 +53,22 @@ export const MalSync = {
if (!imdbId) return null;
}
// 1. Try Offline Mapping Service (Most accurate for perfect season/episode matching)
if (imdbId && type === 'series' && season !== undefined) {
const offlineMalId = mappingService.getMalId(imdbId, season, episode);
if (offlineMalId) {
console.log(`[MalSync] Found offline mapping: ${imdbId} S${season}E${episode} -> MAL ${offlineMalId}`);
return offlineMalId;
// 1. Try ARM + Jikan Sync (Most accurate for perfect season/episode matching)
if (imdbId && type === 'series' && releaseDate) {
try {
const armResult = await ArmSyncService.resolveByDate(imdbId, releaseDate);
if (armResult && armResult.malId) {
console.log(`[MalSync] Found ARM match: ${imdbId} (${releaseDate}) -> MAL ${armResult.malId} Ep ${armResult.episode}`);
// Note: ArmSyncService returns the *absolute* episode number for MAL (e.g. 76)
// but our 'episode' arg is usually relative (e.g. 1).
// scrobbleEpisode uses the malId returned here, and potentially the episode number from ArmSync
// But getMalId just returns the ID.
// Ideally, scrobbleEpisode should call ArmSyncService directly to get both ID and correct Episode number.
// For now, we return the ID.
return armResult.malId;
}
} catch (e) {
console.warn('[MalSync] ARM Sync failed:', e);
}
}
@ -127,7 +137,8 @@ export const MalSync = {
totalEpisodes: number = 0,
type: 'movie' | 'series' = 'series',
season?: number,
imdbId?: string
imdbId?: string,
releaseDate?: string
) => {
try {
// Requirement 9 & 10: Respect user settings and safety
@ -138,7 +149,24 @@ export const MalSync = {
return;
}
const malId = await MalSync.getMalId(animeTitle, type, undefined, season, imdbId, episodeNumber);
let malId: number | null = null;
let finalEpisodeNumber = episodeNumber;
// Try ARM Sync first to get exact MAL ID and absolute episode number
if (imdbId && type === 'series' && releaseDate) {
const armResult = await ArmSyncService.resolveByDate(imdbId, releaseDate);
if (armResult) {
malId = armResult.malId;
finalEpisodeNumber = armResult.episode;
console.log(`[MalSync] ARM Resolved: ${animeTitle} -> MAL ${malId} Ep ${finalEpisodeNumber}`);
}
}
// Fallback to standard lookup if ARM failed or not applicable
if (!malId) {
malId = await MalSync.getMalId(animeTitle, type, undefined, season, imdbId, episodeNumber, releaseDate);
}
if (!malId) return;
// Check current status on MAL to avoid overwriting completed/dropped shows
@ -164,8 +192,8 @@ export const MalSync = {
// If we are just starting (ep 1) or resuming (plan_to_watch/on_hold/null), set to watching
// Also ensure we don't downgrade episode count (though unlikely with scrobbling forward)
if (episodeNumber <= currentEpisodesWatched) {
console.log(`[MalSync] Skipping update for ${animeTitle}: Episode ${episodeNumber} <= Current ${currentEpisodesWatched}`);
if (finalEpisodeNumber <= currentEpisodesWatched) {
console.log(`[MalSync] Skipping update for ${animeTitle}: Episode ${finalEpisodeNumber} <= Current ${currentEpisodesWatched}`);
return;
}
} catch (e) {
@ -188,12 +216,12 @@ export const MalSync = {
// Determine Status
let status: MalListStatus = 'watching';
if (finalTotalEpisodes > 0 && episodeNumber >= finalTotalEpisodes) {
if (finalTotalEpisodes > 0 && finalEpisodeNumber >= finalTotalEpisodes) {
status = 'completed';
}
await MalApiService.updateStatus(malId, status, episodeNumber);
console.log(`[MalSync] Synced ${animeTitle} Ep ${episodeNumber}/${finalTotalEpisodes || '?'} -> MAL ID ${malId} (${status})`);
await MalApiService.updateStatus(malId, status, finalEpisodeNumber);
console.log(`[MalSync] Synced ${animeTitle} Ep ${finalEpisodeNumber}/${finalTotalEpisodes || '?'} -> MAL ID ${malId} (${status})`);
} catch (e) {
console.error('[MalSync] Scrobble failed:', e);
}

View file

@ -5,7 +5,6 @@ import { logger } from '../utils/logger';
import { MalSync } from './mal/MalSync';
import { MalAuth } from './mal/MalAuth';
import { ArmSyncService } from './mal/ArmSyncService';
import { mappingService } from './MappingService';
/**
* WatchedService - Manages "watched" status for movies, episodes, and seasons.
@ -17,8 +16,6 @@ class WatchedService {
private constructor() {
this.traktService = TraktService.getInstance();
// Initialize mapping service
mappingService.init().catch(err => logger.error('[WatchedService] MappingService init failed:', err));
}
public static getInstance(): WatchedService {
@ -130,7 +127,8 @@ class WatchedService {
0,
'series',
season,
showImdbId
showImdbId,
releaseDate // Pass releaseDate for better matching
).catch(err => logger.error('[WatchedService] MAL sync failed:', err));
}
}