mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-18 07:51:46 +00:00
Merge branch 'NuvioMedia:main' into localization-patch
This commit is contained in:
commit
537b072924
11 changed files with 742 additions and 476 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);
|
watchedService.unmarkMovieAsWatched(item.id, 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.
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,6 @@ export const LOCALES = [
|
||||||
{ code: 'ro', key: 'romanian' },
|
{ code: 'ro', key: 'romanian' },
|
||||||
{ code: 'sq', key: 'albanian' },
|
{ code: 'sq', key: 'albanian' },
|
||||||
{ code: 'ca', key: 'catalan' },
|
{ code: 'ca', key: 'catalan' },
|
||||||
{ code: 'vi', key: 'vietnamese' }
|
{ code: 'vi', key: 'vietnamese' },
|
||||||
|
{ code: 'ja', key: 'japanese' }
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -939,6 +939,22 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
if (finalTmdbId) setTmdbId(finalTmdbId);
|
if (finalTmdbId) setTmdbId(finalTmdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the addon returned an imdb_id in its metadata (e.g. Kitsu addon), set it now.
|
||||||
|
// This ensures imdbId state is populated for Trakt scrobbling even without TMDB enrichment.
|
||||||
|
if (!imdbId && (finalMetadata as any).imdb_id) {
|
||||||
|
const resolvedImdb = (finalMetadata as any).imdb_id as string;
|
||||||
|
setImdbId(resolvedImdb);
|
||||||
|
// Also resolve tmdbId from the imdb_id if we still don't have it
|
||||||
|
if (!finalTmdbId) {
|
||||||
|
const foundTmdbId = await tmdbSvc.findTMDBIdByIMDB(resolvedImdb);
|
||||||
|
if (foundTmdbId) {
|
||||||
|
finalTmdbId = foundTmdbId;
|
||||||
|
setTmdbId(foundTmdbId);
|
||||||
|
setMetadata(prev => prev ? { ...prev, tmdbId: foundTmdbId } : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (finalTmdbId) {
|
if (finalTmdbId) {
|
||||||
const lang = settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en';
|
const lang = settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en';
|
||||||
if (normalizedType === 'movie') {
|
if (normalizedType === 'movie') {
|
||||||
|
|
@ -2230,20 +2246,48 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
// Fetch TMDB ID if needed and then recommendations
|
// Fetch TMDB ID if needed and then recommendations
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTmdbIdAndRecommendations = async () => {
|
const fetchTmdbIdAndRecommendations = async () => {
|
||||||
if (!settings.enrichMetadataWithTMDB) {
|
if (!metadata) return;
|
||||||
|
|
||||||
|
const isAnimeId = id.startsWith('kitsu:') || id.startsWith('mal:') || id.startsWith('anilist:');
|
||||||
|
|
||||||
|
// For anime IDs we always try to resolve tmdbId and imdbId regardless of enrichment setting,
|
||||||
|
// because they're needed for Trakt scrobbling even when TMDB enrichment is disabled.
|
||||||
|
if (!settings.enrichMetadataWithTMDB && !isAnimeId) {
|
||||||
if (__DEV__) console.log('[useMetadata] enrichment disabled; skip TMDB id extraction (extract path)');
|
if (__DEV__) console.log('[useMetadata] enrichment disabled; skip TMDB id extraction (extract path)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (metadata && !tmdbId) {
|
|
||||||
|
if (!tmdbId) {
|
||||||
try {
|
try {
|
||||||
const tmdbService = TMDBService.getInstance();
|
const tmdbSvc = TMDBService.getInstance();
|
||||||
const fetchedTmdbId = await tmdbService.extractTMDBIdFromStremioId(id);
|
const fetchedTmdbId = await tmdbSvc.extractTMDBIdFromStremioId(id);
|
||||||
if (fetchedTmdbId) {
|
if (fetchedTmdbId) {
|
||||||
if (__DEV__) console.log('[useMetadata] extracted TMDB id from content id', { id, fetchedTmdbId });
|
if (__DEV__) console.log('[useMetadata] extracted TMDB id from content id', { id, fetchedTmdbId });
|
||||||
setTmdbId(fetchedTmdbId);
|
setTmdbId(fetchedTmdbId);
|
||||||
|
|
||||||
|
// For anime IDs, also resolve the IMDb ID from TMDB external IDs so Trakt can scrobble
|
||||||
|
if (isAnimeId && !imdbId) {
|
||||||
|
try {
|
||||||
|
const externalIds = await tmdbSvc.getShowExternalIds(fetchedTmdbId);
|
||||||
|
if (externalIds?.imdb_id) {
|
||||||
|
if (__DEV__) console.log('[useMetadata] resolved imdbId for anime via TMDB', { id, imdbId: externalIds.imdb_id });
|
||||||
|
setImdbId(externalIds.imdb_id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (__DEV__) console.warn('[useMetadata] could not resolve imdbId from TMDB for anime ID', { id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.enrichMetadataWithTMDB) {
|
||||||
|
// Enrichment is disabled but we still resolved tmdbId for Trakt scrobbling.
|
||||||
|
// Set it on the metadata object so the player can read it via metadata.tmdbId.
|
||||||
|
setMetadata(prev => prev ? { ...prev, tmdbId: fetchedTmdbId } : null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch certification only if granular setting is enabled
|
// Fetch certification only if granular setting is enabled
|
||||||
if (settings.tmdbEnrichCertification) {
|
if (settings.tmdbEnrichCertification) {
|
||||||
const certification = await tmdbService.getCertification(normalizedType, fetchedTmdbId);
|
const certification = await tmdbSvc.getCertification(normalizedType, fetchedTmdbId);
|
||||||
if (certification) {
|
if (certification) {
|
||||||
if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification });
|
if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification });
|
||||||
setMetadata(prev => prev ? {
|
setMetadata(prev => prev ? {
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,10 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
|
|
||||||
// Generate a unique session key for this content instance
|
// Generate a unique session key for this content instance
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const resolvedId = (options.imdbId && options.imdbId.trim()) ? options.imdbId : (options.id || '');
|
||||||
const contentKey = options.type === 'movie'
|
const contentKey = options.type === 'movie'
|
||||||
? `movie:${options.imdbId}`
|
? `movie:${resolvedId}`
|
||||||
: `episode:${options.showImdbId || options.imdbId}:${options.season}:${options.episode}`;
|
: `episode:${options.showImdbId || resolvedId}:${options.season}:${options.episode}`;
|
||||||
sessionKey.current = `${contentKey}:${Date.now()}`;
|
sessionKey.current = `${contentKey}:${Date.now()}`;
|
||||||
|
|
||||||
// Reset all session state for new content
|
// Reset all session state for new content
|
||||||
|
|
@ -109,11 +110,22 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.imdbId || options.imdbId.trim() === '') {
|
// Resolve the best available ID: prefer a proper IMDb ID, fall back to the Stremio content ID.
|
||||||
logger.error('[TraktAutosync] Cannot build content data: missing or empty imdbId');
|
// This allows scrobbling for content with special IDs (e.g. "kitsu:123", "tmdb:456") where
|
||||||
|
// the IMDb ID hasn't been resolved yet — Trakt will match by title + season/episode instead.
|
||||||
|
const imdbIdRaw = options.imdbId && options.imdbId.trim() ? options.imdbId.trim() : '';
|
||||||
|
const stremioIdRaw = options.id && options.id.trim() ? options.id.trim() : '';
|
||||||
|
const resolvedImdbId = imdbIdRaw || stremioIdRaw;
|
||||||
|
|
||||||
|
if (!resolvedImdbId) {
|
||||||
|
logger.error('[TraktAutosync] Cannot build content data: missing or empty imdbId and id');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!imdbIdRaw && stremioIdRaw) {
|
||||||
|
logger.warn(`[TraktAutosync] imdbId is empty, falling back to stremio id "${stremioIdRaw}" — Trakt will match by title`);
|
||||||
|
}
|
||||||
|
|
||||||
const numericYear = parseYear(options.year);
|
const numericYear = parseYear(options.year);
|
||||||
const numericShowYear = parseYear(options.showYear);
|
const numericShowYear = parseYear(options.showYear);
|
||||||
|
|
||||||
|
|
@ -125,7 +137,7 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
if (options.type === 'movie') {
|
if (options.type === 'movie') {
|
||||||
return {
|
return {
|
||||||
type: 'movie',
|
type: 'movie',
|
||||||
imdbId: options.imdbId.trim(),
|
imdbId: resolvedImdbId,
|
||||||
title: options.title.trim(),
|
title: options.title.trim(),
|
||||||
year: numericYear // Can be undefined now
|
year: numericYear // Can be undefined now
|
||||||
};
|
};
|
||||||
|
|
@ -140,26 +152,34 @@ export function useTraktAutosync(options: TraktAutosyncOptions) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedShowImdbId = (options.showImdbId && options.showImdbId.trim())
|
||||||
|
? options.showImdbId.trim()
|
||||||
|
: resolvedImdbId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'episode',
|
type: 'episode',
|
||||||
imdbId: options.imdbId.trim(),
|
imdbId: resolvedImdbId,
|
||||||
title: options.title.trim(),
|
title: options.title.trim(),
|
||||||
year: numericYear,
|
year: numericYear,
|
||||||
season: options.season,
|
season: options.season,
|
||||||
episode: options.episode,
|
episode: options.episode,
|
||||||
showTitle: (options.showTitle || options.title).trim(),
|
showTitle: (options.showTitle || options.title).trim(),
|
||||||
showYear: numericShowYear || numericYear,
|
showYear: numericShowYear || numericYear,
|
||||||
showImdbId: (options.showImdbId || options.imdbId).trim()
|
showImdbId: resolvedShowImdbId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [options]);
|
}, [options]);
|
||||||
|
|
||||||
const buildSimklContentData = useCallback((): SimklContentData => {
|
const buildSimklContentData = useCallback((): SimklContentData => {
|
||||||
|
// Use the same fallback logic: prefer imdbId, fall back to stremio id
|
||||||
|
const resolvedId = (options.imdbId && options.imdbId.trim())
|
||||||
|
? options.imdbId.trim()
|
||||||
|
: (options.id && options.id.trim()) ? options.id.trim() : '';
|
||||||
return {
|
return {
|
||||||
type: options.type === 'series' ? 'episode' : 'movie',
|
type: options.type === 'series' ? 'episode' : 'movie',
|
||||||
title: options.title,
|
title: options.title,
|
||||||
ids: {
|
ids: {
|
||||||
imdb: options.imdbId
|
imdb: resolvedId
|
||||||
},
|
},
|
||||||
season: options.season,
|
season: options.season,
|
||||||
episode: options.episode
|
episode: options.episode
|
||||||
|
|
|
||||||
|
|
@ -669,6 +669,7 @@
|
||||||
"romanian": "Romanian",
|
"romanian": "Romanian",
|
||||||
"albanian": "Albanian",
|
"albanian": "Albanian",
|
||||||
"catalan": "Catalan",
|
"catalan": "Catalan",
|
||||||
|
"vietnamese": "Vietnamese",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"content_discovery": "Content & Discovery",
|
"content_discovery": "Content & Discovery",
|
||||||
"appearance": "Appearance",
|
"appearance": "Appearance",
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,10 @@
|
||||||
"enable": "有効にする",
|
"enable": "有効にする",
|
||||||
"disable": "無効にする",
|
"disable": "無効にする",
|
||||||
"show_more": "もっと見る",
|
"show_more": "もっと見る",
|
||||||
"show_less": "少なく見る",
|
"show_less": "表示を減らす",
|
||||||
"load_more": "さらに読み込む",
|
"load_more": "さらに読み込む",
|
||||||
"unknown_date": "不明な日付",
|
"unknown_date": "日付不明",
|
||||||
"anonymous_user": "匿名ユーザー",
|
"anonymous_user": "匿名",
|
||||||
"time": {
|
"time": {
|
||||||
"now": "たった今",
|
"now": "たった今",
|
||||||
"minutes_ago": "{{count}}分前",
|
"minutes_ago": "{{count}}分前",
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
"channels": "チャンネル"
|
"channels": "チャンネル"
|
||||||
},
|
},
|
||||||
"movies": "映画",
|
"movies": "映画",
|
||||||
"tv_shows": "TVシリーズ",
|
"tv_shows": "シリーズ",
|
||||||
"load_more_catalogs": "カタログをさらに読み込む",
|
"load_more_catalogs": "カタログをさらに読み込む",
|
||||||
"no_content": "利用可能なコンテンツがありません",
|
"no_content": "利用可能なコンテンツがありません",
|
||||||
"add_catalogs": "カタログを追加",
|
"add_catalogs": "カタログを追加",
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
"view_all": "すべて表示",
|
"view_all": "すべて表示",
|
||||||
"this_week": "今週",
|
"this_week": "今週",
|
||||||
"upcoming": "近日公開",
|
"upcoming": "近日公開",
|
||||||
"recently_released": "最近公開",
|
"recently_released": "最近公開された作品",
|
||||||
"no_scheduled_episodes": "予定されたエピソードがありません",
|
"no_scheduled_episodes": "予定されたエピソードがありません",
|
||||||
"check_back_later": "後で確認してください",
|
"check_back_later": "後で確認してください",
|
||||||
"continue_watching": "視聴を続ける",
|
"continue_watching": "視聴を続ける",
|
||||||
|
|
@ -72,15 +72,15 @@
|
||||||
"episode": "エピソード{{episode}}",
|
"episode": "エピソード{{episode}}",
|
||||||
"movie": "映画",
|
"movie": "映画",
|
||||||
"series": "シリーズ",
|
"series": "シリーズ",
|
||||||
"tv_show": "TVシリーズ",
|
"tv_show": "シリーズ",
|
||||||
"percent_watched": "{{percent}}%視聴済み",
|
"percent_watched": "{{percent}}%視聴済み",
|
||||||
"view_details": "詳細を見る",
|
"view_details": "詳細を見る",
|
||||||
"remove": "削除",
|
"remove": "削除",
|
||||||
"play": "再生",
|
"play": "再生",
|
||||||
"play_now": "今すぐ再生",
|
"play_now": "今すぐ再生",
|
||||||
"resume": "再開",
|
"resume": "再開",
|
||||||
"info": "情報",
|
"info": "詳細",
|
||||||
"more_info": "詳細情報",
|
"more_info": "詳細",
|
||||||
"my_list": "マイリスト",
|
"my_list": "マイリスト",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"saved": "保存済み",
|
"saved": "保存済み",
|
||||||
|
|
@ -89,8 +89,8 @@
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"no_featured_content": "注目コンテンツがありません",
|
"no_featured_content": "注目コンテンツがありません",
|
||||||
"couldnt_load_featured": "注目コンテンツを読み込めませんでした",
|
"couldnt_load_featured": "注目コンテンツを読み込めませんでした",
|
||||||
"no_featured_desc": "カタログアドオンをインストールするか、設定でコンテンツソースを変更してください。",
|
"no_featured_desc": "カタログアドオンをインストールするか、設定でコンテンツソースを変更してください",
|
||||||
"load_error_desc": "注目コンテンツの取得に問題が発生しました。接続を確認してもう一度お試しください。",
|
"load_error_desc": "注目コンテンツの取得に問題が発生しました。接続を確認してもう一度お試しください",
|
||||||
"no_featured_available": "利用可能な注目コンテンツがありません",
|
"no_featured_available": "利用可能な注目コンテンツがありません",
|
||||||
"no_description": "説明なし"
|
"no_description": "説明なし"
|
||||||
},
|
},
|
||||||
|
|
@ -106,10 +106,10 @@
|
||||||
"recent_searches": "最近の検索",
|
"recent_searches": "最近の検索",
|
||||||
"discover": "ディスカバー",
|
"discover": "ディスカバー",
|
||||||
"movies": "映画",
|
"movies": "映画",
|
||||||
"tv_shows": "TVシリーズ",
|
"tv_shows": "シリーズ",
|
||||||
"select_catalog": "カタログを選択",
|
"select_catalog": "カタログを選択",
|
||||||
"all_genres": "すべてのジャンル",
|
"all_genres": "すべてのジャンル",
|
||||||
"discovering": "コンテンツを探しています...",
|
"discovering": "コンテンツを検索中...",
|
||||||
"show_more": "もっと見る({{count}})",
|
"show_more": "もっと見る({{count}})",
|
||||||
"no_content_found": "コンテンツが見つかりません",
|
"no_content_found": "コンテンツが見つかりません",
|
||||||
"try_different": "別のジャンルやカタログを試してください",
|
"try_different": "別のジャンルやカタログを試してください",
|
||||||
|
|
@ -122,9 +122,9 @@
|
||||||
"try_keywords": "別のキーワードを試すか、スペルを確認してください",
|
"try_keywords": "別のキーワードを試すか、スペルを確認してください",
|
||||||
"select_type": "タイプを選択",
|
"select_type": "タイプを選択",
|
||||||
"browse_movies": "映画カタログを閲覧",
|
"browse_movies": "映画カタログを閲覧",
|
||||||
"browse_tv": "TVシリーズカタログを閲覧",
|
"browse_tv": "シリーズカタログを閲覧",
|
||||||
"select_genre": "ジャンルを選択",
|
"select_genre": "ジャンルを選択",
|
||||||
"show_all_content": "すべてのコンテンツを表示",
|
"show_all_content": "すべて表示",
|
||||||
"genres_count": "{{count}}ジャンル"
|
"genres_count": "{{count}}ジャンル"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
|
|
@ -182,28 +182,28 @@
|
||||||
"unable_to_load": "コンテンツを読み込めません",
|
"unable_to_load": "コンテンツを読み込めません",
|
||||||
"error_code": "エラーコード: {{code}}",
|
"error_code": "エラーコード: {{code}}",
|
||||||
"content_not_found": "コンテンツが見つかりません",
|
"content_not_found": "コンテンツが見つかりません",
|
||||||
"content_not_found_desc": "このコンテンツは存在しないか、削除された可能性があります。",
|
"content_not_found_desc": "このコンテンツは存在しないか、削除された可能性があります",
|
||||||
"server_error": "サーバーエラー",
|
"server_error": "サーバーエラー",
|
||||||
"server_error_desc": "サーバーが一時的に利用できません。後でもう一度お試しください。",
|
"server_error_desc": "サーバーが一時的に利用できません。後でもう一度お試しください",
|
||||||
"bad_gateway": "ゲートウェイエラー",
|
"bad_gateway": "ゲートウェイエラー",
|
||||||
"bad_gateway_desc": "サーバーで問題が発生しました。後でもう一度お試しください。",
|
"bad_gateway_desc": "サーバーで問題が発生しました。後でもう一度お試しください",
|
||||||
"service_unavailable": "サービス利用不可",
|
"service_unavailable": "サービス利用不可",
|
||||||
"service_unavailable_desc": "サービスは現在メンテナンス中です。後でもう一度お試しください。",
|
"service_unavailable_desc": "サービスは現在メンテナンス中です。後でもう一度お試しください",
|
||||||
"too_many_requests": "リクエストが多すぎます",
|
"too_many_requests": "リクエストが多すぎます",
|
||||||
"too_many_requests_desc": "リクエストが多すぎます。少し待ってからもう一度お試しください。",
|
"too_many_requests_desc": "リクエストが多すぎます。少し待ってからもう一度お試しください",
|
||||||
"request_timeout": "リクエストタイムアウト",
|
"request_timeout": "リクエストタイムアウト",
|
||||||
"request_timeout_desc": "リクエストに時間がかかりすぎました。もう一度お試しください。",
|
"request_timeout_desc": "リクエストに時間がかかりすぎました。もう一度お試しください",
|
||||||
"network_error": "ネットワークエラー",
|
"network_error": "ネットワークエラー",
|
||||||
"network_error_desc": "インターネット接続を確認してもう一度お試しください。",
|
"network_error_desc": "インターネット接続を確認してもう一度お試しください",
|
||||||
"auth_error": "認証エラー",
|
"auth_error": "認証エラー",
|
||||||
"auth_error_desc": "アカウント設定を確認してもう一度お試しください。",
|
"auth_error_desc": "アカウント設定を確認してもう一度お試しください",
|
||||||
"access_denied": "アクセス拒否",
|
"access_denied": "アクセス拒否",
|
||||||
"access_denied_desc": "このコンテンツにアクセスする権限がありません。",
|
"access_denied_desc": "このコンテンツにアクセスする権限がありません",
|
||||||
"connection_error": "接続エラー",
|
"connection_error": "接続エラー",
|
||||||
"streams_unavailable": "ストリームが利用できません",
|
"streams_unavailable": "ストリームが利用できません",
|
||||||
"streams_unavailable_desc": "ストリーミングソースは現在利用できません。後でもう一度お試しください。",
|
"streams_unavailable_desc": "ストリーミングソースは現在利用できません。後でもう一度お試しください",
|
||||||
"unknown_error": "不明なエラー",
|
"unknown_error": "不明なエラー",
|
||||||
"something_went_wrong": "問題が発生しました。もう一度お試しください。",
|
"something_went_wrong": "問題が発生しました。もう一度お試しください",
|
||||||
"cast": "キャスト",
|
"cast": "キャスト",
|
||||||
"more_like_this": "関連作品",
|
"more_like_this": "関連作品",
|
||||||
"collection": "コレクション",
|
"collection": "コレクション",
|
||||||
|
|
@ -217,7 +217,7 @@
|
||||||
"episode_count_plural": "{{count}}エピソード",
|
"episode_count_plural": "{{count}}エピソード",
|
||||||
"no_episodes": "利用可能なエピソードがありません",
|
"no_episodes": "利用可能なエピソードがありません",
|
||||||
"no_episodes_for_season": "シーズン{{season}}の利用可能なエピソードがありません",
|
"no_episodes_for_season": "シーズン{{season}}の利用可能なエピソードがありません",
|
||||||
"episodes_not_released": "エピソードはまだ公開されていない可能性があります",
|
"episodes_not_released": "まだ公開されていない可能性があります",
|
||||||
"no_description": "説明なし",
|
"no_description": "説明なし",
|
||||||
"episode_label": "エピソード{{number}}",
|
"episode_label": "エピソード{{number}}",
|
||||||
"watch_again": "もう一度見る",
|
"watch_again": "もう一度見る",
|
||||||
|
|
@ -232,8 +232,8 @@
|
||||||
"directors": "監督",
|
"directors": "監督",
|
||||||
"creator": "制作者",
|
"creator": "制作者",
|
||||||
"creators": "制作者",
|
"creators": "制作者",
|
||||||
"production": "制作",
|
"production": "制作会社",
|
||||||
"network": "ネットワーク",
|
"network": "放送局",
|
||||||
"mark_watched": "視聴済みにする",
|
"mark_watched": "視聴済みにする",
|
||||||
"mark_unwatched": "未視聴にする",
|
"mark_unwatched": "未視聴にする",
|
||||||
"marking": "マーク中...",
|
"marking": "マーク中...",
|
||||||
|
|
@ -258,7 +258,7 @@
|
||||||
"first_air_date": "初回放送日",
|
"first_air_date": "初回放送日",
|
||||||
"last_air_date": "最終放送日",
|
"last_air_date": "最終放送日",
|
||||||
"total_episodes": "エピソード総数",
|
"total_episodes": "エピソード総数",
|
||||||
"episode_runtime": "エピソード時間",
|
"episode_runtime": "エピソードの長さ",
|
||||||
"created_by": "制作",
|
"created_by": "制作",
|
||||||
"backdrop_gallery": "バックドロップギャラリー",
|
"backdrop_gallery": "バックドロップギャラリー",
|
||||||
"loading_episodes": "エピソードを読み込み中...",
|
"loading_episodes": "エピソードを読み込み中...",
|
||||||
|
|
@ -280,11 +280,11 @@
|
||||||
"cast": {
|
"cast": {
|
||||||
"biography": "経歴",
|
"biography": "経歴",
|
||||||
"known_for": "代表作",
|
"known_for": "代表作",
|
||||||
"personal_info": "個人情報",
|
"personal_info": "基本情報",
|
||||||
"born_in": "{{place}}生まれ",
|
"born_in": "出生地: {{place}}",
|
||||||
"filmography": "フィルモグラフィー",
|
"filmography": "出演作品",
|
||||||
"also_known_as": "別名",
|
"also_known_as": "別名",
|
||||||
"no_info_available": "追加情報なし",
|
"no_info_available": "情報がありません",
|
||||||
"as_character": "{{character}}役",
|
"as_character": "{{character}}役",
|
||||||
"loading_details": "詳細を読み込み中...",
|
"loading_details": "詳細を読み込み中...",
|
||||||
"years_old": "{{age}}歳",
|
"years_old": "{{age}}歳",
|
||||||
|
|
@ -300,16 +300,16 @@
|
||||||
"loading_filmography": "フィルモグラフィーを読み込み中...",
|
"loading_filmography": "フィルモグラフィーを読み込み中...",
|
||||||
"load_more_remaining": "さらに読み込む(残り{{count}}件)",
|
"load_more_remaining": "さらに読み込む(残り{{count}}件)",
|
||||||
"alert_error_title": "エラー",
|
"alert_error_title": "エラー",
|
||||||
"alert_error_message": "「{{title}}」を読み込めませんでした。後でもう一度お試しください。",
|
"alert_error_message": "「{{title}}」を読み込めませんでした。後でもう一度お試しください",
|
||||||
"alert_ok": "OK",
|
"alert_ok": "OK",
|
||||||
"no_upcoming": "この俳優の近日公開作品はありません",
|
"no_upcoming": "この俳優の近日公開作品はありません",
|
||||||
"no_content": "この俳優のコンテンツがありません",
|
"no_content": "この俳優のコンテンツがありません",
|
||||||
"no_movies": "この俳優の映画がありません",
|
"no_movies": "この俳優の映画がありません",
|
||||||
"no_tv": "この俳優のTVシリーズがありません"
|
"no_tv": "この俳優のシリーズがありません"
|
||||||
},
|
},
|
||||||
"comments": {
|
"comments": {
|
||||||
"title": "Traktコメント",
|
"title": "Traktコメント",
|
||||||
"spoiler_warning": "⚠️ このコメントにはネタバレが含まれています。タップして表示。",
|
"spoiler_warning": "⚠️ このコメントにはネタバレが含まれています。タップして表示",
|
||||||
"spoiler": "ネタバレ",
|
"spoiler": "ネタバレ",
|
||||||
"contains_spoilers": "ネタバレあり",
|
"contains_spoilers": "ネタバレあり",
|
||||||
"reveal": "表示",
|
"reveal": "表示",
|
||||||
|
|
@ -325,15 +325,15 @@
|
||||||
"official_trailer": "公式予告編",
|
"official_trailer": "公式予告編",
|
||||||
"teasers": "ティーザー",
|
"teasers": "ティーザー",
|
||||||
"teaser": "ティーザー",
|
"teaser": "ティーザー",
|
||||||
"clips_scenes": "クリップとシーン",
|
"clips_scenes": "シーン",
|
||||||
"clip": "クリップ",
|
"clip": "シーン",
|
||||||
"featurettes": "特典映像",
|
"featurettes": "メイキング",
|
||||||
"featurette": "特典映像",
|
"featurette": "特典映像",
|
||||||
"behind_the_scenes": "メイキング",
|
"behind_the_scenes": "メイキング",
|
||||||
"no_trailers": "利用可能な予告編がありません",
|
"no_trailers": "利用可能な予告編がありません",
|
||||||
"unavailable": "予告編は利用できません",
|
"unavailable": "予告編は利用できません",
|
||||||
"unavailable_desc": "予告編を読み込めませんでした。後でもう一度お試しください。",
|
"unavailable_desc": "予告編を読み込めませんでした。後でもう一度お試しください",
|
||||||
"unable_to_play": "予告編を再生できません。もう一度お試しください。",
|
"unable_to_play": "予告編を再生できません。もう一度お試しください",
|
||||||
"watch_on_youtube": "YouTubeで見る"
|
"watch_on_youtube": "YouTubeで見る"
|
||||||
},
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
|
|
@ -345,13 +345,13 @@
|
||||||
"all": "すべて",
|
"all": "すべて",
|
||||||
"failed_tmdb": "TMDBからコンテンツを読み込めませんでした",
|
"failed_tmdb": "TMDBからコンテンツを読み込めませんでした",
|
||||||
"movies": "映画",
|
"movies": "映画",
|
||||||
"tv_shows": "TVシリーズ",
|
"tv_shows": "シリーズ",
|
||||||
"channels": "チャンネル"
|
"channels": "チャンネル"
|
||||||
},
|
},
|
||||||
"streams": {
|
"streams": {
|
||||||
"back_to_episodes": "エピソードに戻る",
|
"back_to_episodes": "エピソードに戻る",
|
||||||
"back_to_info": "情報に戻る",
|
"back_to_info": "情報に戻る",
|
||||||
"fetching_from": "取得元:",
|
"fetching_from": "ソース:",
|
||||||
"no_sources_available": "利用可能なストリーミングソースがありません",
|
"no_sources_available": "利用可能なストリーミングソースがありません",
|
||||||
"add_sources_desc": "設定でストリーミングソースを追加してください",
|
"add_sources_desc": "設定でストリーミングソースを追加してください",
|
||||||
"add_sources": "ソースを追加",
|
"add_sources": "ソースを追加",
|
||||||
|
|
@ -369,7 +369,7 @@
|
||||||
"playback_speed": "再生速度",
|
"playback_speed": "再生速度",
|
||||||
"on_hold": "保留中",
|
"on_hold": "保留中",
|
||||||
"playback_error": "再生エラー",
|
"playback_error": "再生エラー",
|
||||||
"unknown_error": "再生中に不明なエラーが発生しました。",
|
"unknown_error": "再生中に不明なエラーが発生しました",
|
||||||
"copy_error": "エラー詳細をコピー",
|
"copy_error": "エラー詳細をコピー",
|
||||||
"copied_to_clipboard": "クリップボードにコピーしました",
|
"copied_to_clipboard": "クリップボードにコピーしました",
|
||||||
"dismiss": "閉じる",
|
"dismiss": "閉じる",
|
||||||
|
|
@ -382,7 +382,7 @@
|
||||||
"sources": "ソース",
|
"sources": "ソース",
|
||||||
"finding_sources": "ソースを検索中...",
|
"finding_sources": "ソースを検索中...",
|
||||||
"unknown_source": "不明なソース",
|
"unknown_source": "不明なソース",
|
||||||
"sources_limited": "プロバイダーエラーによりソース数が制限される場合があります。",
|
"sources_limited": "プロバイダーエラーによりソース数が制限される場合があります",
|
||||||
"episodes": "エピソード",
|
"episodes": "エピソード",
|
||||||
"specials": "スペシャル",
|
"specials": "スペシャル",
|
||||||
"season": "シーズン{{season}}",
|
"season": "シーズン{{season}}",
|
||||||
|
|
@ -418,7 +418,7 @@
|
||||||
"line_height": "行の高さ",
|
"line_height": "行の高さ",
|
||||||
"timing_offset": "タイミングオフセット(秒)",
|
"timing_offset": "タイミングオフセット(秒)",
|
||||||
"visual_sync": "ビジュアル同期",
|
"visual_sync": "ビジュアル同期",
|
||||||
"timing_hint": "字幕を早く(-)または遅く(+)ずらして同期させます。",
|
"timing_hint": "字幕を早く(-)または遅く(+)ずらして同期させます",
|
||||||
"reset_defaults": "デフォルトに戻す",
|
"reset_defaults": "デフォルトに戻す",
|
||||||
"mark_intro_start": "イントロ開始をマーク",
|
"mark_intro_start": "イントロ開始をマーク",
|
||||||
"mark_intro_end": "イントロ終了をマーク",
|
"mark_intro_end": "イントロ終了をマーク",
|
||||||
|
|
@ -437,7 +437,7 @@
|
||||||
"incomplete": "ダウンロード未完了",
|
"incomplete": "ダウンロード未完了",
|
||||||
"incomplete_desc": "ダウンロードはまだ完了していません",
|
"incomplete_desc": "ダウンロードはまだ完了していません",
|
||||||
"not_available": "利用不可",
|
"not_available": "利用不可",
|
||||||
"not_available_desc": "ローカルファイルパスはダウンロード完了後に利用可能になります。",
|
"not_available_desc": "ローカルファイルパスはダウンロード完了後に利用可能になります",
|
||||||
"status_downloading": "ダウンロード中",
|
"status_downloading": "ダウンロード中",
|
||||||
"status_completed": "完了",
|
"status_completed": "完了",
|
||||||
"status_paused": "一時停止",
|
"status_paused": "一時停止",
|
||||||
|
|
@ -448,7 +448,7 @@
|
||||||
"streaming_playlist_warning": "動作しない可能性があります - ストリーミングプレイリスト",
|
"streaming_playlist_warning": "動作しない可能性があります - ストリーミングプレイリスト",
|
||||||
"remaining": "残り",
|
"remaining": "残り",
|
||||||
"not_ready": "ダウンロードの準備ができていません",
|
"not_ready": "ダウンロードの準備ができていません",
|
||||||
"not_ready_desc": "ダウンロードが完了するまでお待ちください。",
|
"not_ready_desc": "ダウンロードが完了するまでお待ちください",
|
||||||
"filter_all": "すべて",
|
"filter_all": "すべて",
|
||||||
"filter_active": "アクティブ",
|
"filter_active": "アクティブ",
|
||||||
"filter_done": "完了",
|
"filter_done": "完了",
|
||||||
|
|
@ -456,12 +456,27 @@
|
||||||
"no_filter_results": "フィルター「{{filter}}」に該当するダウンロードがありません",
|
"no_filter_results": "フィルター「{{filter}}」に該当するダウンロードがありません",
|
||||||
"try_different_filter": "別のフィルターをお試しください",
|
"try_different_filter": "別のフィルターをお試しください",
|
||||||
"limitations_title": "ダウンロードの制限",
|
"limitations_title": "ダウンロードの制限",
|
||||||
"limitations_msg": "• 1MB未満のファイルは通常M3U8プレイリストであり、オフライン視聴用にダウンロードできません。これらはオンラインストリーミングのみで動作します。",
|
"limitations_msg": "• 1MB未満のファイルは通常M3U8プレイリストであり、オフライン視聴用にダウンロードできません。これらはオンラインストリーミングのみで動作します",
|
||||||
"remove_title": "ダウンロードを削除",
|
"remove_title": "ダウンロードを削除",
|
||||||
"remove_confirm": "「{{title}}」{{season_episode}}を削除しますか?",
|
"remove_confirm": "「{{title}}」{{season_episode}}を削除しますか?",
|
||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"remove": "削除"
|
"remove": "削除"
|
||||||
},
|
},
|
||||||
|
"parentalGuide": {
|
||||||
|
"labels": {
|
||||||
|
"nudity": "ヌード",
|
||||||
|
"violence": "暴力",
|
||||||
|
"profanity": "不適切な言葉",
|
||||||
|
"alcohol": "飲酒・薬物",
|
||||||
|
"frightening": "恐怖表現"
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"severe": "激しい",
|
||||||
|
"moderate": "中程度",
|
||||||
|
"mild": "軽い",
|
||||||
|
"none": "なし"
|
||||||
|
}
|
||||||
|
},
|
||||||
"addons": {
|
"addons": {
|
||||||
"title": "アドオン",
|
"title": "アドオン",
|
||||||
"reorder_mode": "並び替えモード",
|
"reorder_mode": "並び替えモード",
|
||||||
|
|
@ -485,9 +500,9 @@
|
||||||
"reorder_drag_title": "ドラッグして並び替え",
|
"reorder_drag_title": "ドラッグして並び替え",
|
||||||
"install": "インストール",
|
"install": "インストール",
|
||||||
"config_unavailable_title": "設定不可",
|
"config_unavailable_title": "設定不可",
|
||||||
"config_unavailable_msg": "このアドオンの設定URLを特定できません。",
|
"config_unavailable_msg": "このアドオンの設定URLを特定できません",
|
||||||
"cannot_open_config_title": "設定を開けません",
|
"cannot_open_config_title": "設定を開けません",
|
||||||
"cannot_open_config_msg": "設定URL({{url}})を開けません。アドオンに設定ページがない可能性があります。",
|
"cannot_open_config_msg": "設定URL({{url}})を開けません。アドオンに設定ページがない可能性があります",
|
||||||
"description": "説明",
|
"description": "説明",
|
||||||
"supported_types": "対応タイプ",
|
"supported_types": "対応タイプ",
|
||||||
"catalogs": "カタログ",
|
"catalogs": "カタログ",
|
||||||
|
|
@ -506,7 +521,7 @@
|
||||||
"sign_out_confirm": "Traktアカウントからサインアウトしますか?",
|
"sign_out_confirm": "Traktアカウントからサインアウトしますか?",
|
||||||
"joined": "{{date}}に参加",
|
"joined": "{{date}}に参加",
|
||||||
"sync_settings_title": "同期設定",
|
"sync_settings_title": "同期設定",
|
||||||
"sync_info": "Traktに接続すると、完全な履歴がAPIから直接同期され、ローカルには保存されません。",
|
"sync_info": "Traktに接続すると、完全な履歴がAPIから直接同期され、ローカルには保存されません",
|
||||||
"auto_sync_label": "視聴進捗を自動同期",
|
"auto_sync_label": "視聴進捗を自動同期",
|
||||||
"auto_sync_desc": "視聴進捗をTraktに自動的にアップロード",
|
"auto_sync_desc": "視聴進捗をTraktに自動的にアップロード",
|
||||||
"import_history_label": "視聴履歴をインポート",
|
"import_history_label": "視聴履歴をインポート",
|
||||||
|
|
@ -517,17 +532,17 @@
|
||||||
"show_comments_desc": "利用可能な場合、コンテンツ詳細にTraktコメントを表示",
|
"show_comments_desc": "利用可能な場合、コンテンツ詳細にTraktコメントを表示",
|
||||||
"maintenance_title": "メンテナンス中",
|
"maintenance_title": "メンテナンス中",
|
||||||
"maintenance_unavailable": "Traktが利用できません",
|
"maintenance_unavailable": "Traktが利用できません",
|
||||||
"maintenance_desc": "Trakt連携はメンテナンス作業のため一時停止中です。",
|
"maintenance_desc": "Trakt連携はメンテナンス作業のため一時停止中です",
|
||||||
"maintenance_button": "サービスメンテナンス中",
|
"maintenance_button": "サービスメンテナンス中",
|
||||||
"auth_success_title": "接続に成功しました",
|
"auth_success_title": "接続に成功しました",
|
||||||
"auth_success_msg": "Traktアカウントが正常に接続されました。",
|
"auth_success_msg": "Traktアカウントが正常に接続されました",
|
||||||
"auth_error_title": "認証エラー",
|
"auth_error_title": "認証エラー",
|
||||||
"auth_error_msg": "Traktの認証を完了できませんでした。",
|
"auth_error_msg": "Traktの認証を完了できませんでした",
|
||||||
"auth_error_generic": "認証中にエラーが発生しました。",
|
"auth_error_generic": "認証中にエラーが発生しました",
|
||||||
"sign_out_error": "Traktからサインアウトできませんでした。",
|
"sign_out_error": "Traktからサインアウトできませんでした",
|
||||||
"sync_complete_title": "同期完了",
|
"sync_complete_title": "同期完了",
|
||||||
"sync_success_msg": "Traktと視聴進捗を正常に同期しました。",
|
"sync_success_msg": "Traktと視聴進捗を正常に同期しました",
|
||||||
"sync_error_msg": "同期に失敗しました。もう一度お試しください。"
|
"sync_error_msg": "同期に失敗しました。もう一度お試しください"
|
||||||
},
|
},
|
||||||
"simkl": {
|
"simkl": {
|
||||||
"title": "Simkl設定",
|
"title": "Simkl設定",
|
||||||
|
|
@ -537,35 +552,35 @@
|
||||||
"sign_in": "Simklでサインイン",
|
"sign_in": "Simklでサインイン",
|
||||||
"sign_out": "切断",
|
"sign_out": "切断",
|
||||||
"sign_out_confirm": "Simklアカウントを切断しますか?",
|
"sign_out_confirm": "Simklアカウントを切断しますか?",
|
||||||
"syncing_desc": "視聴済みのアイテムがSimklと同期されます。",
|
"syncing_desc": "視聴済みのアイテムがSimklと同期されます",
|
||||||
"auth_success_title": "接続に成功しました",
|
"auth_success_title": "接続に成功しました",
|
||||||
"auth_success_msg": "Simklアカウントが正常に接続されました。",
|
"auth_success_msg": "Simklアカウントが正常に接続されました",
|
||||||
"auth_error_title": "認証エラー",
|
"auth_error_title": "認証エラー",
|
||||||
"auth_error_msg": "Simklの認証を完了できませんでした。",
|
"auth_error_msg": "Simklの認証を完了できませんでした",
|
||||||
"auth_error_generic": "認証中にエラーが発生しました。",
|
"auth_error_generic": "認証中にエラーが発生しました",
|
||||||
"sign_out_error": "Simklから切断できませんでした。",
|
"sign_out_error": "Simklから切断できませんでした",
|
||||||
"config_error_title": "設定エラー",
|
"config_error_title": "設定エラー",
|
||||||
"config_error_msg": "環境変数にSimkl Client IDがありません。",
|
"config_error_msg": "環境変数にSimkl Client IDがありません",
|
||||||
"conflict_title": "競合",
|
"conflict_title": "競合",
|
||||||
"conflict_msg": "Traktが接続されている場合はSimklに接続できません。先にTraktを切断してください。",
|
"conflict_msg": "Traktが接続されている場合はSimklに接続できません。先にTraktを切断してください",
|
||||||
"disclaimer": "NuvioはSimklと提携していません。"
|
"disclaimer": "NuvioはSimklと提携していません"
|
||||||
},
|
},
|
||||||
"tmdb_settings": {
|
"tmdb_settings": {
|
||||||
"title": "TMDb設定",
|
"title": "TMDb設定",
|
||||||
"metadata_enrichment": "メタデータ補完",
|
"metadata_enrichment": "メタデータ補完",
|
||||||
"metadata_enrichment_desc": "TMDbのデータでコンテンツのメタデータを強化します。",
|
"metadata_enrichment_desc": "TMDbのデータでコンテンツのメタデータを強化します",
|
||||||
"enable_enrichment": "補完を有効にする",
|
"enable_enrichment": "補完を有効にする",
|
||||||
"enable_enrichment_desc": "キャスト、年齢制限、ロゴ/ポスター、制作情報についてTMDbのデータでアドオンのメタデータを補完します。",
|
"enable_enrichment_desc": "キャスト、年齢制限、ロゴ/ポスター、製作情報についてTMDbのデータでアドオンのメタデータを補完します",
|
||||||
"localized_text": "ローカライズされたテキスト",
|
"localized_text": "ローカライズされたテキスト",
|
||||||
"localized_text_desc": "TMDbから希望言語でタイトルと説明を取得します。",
|
"localized_text_desc": "TMDbから希望言語でタイトルと説明を取得します",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
"change": "変更",
|
"change": "変更",
|
||||||
"logo_preview": "ロゴプレビュー",
|
"logo_preview": "ロゴプレビュー",
|
||||||
"logo_preview_desc": "選択した言語でローカライズされたロゴのプレビューです。",
|
"logo_preview_desc": "選択した言語でローカライズされたロゴのプレビューです",
|
||||||
"example": "例:",
|
"example": "例:",
|
||||||
"no_logo": "利用可能なロゴがありません",
|
"no_logo": "利用可能なロゴがありません",
|
||||||
"enrichment_options": "補完オプション",
|
"enrichment_options": "補完オプション",
|
||||||
"enrichment_options_desc": "TMDbから取得するデータを選択します。",
|
"enrichment_options_desc": "TMDbから取得するデータを選択します",
|
||||||
"cast_crew": "キャストとスタッフ",
|
"cast_crew": "キャストとスタッフ",
|
||||||
"cast_crew_desc": "プロフィール写真付きの俳優、監督、脚本家",
|
"cast_crew_desc": "プロフィール写真付きの俳優、監督、脚本家",
|
||||||
"title_description": "タイトルと説明",
|
"title_description": "タイトルと説明",
|
||||||
|
|
@ -579,26 +594,26 @@
|
||||||
"recommendations": "おすすめ",
|
"recommendations": "おすすめ",
|
||||||
"recommendations_desc": "類似コンテンツの提案",
|
"recommendations_desc": "類似コンテンツの提案",
|
||||||
"episode_data": "エピソードデータ",
|
"episode_data": "エピソードデータ",
|
||||||
"episode_data_desc": "TVシリーズのエピソードサムネイル、情報、フォールバックデータ",
|
"episode_data_desc": "シリーズのエピソードサムネイル、情報、フォールバックデータ",
|
||||||
"season_posters": "シーズンポスター",
|
"season_posters": "シーズンポスター",
|
||||||
"season_posters_desc": "特定のシーズンに割り当てられたポスター画像",
|
"season_posters_desc": "特定のシーズンに割り当てられたポスター画像",
|
||||||
"production_info": "制作情報",
|
"production_info": "製作情報",
|
||||||
"production_info_desc": "ロゴ付きのテレビ局と制作会社",
|
"production_info_desc": "ロゴ付きのテレビ局と制作会社",
|
||||||
"movie_details": "映画の詳細",
|
"movie_details": "映画の詳細",
|
||||||
"movie_details_desc": "製作費、興行収入、上映時間、キャッチコピー",
|
"movie_details_desc": "製作費、興行収入、上映時間、キャッチコピー",
|
||||||
"tv_details": "TVシリーズの詳細",
|
"tv_details": "シリーズの詳細",
|
||||||
"tv_details_desc": "ステータス、シーズン数、ネットワーク、制作者",
|
"tv_details_desc": "ステータス、シーズン数、ネットワーク、制作者",
|
||||||
"movie_collections": "映画コレクション",
|
"movie_collections": "映画コレクション",
|
||||||
"movie_collections_desc": "映画シリーズ(マーベル、スターウォーズなど)",
|
"movie_collections_desc": "映画シリーズ(マーベル、スターウォーズなど)",
|
||||||
"api_configuration": "API設定",
|
"api_configuration": "API設定",
|
||||||
"api_configuration_desc": "拡張機能のためにTMDB APIアクセスを設定します。",
|
"api_configuration_desc": "拡張機能のためにTMDB APIアクセスを設定します",
|
||||||
"custom_api_key": "カスタムAPIキー",
|
"custom_api_key": "カスタムAPIキー",
|
||||||
"custom_api_key_desc": "パフォーマンス向上のために独自のTMDB APIキーを使用します。",
|
"custom_api_key_desc": "パフォーマンス向上のために独自のTMDB APIキーを使用します",
|
||||||
"custom_key_active": "カスタムAPIキーが有効",
|
"custom_key_active": "カスタムAPIキーが有効",
|
||||||
"api_key_required": "APIキーが必要",
|
"api_key_required": "APIキーが必要",
|
||||||
"api_key_placeholder": "TMDB APIキー(v3)を貼り付け",
|
"api_key_placeholder": "TMDB APIキー(v3)を貼り付け",
|
||||||
"how_to_get_key": "TMDB APIキーの取得方法",
|
"how_to_get_key": "TMDB APIキーの取得方法",
|
||||||
"built_in_key_msg": "現在、組み込みAPIキーを使用しています。パフォーマンス向上のために独自のキーの使用をご検討ください。",
|
"built_in_key_msg": "現在、組み込みAPIキーを使用しています。パフォーマンス向上のために独自のキーの使用をご検討ください",
|
||||||
"cache_size": "キャッシュサイズ",
|
"cache_size": "キャッシュサイズ",
|
||||||
"clear_cache": "キャッシュをクリア",
|
"clear_cache": "キャッシュをクリア",
|
||||||
"cache_days": "TMDBデータはパフォーマンス向上のため7日間保存されます",
|
"cache_days": "TMDBデータはパフォーマンス向上のため7日間保存されます",
|
||||||
|
|
@ -607,23 +622,23 @@
|
||||||
"popular": "人気",
|
"popular": "人気",
|
||||||
"all_languages": "すべての言語",
|
"all_languages": "すべての言語",
|
||||||
"search_results": "検索結果",
|
"search_results": "検索結果",
|
||||||
"no_languages_found": "「{{query}}」の言語が見つかりません",
|
"no_languages_found": "「{{query}}」に一致する言語が見つかりません",
|
||||||
"clear_search": "検索をクリア",
|
"clear_search": "検索をクリア",
|
||||||
"clear_cache_title": "TMDBキャッシュをクリア",
|
"clear_cache_title": "TMDBキャッシュをクリア",
|
||||||
"clear_cache_msg": "保存されているすべてのTMDBデータ({{size}})が削除されます。",
|
"clear_cache_msg": "保存されているすべてのTMDBデータ({{size}})が削除されます",
|
||||||
"clear_cache_success": "TMDBキャッシュをクリアしました。",
|
"clear_cache_success": "TMDBキャッシュをクリアしました",
|
||||||
"clear_cache_error": "キャッシュのクリアに失敗しました。",
|
"clear_cache_error": "キャッシュのクリアに失敗しました",
|
||||||
"clear_api_key_title": "APIキーを削除",
|
"clear_api_key_title": "APIキーを削除",
|
||||||
"clear_api_key_msg": "カスタムAPIキーを削除してデフォルトに戻しますか?",
|
"clear_api_key_msg": "カスタムAPIキーを削除してデフォルトに戻しますか?",
|
||||||
"clear_api_key_success": "APIキーを削除しました",
|
"clear_api_key_success": "APIキーを削除しました",
|
||||||
"clear_api_key_error": "APIキーの削除に失敗しました",
|
"clear_api_key_error": "APIキーの削除に失敗しました",
|
||||||
"empty_api_key": "APIキーを空にすることはできません。",
|
"empty_api_key": "APIキーを空にすることはできません",
|
||||||
"invalid_api_key": "無効なAPIキーです。確認してもう一度お試しください。",
|
"invalid_api_key": "無効なAPIキーです。確認してもう一度お試しください",
|
||||||
"save_error": "保存中にエラーが発生しました。もう一度お試しください。",
|
"save_error": "保存中にエラーが発生しました。もう一度お試しください",
|
||||||
"using_builtin_key": "組み込みTMDB APIキーを使用しています。",
|
"using_builtin_key": "組み込みTMDB APIキーを使用しています",
|
||||||
"using_custom_key": "カスタムTMDB APIキーを使用しています。",
|
"using_custom_key": "カスタムTMDB APIキーを使用しています",
|
||||||
"enter_custom_key": "カスタムTMDB APIキーを入力して保存してください。",
|
"enter_custom_key": "カスタムTMDB APIキーを入力して保存してください",
|
||||||
"key_verified": "APIキーが正常に確認・保存されました。"
|
"key_verified": "APIキーが正常に確認・保存されました"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
|
|
@ -654,6 +669,7 @@
|
||||||
"romanian": "ルーマニア語",
|
"romanian": "ルーマニア語",
|
||||||
"albanian": "アルバニア語",
|
"albanian": "アルバニア語",
|
||||||
"catalan": "カタルーニャ語",
|
"catalan": "カタルーニャ語",
|
||||||
|
"vietnamese": "ベトナム語",
|
||||||
"japanese": "日本語",
|
"japanese": "日本語",
|
||||||
"account": "アカウント",
|
"account": "アカウント",
|
||||||
"content_discovery": "コンテンツとディスカバー",
|
"content_discovery": "コンテンツとディスカバー",
|
||||||
|
|
@ -661,6 +677,7 @@
|
||||||
"integrations": "連携",
|
"integrations": "連携",
|
||||||
"playback": "再生",
|
"playback": "再生",
|
||||||
"backup_restore": "バックアップと復元",
|
"backup_restore": "バックアップと復元",
|
||||||
|
"backup_restore_desc": "バックアップと復元を作成する",
|
||||||
"updates": "アップデート",
|
"updates": "アップデート",
|
||||||
"about": "アプリについて",
|
"about": "アプリについて",
|
||||||
"developer": "開発者",
|
"developer": "開発者",
|
||||||
|
|
@ -771,7 +788,7 @@
|
||||||
"title": "Nuvio Sync",
|
"title": "Nuvio Sync",
|
||||||
"description": "Nuvioデバイス間でデータを同期",
|
"description": "Nuvioデバイス間でデータを同期",
|
||||||
"hero_title": "クラウド同期",
|
"hero_title": "クラウド同期",
|
||||||
"hero_subtitle": "すべてのデバイスでアドオン、進捗、ライブラリを同期します。",
|
"hero_subtitle": "すべてのデバイスでアドオン、進捗、ライブラリを同期します",
|
||||||
"auth": {
|
"auth": {
|
||||||
"account": "アカウント",
|
"account": "アカウント",
|
||||||
"not_configured": "Supabaseが設定されていません",
|
"not_configured": "Supabaseが設定されていません",
|
||||||
|
|
@ -788,11 +805,11 @@
|
||||||
"watch_progress": "視聴進捗",
|
"watch_progress": "視聴進捗",
|
||||||
"library_items": "ライブラリアイテム",
|
"library_items": "ライブラリアイテム",
|
||||||
"watched_items": "視聴済みアイテム",
|
"watched_items": "視聴済みアイテム",
|
||||||
"signin_required": "リモートデータ数を読み込むにはサインインしてください。"
|
"signin_required": "リモートデータ数を読み込むにはサインインしてください"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"title": "アクション",
|
"title": "アクション",
|
||||||
"description": "クラウドからこのデバイスに取得するか、このデバイスからプッシュします。",
|
"description": "クラウドからこのデバイスに取得するか、このデバイスからプッシュします",
|
||||||
"pull_btn": "クラウドから取得",
|
"pull_btn": "クラウドから取得",
|
||||||
"push_btn": "デバイスからプッシュ",
|
"push_btn": "デバイスからプッシュ",
|
||||||
"manage_account": "アカウントを管理",
|
"manage_account": "アカウントを管理",
|
||||||
|
|
@ -801,11 +818,11 @@
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"pull_success_title": "クラウドデータを取得しました",
|
"pull_success_title": "クラウドデータを取得しました",
|
||||||
"pull_success_msg": "最新のクラウドデータがこのデバイスにダウンロードされました。",
|
"pull_success_msg": "最新のクラウドデータがこのデバイスにダウンロードされました",
|
||||||
"pull_failed_title": "取得に失敗しました",
|
"pull_failed_title": "取得に失敗しました",
|
||||||
"pull_failed_msg": "クラウドからデータをダウンロードできませんでした",
|
"pull_failed_msg": "クラウドからデータをダウンロードできませんでした",
|
||||||
"push_success_title": "プッシュ完了",
|
"push_success_title": "プッシュ完了",
|
||||||
"push_success_msg": "デバイスデータがクラウドにアップロードされました。",
|
"push_success_msg": "デバイスデータがクラウドにアップロードされました",
|
||||||
"push_failed_title": "プッシュに失敗しました",
|
"push_failed_title": "プッシュに失敗しました",
|
||||||
"push_failed_msg": "ローカルデータのアップロードに失敗しました",
|
"push_failed_msg": "ローカルデータのアップロードに失敗しました",
|
||||||
"sign_out_failed": "サインアウトに失敗しました",
|
"sign_out_failed": "サインアウトに失敗しました",
|
||||||
|
|
@ -813,15 +830,15 @@
|
||||||
},
|
},
|
||||||
"external_sync": {
|
"external_sync": {
|
||||||
"title": "外部同期の優先度",
|
"title": "外部同期の優先度",
|
||||||
"active_msg": "{{services}}がアクティブです。視聴進捗とライブラリの更新はNuvioクラウドの代わりにこれらのサービスによって管理されます。",
|
"active_msg": "{{services}}がアクティブです。視聴進捗とライブラリの更新はNuvioクラウドの代わりにこれらのサービスによって管理されます",
|
||||||
"inactive_msg": "TraktまたはSimklの同期が有効な場合、視聴進捗とライブラリの更新はNuvioクラウドの代わりにそれらのサービスを使用します。"
|
"inactive_msg": "TraktまたはSimklの同期が有効な場合、視聴進捗とライブラリの更新はNuvioクラウドの代わりにそれらのサービスを使用します"
|
||||||
},
|
},
|
||||||
"pre_auth": {
|
"pre_auth": {
|
||||||
"title": "同期前に",
|
"title": "同期前に",
|
||||||
"description": "クラウド同期を開始するにはサインインしてください。",
|
"description": "クラウド同期を開始するにはサインインしてください",
|
||||||
"point_1": "• アドオンとプラグインの設定",
|
"point_1": "• アドオンとプラグインの設定",
|
||||||
"point_2": "• 視聴進捗とライブラリ",
|
"point_2": "• 視聴進捗とライブラリ",
|
||||||
"env_warning": "同期を有効にするにはEXPO_PUBLIC_SUPABASE_URLとEXPO_PUBLIC_SUPABASE_ANON_KEYを設定してください。"
|
"env_warning": "同期を有効にするにはEXPO_PUBLIC_SUPABASE_URLとEXPO_PUBLIC_SUPABASE_ANON_KEYを設定してください"
|
||||||
},
|
},
|
||||||
"connection": "接続"
|
"connection": "接続"
|
||||||
}
|
}
|
||||||
|
|
@ -830,22 +847,22 @@
|
||||||
"title": "プライバシーとデータ",
|
"title": "プライバシーとデータ",
|
||||||
"settings_desc": "テレメトリーとデータ収集の管理",
|
"settings_desc": "テレメトリーとデータ収集の管理",
|
||||||
"info_title": "あなたのプライバシーは重要です",
|
"info_title": "あなたのプライバシーは重要です",
|
||||||
"info_description": "収集・共有されるデータを管理してください。分析はデフォルトで無効、エラーレポートはデフォルトで匿名です。",
|
"info_description": "収集・共有されるデータを管理してください。分析はデフォルトで無効、エラーレポートはデフォルトで匿名です",
|
||||||
"analytics_enabled_title": "分析が有効になりました",
|
"analytics_enabled_title": "分析が有効になりました",
|
||||||
"analytics_enabled_message": "アプリ改善のために使用データが収集されます。いつでも無効にできます。",
|
"analytics_enabled_message": "アプリ改善のために使用データが収集されます。いつでも無効にできます",
|
||||||
"disable_error_reporting_title": "エラーレポートを無効にしますか?",
|
"disable_error_reporting_title": "エラーレポートを無効にしますか?",
|
||||||
"disable_error_reporting_message": "エラーレポートを無効にすると、クラッシュや問題について通知されなくなります。",
|
"disable_error_reporting_message": "エラーレポートを無効にすると、クラッシュや問題について通知されなくなります",
|
||||||
"enable_session_replay_title": "セッションリプレイを有効にしますか?",
|
"enable_session_replay_title": "セッションリプレイを有効にしますか?",
|
||||||
"enable_session_replay_message": "セッションリプレイはエラー発生時に画面を記録します。画面上に表示されたコンテンツが記録される可能性があります。",
|
"enable_session_replay_message": "セッションリプレイはエラー発生時に画面を記録します。画面上に表示されたコンテンツが記録される可能性があります",
|
||||||
"enable_pii_title": "PII収集を有効にしますか?",
|
"enable_pii_title": "PII収集を有効にしますか?",
|
||||||
"enable_pii_message": "IPアドレスやデバイスの詳細などの個人情報の収集が可能になります。",
|
"enable_pii_message": "IPアドレスやデバイスの詳細などの個人情報の収集が可能になります",
|
||||||
"disable_all_title": "すべてのテレメトリーを無効にしますか?",
|
"disable_all_title": "すべてのテレメトリーを無効にしますか?",
|
||||||
"disable_all_message": "分析、エラーレポート、セッションリプレイがすべて無効になります。",
|
"disable_all_message": "分析、エラーレポート、セッションリプレイがすべて無効になります",
|
||||||
"disable_all_button": "すべて無効にする",
|
"disable_all_button": "すべて無効にする",
|
||||||
"all_disabled_title": "テレメトリーが無効になりました",
|
"all_disabled_title": "テレメトリーが無効になりました",
|
||||||
"all_disabled_message": "すべてのデータ収集が無効になりました。変更はアプリの再起動後に有効になります。",
|
"all_disabled_message": "すべてのデータ収集が無効になりました。変更はアプリの再起動後に有効になります",
|
||||||
"reset_title": "推奨設定に戻す",
|
"reset_title": "推奨設定に戻す",
|
||||||
"reset_message": "プライバシー設定が推奨デフォルト値にリセットされました。",
|
"reset_message": "プライバシー設定が推奨デフォルト値にリセットされました",
|
||||||
"section_analytics": "分析",
|
"section_analytics": "分析",
|
||||||
"analytics_title": "使用状況統計",
|
"analytics_title": "使用状況統計",
|
||||||
"analytics_description": "匿名の使用パターンと画面ビューを収集",
|
"analytics_description": "匿名の使用パターンと画面ビューを収集",
|
||||||
|
|
@ -868,12 +885,12 @@
|
||||||
"summary_errors": "エラーレポート",
|
"summary_errors": "エラーレポート",
|
||||||
"summary_replay": "セッションリプレイ",
|
"summary_replay": "セッションリプレイ",
|
||||||
"summary_pii": "デバイス情報",
|
"summary_pii": "デバイス情報",
|
||||||
"restart_note_detailed": "* 分析とエラーレポートの変更は即座に有効になります。セッションリプレイとPII設定はアプリの再起動が必要です。"
|
"restart_note_detailed": "* 分析とエラーレポートの変更は即座に有効になります。セッションリプレイとPII設定はアプリの再起動が必要です"
|
||||||
},
|
},
|
||||||
"ai_settings": {
|
"ai_settings": {
|
||||||
"title": "AIアシスタント",
|
"title": "AIアシスタント",
|
||||||
"info_title": "AIを使ったチャット",
|
"info_title": "AIを使ったチャット",
|
||||||
"info_desc": "高度なAIを使って映画やエピソードについて質問できます。",
|
"info_desc": "高度なAIを使って映画やエピソードについて質問できます",
|
||||||
"feature_1": "エピソード固有の分析とコンテキスト",
|
"feature_1": "エピソード固有の分析とコンテキスト",
|
||||||
"feature_2": "ストーリーの説明とキャラクターの考察",
|
"feature_2": "ストーリーの説明とキャラクターの考察",
|
||||||
"feature_3": "トリビアと舞台裏の事実",
|
"feature_3": "トリビアと舞台裏の事実",
|
||||||
|
|
@ -887,9 +904,9 @@
|
||||||
"remove": "削除",
|
"remove": "削除",
|
||||||
"get_free_key": "OpenRouterから無料のAPIキーを取得",
|
"get_free_key": "OpenRouterから無料のAPIキーを取得",
|
||||||
"enable_chat": "AIチャットを有効にする",
|
"enable_chat": "AIチャットを有効にする",
|
||||||
"enable_chat_desc": "有効にすると、コンテンツページに「AIに聞く」ボタンが表示されます。",
|
"enable_chat_desc": "有効にすると、コンテンツページに「AIに聞く」ボタンが表示されます",
|
||||||
"chat_enabled": "AIチャットが有効になりました",
|
"chat_enabled": "AIチャットが有効になりました",
|
||||||
"chat_enabled_desc": "映画やシリーズについて質問できるようになりました。",
|
"chat_enabled_desc": "映画やシリーズについて質問できるようになりました",
|
||||||
"how_it_works": "仕組み",
|
"how_it_works": "仕組み",
|
||||||
"how_it_works_desc": "• OpenRouterは複数のAIモデルへのアクセスを提供します\n• APIキーはプライベートで安全に保たれます\n• 無料プランには十分な使用制限があります\n• 特定のエピソード/映画のコンテキストでチャットできます\n• 詳細な分析と説明を受け取れます",
|
"how_it_works_desc": "• OpenRouterは複数のAIモデルへのアクセスを提供します\n• APIキーはプライベートで安全に保たれます\n• 無料プランには十分な使用制限があります\n• 特定のエピソード/映画のコンテキストでチャットできます\n• 詳細な分析と説明を受け取れます",
|
||||||
"error_invalid_key": "有効なAPIキーを入力してください",
|
"error_invalid_key": "有効なAPIキーを入力してください",
|
||||||
|
|
@ -897,7 +914,7 @@
|
||||||
"success_saved": "OpenRouter APIキーを正常に保存しました!",
|
"success_saved": "OpenRouter APIキーを正常に保存しました!",
|
||||||
"error_save": "APIキーの保存に失敗しました",
|
"error_save": "APIキーの保存に失敗しました",
|
||||||
"confirm_remove_title": "APIキーを削除",
|
"confirm_remove_title": "APIキーを削除",
|
||||||
"confirm_remove_msg": "OpenRouter APIキーを削除しますか?これによりAIチャット機能が無効になります。",
|
"confirm_remove_msg": "OpenRouter APIキーを削除しますか?これによりAIチャット機能が無効になります",
|
||||||
"success_removed": "APIキーを削除しました",
|
"success_removed": "APIキーを削除しました",
|
||||||
"error_remove": "APIキーの削除に失敗しました"
|
"error_remove": "APIキーの削除に失敗しました"
|
||||||
},
|
},
|
||||||
|
|
@ -908,21 +925,21 @@
|
||||||
"auto": "自動",
|
"auto": "自動",
|
||||||
"show_titles": "ポスターにタイトルを表示",
|
"show_titles": "ポスターにタイトルを表示",
|
||||||
"show_titles_desc": "各ポスターの下にタイトルテキストを表示",
|
"show_titles_desc": "各ポスターの下にタイトルテキストを表示",
|
||||||
"phone_only_hint": "電話のみ対象。タブレットはアダプティブレイアウトを維持します。",
|
"phone_only_hint": "電話のみ対象。タブレットはアダプティブレイアウトを維持します",
|
||||||
"catalogs_group": "カタログ",
|
"catalogs_group": "カタログ",
|
||||||
"enabled_count": "{{total}}中{{enabled}}つが有効",
|
"enabled_count": "{{total}}中{{enabled}}つが有効",
|
||||||
"rename_hint": "カタログを長押しして名前を変更",
|
"rename_hint": "カタログを長押しして名前を変更",
|
||||||
"rename_modal_title": "カタログ名を変更",
|
"rename_modal_title": "カタログ名を変更",
|
||||||
"rename_placeholder": "新しいカタログ名を入力",
|
"rename_placeholder": "新しいカタログ名を入力",
|
||||||
"error_save_name": "カスタム名の保存に失敗しました。"
|
"error_save_name": "カスタム名の保存に失敗しました"
|
||||||
},
|
},
|
||||||
"continue_watching_settings": {
|
"continue_watching_settings": {
|
||||||
"title": "視聴を続ける",
|
"title": "視聴を続ける",
|
||||||
"playback_behavior": "再生の動作",
|
"playback_behavior": "再生の動作",
|
||||||
"use_cached": "保存されたストリームを使用",
|
"use_cached": "保存されたストリームを使用",
|
||||||
"use_cached_desc": "有効にすると、以前再生したストリームを使って直接プレーヤーが開きます。",
|
"use_cached_desc": "有効にすると、以前再生したストリームを使って直接プレーヤーが開きます",
|
||||||
"open_metadata": "詳細画面を開く",
|
"open_metadata": "詳細画面を開く",
|
||||||
"open_metadata_desc": "保存されたストリームが無効の場合、メタデータ画面を開きます。",
|
"open_metadata_desc": "保存されたストリームが無効の場合、メタデータ画面を開きます",
|
||||||
"card_appearance": "カードの外観",
|
"card_appearance": "カードの外観",
|
||||||
"card_style": "カードスタイル",
|
"card_style": "カードスタイル",
|
||||||
"card_style_desc": "ホーム画面での「視聴を続ける」アイテムの表示方法を選択",
|
"card_style_desc": "ホーム画面での「視聴を続ける」アイテムの表示方法を選択",
|
||||||
|
|
@ -932,7 +949,7 @@
|
||||||
"cache_duration": "ストリームキャッシュ期間",
|
"cache_duration": "ストリームキャッシュ期間",
|
||||||
"cache_duration_desc": "ストリームリンクの有効期限が切れるまでの保存期間",
|
"cache_duration_desc": "ストリームリンクの有効期限が切れるまでの保存期間",
|
||||||
"important_note": "重要なお知らせ",
|
"important_note": "重要なお知らせ",
|
||||||
"important_note_text": "すべてのストリームリンクが保存期間中ずっとアクティブとは限りません。",
|
"important_note_text": "すべてのストリームリンクが保存期間中ずっとアクティブとは限りません",
|
||||||
"how_it_works": "仕組み",
|
"how_it_works": "仕組み",
|
||||||
"how_it_works_cached": "• ストリームは再生後に選択した期間保存されます\n• 保存されたストリームは使用前に検証されます\n• キャッシュが期限切れの場合はコンテンツ画面に戻ります",
|
"how_it_works_cached": "• ストリームは再生後に選択した期間保存されます\n• 保存されたストリームは使用前に検証されます\n• キャッシュが期限切れの場合はコンテンツ画面に戻ります",
|
||||||
"how_it_works_uncached": "• 保存されたストリームが無効の場合、アイテムをタップするとコンテンツ画面が開きます\n• メタデータ画面は詳細を表示して手動選択ができます",
|
"how_it_works_uncached": "• 保存されたストリームが無効の場合、アイテムをタップするとコンテンツ画面が開きます\n• メタデータ画面は詳細を表示して手動選択ができます",
|
||||||
|
|
@ -960,21 +977,21 @@
|
||||||
"gratitude_desc": "コードの1行1行、バグ報告、提案がすべての人のためにNuvioを改善します",
|
"gratitude_desc": "コードの1行1行、バグ報告、提案がすべての人のためにNuvioを改善します",
|
||||||
"special_thanks_title": "特別な感謝",
|
"special_thanks_title": "特別な感謝",
|
||||||
"special_thanks_desc": "これらの素晴らしい人たちがNuvioコミュニティとサーバーの維持を支援しています",
|
"special_thanks_desc": "これらの素晴らしい人たちがNuvioコミュニティとサーバーの維持を支援しています",
|
||||||
"donors_desc": "私たちが構築しているものを信じてくれてありがとうございます。",
|
"donors_desc": "私たちが構築しているものを信じてくれてありがとうございます",
|
||||||
"latest_donations": "最新",
|
"latest_donations": "最新",
|
||||||
"leaderboard": "ランキング",
|
"leaderboard": "ランキング",
|
||||||
"loading_donors": "寄付者を読み込み中...",
|
"loading_donors": "寄付者を読み込み中...",
|
||||||
"no_donors": "寄付者がいません",
|
"no_donors": "寄付者がいません",
|
||||||
"error_rate_limit": "GitHub APIレート制限を超えました。後でもう一度試してください。",
|
"error_rate_limit": "GitHub APIレート制限を超えました。後でもう一度試してください",
|
||||||
"error_failed": "貢献者の読み込みに失敗しました。インターネット接続を確認してください。",
|
"error_failed": "貢献者の読み込みに失敗しました。インターネット接続を確認してください",
|
||||||
"retry": "もう一度試す",
|
"retry": "もう一度試す",
|
||||||
"no_contributors": "貢献者が見つかりません",
|
"no_contributors": "貢献者が見つかりません",
|
||||||
"loading_contributors": "貢献者を読み込み中..."
|
"loading_contributors": "貢献者を読み込み中..."
|
||||||
},
|
},
|
||||||
"debrid": {
|
"debrid": {
|
||||||
"title": "Debrid連携",
|
"title": "Debrid連携",
|
||||||
"description_torbox": "Torbox連携で4K品質のストリームと超高速スピードを解放。",
|
"description_torbox": "Torbox連携で4K品質のストリームと超高速スピードを解放",
|
||||||
"description_torrentio": "映画やシリーズのトレントストリームを受け取るようにTorrentioを設定します。",
|
"description_torrentio": "映画やシリーズのトレントストリームを受け取るようにTorrentioを設定します",
|
||||||
"tab_torbox": "TorBox",
|
"tab_torbox": "TorBox",
|
||||||
"tab_torrentio": "Torrentio",
|
"tab_torrentio": "Torrentio",
|
||||||
"status_connected": "接続済み",
|
"status_connected": "接続済み",
|
||||||
|
|
@ -993,23 +1010,23 @@
|
||||||
"downloaded": "ダウンロード済み",
|
"downloaded": "ダウンロード済み",
|
||||||
"status_active": "アクティブ",
|
"status_active": "アクティブ",
|
||||||
"connected_title": "✓ TorBoxに接続済み",
|
"connected_title": "✓ TorBoxに接続済み",
|
||||||
"connected_desc": "TorBoxアドオンがアクティブでプレミアムストリームを提供しています。",
|
"connected_desc": "TorBoxアドオンがアクティブでプレミアムストリームを提供しています",
|
||||||
"configure_title": "アドオンを設定",
|
"configure_title": "アドオンを設定",
|
||||||
"configure_desc": "体験をカスタマイズしてください。",
|
"configure_desc": "体験をカスタマイズしてください",
|
||||||
"open_settings": "設定を開く",
|
"open_settings": "設定を開く",
|
||||||
"what_is_debrid": "Debridサービスとは?",
|
"what_is_debrid": "Debridサービスとは?",
|
||||||
"enter_api_key": "APIキーを入力",
|
"enter_api_key": "APIキーを入力",
|
||||||
"connect_button": "接続してインストール",
|
"connect_button": "接続してインストール",
|
||||||
"connecting": "接続中...",
|
"connecting": "接続中...",
|
||||||
"unlock_speeds_title": "プレミアム速度を解放",
|
"unlock_speeds_title": "プレミアム速度を解放",
|
||||||
"unlock_speeds_desc": "Torboxサブスクリプションを購入して、バッファリングなしの高品質ストリームにアクセスしましょう。",
|
"unlock_speeds_desc": "Torboxサブスクリプションを購入して、バッファリングなしの高品質ストリームにアクセスしましょう",
|
||||||
"get_subscription": "サブスクリプションを取得",
|
"get_subscription": "サブスクリプションを取得",
|
||||||
"powered_by": "提供元",
|
"powered_by": "提供元",
|
||||||
"disclaimer_torbox": "NuvioはTorboxと一切関係ありません。",
|
"disclaimer_torbox": "NuvioはTorboxと一切関係ありません",
|
||||||
"disclaimer_torrentio": "NuvioはTorrentioと一切関係ありません。",
|
"disclaimer_torrentio": "NuvioはTorrentioと一切関係ありません",
|
||||||
"installed_badge": "✓ インストール済み",
|
"installed_badge": "✓ インストール済み",
|
||||||
"promo_title": "⚡ Debridサービスが必要ですか?",
|
"promo_title": "⚡ Debridサービスが必要ですか?",
|
||||||
"promo_desc": "バッファリングなしの超高速4Kストリーミング。プレミアムトレントと即時ダウンロード。",
|
"promo_desc": "バッファリングなしの超高速4Kストリーミング。プレミアムトレントと即時ダウンロード",
|
||||||
"promo_button": "TorBoxサブスクリプションを購入",
|
"promo_button": "TorBoxサブスクリプションを購入",
|
||||||
"service_label": "Debridサービス *",
|
"service_label": "Debridサービス *",
|
||||||
"api_key_label": "APIキー *",
|
"api_key_label": "APIキー *",
|
||||||
|
|
@ -1026,11 +1043,11 @@
|
||||||
"updating": "更新中...",
|
"updating": "更新中...",
|
||||||
"remove_button": "Torrentioを削除",
|
"remove_button": "Torrentioを削除",
|
||||||
"error_api_required": "APIキーが必要",
|
"error_api_required": "APIキーが必要",
|
||||||
"error_api_required_desc": "TorrentioをインストールするにはDebridサービスのAPIキーを入力してください。",
|
"error_api_required_desc": "TorrentioをインストールするにはDebridサービスのAPIキーを入力してください",
|
||||||
"success_installed": "Torrentioアドオンを正常にインストールしました!",
|
"success_installed": "Torrentioアドオンを正常にインストールしました!",
|
||||||
"success_removed": "Torrentioアドオンを削除しました",
|
"success_removed": "Torrentioアドオンを削除しました",
|
||||||
"alert_disconnect_title": "Torboxを切断",
|
"alert_disconnect_title": "Torboxを切断",
|
||||||
"alert_disconnect_msg": "Torboxを切断しますか?これによりアドオンが削除されAPIキーがクリアされます。"
|
"alert_disconnect_msg": "Torboxを切断しますか?これによりアドオンが削除されAPIキーがクリアされます"
|
||||||
},
|
},
|
||||||
"home_screen": {
|
"home_screen": {
|
||||||
"title": "ホーム画面設定",
|
"title": "ホーム画面設定",
|
||||||
|
|
@ -1055,7 +1072,7 @@
|
||||||
"manage_selected_catalogs": "選択したカタログを管理",
|
"manage_selected_catalogs": "選択したカタログを管理",
|
||||||
"dynamic_bg": "ダイナミックヒーロー背景",
|
"dynamic_bg": "ダイナミックヒーロー背景",
|
||||||
"dynamic_bg_desc": "カルーセルの後ろにぼかしたバナー",
|
"dynamic_bg_desc": "カルーセルの後ろにぼかしたバナー",
|
||||||
"performance_note": "低スペックデバイスではパフォーマンスに影響する可能性があります。",
|
"performance_note": "低スペックデバイスではパフォーマンスに影響する可能性があります",
|
||||||
"posters": "ポスター",
|
"posters": "ポスター",
|
||||||
"show_titles": "タイトルを表示",
|
"show_titles": "タイトルを表示",
|
||||||
"poster_size": "ポスターサイズ",
|
"poster_size": "ポスターサイズ",
|
||||||
|
|
@ -1067,23 +1084,23 @@
|
||||||
"corners_rounded": "丸め",
|
"corners_rounded": "丸め",
|
||||||
"corners_pill": "ピル",
|
"corners_pill": "ピル",
|
||||||
"about_these_settings": "これらの設定について",
|
"about_these_settings": "これらの設定について",
|
||||||
"about_desc": "これらの設定はホーム画面でのコンテンツ表示方法を制御します。変更はすぐに適用されます。",
|
"about_desc": "これらの設定はホーム画面でのコンテンツ表示方法を制御します。変更はすぐに適用されます",
|
||||||
"hero_catalogs": {
|
"hero_catalogs": {
|
||||||
"title": "ヒーローカタログ",
|
"title": "ヒーローカタログ",
|
||||||
"select_all": "すべて選択",
|
"select_all": "すべて選択",
|
||||||
"clear_all": "すべてクリア",
|
"clear_all": "すべてクリア",
|
||||||
"info": "ヒーローセクションに表示するカタログを選択してください。変更を保存することを忘れずに。",
|
"info": "ヒーローセクションに表示するカタログを選択してください。変更を保存することを忘れずに",
|
||||||
"settings_saved": "設定を保存しました",
|
"settings_saved": "設定を保存しました",
|
||||||
"error_load": "カタログの読み込みに失敗しました",
|
"error_load": "カタログの読み込みに失敗しました",
|
||||||
"movies": "映画",
|
"movies": "映画",
|
||||||
"tv_shows": "TVシリーズ"
|
"tv_shows": "シリーズ"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "カレンダー",
|
"title": "カレンダー",
|
||||||
"loading": "カレンダーを読み込み中...",
|
"loading": "カレンダーを読み込み中...",
|
||||||
"no_scheduled_episodes": "予定されたエピソードがありません",
|
"no_scheduled_episodes": "予定されたエピソードがありません",
|
||||||
"check_back_later": "後で確認してください",
|
"check_back_later": "後でもう一度確認してください",
|
||||||
"showing_episodes_for": "{{date}}のエピソード",
|
"showing_episodes_for": "{{date}}のエピソード",
|
||||||
"show_all_episodes": "すべてのエピソードを表示",
|
"show_all_episodes": "すべてのエピソードを表示",
|
||||||
"no_episodes_for": "{{date}}のエピソードがありません",
|
"no_episodes_for": "{{date}}のエピソードがありません",
|
||||||
|
|
@ -1095,9 +1112,9 @@
|
||||||
"status_disabled": "MDBListが無効",
|
"status_disabled": "MDBListが無効",
|
||||||
"status_active": "APIキーがアクティブ",
|
"status_active": "APIキーがアクティブ",
|
||||||
"status_required": "APIキーが必要",
|
"status_required": "APIキーが必要",
|
||||||
"status_disabled_desc": "MDBList機能は現在無効です。",
|
"status_disabled_desc": "MDBList機能は現在無効です",
|
||||||
"status_active_desc": "MDBListの評価が有効です。",
|
"status_active_desc": "MDBListの評価が有効です",
|
||||||
"status_required_desc": "評価を有効にするには以下にキーを追加してください。",
|
"status_required_desc": "評価を有効にするには以下にキーを追加してください",
|
||||||
"enable_toggle": "MDBListを有効にする",
|
"enable_toggle": "MDBListを有効にする",
|
||||||
"enable_toggle_desc": "すべてのMDBList機能を有効/無効にする",
|
"enable_toggle_desc": "すべてのMDBList機能を有効/無効にする",
|
||||||
"api_section": "APIキー",
|
"api_section": "APIキー",
|
||||||
|
|
@ -1113,14 +1130,14 @@
|
||||||
"step_2_settings": "設定",
|
"step_2_settings": "設定",
|
||||||
"step_2_api": "API",
|
"step_2_api": "API",
|
||||||
"step_2_end": "。",
|
"step_2_end": "。",
|
||||||
"step_3": "新しいキーを生成してコピーしてください。",
|
"step_3": "新しいキーを生成してコピーしてください",
|
||||||
"go_to_website": "MDBListに移動",
|
"go_to_website": "MDBListに移動",
|
||||||
"alert_clear_title": "APIキーを削除",
|
"alert_clear_title": "APIキーを削除",
|
||||||
"alert_clear_msg": "保存されたAPIキーを削除しますか?",
|
"alert_clear_msg": "保存されたAPIキーを削除しますか?",
|
||||||
"success_saved": "APIキーを正常に保存しました。",
|
"success_saved": "APIキーを正常に保存しました",
|
||||||
"error_empty": "APIキーを空にすることはできません。",
|
"error_empty": "APIキーを空にすることはできません",
|
||||||
"error_save": "保存中にエラーが発生しました。もう一度お試しください。",
|
"error_save": "保存中にエラーが発生しました。もう一度お試しください",
|
||||||
"api_key_empty_error": "APIキーを空にすることはできません。",
|
"api_key_empty_error": "APIキーを空にすることはできません",
|
||||||
"success_cleared": "APIキーを削除しました",
|
"success_cleared": "APIキーを削除しました",
|
||||||
"error_clear": "APIキーの削除に失敗しました"
|
"error_clear": "APIキーの削除に失敗しました"
|
||||||
},
|
},
|
||||||
|
|
@ -1142,7 +1159,7 @@
|
||||||
"stats_total": "合計",
|
"stats_total": "合計",
|
||||||
"sync_button": "ライブラリとTraktを同期",
|
"sync_button": "ライブラリとTraktを同期",
|
||||||
"syncing": "同期中...",
|
"syncing": "同期中...",
|
||||||
"sync_desc": "ライブラリとTraktのウォッチリスト/コレクション内のすべてのシリーズの通知を自動的に同期します。",
|
"sync_desc": "ライブラリとTraktのウォッチリスト/コレクション内のすべてのシリーズの通知を自動的に同期します",
|
||||||
"section_advanced": "詳細",
|
"section_advanced": "詳細",
|
||||||
"reset_button": "すべての通知をリセット",
|
"reset_button": "すべての通知をリセット",
|
||||||
"test_button": "通知をテスト(5秒)",
|
"test_button": "通知をテスト(5秒)",
|
||||||
|
|
@ -1185,18 +1202,18 @@
|
||||||
"section_info": "バックアップについて",
|
"section_info": "バックアップについて",
|
||||||
"info_text": "• 上のトグルでバックアップの範囲をカスタマイズ\n• バックアップファイルはデバイスにローカル保存\n• バックアップを共有してデバイス間でデータを移動\n• 復元すると現在のデータが上書きされます",
|
"info_text": "• 上のトグルでバックアップの範囲をカスタマイズ\n• バックアップファイルはデバイスにローカル保存\n• バックアップを共有してデバイス間でデータを移動\n• 復元すると現在のデータが上書きされます",
|
||||||
"alert_create_title": "バックアップを作成",
|
"alert_create_title": "バックアップを作成",
|
||||||
"alert_no_content": "バックアップするコンテンツが選択されていません。\n\n上のオプションを少なくとも1つ有効にしてください。",
|
"alert_no_content": "バックアップするコンテンツが選択されていません。\n\n上のオプションを少なくとも1つ有効にしてください",
|
||||||
"alert_backup_created_title": "バックアップを作成しました",
|
"alert_backup_created_title": "バックアップを作成しました",
|
||||||
"alert_backup_created_msg": "バックアップが作成され共有の準備ができました。",
|
"alert_backup_created_msg": "バックアップが作成され共有の準備ができました",
|
||||||
"alert_backup_failed_title": "バックアップに失敗しました",
|
"alert_backup_failed_title": "バックアップに失敗しました",
|
||||||
"alert_restore_confirm_title": "復元を確認",
|
"alert_restore_confirm_title": "復元を確認",
|
||||||
"alert_restore_confirm_msg": "{{date}}に作成されたバックアップからデータを復元します。\n\nこのアクションにより現在のデータが上書きされます。続けますか?",
|
"alert_restore_confirm_msg": "{{date}}に作成されたバックアップからデータを復元します。\n\nこのアクションにより現在のデータが上書きされます。続けますか?",
|
||||||
"alert_restore_complete_title": "復元完了",
|
"alert_restore_complete_title": "復元完了",
|
||||||
"alert_restore_complete_msg": "データが正常に復元されました。変更を確認するにはアプリを再起動してください。",
|
"alert_restore_complete_msg": "データが正常に復元されました。変更を確認するにはアプリを再起動してください",
|
||||||
"alert_restore_failed_title": "復元に失敗しました",
|
"alert_restore_failed_title": "復元に失敗しました",
|
||||||
"restart_app": "アプリを再起動",
|
"restart_app": "アプリを再起動",
|
||||||
"alert_restart_failed_title": "再起動に失敗しました",
|
"alert_restart_failed_title": "再起動に失敗しました",
|
||||||
"alert_restart_failed_msg": "アプリの再起動に失敗しました。手動で閉じて再度開いてください。"
|
"alert_restart_failed_msg": "アプリの再起動に失敗しました。手動で閉じて再度開いてください"
|
||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"title": "アプリのアップデート",
|
"title": "アプリのアップデート",
|
||||||
|
|
@ -1225,10 +1242,10 @@
|
||||||
"major_alerts_label": "メジャーアップデートアラート",
|
"major_alerts_label": "メジャーアップデートアラート",
|
||||||
"major_alerts_desc": "GitHubの新バージョンの通知を表示",
|
"major_alerts_desc": "GitHubの新バージョンの通知を表示",
|
||||||
"alert_disable_ota_title": "OTAアップデートアラートを無効にしますか?",
|
"alert_disable_ota_title": "OTAアップデートアラートを無効にしますか?",
|
||||||
"alert_disable_ota_msg": "OTAアップデートの自動通知を受け取らなくなります。",
|
"alert_disable_ota_msg": "OTAアップデートの自動通知を受け取らなくなります",
|
||||||
"alert_disable_major_title": "メジャーアップデートアラートを無効にしますか?",
|
"alert_disable_major_title": "メジャーアップデートアラートを無効にしますか?",
|
||||||
"alert_disable_major_msg": "再インストールが必要なメジャーアップデートの通知を受け取らなくなります。",
|
"alert_disable_major_msg": "再インストールが必要なメジャーアップデートの通知を受け取らなくなります",
|
||||||
"warning_note": "有効なアラートにより、バグ修正を受け取りクラッシュレポートを提出できます。",
|
"warning_note": "有効なアラートにより、バグ修正を受け取りクラッシュレポートを提出できます",
|
||||||
"disable": "無効にする",
|
"disable": "無効にする",
|
||||||
"alert_no_update_to_install": "インストールするアップデートがありません",
|
"alert_no_update_to_install": "インストールするアップデートがありません",
|
||||||
"alert_install_failed": "アップデートのインストールエラー",
|
"alert_install_failed": "アップデートのインストールエラー",
|
||||||
|
|
@ -1256,20 +1273,20 @@
|
||||||
"skip_intro_settings_title": "イントロをスキップ",
|
"skip_intro_settings_title": "イントロをスキップ",
|
||||||
"powered_by_introdb": "IntroDBを使用",
|
"powered_by_introdb": "IntroDBを使用",
|
||||||
"autoplay_title": "最初のストリームを自動再生",
|
"autoplay_title": "最初のストリームを自動再生",
|
||||||
"autoplay_desc": "リストに表示されている最初のストリームを自動的に起動します。",
|
"autoplay_desc": "リストに表示されている最初のストリームを自動的に起動します",
|
||||||
"resume_title": "常に再開",
|
"resume_title": "常に再開",
|
||||||
"resume_desc": "再開の確認をスキップして中断した場所から続ける(85%未満視聴の場合)。",
|
"resume_desc": "再開の確認をスキップして中断した場所から続ける(85%未満視聴の場合)",
|
||||||
"engine_title": "プレーヤーエンジン",
|
"engine_title": "プレーヤーエンジン",
|
||||||
"engine_desc": "AutoモードはExoPlayerとMPVフォールバックを使用します。Autoモードを推奨します。",
|
"engine_desc": "AutoモードはExoPlayerとMPVフォールバックを使用します。Autoモードを推奨します",
|
||||||
"decoder_title": "デコーダーモード",
|
"decoder_title": "デコーダーモード",
|
||||||
"decoder_desc": "動画のデコード方法。最適なバランスのためAutoモードを推奨します。",
|
"decoder_desc": "動画のデコード方法。最適なバランスのためAutoモードを推奨します",
|
||||||
"gpu_title": "GPUレンダリング",
|
"gpu_title": "GPUレンダリング",
|
||||||
"gpu_desc": "GPU-NextはHDRとカラー管理が改善されています。",
|
"gpu_desc": "GPU-NextはHDRとカラー管理が改善されています",
|
||||||
"external_downloads_title": "ダウンロード用外部プレーヤー",
|
"external_downloads_title": "ダウンロード用外部プレーヤー",
|
||||||
"external_downloads_desc": "好みの外部プレーヤーでダウンロードしたコンテンツを再生します。",
|
"external_downloads_desc": "好みの外部プレーヤーでダウンロードしたコンテンツを再生します",
|
||||||
"restart_required": "再起動が必要",
|
"restart_required": "再起動が必要",
|
||||||
"restart_msg_decoder": "デコーダーの変更を有効にするにはアプリを再起動してください。",
|
"restart_msg_decoder": "デコーダーの変更を有効にするにはアプリを再起動してください",
|
||||||
"restart_msg_gpu": "GPUモードの変更を有効にするにはアプリを再起動してください。",
|
"restart_msg_gpu": "GPUモードの変更を有効にするにはアプリを再起動してください",
|
||||||
"option_auto": "Auto",
|
"option_auto": "Auto",
|
||||||
"option_auto_desc_engine": "ExoPlayer + MPVフォールバック",
|
"option_auto_desc_engine": "ExoPlayer + MPVフォールバック",
|
||||||
"option_mpv": "MPV",
|
"option_mpv": "MPV",
|
||||||
|
|
@ -1289,9 +1306,9 @@
|
||||||
"enable_title": "プラグインを有効にする",
|
"enable_title": "プラグインを有効にする",
|
||||||
"enable_desc": "外部メディアソースを取得するためにプラグインエンジンを有効にする",
|
"enable_desc": "外部メディアソースを取得するためにプラグインエンジンを有効にする",
|
||||||
"repo_config_title": "リポジトリ設定",
|
"repo_config_title": "リポジトリ設定",
|
||||||
"repo_config_desc": "外部プラグインリポジトリを管理します。",
|
"repo_config_desc": "外部プラグインリポジトリを管理します",
|
||||||
"your_repos": "リポジトリ",
|
"your_repos": "リポジトリ",
|
||||||
"your_repos_desc": "外部プラグインソースを設定してください。",
|
"your_repos_desc": "外部プラグインソースを設定してください",
|
||||||
"add_repo_button": "リポジトリを追加",
|
"add_repo_button": "リポジトリを追加",
|
||||||
"refresh": "更新",
|
"refresh": "更新",
|
||||||
"remove": "削除",
|
"remove": "削除",
|
||||||
|
|
@ -1312,22 +1329,22 @@
|
||||||
"platform_disabled": "プラットフォーム無効",
|
"platform_disabled": "プラットフォーム無効",
|
||||||
"limited": "制限付き",
|
"limited": "制限付き",
|
||||||
"clear_all": "すべてのプラグインを削除",
|
"clear_all": "すべてのプラグインを削除",
|
||||||
"clear_all_desc": "インストールされたすべてのプラグインを削除しますか?この操作は元に戻せません。",
|
"clear_all_desc": "インストールされたすべてのプラグインを削除しますか?この操作は元に戻せません",
|
||||||
"clear_cache": "リポジトリキャッシュをクリア",
|
"clear_cache": "リポジトリキャッシュをクリア",
|
||||||
"clear_cache_desc": "これによりリポジトリURLが削除されキャッシュデータがクリアされます。",
|
"clear_cache_desc": "これによりリポジトリURLが削除されキャッシュデータがクリアされます",
|
||||||
"add_new_repo": "新しいリポジトリを追加",
|
"add_new_repo": "新しいリポジトリを追加",
|
||||||
"available_plugins": "利用可能なプラグイン({{count}})",
|
"available_plugins": "利用可能なプラグイン({{count}})",
|
||||||
"placeholder": "プラグインを検索...",
|
"placeholder": "プラグインを検索...",
|
||||||
"all": "すべて",
|
"all": "すべて",
|
||||||
"filter_all": "すべてのタイプ",
|
"filter_all": "すべてのタイプ",
|
||||||
"filter_movies": "映画",
|
"filter_movies": "映画",
|
||||||
"filter_tv": "TVシリーズ",
|
"filter_tv": "シリーズ",
|
||||||
"enable_all": "すべて有効",
|
"enable_all": "すべて有効",
|
||||||
"disable_all": "すべて無効",
|
"disable_all": "すべて無効",
|
||||||
"no_plugins_found": "プラグインが見つかりません",
|
"no_plugins_found": "プラグインが見つかりません",
|
||||||
"no_plugins_available": "利用可能なプラグインがありません",
|
"no_plugins_available": "利用可能なプラグインがありません",
|
||||||
"no_match_desc": "「{{query}}」に一致するプラグインがありません。別のキーワードをお試しください。",
|
"no_match_desc": "「{{query}}」に一致するプラグインがありません。別のキーワードをお試しください",
|
||||||
"configure_repo_desc": "利用可能なプラグインを確認するには上でリポジトリを設定してください。",
|
"configure_repo_desc": "利用可能なプラグインを確認するには上でリポジトリを設定してください",
|
||||||
"clear_search": "検索をクリア",
|
"clear_search": "検索をクリア",
|
||||||
"no_external_player": "外部プレーヤーなし",
|
"no_external_player": "外部プレーヤーなし",
|
||||||
"showbox_token": "ShowBox UIトークン",
|
"showbox_token": "ShowBox UIトークン",
|
||||||
|
|
@ -1338,22 +1355,22 @@
|
||||||
"enable_url_validation": "URL検証を有効にする",
|
"enable_url_validation": "URL検証を有効にする",
|
||||||
"url_validation_desc": "メディアURLを返す前に検証します",
|
"url_validation_desc": "メディアURLを返す前に検証します",
|
||||||
"group_streams": "プラグインソースをグループ化",
|
"group_streams": "プラグインソースをグループ化",
|
||||||
"group_streams_desc": "有効にするとソースはリポジトリ別にグループ化されます。",
|
"group_streams_desc": "有効にするとソースはリポジトリ別にグループ化されます",
|
||||||
"sort_quality": "まず品質でソート",
|
"sort_quality": "まず品質でソート",
|
||||||
"sort_quality_desc": "有効にするとソースは品質でソートされます。グループ化が有効な場合のみ動作します。",
|
"sort_quality_desc": "有効にするとソースは品質でソートされます。グループ化が有効な場合のみ動作します",
|
||||||
"show_logos": "プラグインロゴを表示",
|
"show_logos": "プラグインロゴを表示",
|
||||||
"show_logos_desc": "ソース画面のメディアリンクの横にプラグインロゴを表示します。",
|
"show_logos_desc": "ソース画面のメディアリンクの横にプラグインロゴを表示します",
|
||||||
"quality_filtering": "品質フィルタリング",
|
"quality_filtering": "品質フィルタリング",
|
||||||
"quality_filtering_desc": "検索結果から特定の解像度を除外します。品質をタップして除外します。",
|
"quality_filtering_desc": "検索結果から特定の解像度を除外します。品質をタップして除外します",
|
||||||
"excluded_qualities": "除外された品質:",
|
"excluded_qualities": "除外された品質:",
|
||||||
"language_filtering": "言語フィルタリング",
|
"language_filtering": "言語フィルタリング",
|
||||||
"language_filtering_desc": "検索結果から特定の言語を除外します。言語をタップして除外します。",
|
"language_filtering_desc": "検索結果から特定の言語を除外します。言語をタップして除外します",
|
||||||
"note": "注意:",
|
"note": "注意:",
|
||||||
"language_filtering_note": "このフィルターは言語情報を提供するプロバイダーにのみ適用されます。",
|
"language_filtering_note": "このフィルターは言語情報を提供するプロバイダーにのみ適用されます",
|
||||||
"excluded_languages": "除外された言語:",
|
"excluded_languages": "除外された言語:",
|
||||||
"about_title": "プラグインについて",
|
"about_title": "プラグインについて",
|
||||||
"about_desc_1": "プラグインはさまざまなプロトコルからコンテンツを取得するモジュール式コンポーネントです。デバイス上でローカルに動作します。",
|
"about_desc_1": "プラグインはさまざまなプロトコルからコンテンツを取得するモジュール式コンポーネントです。デバイス上でローカルに動作します",
|
||||||
"about_desc_2": "「制限付き」としてマークされたプラグインは特定の外部設定が必要な場合があります。",
|
"about_desc_2": "「制限付き」としてマークされたプラグインは特定の外部設定が必要な場合があります",
|
||||||
"help_title": "プラグインの設定",
|
"help_title": "プラグインの設定",
|
||||||
"help_step_1": "1. **プラグインを有効にする** - メインスイッチをオンにする",
|
"help_step_1": "1. **プラグインを有効にする** - メインスイッチをオンにする",
|
||||||
"help_step_2": "2. **リポジトリを追加する** - 有効なリポジトリURLを入力する",
|
"help_step_2": "2. **リポジトリを追加する** - 有効なリポジトリURLを入力する",
|
||||||
|
|
@ -1397,15 +1414,15 @@
|
||||||
"legal": {
|
"legal": {
|
||||||
"title": "法的情報",
|
"title": "法的情報",
|
||||||
"intro_title": "アプリの性質",
|
"intro_title": "アプリの性質",
|
||||||
"intro_text": "Nuvioはメディアプレーヤーおよびメタデータ管理アプリケーションです。Nuvioはいかなるメディアコンテンツもホスト、保存、配布、またはインデックス化しません。",
|
"intro_text": "Nuvioはメディアプレーヤーおよびメタデータ管理アプリケーションです。Nuvioはいかなるメディアコンテンツもホスト、保存、配布、またはインデックス化しません",
|
||||||
"extensions_title": "サードパーティ拡張機能",
|
"extensions_title": "サードパーティ拡張機能",
|
||||||
"extensions_text": "Nuvioはユーザーがサードパーティプラグインをインストールできる拡張可能なアーキテクチャを使用しています。サードパーティ拡張機能のコンテンツ、合法性、機能について責任を負いません。",
|
"extensions_text": "Nuvioはユーザーがサードパーティプラグインをインストールできる拡張可能なアーキテクチャを使用しています。サードパーティ拡張機能のコンテンツ、合法性、機能について責任を負いません",
|
||||||
"user_resp_title": "ユーザーの責任",
|
"user_resp_title": "ユーザーの責任",
|
||||||
"user_resp_text": "ユーザーはインストールするプラグインとアクセスするコンテンツに対して単独で責任を負います。Nuvioの制作者は著作権侵害を奨励しません。",
|
"user_resp_text": "ユーザーはインストールするプラグインとアクセスするコンテンツに対して単独で責任を負います。Nuvioの制作者は著作権侵害を奨励しません",
|
||||||
"dmca_title": "著作権とDMCA",
|
"dmca_title": "著作権とDMCA",
|
||||||
"dmca_text": "私たちは他者の知的財産権を尊重します。アプリのインターフェース自体があなたの権利を侵害していると思われる場合はお問い合わせください。",
|
"dmca_text": "私たちは他者の知的財産権を尊重します。アプリのインターフェース自体があなたの権利を侵害していると思われる場合はお問い合わせください",
|
||||||
"warranty_title": "無保証",
|
"warranty_title": "無保証",
|
||||||
"warranty_text": "このソフトウェアは「現状のまま」で提供され、いかなる保証もありません。"
|
"warranty_text": "このソフトウェアは「現状のまま」で提供され、いかなる保証もありません"
|
||||||
},
|
},
|
||||||
"plugin_tester": {
|
"plugin_tester": {
|
||||||
"title": "プラグインテスター",
|
"title": "プラグインテスター",
|
||||||
|
|
@ -1421,7 +1438,7 @@
|
||||||
"error": "エラー",
|
"error": "エラー",
|
||||||
"success": "成功",
|
"success": "成功",
|
||||||
"movie": "映画",
|
"movie": "映画",
|
||||||
"tv": "TVシリーズ",
|
"tv": "シリーズ",
|
||||||
"tmdb_id": "TMDB ID",
|
"tmdb_id": "TMDB ID",
|
||||||
"season": "シーズン",
|
"season": "シーズン",
|
||||||
"episode": "エピソード",
|
"episode": "エピソード",
|
||||||
|
|
@ -1434,7 +1451,7 @@
|
||||||
},
|
},
|
||||||
"individual": {
|
"individual": {
|
||||||
"load_from_url": "URLから読み込む",
|
"load_from_url": "URLから読み込む",
|
||||||
"load_from_url_desc": "GitHubのrawURLまたはローカルIPを貼り付けて取得をタップ。",
|
"load_from_url_desc": "GitHubのrawURLまたはローカルIPを貼り付けて取得をタップ",
|
||||||
"enter_url_error": "URLを入力してください",
|
"enter_url_error": "URLを入力してください",
|
||||||
"code_loaded": "URLからコードを読み込みました",
|
"code_loaded": "URLからコードを読み込みました",
|
||||||
"fetch_error": "取得エラー: {{message}}",
|
"fetch_error": "取得エラー: {{message}}",
|
||||||
|
|
@ -1443,11 +1460,11 @@
|
||||||
"focus_editor": "コードエディターをアクティブにする",
|
"focus_editor": "コードエディターをアクティブにする",
|
||||||
"code_placeholder": "// プラグインコードをここに貼り付け...",
|
"code_placeholder": "// プラグインコードをここに貼り付け...",
|
||||||
"test_parameters": "テストパラメーター",
|
"test_parameters": "テストパラメーター",
|
||||||
"no_logs": "ログなし。テストを実行して出力を確認してください。",
|
"no_logs": "ログなし。テストを実行して出力を確認してください",
|
||||||
"no_streams": "まだストリームが見つかりません。",
|
"no_streams": "まだストリームが見つかりません",
|
||||||
"streams_found": "{{count}}件のストリームが見つかりました",
|
"streams_found": "{{count}}件のストリームが見つかりました",
|
||||||
"streams_found_plural": "{{count}}件のストリームが見つかりました",
|
"streams_found_plural": "{{count}}件のストリームが見つかりました",
|
||||||
"tap_play_hint": "再生をタップしてネイティブプレーヤーでストリームをテストします。",
|
"tap_play_hint": "再生をタップしてネイティブプレーヤーでストリームをテストします",
|
||||||
"unnamed_stream": "無名のストリーム",
|
"unnamed_stream": "無名のストリーム",
|
||||||
"quality": "品質: {{quality}}",
|
"quality": "品質: {{quality}}",
|
||||||
"size": "サイズ: {{size}}",
|
"size": "サイズ: {{size}}",
|
||||||
|
|
@ -1459,10 +1476,10 @@
|
||||||
},
|
},
|
||||||
"repo": {
|
"repo": {
|
||||||
"title": "リポジトリテスター",
|
"title": "リポジトリテスター",
|
||||||
"description": "リポジトリを取得して各プロバイダーをテストします。",
|
"description": "リポジトリを取得して各プロバイダーをテストします",
|
||||||
"enter_repo_url_error": "リポジトリURLを入力してください",
|
"enter_repo_url_error": "リポジトリURLを入力してください",
|
||||||
"invalid_url_title": "無効なURL",
|
"invalid_url_title": "無効なURL",
|
||||||
"invalid_url_msg": "GitHub rawURLまたはローカルhttp(s)アドレスを使用してください。",
|
"invalid_url_msg": "GitHub rawURLまたはローカルhttp(s)アドレスを使用してください",
|
||||||
"manifest_build_error": "マニフェストURLを構築できません",
|
"manifest_build_error": "マニフェストURLを構築できません",
|
||||||
"manifest_fetch_error": "マニフェストの取得エラー",
|
"manifest_fetch_error": "マニフェストの取得エラー",
|
||||||
"repo_manifest_fetch_error": "リポジトリマニフェストの取得エラー",
|
"repo_manifest_fetch_error": "リポジトリマニフェストの取得エラー",
|
||||||
|
|
@ -1471,13 +1488,13 @@
|
||||||
"download_scraper_error": "スクレイパーのダウンロードエラー",
|
"download_scraper_error": "スクレイパーのダウンロードエラー",
|
||||||
"test_failed": "テストに失敗しました",
|
"test_failed": "テストに失敗しました",
|
||||||
"test_parameters": "リポジトリテストパラメーター",
|
"test_parameters": "リポジトリテストパラメーター",
|
||||||
"test_parameters_desc": "これらのパラメーターはリポジトリテスターのみに使用されます。",
|
"test_parameters_desc": "これらのパラメーターはリポジトリテスターのみに使用されます",
|
||||||
"using_info": "使用中: {{mediaType}} • TMDB {{tmdbId}}",
|
"using_info": "使用中: {{mediaType}} • TMDB {{tmdbId}}",
|
||||||
"using_info_tv": "使用中: {{mediaType}} • TMDB {{tmdbId}} • S{{season}}E{{episode}}",
|
"using_info_tv": "使用中: {{mediaType}} • TMDB {{tmdbId}} • S{{season}}E{{episode}}",
|
||||||
"providers_title": "プロバイダー",
|
"providers_title": "プロバイダー",
|
||||||
"repository_default": "リポジトリ",
|
"repository_default": "リポジトリ",
|
||||||
"providers_count": "{{count}}件のプロバイダー",
|
"providers_count": "{{count}}件のプロバイダー",
|
||||||
"fetch_hint": "プロバイダーを表示するにはリポジトリを取得してください。",
|
"fetch_hint": "プロバイダーを表示するにはリポジトリを取得してください",
|
||||||
"test_all": "すべてテスト",
|
"test_all": "すべてテスト",
|
||||||
"status_running": "実行中",
|
"status_running": "実行中",
|
||||||
"status_ok": "OK({{count}})",
|
"status_ok": "OK({{count}})",
|
||||||
|
|
@ -1486,7 +1503,7 @@
|
||||||
"status_idle": "待機中",
|
"status_idle": "待機中",
|
||||||
"tried_url": "試したURL: {{url}}",
|
"tried_url": "試したURL: {{url}}",
|
||||||
"provider_logs": "プロバイダーログ",
|
"provider_logs": "プロバイダーログ",
|
||||||
"no_logs_captured": "ログが記録されていません。"
|
"no_logs_captured": "ログが記録されていません"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"unknown": "Không xác định",
|
"unknown": "Không xác định",
|
||||||
"retry": "Thử lại",
|
"retry": "Thử lại",
|
||||||
"try_again": "Thử lại",
|
"try_again": "Vui lòng thử lại",
|
||||||
"go_back": "Quay lại",
|
"go_back": "Quay lại",
|
||||||
"settings": "Cài đặt",
|
"settings": "Cài đặt",
|
||||||
"close": "Đóng",
|
"close": "Đóng",
|
||||||
|
|
@ -21,48 +21,48 @@
|
||||||
"show_less": "Thu gọn",
|
"show_less": "Thu gọn",
|
||||||
"load_more": "Tải thêm",
|
"load_more": "Tải thêm",
|
||||||
"unknown_date": "Ngày không xác định",
|
"unknown_date": "Ngày không xác định",
|
||||||
"anonymous_user": "Người dùng khách",
|
"anonymous_user": "Khách",
|
||||||
"time": {
|
"time": {
|
||||||
"now": "Vừa xong",
|
"now": "Vừa xong",
|
||||||
"minutes_ago": "{{count}} phút trước",
|
"minutes_ago": "{{count}} phút",
|
||||||
"hours_ago": "{{count}} giờ trước",
|
"hours_ago": "{{count}} giờ",
|
||||||
"days_ago": "{{count}} ngày trước"
|
"days_ago": "{{count}} ngày"
|
||||||
},
|
},
|
||||||
"days_short": {
|
"days_short": {
|
||||||
"sun": "Chủ nhật",
|
"sun": "CN",
|
||||||
"mon": "Thứ 2",
|
"mon": "Th 2",
|
||||||
"tue": "Thứ 3",
|
"tue": "Th 3",
|
||||||
"wed": "Thứ 4",
|
"wed": "Th 4",
|
||||||
"thu": "Thứ 5",
|
"thu": "Th 5",
|
||||||
"fri": "Thứ 6",
|
"fri": "Th 6",
|
||||||
"sat": "Thứ 7"
|
"sat": "Th 7"
|
||||||
},
|
},
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"status": "Trạng thái"
|
"status": "Trạng thái"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"categories": {
|
"categories": {
|
||||||
"movies": "Phim lẻ",
|
"movies": "Phim",
|
||||||
"series": "Phim bộ",
|
"series": "Chương trình TV",
|
||||||
"channels": "Truyền hình"
|
"channels": "Kênh truyền hình"
|
||||||
},
|
},
|
||||||
"movies": "Phim lẻ",
|
"movies": "Phim",
|
||||||
"tv_shows": "Chương trình truyền hình",
|
"tv_shows": "Chương trình TV",
|
||||||
"load_more_catalogs": "Tải thêm danh mục",
|
"load_more_catalogs": "Tải thêm danh mục",
|
||||||
"no_content": "Không có nội dung",
|
"no_content": "Không có nội dung",
|
||||||
"add_catalogs": "Thêm danh mục",
|
"add_catalogs": "Thêm danh mục",
|
||||||
"sign_in_available": "Đăng nhập ngay",
|
"sign_in_available": "Đăng nhập ngay",
|
||||||
"sign_in_desc": "Bạn có thể đăng nhập bất cứ lúc nào trong Cài đặt → Tài khoản",
|
"sign_in_desc": "Bạn có thể đăng nhập bất cứ lúc nào trong Cài đặt → Tài khoản",
|
||||||
"view_all": "Xem tất cả",
|
"view_all": "Xem thêm",
|
||||||
"this_week": "Tuần này",
|
"this_week": "Tuần này",
|
||||||
"upcoming": "Sắp chiếu",
|
"upcoming": "Sắp chiếu",
|
||||||
"recently_released": "Vừa phát hành",
|
"recently_released": "Mới ra mắt",
|
||||||
"no_scheduled_episodes": "Phim bộ chưa có lịch chiếu",
|
"no_scheduled_episodes": "Phim chưa có lịch chiếu",
|
||||||
"check_back_later": "Quay lại sau nhé",
|
"check_back_later": "Quay lại sau nhé",
|
||||||
"continue_watching": "Xem tiếp",
|
"continue_watching": "Xem tiếp",
|
||||||
"up_next": "Tiếp theo",
|
"up_next": "Tiếp theo",
|
||||||
"up_next_caps": "TIẾP THEO",
|
"up_next_caps": "TIẾP THEO",
|
||||||
"released": "Đã phát hành",
|
"released": "Đang chiếu",
|
||||||
"new": "Mới",
|
"new": "Mới",
|
||||||
"tba": "Chưa xác định",
|
"tba": "Chưa xác định",
|
||||||
"new_episodes": "{{count}} tập mới",
|
"new_episodes": "{{count}} tập mới",
|
||||||
|
|
@ -70,17 +70,17 @@
|
||||||
"episode_short": "T{{episode}}",
|
"episode_short": "T{{episode}}",
|
||||||
"season": "Phần {{season}}",
|
"season": "Phần {{season}}",
|
||||||
"episode": "Tập {{episode}}",
|
"episode": "Tập {{episode}}",
|
||||||
"movie": "Phim lẻ",
|
"movie": "Phim",
|
||||||
"series": "Phim bộ",
|
"series": "Chương trình TV",
|
||||||
"tv_show": "Chương trình truyền hình",
|
"tv_show": "Chương trình TV",
|
||||||
"percent_watched": "Đã xem {{percent}}%",
|
"percent_watched": "Đã xem {{percent}}%",
|
||||||
"view_details": "Xem chi tiết",
|
"view_details": "Xem chi tiết",
|
||||||
"remove": "Xoá",
|
"remove": "Xoá",
|
||||||
"play": "Phát",
|
"play": "Xem",
|
||||||
"play_now": "Xem ngay",
|
"play_now": "Xem ngay",
|
||||||
"resume": "Xem tiếp",
|
"resume": "Xem tiếp",
|
||||||
"info": "Thông tin",
|
"info": "Thông tin",
|
||||||
"more_info": "Thêm thông tin",
|
"more_info": "Thêm",
|
||||||
"my_list": "Danh sách của tôi",
|
"my_list": "Danh sách của tôi",
|
||||||
"save": "Lưu",
|
"save": "Lưu",
|
||||||
"saved": "Đã lưu",
|
"saved": "Đã lưu",
|
||||||
|
|
@ -105,8 +105,8 @@
|
||||||
"title": "Tìm kiếm",
|
"title": "Tìm kiếm",
|
||||||
"recent_searches": "Tìm kiếm gần đây",
|
"recent_searches": "Tìm kiếm gần đây",
|
||||||
"discover": "Khám phá",
|
"discover": "Khám phá",
|
||||||
"movies": "Phim lẻ",
|
"movies": "Phim",
|
||||||
"tv_shows": "Phim bộ",
|
"tv_shows": "Chương trình TV",
|
||||||
"select_catalog": "Chọn danh mục",
|
"select_catalog": "Chọn danh mục",
|
||||||
"all_genres": "Tất cả thể loại",
|
"all_genres": "Tất cả thể loại",
|
||||||
"discovering": "Đang tải nội dung...",
|
"discovering": "Đang tải nội dung...",
|
||||||
|
|
@ -115,14 +115,14 @@
|
||||||
"try_different": "Thử thể loại hoặc danh mục khác",
|
"try_different": "Thử thể loại hoặc danh mục khác",
|
||||||
"select_catalog_desc": "Chọn một danh mục để khám phá",
|
"select_catalog_desc": "Chọn một danh mục để khám phá",
|
||||||
"tap_catalog_desc": "Nhấn vào danh mục bên trên để bắt đầu",
|
"tap_catalog_desc": "Nhấn vào danh mục bên trên để bắt đầu",
|
||||||
"placeholder": "Tìm phim, chương trình...",
|
"placeholder": "Chương trình, Phim, v.v.",
|
||||||
"keep_typing": "Tiếp tục nhập...",
|
"keep_typing": "Vui lòng nhập tiếp...",
|
||||||
"type_characters": "Nhập ít nhất 2 ký tự để tìm kiếm",
|
"type_characters": "Nhập ít nhất từ 2 ký tự để tìm kiếm",
|
||||||
"no_results": "Không có kết quả",
|
"no_results": "Không có kết quả",
|
||||||
"try_keywords": "Thử từ khoá khác hoặc kiểm tra chính tả",
|
"try_keywords": "Thử từ khoá khác hoặc kiểm tra chính tả",
|
||||||
"select_type": "Chọn loại",
|
"select_type": "Chọn loại danh mục",
|
||||||
"browse_movies": "Duyệt danh mục phim lẻ",
|
"browse_movies": "Duyệt danh mục phim",
|
||||||
"browse_tv": "Duyệt danh mục phim bộ",
|
"browse_tv": "Duyệt danh mục chương trình TV",
|
||||||
"select_genre": "Chọn thể loại",
|
"select_genre": "Chọn thể loại",
|
||||||
"show_all_content": "Hiển thị tất cả nội dung",
|
"show_all_content": "Hiển thị tất cả nội dung",
|
||||||
"genres_count": "{{count}} thể loại"
|
"genres_count": "{{count}} thể loại"
|
||||||
|
|
@ -143,8 +143,8 @@
|
||||||
"empty_folder": "Không có nội dung trong {{folder}}",
|
"empty_folder": "Không có nội dung trong {{folder}}",
|
||||||
"empty_folder_desc": "Bộ sưu tập này đang trống",
|
"empty_folder_desc": "Bộ sưu tập này đang trống",
|
||||||
"refresh": "Làm mới",
|
"refresh": "Làm mới",
|
||||||
"no_movies": "Chưa có phim lẻ",
|
"no_movies": "Chưa có phim",
|
||||||
"no_series": "Chưa có phim bộ",
|
"no_series": "Chưa có chương trình TV",
|
||||||
"no_content": "Chưa có nội dung",
|
"no_content": "Chưa có nội dung",
|
||||||
"add_content_desc": "Thêm nội dung vào thư viện để xem ở đây",
|
"add_content_desc": "Thêm nội dung vào thư viện để xem ở đây",
|
||||||
"find_something": "Tìm gì đó để xem",
|
"find_something": "Tìm gì đó để xem",
|
||||||
|
|
@ -200,7 +200,7 @@
|
||||||
"access_denied": "Truy cập bị từ chối",
|
"access_denied": "Truy cập bị từ chối",
|
||||||
"access_denied_desc": "Bạn không có quyền truy cập nội dung này.",
|
"access_denied_desc": "Bạn không có quyền truy cập nội dung này.",
|
||||||
"connection_error": "Lỗi kết nối",
|
"connection_error": "Lỗi kết nối",
|
||||||
"streams_unavailable": "Không có luồng phát",
|
"streams_unavailable": "Không có nguồn phát",
|
||||||
"streams_unavailable_desc": "Nguồn phát hiện tại không khả dụng. Vui lòng thử lại sau.",
|
"streams_unavailable_desc": "Nguồn phát hiện tại không khả dụng. Vui lòng thử lại sau.",
|
||||||
"unknown_error": "Lỗi không xác định",
|
"unknown_error": "Lỗi không xác định",
|
||||||
"something_went_wrong": "Đã xảy ra sự cố. Vui lòng thử lại.",
|
"something_went_wrong": "Đã xảy ra sự cố. Vui lòng thử lại.",
|
||||||
|
|
@ -222,16 +222,16 @@
|
||||||
"episode_label": "TẬP {{number}}",
|
"episode_label": "TẬP {{number}}",
|
||||||
"watch_again": "Xem lại",
|
"watch_again": "Xem lại",
|
||||||
"completed": "Đã xem xong",
|
"completed": "Đã xem xong",
|
||||||
"play_episode": "Phát P{{season}}T{{episode}}",
|
"play_episode": "Xem P{{season}}T{{episode}}",
|
||||||
"play": "Phát",
|
"play": "Xem",
|
||||||
"watched": "Đã xem",
|
"watched": "Đã xem",
|
||||||
"watched_on_trakt": "Đã xem trên Trakt",
|
"watched_on_trakt": "Đã xem trên Trakt",
|
||||||
"synced_with_trakt": "Đồng bộ với Trakt",
|
"synced_with_trakt": "Đồng bộ với Trakt",
|
||||||
"saved": "Đã lưu",
|
"saved": "Đã lưu",
|
||||||
"director": "Đạo diễn",
|
"director": "Đạo diễn",
|
||||||
"directors": "Đạo diễn",
|
"directors": "Đạo diễn",
|
||||||
"creator": "Người tạo",
|
"creator": "Tác giả",
|
||||||
"creators": "Những người tạo",
|
"creators": "Tác giả",
|
||||||
"production": "Sản xuất",
|
"production": "Sản xuất",
|
||||||
"network": "Kênh phát sóng",
|
"network": "Kênh phát sóng",
|
||||||
"mark_watched": "Đánh dấu đã xem",
|
"mark_watched": "Đánh dấu đã xem",
|
||||||
|
|
@ -242,12 +242,12 @@
|
||||||
"mark_season": "Đánh dấu Phần {{season}}",
|
"mark_season": "Đánh dấu Phần {{season}}",
|
||||||
"resume": "Xem tiếp",
|
"resume": "Xem tiếp",
|
||||||
"spoiler_warning": "Cảnh báo tiết lộ nội dung",
|
"spoiler_warning": "Cảnh báo tiết lộ nội dung",
|
||||||
"spoiler_warning_desc": "Bình luận này chứa nội dung tiết lộ. Bạn có muốn hiển thị không?",
|
"spoiler_warning_desc": "Bình luận này có tiết lộ nội dung phim. Bạn có muốn hiển thị không?",
|
||||||
"cancel": "Huỷ",
|
"cancel": "Huỷ",
|
||||||
"reveal_spoilers": "Hiện nội dung tiết lộ",
|
"reveal_spoilers": "Hiện nội dung tiết lộ",
|
||||||
"movie_details": "Thông tin phim",
|
"movie_details": "Thông tin phim",
|
||||||
"show_details": "Thông tin chương trình",
|
"show_details": "Thông tin chương trình",
|
||||||
"tagline": "Khẩu hiệu",
|
"tagline": "Slogan",
|
||||||
"status": "Trạng thái",
|
"status": "Trạng thái",
|
||||||
"release_date": "Ngày phát hành",
|
"release_date": "Ngày phát hành",
|
||||||
"runtime": "Thời lượng",
|
"runtime": "Thời lượng",
|
||||||
|
|
@ -260,7 +260,7 @@
|
||||||
"total_episodes": "Tổng số tập",
|
"total_episodes": "Tổng số tập",
|
||||||
"episode_runtime": "Thời lượng mỗi tập",
|
"episode_runtime": "Thời lượng mỗi tập",
|
||||||
"created_by": "Tạo bởi",
|
"created_by": "Tạo bởi",
|
||||||
"backdrop_gallery": "Thư viện ảnh nền",
|
"backdrop_gallery": "Thư viện ảnh",
|
||||||
"loading_episodes": "Đang tải tập phim...",
|
"loading_episodes": "Đang tải tập phim...",
|
||||||
"no_episodes_available": "Không có tập phim",
|
"no_episodes_available": "Không có tập phim",
|
||||||
"play_next": "Phát P{{season}}T{{episode}}",
|
"play_next": "Phát P{{season}}T{{episode}}",
|
||||||
|
|
@ -304,12 +304,12 @@
|
||||||
"alert_ok": "OK",
|
"alert_ok": "OK",
|
||||||
"no_upcoming": "Không có tác phẩm sắp tới của diễn viên này",
|
"no_upcoming": "Không có tác phẩm sắp tới của diễn viên này",
|
||||||
"no_content": "Không có nội dung của diễn viên này",
|
"no_content": "Không có nội dung của diễn viên này",
|
||||||
"no_movies": "Không có phim lẻ của diễn viên này",
|
"no_movies": "Không có phim của diễn viên này",
|
||||||
"no_tv": "Không có phim bộ của diễn viên này"
|
"no_tv": "Không có chương trình TV của diễn viên này"
|
||||||
},
|
},
|
||||||
"comments": {
|
"comments": {
|
||||||
"title": "Bình luận Trakt",
|
"title": "Bình luận Trakt",
|
||||||
"spoiler_warning": "⚠️ Bình luận này chứa nội dung tiết lộ. Nhấn để hiện.",
|
"spoiler_warning": "⚠️ Bình luận này có tiết lộ nội dung phim. Nhấn để hiện.",
|
||||||
"spoiler": "Tiết lộ nội dung",
|
"spoiler": "Tiết lộ nội dung",
|
||||||
"contains_spoilers": "Chứa nội dung tiết lộ",
|
"contains_spoilers": "Chứa nội dung tiết lộ",
|
||||||
"reveal": "Hiện",
|
"reveal": "Hiện",
|
||||||
|
|
@ -327,8 +327,8 @@
|
||||||
"teaser": "Teaser",
|
"teaser": "Teaser",
|
||||||
"clips_scenes": "Clip & Cảnh phim",
|
"clips_scenes": "Clip & Cảnh phim",
|
||||||
"clip": "Clip",
|
"clip": "Clip",
|
||||||
"featurettes": "Hậu trường ngắn",
|
"featurettes": "Hậu trường",
|
||||||
"featurette": "Hậu trường ngắn",
|
"featurette": "Hậu trường",
|
||||||
"behind_the_scenes": "Hậu trường",
|
"behind_the_scenes": "Hậu trường",
|
||||||
"no_trailers": "Không có trailer",
|
"no_trailers": "Không có trailer",
|
||||||
"unavailable": "Trailer không khả dụng",
|
"unavailable": "Trailer không khả dụng",
|
||||||
|
|
@ -341,11 +341,11 @@
|
||||||
"no_content_filters": "Không có nội dung cho bộ lọc đã chọn",
|
"no_content_filters": "Không có nội dung cho bộ lọc đã chọn",
|
||||||
"loading_content": "Đang tải nội dung...",
|
"loading_content": "Đang tải nội dung...",
|
||||||
"back": "Quay lại",
|
"back": "Quay lại",
|
||||||
"in_theaters": "Đang chiếu",
|
"in_theaters": "Đang chiếu rạp",
|
||||||
"all": "Tất cả",
|
"all": "Tất cả",
|
||||||
"failed_tmdb": "Không thể tải nội dung từ TMDB",
|
"failed_tmdb": "Không thể tải nội dung từ TMDB",
|
||||||
"movies": "Phim lẻ",
|
"movies": "Phim",
|
||||||
"tv_shows": "Phim bộ",
|
"tv_shows": "Chương trình TV",
|
||||||
"channels": "Kênh truyền hình"
|
"channels": "Kênh truyền hình"
|
||||||
},
|
},
|
||||||
"streams": {
|
"streams": {
|
||||||
|
|
@ -364,8 +364,8 @@
|
||||||
},
|
},
|
||||||
"player_ui": {
|
"player_ui": {
|
||||||
"via": "từ {{name}}",
|
"via": "từ {{name}}",
|
||||||
"audio_tracks": "Rãnh âm thanh",
|
"audio_tracks": "Âm thanh",
|
||||||
"no_audio_tracks": "Không có rãnh âm thanh",
|
"no_audio_tracks": "Không có âm thanh",
|
||||||
"playback_speed": "Tốc độ phát",
|
"playback_speed": "Tốc độ phát",
|
||||||
"on_hold": "Tạm dừng",
|
"on_hold": "Tạm dừng",
|
||||||
"playback_error": "Lỗi phát",
|
"playback_error": "Lỗi phát",
|
||||||
|
|
@ -374,7 +374,7 @@
|
||||||
"copied_to_clipboard": "Đã sao chép",
|
"copied_to_clipboard": "Đã sao chép",
|
||||||
"dismiss": "Bỏ qua",
|
"dismiss": "Bỏ qua",
|
||||||
"continue_watching": "Xem tiếp",
|
"continue_watching": "Xem tiếp",
|
||||||
"start_over": "Xem từ đầu",
|
"start_over": "Xem lại từ đầu",
|
||||||
"resume": "Tiếp tục",
|
"resume": "Tiếp tục",
|
||||||
"change_source": "Đổi nguồn",
|
"change_source": "Đổi nguồn",
|
||||||
"switching_source": "Đang chuyển nguồn...",
|
"switching_source": "Đang chuyển nguồn...",
|
||||||
|
|
@ -386,7 +386,7 @@
|
||||||
"episodes": "Tập phim",
|
"episodes": "Tập phim",
|
||||||
"specials": "Tập đặc biệt",
|
"specials": "Tập đặc biệt",
|
||||||
"season": "Phần {{season}}",
|
"season": "Phần {{season}}",
|
||||||
"stream": "Luồng {{number}}",
|
"stream": "{{number}} nguồn",
|
||||||
"subtitles": "Phụ đề",
|
"subtitles": "Phụ đề",
|
||||||
"built_in": "Tích hợp sẵn",
|
"built_in": "Tích hợp sẵn",
|
||||||
"addons": "Addon",
|
"addons": "Addon",
|
||||||
|
|
@ -462,6 +462,21 @@
|
||||||
"cancel": "Huỷ",
|
"cancel": "Huỷ",
|
||||||
"remove": "Xoá"
|
"remove": "Xoá"
|
||||||
},
|
},
|
||||||
|
"parentalGuide": {
|
||||||
|
"labels": {
|
||||||
|
"nudity": "Cảnh khoả thân",
|
||||||
|
"violence": "Bạo lực",
|
||||||
|
"profanity": "Ngôn từ thô tục",
|
||||||
|
"alcohol": "Chất kích thích",
|
||||||
|
"frightening": "Cảnh đáng sợ"
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"severe": "Nặng",
|
||||||
|
"moderate": "Vừa phải",
|
||||||
|
"mild": "Nhẹ",
|
||||||
|
"none": "Không có"
|
||||||
|
}
|
||||||
|
},
|
||||||
"addons": {
|
"addons": {
|
||||||
"title": "Addon",
|
"title": "Addon",
|
||||||
"reorder_mode": "Sắp xếp lại",
|
"reorder_mode": "Sắp xếp lại",
|
||||||
|
|
@ -579,14 +594,14 @@
|
||||||
"recommendations": "Gợi ý",
|
"recommendations": "Gợi ý",
|
||||||
"recommendations_desc": "Nội dung tương tự",
|
"recommendations_desc": "Nội dung tương tự",
|
||||||
"episode_data": "Dữ liệu tập phim",
|
"episode_data": "Dữ liệu tập phim",
|
||||||
"episode_data_desc": "Thumbnail tập, thông tin & dự phòng cho phim bộ",
|
"episode_data_desc": "Thumbnail tập, thông tin & dự phòng cho chương trình TV",
|
||||||
"season_posters": "Poster phần",
|
"season_posters": "Poster phần",
|
||||||
"season_posters_desc": "Poster riêng cho từng phần",
|
"season_posters_desc": "Poster riêng cho từng phần",
|
||||||
"production_info": "Thông tin sản xuất",
|
"production_info": "Thông tin sản xuất",
|
||||||
"production_info_desc": "Kênh truyền hình & công ty sản xuất kèm logo",
|
"production_info_desc": "Kênh truyền hình & công ty sản xuất kèm logo",
|
||||||
"movie_details": "Chi tiết phim lẻ",
|
"movie_details": "Chi tiết phim",
|
||||||
"movie_details_desc": "Ngân sách, doanh thu, thời lượng, khẩu hiệu",
|
"movie_details_desc": "Ngân sách, doanh thu, thời lượng, khẩu hiệu",
|
||||||
"tv_details": "Chi tiết phim bộ",
|
"tv_details": "Chi tiết chương trình TV",
|
||||||
"tv_details_desc": "Trạng thái, số phần, kênh, người tạo",
|
"tv_details_desc": "Trạng thái, số phần, kênh, người tạo",
|
||||||
"movie_collections": "Bộ phim series",
|
"movie_collections": "Bộ phim series",
|
||||||
"movie_collections_desc": "Phim trong cùng vũ trụ (Marvel, Star Wars...)",
|
"movie_collections_desc": "Phim trong cùng vũ trụ (Marvel, Star Wars...)",
|
||||||
|
|
@ -654,11 +669,12 @@
|
||||||
"romanian": "Tiếng Romania",
|
"romanian": "Tiếng Romania",
|
||||||
"albanian": "Tiếng Albania",
|
"albanian": "Tiếng Albania",
|
||||||
"catalan": "Tiếng Catalan",
|
"catalan": "Tiếng Catalan",
|
||||||
|
"vietnamese": "Tiếng Việt",
|
||||||
"account": "Tài khoản",
|
"account": "Tài khoản",
|
||||||
"content_discovery": "Nội dung & Khám phá",
|
"content_discovery": "Nội dung & Khám phá",
|
||||||
"appearance": "Giao diện",
|
"appearance": "Giao diện",
|
||||||
"integrations": "Tích hợp",
|
"integrations": "Tích hợp",
|
||||||
"playback": "Phát",
|
"playback": "Trình phát",
|
||||||
"backup_restore": "Sao lưu & Khôi phục",
|
"backup_restore": "Sao lưu & Khôi phục",
|
||||||
"backup_restore_desc": "Tạo và khôi phục bản sao lưu ứng dụng",
|
"backup_restore_desc": "Tạo và khôi phục bản sao lưu ứng dụng",
|
||||||
"updates": "Cập nhật",
|
"updates": "Cập nhật",
|
||||||
|
|
@ -675,7 +691,7 @@
|
||||||
"clear_mdblist_cache": "Xoá bộ nhớ đệm MDBList",
|
"clear_mdblist_cache": "Xoá bộ nhớ đệm MDBList",
|
||||||
"cache_management": "QUẢN LÝ BỘ NHỚ ĐỆM",
|
"cache_management": "QUẢN LÝ BỘ NHỚ ĐỆM",
|
||||||
"downloads_counter": "lượt tải và đang tăng",
|
"downloads_counter": "lượt tải và đang tăng",
|
||||||
"made_with_love": "Được tạo với ❤️ bởi Tapframe và cộng sự",
|
"made_with_love": "Được tạo với ❤️ bởi Tapframe và cộng đồng",
|
||||||
"sections": {
|
"sections": {
|
||||||
"information": "THÔNG TIN",
|
"information": "THÔNG TIN",
|
||||||
"account": "TÀI KHOẢN",
|
"account": "TÀI KHOẢN",
|
||||||
|
|
@ -694,12 +710,12 @@
|
||||||
"danger_zone": "KHU VỰC NGUY HIỂM"
|
"danger_zone": "KHU VỰC NGUY HIỂM"
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"legal": "Pháp lý & Tuyên bố miễn trách",
|
"legal": "Pháp lý & Quy định",
|
||||||
"privacy_policy": "Chính sách quyền riêng tư",
|
"privacy_policy": "Chính sách bảo mật",
|
||||||
"report_issue": "Báo cáo sự cố",
|
"report_issue": "Báo cáo sự cố",
|
||||||
"version": "Phiên bản",
|
"version": "Phiên bản",
|
||||||
"contributors": "Đóng góp viên",
|
"contributors": "Người đóng góp",
|
||||||
"view_contributors": "Xem tất cả đóng góp viên",
|
"view_contributors": "Xem tất cả người đóng góp",
|
||||||
"theme": "Giao diện",
|
"theme": "Giao diện",
|
||||||
"episode_layout": "Bố cục tập phim",
|
"episode_layout": "Bố cục tập phim",
|
||||||
"streams_backdrop": "Ảnh nền nguồn phát",
|
"streams_backdrop": "Ảnh nền nguồn phát",
|
||||||
|
|
@ -715,7 +731,7 @@
|
||||||
"home_screen": "Màn hình chính",
|
"home_screen": "Màn hình chính",
|
||||||
"home_screen_desc": "Bố cục và nội dung",
|
"home_screen_desc": "Bố cục và nội dung",
|
||||||
"continue_watching": "Xem tiếp",
|
"continue_watching": "Xem tiếp",
|
||||||
"continue_watching_desc": "Bộ nhớ đệm và hành vi phát",
|
"continue_watching_desc": "Bộ nhớ đệm và chế độ phát",
|
||||||
"show_discover": "Hiện phần Khám phá",
|
"show_discover": "Hiện phần Khám phá",
|
||||||
"show_discover_desc": "Hiển thị nội dung khám phá trong Tìm kiếm",
|
"show_discover_desc": "Hiển thị nội dung khám phá trong Tìm kiếm",
|
||||||
"mdblist": "MDBList",
|
"mdblist": "MDBList",
|
||||||
|
|
@ -738,7 +754,7 @@
|
||||||
"auto_select_subs": "Tự động chọn phụ đề",
|
"auto_select_subs": "Tự động chọn phụ đề",
|
||||||
"auto_select_subs_desc": "Tự động chọn phụ đề theo tuỳ chọn của bạn",
|
"auto_select_subs_desc": "Tự động chọn phụ đề theo tuỳ chọn của bạn",
|
||||||
"show_trailers": "Hiện trailer",
|
"show_trailers": "Hiện trailer",
|
||||||
"show_trailers_desc": "Hiển thị trailer trong phần hero",
|
"show_trailers_desc": "Hiển thị trailer trong banner nổi bật",
|
||||||
"enable_downloads": "Bật tải về",
|
"enable_downloads": "Bật tải về",
|
||||||
"enable_downloads_desc": "Hiện tab Tải về và cho phép lưu luồng phát",
|
"enable_downloads_desc": "Hiện tab Tải về và cho phép lưu luồng phát",
|
||||||
"notifications": "Thông báo",
|
"notifications": "Thông báo",
|
||||||
|
|
@ -760,9 +776,9 @@
|
||||||
"internal_first": "Ưu tiên tích hợp",
|
"internal_first": "Ưu tiên tích hợp",
|
||||||
"internal_first_desc": "Ưu tiên phụ đề nhúng sẵn, rồi đến bên ngoài",
|
"internal_first_desc": "Ưu tiên phụ đề nhúng sẵn, rồi đến bên ngoài",
|
||||||
"external_first": "Ưu tiên bên ngoài",
|
"external_first": "Ưu tiên bên ngoài",
|
||||||
"external_first_desc": "Ưu tiên phụ đề addon, rồi đến nhúng sẵn",
|
"external_first_desc": "Ưu tiên phụ đề từ addon, rồi đến nhúng sẵn",
|
||||||
"any_available": "Tuỳ có",
|
"any_available": "Nguồn bất kỳ",
|
||||||
"any_available_desc": "Dùng rãnh phụ đề đầu tiên có sẵn"
|
"any_available_desc": "Tự động chọn phụ đề đầu tiên"
|
||||||
},
|
},
|
||||||
"clear_data_desc": "Thao tác này sẽ đặt lại tất cả cài đặt và xoá dữ liệu đệm. Bạn có chắc không?",
|
"clear_data_desc": "Thao tác này sẽ đặt lại tất cả cài đặt và xoá dữ liệu đệm. Bạn có chắc không?",
|
||||||
"app_updates": "Cập nhật ứng dụng",
|
"app_updates": "Cập nhật ứng dụng",
|
||||||
|
|
@ -873,7 +889,7 @@
|
||||||
"ai_settings": {
|
"ai_settings": {
|
||||||
"title": "Trợ lý AI",
|
"title": "Trợ lý AI",
|
||||||
"info_title": "Chat hỗ trợ bởi AI",
|
"info_title": "Chat hỗ trợ bởi AI",
|
||||||
"info_desc": "Đặt câu hỏi về bất kỳ phim hay tập phim bộ nào bằng AI. Nhận thông tin về cốt truyện, nhân vật, chủ đề, kiến thức thú vị và nhiều hơn nữa - hỗ trợ bởi dữ liệu TMDB toàn diện.",
|
"info_desc": "Đặt câu hỏi về bất kỳ phim hay tập chương trình TV nào bằng AI. Nhận thông tin về cốt truyện, nhân vật, chủ đề, kiến thức thú vị và nhiều hơn nữa - hỗ trợ bởi dữ liệu TMDB toàn diện.",
|
||||||
"feature_1": "Ngữ cảnh và phân tích từng tập",
|
"feature_1": "Ngữ cảnh và phân tích từng tập",
|
||||||
"feature_2": "Giải thích cốt truyện và nhân vật",
|
"feature_2": "Giải thích cốt truyện và nhân vật",
|
||||||
"feature_3": "Kiến thức hậu trường thú vị",
|
"feature_3": "Kiến thức hậu trường thú vị",
|
||||||
|
|
@ -889,7 +905,7 @@
|
||||||
"enable_chat": "Bật chat AI",
|
"enable_chat": "Bật chat AI",
|
||||||
"enable_chat_desc": "Khi bật, nút Hỏi AI sẽ xuất hiện trên trang nội dung.",
|
"enable_chat_desc": "Khi bật, nút Hỏi AI sẽ xuất hiện trên trang nội dung.",
|
||||||
"chat_enabled": "Đã bật chat AI",
|
"chat_enabled": "Đã bật chat AI",
|
||||||
"chat_enabled_desc": "Bạn có thể đặt câu hỏi về phim và phim bộ. Tìm nút \"Hỏi AI\" trên trang nội dung!",
|
"chat_enabled_desc": "Bạn có thể đặt câu hỏi về phim và chương trình TV. Tìm nút \"Hỏi AI\" trên trang nội dung!",
|
||||||
"how_it_works": "Cách hoạt động",
|
"how_it_works": "Cách hoạt động",
|
||||||
"how_it_works_desc": "• OpenRouter cung cấp quyền truy cập nhiều mô hình AI\n• API key của bạn được giữ bí mật và bảo mật\n• Gói miễn phí bao gồm giới hạn sử dụng hào phóng\n• Chat với ngữ cảnh về tập/phim cụ thể\n• Nhận phân tích và giải thích chi tiết",
|
"how_it_works_desc": "• OpenRouter cung cấp quyền truy cập nhiều mô hình AI\n• API key của bạn được giữ bí mật và bảo mật\n• Gói miễn phí bao gồm giới hạn sử dụng hào phóng\n• Chat với ngữ cảnh về tập/phim cụ thể\n• Nhận phân tích và giải thích chi tiết",
|
||||||
"error_invalid_key": "Vui lòng nhập API key hợp lệ",
|
"error_invalid_key": "Vui lòng nhập API key hợp lệ",
|
||||||
|
|
@ -918,7 +934,7 @@
|
||||||
},
|
},
|
||||||
"continue_watching_settings": {
|
"continue_watching_settings": {
|
||||||
"title": "Xem tiếp",
|
"title": "Xem tiếp",
|
||||||
"playback_behavior": "HÀNH VI PHÁT",
|
"playback_behavior": "CHẾ ĐỘ PHÁT",
|
||||||
"use_cached": "Dùng luồng đã lưu đệm",
|
"use_cached": "Dùng luồng đã lưu đệm",
|
||||||
"use_cached_desc": "Khi bật, nhấn mục Xem tiếp sẽ mở trình phát trực tiếp dùng luồng đã phát trước đó. Khi tắt, sẽ mở màn hình nội dung.",
|
"use_cached_desc": "Khi bật, nhấn mục Xem tiếp sẽ mở trình phát trực tiếp dùng luồng đã phát trước đó. Khi tắt, sẽ mở màn hình nội dung.",
|
||||||
"open_metadata": "Mở màn hình thông tin",
|
"open_metadata": "Mở màn hình thông tin",
|
||||||
|
|
@ -942,9 +958,9 @@
|
||||||
"hours": "giờ"
|
"hours": "giờ"
|
||||||
},
|
},
|
||||||
"contributors": {
|
"contributors": {
|
||||||
"title": "Đóng góp viên",
|
"title": "Người đóng góp",
|
||||||
"special_mentions": "Nhắc đến đặc biệt",
|
"special_mentions": "Nhắc đến đặc biệt",
|
||||||
"tab_contributors": "Đóng góp viên",
|
"tab_contributors": "Người đóng góp",
|
||||||
"tab_special": "Nhắc đến đặc biệt",
|
"tab_special": "Nhắc đến đặc biệt",
|
||||||
"tab_donors": "Nhà tài trợ",
|
"tab_donors": "Nhà tài trợ",
|
||||||
"manager_role": "Quản lý cộng đồng",
|
"manager_role": "Quản lý cộng đồng",
|
||||||
|
|
@ -966,10 +982,10 @@
|
||||||
"loading_donors": "Đang tải nhà tài trợ...",
|
"loading_donors": "Đang tải nhà tài trợ...",
|
||||||
"no_donors": "Chưa có nhà tài trợ",
|
"no_donors": "Chưa có nhà tài trợ",
|
||||||
"error_rate_limit": "Đã vượt giới hạn tốc độ GitHub API. Vui lòng thử lại sau hoặc kéo để làm mới.",
|
"error_rate_limit": "Đã vượt giới hạn tốc độ GitHub API. Vui lòng thử lại sau hoặc kéo để làm mới.",
|
||||||
"error_failed": "Không thể tải đóng góp viên. Vui lòng kiểm tra kết nối internet.",
|
"error_failed": "Không thể tải người đóng góp. Vui lòng kiểm tra kết nối internet.",
|
||||||
"retry": "Thử lại",
|
"retry": "Thử lại",
|
||||||
"no_contributors": "Không tìm thấy đóng góp viên",
|
"no_contributors": "Không tìm thấy người đóng góp",
|
||||||
"loading_contributors": "Đang tải đóng góp viên..."
|
"loading_contributors": "Đang tải người đóng góp..."
|
||||||
},
|
},
|
||||||
"debrid": {
|
"debrid": {
|
||||||
"title": "Tích hợp Debrid",
|
"title": "Tích hợp Debrid",
|
||||||
|
|
@ -1036,7 +1052,7 @@
|
||||||
"title": "Cài đặt màn hình chính",
|
"title": "Cài đặt màn hình chính",
|
||||||
"changes_applied": "Đã áp dụng thay đổi",
|
"changes_applied": "Đã áp dụng thay đổi",
|
||||||
"display_options": "TUỲ CHỌN HIỂN THỊ",
|
"display_options": "TUỲ CHỌN HIỂN THỊ",
|
||||||
"show_hero": "Hiện phần hero",
|
"show_hero": "Hiện banner nổi bật",
|
||||||
"show_hero_desc": "Nội dung nổi bật ở trên cùng",
|
"show_hero_desc": "Nội dung nổi bật ở trên cùng",
|
||||||
"show_this_week": "Hiện phần Tuần này",
|
"show_this_week": "Hiện phần Tuần này",
|
||||||
"show_this_week_desc": "Tập mới trong tuần hiện tại",
|
"show_this_week_desc": "Tập mới trong tuần hiện tại",
|
||||||
|
|
@ -1045,16 +1061,16 @@
|
||||||
"selected": "đã chọn",
|
"selected": "đã chọn",
|
||||||
"prefer_external_meta": "Ưu tiên Addon metadata bên ngoài",
|
"prefer_external_meta": "Ưu tiên Addon metadata bên ngoài",
|
||||||
"prefer_external_meta_desc": "Dùng metadata bên ngoài trên trang chi tiết",
|
"prefer_external_meta_desc": "Dùng metadata bên ngoài trên trang chi tiết",
|
||||||
"hero_layout": "Bố cục hero",
|
"hero_layout": "Bố cục banner nổi bật",
|
||||||
"layout_legacy": "Cổ điển",
|
"layout_legacy": "Cổ điển",
|
||||||
"layout_carousel": "Băng chuyền",
|
"layout_carousel": "Danh sách",
|
||||||
"layout_appletv": "Apple TV",
|
"layout_appletv": "Apple TV",
|
||||||
"layout_desc": "Banner toàn màn hình, thẻ vuốt, hoặc kiểu Apple TV",
|
"layout_desc": "Banner toàn màn hình, thẻ vuốt, hoặc kiểu Apple TV",
|
||||||
"featured_source": "Nguồn nổi bật",
|
"featured_source": "Nguồn nổi bật",
|
||||||
"using_catalogs": "Dùng danh mục",
|
"using_catalogs": "Dùng danh mục",
|
||||||
"manage_selected_catalogs": "Quản lý danh mục đã chọn",
|
"manage_selected_catalogs": "Quản lý danh mục đã chọn",
|
||||||
"dynamic_bg": "Nền hero động",
|
"dynamic_bg": "Nền banner nổi bật động",
|
||||||
"dynamic_bg_desc": "Ảnh nền mờ phía sau băng chuyền",
|
"dynamic_bg_desc": "Ảnh nền mờ phía sau danh sách",
|
||||||
"performance_note": "Có thể ảnh hưởng hiệu suất trên thiết bị cấu hình thấp.",
|
"performance_note": "Có thể ảnh hưởng hiệu suất trên thiết bị cấu hình thấp.",
|
||||||
"posters": "Poster",
|
"posters": "Poster",
|
||||||
"show_titles": "Hiện tiêu đề",
|
"show_titles": "Hiện tiêu đề",
|
||||||
|
|
@ -1069,14 +1085,14 @@
|
||||||
"about_these_settings": "VỀ CÁC CÀI ĐẶT NÀY",
|
"about_these_settings": "VỀ CÁC CÀI ĐẶT NÀY",
|
||||||
"about_desc": "Các cài đặt này kiểm soát cách hiển thị nội dung trên màn hình chính. Thay đổi được áp dụng ngay mà không cần khởi động lại ứng dụng.",
|
"about_desc": "Các cài đặt này kiểm soát cách hiển thị nội dung trên màn hình chính. Thay đổi được áp dụng ngay mà không cần khởi động lại ứng dụng.",
|
||||||
"hero_catalogs": {
|
"hero_catalogs": {
|
||||||
"title": "Danh mục phần hero",
|
"title": "Danh mục banner nổi bật",
|
||||||
"select_all": "Chọn tất cả",
|
"select_all": "Chọn tất cả",
|
||||||
"clear_all": "Bỏ chọn tất cả",
|
"clear_all": "Bỏ chọn tất cả",
|
||||||
"info": "Chọn danh mục hiển thị trong phần hero. Nếu không chọn, tất cả danh mục sẽ được dùng. Đừng quên nhấn Lưu khi xong.",
|
"info": "Chọn danh mục hiển thị trong banner nổi bật. Nếu không chọn, tất cả danh mục sẽ được dùng. Đừng quên nhấn Lưu khi xong.",
|
||||||
"settings_saved": "Đã lưu cài đặt",
|
"settings_saved": "Đã lưu cài đặt",
|
||||||
"error_load": "Tải danh mục thất bại",
|
"error_load": "Tải danh mục thất bại",
|
||||||
"movies": "Phim lẻ",
|
"movies": "Phim",
|
||||||
"tv_shows": "Phim bộ"
|
"tv_shows": "Chương trình TV"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
|
|
@ -1088,7 +1104,7 @@
|
||||||
"show_all_episodes": "Hiện tất cả tập",
|
"show_all_episodes": "Hiện tất cả tập",
|
||||||
"no_episodes_for": "Không có tập cho {{date}}",
|
"no_episodes_for": "Không có tập cho {{date}}",
|
||||||
"no_upcoming_found": "Không tìm thấy tập sắp tới",
|
"no_upcoming_found": "Không tìm thấy tập sắp tới",
|
||||||
"add_series_desc": "Thêm phim bộ vào thư viện để xem các tập sắp tới ở đây"
|
"add_series_desc": "Thêm chương trình TV vào thư viện để xem các tập sắp tới ở đây"
|
||||||
},
|
},
|
||||||
"mdblist": {
|
"mdblist": {
|
||||||
"title": "Nguồn xếp hạng",
|
"title": "Nguồn xếp hạng",
|
||||||
|
|
@ -1142,7 +1158,7 @@
|
||||||
"stats_total": "Tổng cộng",
|
"stats_total": "Tổng cộng",
|
||||||
"sync_button": "Đồng bộ Thư viện & Trakt",
|
"sync_button": "Đồng bộ Thư viện & Trakt",
|
||||||
"syncing": "Đang đồng bộ...",
|
"syncing": "Đang đồng bộ...",
|
||||||
"sync_desc": "Tự động đồng bộ thông báo cho tất cả phim bộ trong thư viện và danh sách/bộ sưu tập Trakt.",
|
"sync_desc": "Tự động đồng bộ thông báo cho tất cả chương trình TV trong thư viện và danh sách/bộ sưu tập Trakt.",
|
||||||
"section_advanced": "Nâng cao",
|
"section_advanced": "Nâng cao",
|
||||||
"reset_button": "Đặt lại tất cả thông báo",
|
"reset_button": "Đặt lại tất cả thông báo",
|
||||||
"test_button": "Kiểm tra thông báo (5 giây)",
|
"test_button": "Kiểm tra thông báo (5 giây)",
|
||||||
|
|
@ -1163,7 +1179,7 @@
|
||||||
"section_addons": "Addon & Tích hợp",
|
"section_addons": "Addon & Tích hợp",
|
||||||
"section_settings": "Cài đặt & Tuỳ chọn",
|
"section_settings": "Cài đặt & Tuỳ chọn",
|
||||||
"library_label": "Thư viện",
|
"library_label": "Thư viện",
|
||||||
"library_desc": "Phim lẻ và phim bộ đã lưu",
|
"library_desc": "Phim và chương trình TV đã lưu",
|
||||||
"watch_progress_label": "Tiến độ xem",
|
"watch_progress_label": "Tiến độ xem",
|
||||||
"watch_progress_desc": "Vị trí xem tiếp",
|
"watch_progress_desc": "Vị trí xem tiếp",
|
||||||
"addons_label": "Addon",
|
"addons_label": "Addon",
|
||||||
|
|
@ -1208,8 +1224,8 @@
|
||||||
"status_error": "Cập nhật thất bại",
|
"status_error": "Cập nhật thất bại",
|
||||||
"status_ready": "Sẵn sàng kiểm tra cập nhật",
|
"status_ready": "Sẵn sàng kiểm tra cập nhật",
|
||||||
"action_check": "Kiểm tra cập nhật",
|
"action_check": "Kiểm tra cập nhật",
|
||||||
"action_install": "Cài cập nhật",
|
"action_install": "Cài bản cập nhật",
|
||||||
"release_notes": "Ghi chú phát hành:",
|
"release_notes": "Chi tiết bản cập nhật:",
|
||||||
"version": "Phiên bản:",
|
"version": "Phiên bản:",
|
||||||
"last_checked": "Kiểm tra lần cuối:",
|
"last_checked": "Kiểm tra lần cuối:",
|
||||||
"current_version": "Phiên bản hiện tại:",
|
"current_version": "Phiên bản hiện tại:",
|
||||||
|
|
@ -1320,8 +1336,8 @@
|
||||||
"placeholder": "Tìm plugin...",
|
"placeholder": "Tìm plugin...",
|
||||||
"all": "Tất cả",
|
"all": "Tất cả",
|
||||||
"filter_all": "Tất cả loại",
|
"filter_all": "Tất cả loại",
|
||||||
"filter_movies": "Phim lẻ",
|
"filter_movies": "Phim",
|
||||||
"filter_tv": "Phim bộ",
|
"filter_tv": "Chương trình TV",
|
||||||
"enable_all": "Bật tất cả",
|
"enable_all": "Bật tất cả",
|
||||||
"disable_all": "Tắt tất cả",
|
"disable_all": "Tắt tất cả",
|
||||||
"no_plugins_found": "Không tìm thấy plugin",
|
"no_plugins_found": "Không tìm thấy plugin",
|
||||||
|
|
@ -1395,9 +1411,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"legal": {
|
"legal": {
|
||||||
"title": "Pháp lý & Tuyên bố miễn trách",
|
"title": "Pháp lý & Quy định",
|
||||||
"intro_title": "Bản chất ứng dụng",
|
"intro_title": "Bản chất ứng dụng",
|
||||||
"intro_text": "Nuvio là ứng dụng trình phát phương tiện và quản lý metadata. Nó chỉ hoạt động như giao diện phía máy khách để duyệt metadata công khai (phim, phim bộ...) và phát file phương tiện do người dùng hoặc tiện ích mở rộng bên thứ ba cung cấp. Nuvio không lưu trữ, phân phối hay lập chỉ mục nội dung phương tiện.",
|
"intro_text": "Nuvio là ứng dụng trình phát phương tiện và quản lý metadata. Nó chỉ hoạt động như giao diện phía máy khách để duyệt metadata công khai (phim, chương trình TV...) và phát file phương tiện do người dùng hoặc tiện ích mở rộng bên thứ ba cung cấp. Nuvio không lưu trữ, phân phối hay lập chỉ mục nội dung phương tiện.",
|
||||||
"extensions_title": "Plugin bên thứ ba",
|
"extensions_title": "Plugin bên thứ ba",
|
||||||
"extensions_text": "Nuvio sử dụng kiến trúc mở rộng cho phép người dùng cài plugin bên thứ ba. Các plugin này được phát triển và duy trì bởi các nhà phát triển độc lập không liên kết với Nuvio. Chúng tôi không kiểm soát và không chịu trách nhiệm về nội dung, tính hợp pháp hay chức năng của bất kỳ plugin bên thứ ba nào.",
|
"extensions_text": "Nuvio sử dụng kiến trúc mở rộng cho phép người dùng cài plugin bên thứ ba. Các plugin này được phát triển và duy trì bởi các nhà phát triển độc lập không liên kết với Nuvio. Chúng tôi không kiểm soát và không chịu trách nhiệm về nội dung, tính hợp pháp hay chức năng của bất kỳ plugin bên thứ ba nào.",
|
||||||
"user_resp_title": "Trách nhiệm người dùng",
|
"user_resp_title": "Trách nhiệm người dùng",
|
||||||
|
|
@ -1420,8 +1436,8 @@
|
||||||
"common": {
|
"common": {
|
||||||
"error": "Lỗi",
|
"error": "Lỗi",
|
||||||
"success": "Thành công",
|
"success": "Thành công",
|
||||||
"movie": "Phim lẻ",
|
"movie": "Phim",
|
||||||
"tv": "Phim bộ",
|
"tv": "Chương trình TV",
|
||||||
"tmdb_id": "TMDB ID",
|
"tmdb_id": "TMDB ID",
|
||||||
"season": "Phần",
|
"season": "Phần",
|
||||||
"episode": "Tập",
|
"episode": "Tập",
|
||||||
|
|
@ -1444,18 +1460,18 @@
|
||||||
"code_placeholder": "// Dán code plugin vào đây...",
|
"code_placeholder": "// Dán code plugin vào đây...",
|
||||||
"test_parameters": "Tham số kiểm tra",
|
"test_parameters": "Tham số kiểm tra",
|
||||||
"no_logs": "Chưa có log. Chạy kiểm tra để xem đầu ra.",
|
"no_logs": "Chưa có log. Chạy kiểm tra để xem đầu ra.",
|
||||||
"no_streams": "Chưa tìm thấy luồng nào.",
|
"no_streams": "Chưa tìm thấy nguồn nào.",
|
||||||
"streams_found": "Tìm thấy {{count}} luồng",
|
"streams_found": "Tìm thấy {{count}} nguồn",
|
||||||
"streams_found_plural": "Tìm thấy {{count}} luồng",
|
"streams_found_plural": "Tìm thấy {{count}} nguồn",
|
||||||
"tap_play_hint": "Nhấn Phát để kiểm tra luồng trong trình phát.",
|
"tap_play_hint": "Nhấn Xem để kiểm tra nguồn trong trình phát.",
|
||||||
"unnamed_stream": "Luồng không tên",
|
"unnamed_stream": "Nguồn không tên",
|
||||||
"quality": "Chất lượng: {{quality}}",
|
"quality": "Chất lượng: {{quality}}",
|
||||||
"size": "Kích thước: {{size}}",
|
"size": "Kích thước: {{size}}",
|
||||||
"url_label": "URL: {{url}}",
|
"url_label": "URL: {{url}}",
|
||||||
"headers_info": "Header: {{count}} header tuỳ chỉnh",
|
"headers_info": "Header: {{count}} header tuỳ chỉnh",
|
||||||
"find_placeholder": "Tìm trong code…",
|
"find_placeholder": "Tìm trong code…",
|
||||||
"edit_code_title": "Sửa Code",
|
"edit_code_title": "Sửa Code",
|
||||||
"no_url_stream_error": "Không tìm thấy URL cho luồng này"
|
"no_url_stream_error": "Không tìm thấy URL cho nguồn này"
|
||||||
},
|
},
|
||||||
"repo": {
|
"repo": {
|
||||||
"title": "Kiểm tra kho",
|
"title": "Kiểm tra kho",
|
||||||
|
|
|
||||||
|
|
@ -1536,8 +1536,29 @@ class CatalogService {
|
||||||
const addonOrderRef: Record<string, number> = {};
|
const addonOrderRef: Record<string, number> = {};
|
||||||
searchableAddons.forEach((addon, i) => { addonOrderRef[addon.id] = i; });
|
searchableAddons.forEach((addon, i) => { addonOrderRef[addon.id] = i; });
|
||||||
|
|
||||||
// Global dedupe across emitted results
|
// Human-readable labels for known content types
|
||||||
const globalSeen = new Set<string>();
|
const CATALOG_TYPE_LABELS: Record<string, string> = {
|
||||||
|
'movie': 'Movies',
|
||||||
|
'series': 'TV Shows',
|
||||||
|
'anime.series': 'Anime Series',
|
||||||
|
'anime.movie': 'Anime Movies',
|
||||||
|
'other': 'Other',
|
||||||
|
'tv': 'TV',
|
||||||
|
'channel': 'Channels',
|
||||||
|
};
|
||||||
|
const GENERIC_CATALOG_NAMES = new Set(['search', 'Search']);
|
||||||
|
|
||||||
|
// Collect all sections from all addons first, then sort and dedup before emitting.
|
||||||
|
// This avoids race conditions where concurrent addon workers steal each other's IDs
|
||||||
|
// from a shared globalSeen set before they get a chance to emit.
|
||||||
|
type PendingSection = {
|
||||||
|
addonId: string;
|
||||||
|
addonName: string;
|
||||||
|
sectionName: string;
|
||||||
|
catalogIndex: number;
|
||||||
|
results: StreamingContent[];
|
||||||
|
};
|
||||||
|
const allPendingSections: PendingSection[] = [];
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
searchableAddons.map(async (addon) => {
|
searchableAddons.map(async (addon) => {
|
||||||
|
|
@ -1552,47 +1573,24 @@ class CatalogService {
|
||||||
const searchableCatalogs = (addon.catalogs || []).filter(catalog => this.canSearchCatalog(catalog));
|
const searchableCatalogs = (addon.catalogs || []).filter(catalog => this.canSearchCatalog(catalog));
|
||||||
logger.log(`Searching ${addon.name} (${addon.id}) with ${searchableCatalogs.length} searchable catalogs`);
|
logger.log(`Searching ${addon.name} (${addon.id}) with ${searchableCatalogs.length} searchable catalogs`);
|
||||||
|
|
||||||
// Fetch all catalogs for this addon in parallel
|
|
||||||
const settled = await Promise.allSettled(
|
const settled = await Promise.allSettled(
|
||||||
searchableCatalogs.map(c => this.searchAddonCatalog(manifest, c.type, c.id, trimmedQuery))
|
searchableCatalogs.map(c => this.searchAddonCatalog(manifest, c.type, c.id, trimmedQuery))
|
||||||
);
|
);
|
||||||
if (controller.cancelled) return;
|
if (controller.cancelled) return;
|
||||||
|
|
||||||
// If addon has multiple search catalogs, emit each as its own section.
|
|
||||||
// If only one, emit as a single addon section (original behaviour).
|
|
||||||
const hasMultipleCatalogs = searchableCatalogs.length > 1;
|
const hasMultipleCatalogs = searchableCatalogs.length > 1;
|
||||||
|
const addonRank = addonOrderRef[addon.id] ?? Number.MAX_SAFE_INTEGER;
|
||||||
const catalogResultsList: { catalog: any; results: StreamingContent[] }[] = [];
|
|
||||||
for (let i = 0; i < searchableCatalogs.length; i++) {
|
|
||||||
const s = settled[i];
|
|
||||||
if (s.status === 'fulfilled' && Array.isArray(s.value) && s.value.length > 0) {
|
|
||||||
catalogResultsList.push({ catalog: searchableCatalogs[i], results: s.value });
|
|
||||||
} else if (s.status === 'rejected') {
|
|
||||||
logger.warn(`Search failed for catalog ${searchableCatalogs[i].id} in ${addon.name}:`, s.reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (catalogResultsList.length === 0) {
|
|
||||||
logger.log(`No results from ${addon.name}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasMultipleCatalogs) {
|
if (hasMultipleCatalogs) {
|
||||||
// Human-readable labels for known content types used as fallback section names
|
for (let ci = 0; ci < searchableCatalogs.length; ci++) {
|
||||||
const CATALOG_TYPE_LABELS: Record<string, string> = {
|
const s = settled[ci];
|
||||||
'movie': 'Movies',
|
const catalog = searchableCatalogs[ci];
|
||||||
'series': 'TV Shows',
|
if (s.status === 'rejected' || !(s as PromiseFulfilledResult<StreamingContent[]>).value?.length) {
|
||||||
'anime.series': 'Anime Series',
|
if (s.status === 'rejected') logger.warn(`Search failed for ${catalog.id} in ${addon.name}:`, s.reason);
|
||||||
'anime.movie': 'Anime Movies',
|
continue;
|
||||||
'other': 'Other',
|
}
|
||||||
'tv': 'TV',
|
|
||||||
'channel': 'Channels',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Emit each catalog as its own section, in manifest order
|
const results = (s as PromiseFulfilledResult<StreamingContent[]>).value;
|
||||||
for (let ci = 0; ci < catalogResultsList.length; ci++) {
|
|
||||||
const { catalog, results } = catalogResultsList[ci];
|
|
||||||
if (controller.cancelled) return;
|
|
||||||
|
|
||||||
// Within-catalog dedup: prefer dot-type over generic for same ID
|
// Within-catalog dedup: prefer dot-type over generic for same ID
|
||||||
const bestById = new Map<string, StreamingContent>();
|
const bestById = new Map<string, StreamingContent>();
|
||||||
|
|
@ -1604,74 +1602,63 @@ class CatalogService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stamp catalog type onto results
|
// Stamp catalog type onto results
|
||||||
const stamped = Array.from(bestById.values()).map(item => {
|
const stamped = Array.from(bestById.values()).map(item =>
|
||||||
if (catalog.type && item.type !== catalog.type) {
|
catalog.type && item.type !== catalog.type ? { ...item, type: catalog.type } : item
|
||||||
return { ...item, type: catalog.type };
|
);
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dedupe against global seen
|
// Build section name — use type label if catalog name is generic
|
||||||
const unique = stamped.filter(item => {
|
const typeLabel = CATALOG_TYPE_LABELS[catalog.type]
|
||||||
const key = `${item.type}:${item.id}`;
|
|| catalog.type.replace(/[._]/g, ' ').replace(/\w/g, (c: string) => c.toUpperCase());
|
||||||
if (globalSeen.has(key)) return false;
|
const catalogLabel = (!catalog.name || GENERIC_CATALOG_NAMES.has(catalog.name) || catalog.name === addon.name)
|
||||||
globalSeen.add(key);
|
? typeLabel
|
||||||
return true;
|
: catalog.name;
|
||||||
});
|
const sectionName = `${addon.name} - ${catalogLabel}`;
|
||||||
|
const catalogIndex = addonRank * 1000 + ci;
|
||||||
|
|
||||||
if (unique.length > 0 && !controller.cancelled) {
|
allPendingSections.push({ addonId: `${addon.id}||${catalog.id}`, addonName: addon.name, sectionName, catalogIndex, results: stamped });
|
||||||
// Build section name:
|
|
||||||
// - If catalog.name is generic ("Search") or same as addon name, use type label instead
|
|
||||||
// - Otherwise use catalog.name as-is
|
|
||||||
const GENERIC_NAMES = new Set(['search', 'Search']);
|
|
||||||
const typeLabel = CATALOG_TYPE_LABELS[catalog.type]
|
|
||||||
|| catalog.type.replace(/[._]/g, ' ').replace(/\w/g, (c: string) => c.toUpperCase());
|
|
||||||
const catalogLabel = (!catalog.name || GENERIC_NAMES.has(catalog.name) || catalog.name === addon.name)
|
|
||||||
? typeLabel
|
|
||||||
: catalog.name;
|
|
||||||
const sectionName = `${addon.name} - ${catalogLabel}`;
|
|
||||||
|
|
||||||
// catalogIndex encodes addon rank + position within addon for deterministic ordering
|
|
||||||
const addonRank = addonOrderRef[addon.id] ?? Number.MAX_SAFE_INTEGER;
|
|
||||||
const catalogIndex = addonRank * 1000 + ci;
|
|
||||||
|
|
||||||
logger.log(`Emitting ${unique.length} results from ${sectionName}`);
|
|
||||||
onAddonResults({ addonId: `${addon.id}||${catalog.id}`, addonName: addon.name, sectionName, catalogIndex, results: unique });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Single catalog — one section per addon
|
const s = settled[0];
|
||||||
const allResults = catalogResultsList.flatMap(c => c.results);
|
const catalog = searchableCatalogs[0];
|
||||||
|
if (!s || s.status === 'rejected' || !(s as PromiseFulfilledResult<StreamingContent[]>).value?.length) {
|
||||||
|
if (s?.status === 'rejected') logger.warn(`Search failed for ${addon.name}:`, s.reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const bestByIdWithinAddon = new Map<string, StreamingContent>();
|
const results = (s as PromiseFulfilledResult<StreamingContent[]>).value;
|
||||||
for (const item of allResults) {
|
const bestById = new Map<string, StreamingContent>();
|
||||||
const existing = bestByIdWithinAddon.get(item.id);
|
for (const item of results) {
|
||||||
|
const existing = bestById.get(item.id);
|
||||||
if (!existing || (!existing.type.includes('.') && item.type.includes('.'))) {
|
if (!existing || (!existing.type.includes('.') && item.type.includes('.'))) {
|
||||||
bestByIdWithinAddon.set(item.id, item);
|
bestById.set(item.id, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const deduped = Array.from(bestByIdWithinAddon.values());
|
const stamped = Array.from(bestById.values()).map(item =>
|
||||||
|
catalog.type && item.type !== catalog.type ? { ...item, type: catalog.type } : item
|
||||||
|
);
|
||||||
|
|
||||||
const localSeen = new Set<string>();
|
allPendingSections.push({ addonId: addon.id, addonName: addon.name, sectionName: addon.name, catalogIndex: addonRank * 1000, results: stamped });
|
||||||
const unique = deduped.filter(item => {
|
|
||||||
const key = `${item.type}:${item.id}`;
|
|
||||||
if (localSeen.has(key) || globalSeen.has(key)) return false;
|
|
||||||
localSeen.add(key);
|
|
||||||
globalSeen.add(key);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (unique.length > 0 && !controller.cancelled) {
|
|
||||||
const addonRank = addonOrderRef[addon.id] ?? Number.MAX_SAFE_INTEGER;
|
|
||||||
logger.log(`Emitting ${unique.length} results from ${addon.name}`);
|
|
||||||
onAddonResults({ addonId: addon.id, addonName: addon.name, sectionName: addon.name, catalogIndex: addonRank * 1000, results: unique });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Error searching addon ${addon.name} (${addon.id}):`, e);
|
logger.error(`Error searching addon ${addon.name} (${addon.id}):`, e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (controller.cancelled) return;
|
||||||
|
|
||||||
|
// Sort by catalogIndex (addon manifest order + position within addon) then emit.
|
||||||
|
// No cross-section dedup — each section is shown separately so duplicates across
|
||||||
|
// sections are intentional (e.g. same movie in Cinemeta and People Search).
|
||||||
|
allPendingSections.sort((a, b) => a.catalogIndex - b.catalogIndex);
|
||||||
|
|
||||||
|
for (const section of allPendingSections) {
|
||||||
|
if (controller.cancelled) return;
|
||||||
|
if (section.results.length > 0) {
|
||||||
|
logger.log(`Emitting ${section.results.length} results from ${section.sectionName}`);
|
||||||
|
onAddonResults({ addonId: section.addonId, addonName: section.addonName, sectionName: section.sectionName, catalogIndex: section.catalogIndex, results: section.results });
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -523,24 +523,58 @@ export class TMDBService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract TMDB ID from Stremio ID
|
* Extract TMDB ID from Stremio ID.
|
||||||
* Stremio IDs for series are typically in the format: tt1234567:1:1 (imdbId:season:episode)
|
* Handles standard IMDb IDs (tt1234567) as well as anime provider IDs:
|
||||||
* or just tt1234567 for the series itself
|
* - kitsu:12345 → looks up via ARM (arm.haglund.dev)
|
||||||
|
* - mal:12345 → looks up via ARM
|
||||||
|
* - anilist:12345 → looks up via ARM
|
||||||
*/
|
*/
|
||||||
async extractTMDBIdFromStremioId(stremioId: string): Promise<number | null> {
|
async extractTMDBIdFromStremioId(stremioId: string): Promise<number | null> {
|
||||||
try {
|
try {
|
||||||
// Extract the base ID (remove season/episode info if present)
|
// Strip season/episode suffix — e.g. "kitsu:7936:5" → "kitsu:7936"
|
||||||
const baseId = stremioId.split(':')[0];
|
const parts = stremioId.split(':');
|
||||||
|
const prefix = parts[0];
|
||||||
|
const numericId = parts[1];
|
||||||
|
|
||||||
// Only try to convert if it's an IMDb ID (starts with 'tt')
|
// Standard IMDb ID
|
||||||
if (!baseId.startsWith('tt')) {
|
if (prefix.startsWith('tt') || /^\d{7,}$/.test(prefix)) {
|
||||||
|
const baseId = prefix.startsWith('tt') ? prefix : `tt${prefix}`;
|
||||||
|
return await this.findTMDBIdByIMDB(baseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anime provider IDs — resolve via ARM (https://arm.haglund.dev/api/v2)
|
||||||
|
const ARM_SOURCES: Record<string, string> = {
|
||||||
|
kitsu: 'kitsu',
|
||||||
|
mal: 'myanimelist',
|
||||||
|
anilist: 'anilist',
|
||||||
|
};
|
||||||
|
|
||||||
|
const armSource = ARM_SOURCES[prefix];
|
||||||
|
if (armSource && numericId && /^\d+$/.test(numericId)) {
|
||||||
|
const cacheKey = this.generateCacheKey('arm_tmdb', { source: armSource, id: numericId });
|
||||||
|
const cached = this.getCachedData<number>(cacheKey);
|
||||||
|
if (cached !== null) return cached;
|
||||||
|
|
||||||
|
logger.log(`[TMDB] Resolving TMDB ID for ${prefix}:${numericId} via ARM`);
|
||||||
|
const response = await axios.get('https://arm.haglund.dev/api/v2/ids', {
|
||||||
|
params: { source: armSource, id: numericId },
|
||||||
|
timeout: 8000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tmdbId: number | undefined = response.data?.themoviedb;
|
||||||
|
if (tmdbId) {
|
||||||
|
this.setCachedData(cacheKey, tmdbId);
|
||||||
|
logger.log(`[TMDB] ARM resolved ${prefix}:${numericId} → TMDB ${tmdbId}`);
|
||||||
|
return tmdbId;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(`[TMDB] ARM did not return a TMDB ID for ${prefix}:${numericId}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the existing findTMDBIdByIMDB function to get the TMDB ID
|
return null;
|
||||||
const tmdbId = await this.findTMDBIdByIMDB(baseId);
|
|
||||||
return tmdbId;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.warn('[TMDB] extractTMDBIdFromStremioId failed:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1155,17 +1155,19 @@ export class TraktService {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async isMovieWatchedAccurate(imdbId: string): Promise<boolean> {
|
public async isMovieWatchedAccurate(imdbId: string, fallbackImdbId?: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const imdb = imdbId.startsWith('tt')
|
const normalise = (id: string) => id.startsWith('tt') ? id : `tt${id}`;
|
||||||
? imdbId
|
const imdb = normalise(imdbId);
|
||||||
: `tt${imdbId}`;
|
const fallback = fallbackImdbId && fallbackImdbId !== imdbId && fallbackImdbId.trim()
|
||||||
|
? normalise(fallbackImdbId)
|
||||||
|
: null;
|
||||||
|
|
||||||
const movies = await this.apiRequest<any[]>('/sync/watched/movies');
|
const movies = await this.apiRequest<any[]>('/sync/watched/movies');
|
||||||
const moviesArray = Array.isArray(movies) ? movies : [];
|
const moviesArray = Array.isArray(movies) ? movies : [];
|
||||||
|
|
||||||
return moviesArray.some(
|
return moviesArray.some(
|
||||||
(m: any) => m.movie?.ids?.imdb === imdb
|
(m: any) => m.movie?.ids?.imdb === imdb || (fallback && m.movie?.ids?.imdb === fallback)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('[TraktService] Movie watched check failed', err);
|
logger.warn('[TraktService] Movie watched check failed', err);
|
||||||
|
|
@ -1176,14 +1178,20 @@ export class TraktService {
|
||||||
public async isEpisodeWatchedAccurate(
|
public async isEpisodeWatchedAccurate(
|
||||||
showImdbId: string,
|
showImdbId: string,
|
||||||
season: number,
|
season: number,
|
||||||
episode: number
|
episode: number,
|
||||||
|
fallbackImdbId?: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
if (season === 0) return false;
|
if (season === 0) return false;
|
||||||
|
|
||||||
const imdb = showImdbId.startsWith('tt')
|
const normalise = (id: string) => id.startsWith('tt') ? id : `tt${id}`;
|
||||||
? showImdbId
|
const isRealImdbId = (id: string) => /^tt\d+$/.test(id) || /^\d{7,}$/.test(id);
|
||||||
: `tt${showImdbId}`;
|
|
||||||
|
// Use fallback if primary isn't a real IMDb ID
|
||||||
|
const resolvedId = isRealImdbId(showImdbId) ? showImdbId
|
||||||
|
: (fallbackImdbId && isRealImdbId(fallbackImdbId) ? fallbackImdbId : showImdbId);
|
||||||
|
|
||||||
|
const imdb = normalise(resolvedId);
|
||||||
|
|
||||||
const watchedShows = await this.apiRequest<any[]>(
|
const watchedShows = await this.apiRequest<any[]>(
|
||||||
'/sync/watched/shows'
|
'/sync/watched/shows'
|
||||||
|
|
@ -1521,16 +1529,25 @@ export class TraktService {
|
||||||
imdbId: string,
|
imdbId: string,
|
||||||
season: number,
|
season: number,
|
||||||
episode: number,
|
episode: number,
|
||||||
watchedAt: Date = new Date()
|
watchedAt: Date = new Date(),
|
||||||
|
fallbackImdbId?: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const traktId = await this.getTraktIdFromImdbId(imdbId, 'show');
|
const isImdbFormat = (id: string) => /^tt\d+$/.test(id) || /^\d{7,}$/.test(id);
|
||||||
|
const resolvedId = isImdbFormat(imdbId) ? imdbId
|
||||||
|
: (fallbackImdbId && isImdbFormat(fallbackImdbId) ? fallbackImdbId : imdbId);
|
||||||
|
|
||||||
|
if (resolvedId !== imdbId) {
|
||||||
|
logger.log(`[TraktService] addToWatchedEpisodes: falling back from "${imdbId}" to "${resolvedId}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const traktId = await this.getTraktIdFromImdbId(resolvedId, 'show');
|
||||||
if (!traktId) {
|
if (!traktId) {
|
||||||
logger.warn(`[TraktService] Could not find Trakt ID for show: ${imdbId}`);
|
logger.warn(`[TraktService] Could not find Trakt ID for show: ${resolvedId}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(`[TraktService] Marking S${season}E${episode} as watched for show ${imdbId} (trakt: ${traktId})`);
|
logger.log(`[TraktService] Marking S${season}E${episode} as watched for show ${resolvedId} (trakt: ${traktId})`);
|
||||||
|
|
||||||
// Use shows array with seasons/episodes structure per Trakt API docs
|
// Use shows array with seasons/episodes structure per Trakt API docs
|
||||||
await this.apiRequest('/sync/history', 'POST', {
|
await this.apiRequest('/sync/history', 'POST', {
|
||||||
|
|
@ -1570,16 +1587,22 @@ export class TraktService {
|
||||||
public async markSeasonAsWatched(
|
public async markSeasonAsWatched(
|
||||||
imdbId: string,
|
imdbId: string,
|
||||||
season: number,
|
season: number,
|
||||||
watchedAt: Date = new Date()
|
watchedAt: Date = new Date(),
|
||||||
|
fallbackImdbId?: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const traktId = await this.getTraktIdFromImdbId(imdbId, 'show');
|
const isImdbFormat = (id: string) => /^tt\d+$/.test(id) || /^\d{7,}$/.test(id);
|
||||||
|
const resolvedId = isImdbFormat(imdbId) ? imdbId
|
||||||
|
: (fallbackImdbId && isImdbFormat(fallbackImdbId) ? fallbackImdbId : imdbId);
|
||||||
|
if (resolvedId !== imdbId) logger.log(`[TraktService] markSeasonAsWatched: falling back from "${imdbId}" to "${resolvedId}"`);
|
||||||
|
|
||||||
|
const traktId = await this.getTraktIdFromImdbId(resolvedId, 'show');
|
||||||
if (!traktId) {
|
if (!traktId) {
|
||||||
logger.warn(`[TraktService] Could not find Trakt ID for show: ${imdbId}`);
|
logger.warn(`[TraktService] Could not find Trakt ID for show: ${resolvedId}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(`[TraktService] Marking entire season ${season} as watched for show ${imdbId} (trakt: ${traktId})`);
|
logger.log(`[TraktService] Marking entire season ${season} as watched for show ${resolvedId} (trakt: ${traktId})`);
|
||||||
|
|
||||||
// Mark entire season - Trakt will mark all episodes in the season
|
// Mark entire season - Trakt will mark all episodes in the season
|
||||||
await this.apiRequest('/sync/history', 'POST', {
|
await this.apiRequest('/sync/history', 'POST', {
|
||||||
|
|
@ -1614,7 +1637,8 @@ export class TraktService {
|
||||||
public async markEpisodesAsWatched(
|
public async markEpisodesAsWatched(
|
||||||
imdbId: string,
|
imdbId: string,
|
||||||
episodes: Array<{ season: number; episode: number }>,
|
episodes: Array<{ season: number; episode: number }>,
|
||||||
watchedAt: Date = new Date()
|
watchedAt: Date = new Date(),
|
||||||
|
fallbackImdbId?: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
if (episodes.length === 0) {
|
if (episodes.length === 0) {
|
||||||
|
|
@ -1622,13 +1646,18 @@ export class TraktService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const traktId = await this.getTraktIdFromImdbId(imdbId, 'show');
|
const isImdbFormat = (id: string) => /^tt\d+$/.test(id) || /^\d{7,}$/.test(id);
|
||||||
|
const resolvedId = isImdbFormat(imdbId) ? imdbId
|
||||||
|
: (fallbackImdbId && isImdbFormat(fallbackImdbId) ? fallbackImdbId : imdbId);
|
||||||
|
if (resolvedId !== imdbId) logger.log(`[TraktService] markEpisodesAsWatched: falling back from "${imdbId}" to "${resolvedId}"`);
|
||||||
|
|
||||||
|
const traktId = await this.getTraktIdFromImdbId(resolvedId, 'show');
|
||||||
if (!traktId) {
|
if (!traktId) {
|
||||||
logger.warn(`[TraktService] Could not find Trakt ID for show: ${imdbId}`);
|
logger.warn(`[TraktService] Could not find Trakt ID for show: ${resolvedId}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(`[TraktService] Marking ${episodes.length} episodes as watched for show ${imdbId}`);
|
logger.log(`[TraktService] Marking ${episodes.length} episodes as watched for show ${resolvedId}`);
|
||||||
|
|
||||||
// Group episodes by season for the API call
|
// Group episodes by season for the API call
|
||||||
const seasonMap = new Map<number, Array<{ number: number; watched_at: string }>>();
|
const seasonMap = new Map<number, Array<{ number: number; watched_at: string }>>();
|
||||||
|
|
@ -1709,12 +1738,18 @@ export class TraktService {
|
||||||
*/
|
*/
|
||||||
public async removeSeasonFromHistory(
|
public async removeSeasonFromHistory(
|
||||||
imdbId: string,
|
imdbId: string,
|
||||||
season: number
|
season: number,
|
||||||
|
fallbackImdbId?: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
logger.log(`[TraktService] Removing season ${season} from history for show: ${imdbId}`);
|
logger.log(`[TraktService] Removing season ${season} from history for show: ${imdbId}`);
|
||||||
|
|
||||||
const fullImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
const isImdbFormat = (id: string) => /^tt\d+$/.test(id) || /^\d{7,}$/.test(id);
|
||||||
|
const resolvedId = (!isImdbFormat(imdbId) && fallbackImdbId && isImdbFormat(fallbackImdbId))
|
||||||
|
? fallbackImdbId
|
||||||
|
: imdbId;
|
||||||
|
|
||||||
|
const fullImdbId = resolvedId.startsWith('tt') ? resolvedId : `tt${resolvedId}`;
|
||||||
|
|
||||||
const payload: TraktHistoryRemovePayload = {
|
const payload: TraktHistoryRemovePayload = {
|
||||||
shows: [
|
shows: [
|
||||||
|
|
@ -1735,6 +1770,19 @@ export class TraktService {
|
||||||
|
|
||||||
const result = await this.removeFromHistory(payload);
|
const result = await this.removeFromHistory(payload);
|
||||||
|
|
||||||
|
if ((result === null || result.deleted.episodes === 0) && fallbackImdbId && fallbackImdbId !== resolvedId && isImdbFormat(fallbackImdbId)) {
|
||||||
|
logger.log(`[TraktService] removeSeasonFromHistory: retrying with fallback ID "${fallbackImdbId}"`);
|
||||||
|
const fb = fallbackImdbId.startsWith('tt') ? fallbackImdbId : `tt${fallbackImdbId}`;
|
||||||
|
const fallbackResult = await this.removeFromHistory({
|
||||||
|
shows: [{ ids: { imdb: fb }, seasons: [{ number: season }] }]
|
||||||
|
});
|
||||||
|
if (fallbackResult) {
|
||||||
|
logger.log(`[TraktService] Season removal success via fallback: ${fallbackResult.deleted.episodes} episodes deleted`);
|
||||||
|
return fallbackResult.deleted.episodes > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const success = result.deleted.episodes > 0;
|
const success = result.deleted.episodes > 0;
|
||||||
logger.log(`[TraktService] Season removal success: ${success} (${result.deleted.episodes} episodes deleted)`);
|
logger.log(`[TraktService] Season removal success: ${success} (${result.deleted.episodes} episodes deleted)`);
|
||||||
|
|
@ -1902,7 +1950,11 @@ export class TraktService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate content data before making API calls
|
* Validate content data before making API calls.
|
||||||
|
*
|
||||||
|
* IMDb ID validation is intentionally lenient: a non-IMDb provider ID (e.g. "kitsu:123")
|
||||||
|
* is allowed through with a warning. Trakt can still scrobble via title + season/episode.
|
||||||
|
* A truly empty ID is still blocked.
|
||||||
*/
|
*/
|
||||||
private validateContentData(contentData: TraktContentData): { isValid: boolean; errors: string[] } {
|
private validateContentData(contentData: TraktContentData): { isValid: boolean; errors: string[] } {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
@ -1915,8 +1967,11 @@ export class TraktService {
|
||||||
errors.push('Missing or empty title');
|
errors.push('Missing or empty title');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block only truly empty IDs — non-IMDb provider IDs are allowed (warn, don't fail)
|
||||||
if (!contentData.imdbId || contentData.imdbId.trim() === '') {
|
if (!contentData.imdbId || contentData.imdbId.trim() === '') {
|
||||||
errors.push('Missing or empty IMDb ID');
|
errors.push('Missing or empty IMDb ID');
|
||||||
|
} else if (!/^tt\d+$/.test(contentData.imdbId) && !/^\d{7,}$/.test(contentData.imdbId)) {
|
||||||
|
logger.warn(`[TraktService] imdbId "${contentData.imdbId}" is not a standard IMDb ID — Trakt will match by title/season/episode`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentData.type === 'episode') {
|
if (contentData.type === 'episode') {
|
||||||
|
|
@ -1929,8 +1984,11 @@ export class TraktService {
|
||||||
if (!contentData.showTitle || contentData.showTitle.trim() === '') {
|
if (!contentData.showTitle || contentData.showTitle.trim() === '') {
|
||||||
errors.push('Missing or empty show title');
|
errors.push('Missing or empty show title');
|
||||||
}
|
}
|
||||||
if (!contentData.showYear || contentData.showYear < 1900) {
|
// showYear is intentionally not required — Trakt can match episodes by
|
||||||
errors.push('Invalid show year');
|
// show title + season + episode number alone. Anime and many non-Western
|
||||||
|
// shows often have year=0 or missing; blocking scrobble for them is wrong.
|
||||||
|
if (contentData.showYear !== undefined && contentData.showYear > 0 && contentData.showYear < 1900) {
|
||||||
|
logger.warn(`[TraktService] showYear ${contentData.showYear} looks invalid, omitting from payload`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1991,19 +2049,25 @@ export class TraktService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure IMDb ID includes the 'tt' prefix for Trakt scrobble payloads
|
const isRealImdbId = /^tt\d+$/.test(contentData.imdbId) || /^\d{7,}$/.test(contentData.imdbId);
|
||||||
const imdbIdWithPrefix = contentData.imdbId.startsWith('tt')
|
|
||||||
? contentData.imdbId
|
|
||||||
: `tt${contentData.imdbId}`;
|
|
||||||
|
|
||||||
// Build movie payload - only include year if valid
|
// Build movie payload - only include year if valid
|
||||||
const movieData: { title: string; year?: number; ids: { imdb: string } } = {
|
const movieData: { title: string; year?: number; ids: { imdb?: string } } = {
|
||||||
title: contentData.title.trim(),
|
title: contentData.title.trim(),
|
||||||
ids: {
|
ids: {}
|
||||||
imdb: imdbIdWithPrefix
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Only add IMDb ID to payload when it's a real IMDb format — sending a provider ID
|
||||||
|
// (e.g. "kitsu:123") causes Trakt to fail the lookup. Without it, Trakt matches by title.
|
||||||
|
if (isRealImdbId) {
|
||||||
|
const imdbIdWithPrefix = contentData.imdbId.startsWith('tt')
|
||||||
|
? contentData.imdbId
|
||||||
|
: `tt${contentData.imdbId}`;
|
||||||
|
(movieData.ids as any).imdb = imdbIdWithPrefix;
|
||||||
|
} else {
|
||||||
|
logger.warn(`[TraktService] Movie imdbId "${contentData.imdbId}" is not IMDb format — omitting from scrobble payload, Trakt will match by title`);
|
||||||
|
}
|
||||||
|
|
||||||
// Only add year if it's valid (prevents year: 0 or invalid years)
|
// Only add year if it's valid (prevents year: 0 or invalid years)
|
||||||
if (isValidYear(contentData.year)) {
|
if (isValidYear(contentData.year)) {
|
||||||
movieData.year = contentData.year;
|
movieData.year = contentData.year;
|
||||||
|
|
@ -2067,12 +2131,17 @@ export class TraktService {
|
||||||
progress: clampedProgress
|
progress: clampedProgress
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add show IMDB ID if available
|
// Add show IMDB ID if available and valid IMDb format
|
||||||
if (contentData.showImdbId && contentData.showImdbId.trim() !== '') {
|
if (contentData.showImdbId && contentData.showImdbId.trim() !== '') {
|
||||||
const showImdbWithPrefix = contentData.showImdbId.startsWith('tt')
|
const isRealShowImdbId = /^tt\d+$/.test(contentData.showImdbId) || /^\d{7,}$/.test(contentData.showImdbId);
|
||||||
? contentData.showImdbId
|
if (isRealShowImdbId) {
|
||||||
: `tt${contentData.showImdbId}`;
|
const showImdbWithPrefix = contentData.showImdbId.startsWith('tt')
|
||||||
payload.show.ids.imdb = showImdbWithPrefix;
|
? contentData.showImdbId
|
||||||
|
: `tt${contentData.showImdbId}`;
|
||||||
|
payload.show.ids.imdb = showImdbWithPrefix;
|
||||||
|
} else {
|
||||||
|
logger.warn(`[TraktService] showImdbId "${contentData.showImdbId}" is not IMDb format — omitting from scrobble payload, Trakt will match by title`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add episode IMDB ID if available (for specific episode IDs)
|
// Add episode IMDB ID if available (for specific episode IDs)
|
||||||
|
|
@ -2679,21 +2748,43 @@ export class TraktService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a movie from watched history by IMDB ID
|
* Remove a movie from watched history by IMDB ID.
|
||||||
|
* If the primary imdbId is not a valid IMDb ID (e.g. a provider ID like "kitsu:123"),
|
||||||
|
* falls back to fallbackImdbId (typically the resolved IMDb ID from metadata).
|
||||||
*/
|
*/
|
||||||
public async removeMovieFromHistory(imdbId: string): Promise<boolean> {
|
public async removeMovieFromHistory(imdbId: string, fallbackImdbId?: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
const isImdbFormat = (id: string) => /^tt\d+$/.test(id) || /^\d{7,}$/.test(id);
|
||||||
|
|
||||||
|
// Resolve which ID to use: prefer a proper IMDb-format ID
|
||||||
|
let resolvedId = imdbId;
|
||||||
|
if (!isImdbFormat(imdbId) && fallbackImdbId && isImdbFormat(fallbackImdbId) && fallbackImdbId !== imdbId) {
|
||||||
|
logger.log(`[TraktService] removeMovieFromHistory: "${imdbId}" is not IMDb format, falling back to "${fallbackImdbId}"`);
|
||||||
|
resolvedId = fallbackImdbId;
|
||||||
|
}
|
||||||
|
|
||||||
const payload: TraktHistoryRemovePayload = {
|
const payload: TraktHistoryRemovePayload = {
|
||||||
movies: [
|
movies: [
|
||||||
{
|
{
|
||||||
ids: {
|
ids: {
|
||||||
imdb: imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`
|
imdb: resolvedId.startsWith('tt') ? resolvedId : `tt${resolvedId}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.removeFromHistory(payload);
|
const result = await this.removeFromHistory(payload);
|
||||||
|
|
||||||
|
// If primary attempt deleted nothing and we haven't tried the fallback yet, try it
|
||||||
|
if ((result === null || result.deleted.movies === 0) && fallbackImdbId && fallbackImdbId !== imdbId && fallbackImdbId !== resolvedId && isImdbFormat(fallbackImdbId)) {
|
||||||
|
logger.log(`[TraktService] removeMovieFromHistory: primary attempt found nothing, retrying with fallback ID "${fallbackImdbId}"`);
|
||||||
|
const fallbackPayload: TraktHistoryRemovePayload = {
|
||||||
|
movies: [{ ids: { imdb: fallbackImdbId.startsWith('tt') ? fallbackImdbId : `tt${fallbackImdbId}` } }]
|
||||||
|
};
|
||||||
|
const fallbackResult = await this.removeFromHistory(fallbackPayload);
|
||||||
|
return fallbackResult !== null && fallbackResult.deleted.movies > 0;
|
||||||
|
}
|
||||||
|
|
||||||
return result !== null && result.deleted.movies > 0;
|
return result !== null && result.deleted.movies > 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[TraktService] Failed to remove movie from history:', error);
|
logger.error('[TraktService] Failed to remove movie from history:', error);
|
||||||
|
|
@ -2704,14 +2795,24 @@ export class TraktService {
|
||||||
/**
|
/**
|
||||||
* Remove an episode from watched history by IMDB IDs
|
* Remove an episode from watched history by IMDB IDs
|
||||||
*/
|
*/
|
||||||
public async removeEpisodeFromHistory(showImdbId: string, season: number, episode: number): Promise<boolean> {
|
public async removeEpisodeFromHistory(showImdbId: string, season: number, episode: number, fallbackImdbId?: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
logger.log(`🔍 [TraktService] removeEpisodeFromHistory called for ${showImdbId} S${season}E${episode}`);
|
logger.log(`🔍 [TraktService] removeEpisodeFromHistory called for ${showImdbId} S${season}E${episode}`);
|
||||||
|
|
||||||
|
const isImdbFormat = (id: string) => /^tt\d+$/.test(id) || /^\d{7,}$/.test(id);
|
||||||
|
const resolvedId = (!isImdbFormat(showImdbId) && fallbackImdbId && isImdbFormat(fallbackImdbId))
|
||||||
|
? fallbackImdbId
|
||||||
|
: showImdbId;
|
||||||
|
|
||||||
|
if (resolvedId !== showImdbId) {
|
||||||
|
logger.log(`[TraktService] removeEpisodeFromHistory: "${showImdbId}" is not IMDb format, falling back to "${resolvedId}"`);
|
||||||
|
}
|
||||||
|
|
||||||
const payload: TraktHistoryRemovePayload = {
|
const payload: TraktHistoryRemovePayload = {
|
||||||
shows: [
|
shows: [
|
||||||
{
|
{
|
||||||
ids: {
|
ids: {
|
||||||
imdb: showImdbId.startsWith('tt') ? showImdbId : `tt${showImdbId}`
|
imdb: resolvedId.startsWith('tt') ? resolvedId : `tt${resolvedId}`
|
||||||
},
|
},
|
||||||
seasons: [
|
seasons: [
|
||||||
{
|
{
|
||||||
|
|
@ -2731,6 +2832,23 @@ export class TraktService {
|
||||||
|
|
||||||
const result = await this.removeFromHistory(payload);
|
const result = await this.removeFromHistory(payload);
|
||||||
|
|
||||||
|
// If nothing was deleted and we haven't tried the fallback yet, retry with it
|
||||||
|
if ((result === null || result.deleted.episodes === 0) && fallbackImdbId && fallbackImdbId !== resolvedId && isImdbFormat(fallbackImdbId)) {
|
||||||
|
logger.log(`[TraktService] removeEpisodeFromHistory: retrying with fallback ID "${fallbackImdbId}"`);
|
||||||
|
const fallbackPayload: TraktHistoryRemovePayload = {
|
||||||
|
shows: [{
|
||||||
|
ids: { imdb: fallbackImdbId.startsWith('tt') ? fallbackImdbId : `tt${fallbackImdbId}` },
|
||||||
|
seasons: [{ number: season, episodes: [{ number: episode }] }]
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
const fallbackResult = await this.removeFromHistory(fallbackPayload);
|
||||||
|
if (fallbackResult) {
|
||||||
|
logger.log(`✅ [TraktService] Episode removal success via fallback: ${fallbackResult.deleted.episodes} episodes deleted`);
|
||||||
|
return fallbackResult.deleted.episodes > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const success = result.deleted.episodes > 0;
|
const success = result.deleted.episodes > 0;
|
||||||
logger.log(`✅ [TraktService] Episode removal success: ${success} (${result.deleted.episodes} episodes deleted)`);
|
logger.log(`✅ [TraktService] Episode removal success: ${success} (${result.deleted.episodes} episodes deleted)`);
|
||||||
|
|
|
||||||
|
|
@ -316,12 +316,15 @@ class WatchedService {
|
||||||
let syncedToTrakt = false;
|
let syncedToTrakt = false;
|
||||||
|
|
||||||
// Sync to Trakt
|
// Sync to Trakt
|
||||||
|
// showId is the Stremio content ID — pass it as fallback so Trakt can resolve
|
||||||
|
// anime/provider IDs (e.g. kitsu:123) that aren't valid IMDb IDs
|
||||||
if (isTraktAuth) {
|
if (isTraktAuth) {
|
||||||
syncedToTrakt = await this.traktService.addToWatchedEpisodes(
|
syncedToTrakt = await this.traktService.addToWatchedEpisodes(
|
||||||
showImdbId,
|
showImdbId,
|
||||||
season,
|
season,
|
||||||
episode,
|
episode,
|
||||||
watchedAt
|
watchedAt,
|
||||||
|
showId !== showImdbId ? showId : undefined
|
||||||
);
|
);
|
||||||
logger.log(`[WatchedService] Trakt sync result for episode: ${syncedToTrakt}`);
|
logger.log(`[WatchedService] Trakt sync result for episode: ${syncedToTrakt}`);
|
||||||
}
|
}
|
||||||
|
|
@ -445,7 +448,8 @@ class WatchedService {
|
||||||
syncedToTrakt = await this.traktService.markEpisodesAsWatched(
|
syncedToTrakt = await this.traktService.markEpisodesAsWatched(
|
||||||
showImdbId,
|
showImdbId,
|
||||||
episodes,
|
episodes,
|
||||||
watchedAt
|
watchedAt,
|
||||||
|
showId !== showImdbId ? showId : undefined
|
||||||
);
|
);
|
||||||
logger.log(`[WatchedService] Trakt batch sync result: ${syncedToTrakt}`);
|
logger.log(`[WatchedService] Trakt batch sync result: ${syncedToTrakt}`);
|
||||||
}
|
}
|
||||||
|
|
@ -523,7 +527,8 @@ class WatchedService {
|
||||||
syncedToTrakt = await this.traktService.markSeasonAsWatched(
|
syncedToTrakt = await this.traktService.markSeasonAsWatched(
|
||||||
showImdbId,
|
showImdbId,
|
||||||
season,
|
season,
|
||||||
watchedAt
|
watchedAt,
|
||||||
|
showId !== showImdbId ? showId : undefined
|
||||||
);
|
);
|
||||||
logger.log(`[WatchedService] Trakt season sync result: ${syncedToTrakt}`);
|
logger.log(`[WatchedService] Trakt season sync result: ${syncedToTrakt}`);
|
||||||
}
|
}
|
||||||
|
|
@ -570,32 +575,40 @@ 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 fallbackImdbId - The resolved IMDb ID from metadata (used when imdbId isn't IMDb format)
|
||||||
*/
|
*/
|
||||||
public async unmarkMovieAsWatched(
|
public async unmarkMovieAsWatched(
|
||||||
imdbId: string
|
imdbId: string,
|
||||||
|
fallbackImdbId?: string
|
||||||
): Promise<{ success: boolean; syncedToTrakt: boolean }> {
|
): Promise<{ success: boolean; syncedToTrakt: boolean }> {
|
||||||
try {
|
try {
|
||||||
logger.log(`[WatchedService] Unmarking movie as watched: ${imdbId}`);
|
logger.log(`[WatchedService] Unmarking movie as watched: ${imdbId}${fallbackImdbId && fallbackImdbId !== imdbId ? ` (fallback: ${fallbackImdbId})` : ''}`);
|
||||||
|
|
||||||
const isTraktAuth = await this.traktService.isAuthenticated();
|
const isTraktAuth = await this.traktService.isAuthenticated();
|
||||||
let syncedToTrakt = false;
|
let syncedToTrakt = false;
|
||||||
|
|
||||||
if (isTraktAuth) {
|
if (isTraktAuth) {
|
||||||
syncedToTrakt = await this.traktService.removeMovieFromHistory(imdbId);
|
syncedToTrakt = await this.traktService.removeMovieFromHistory(imdbId, fallbackImdbId);
|
||||||
logger.log(`[WatchedService] Trakt remove result for movie: ${syncedToTrakt}`);
|
logger.log(`[WatchedService] Trakt remove result for movie: ${syncedToTrakt}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simkl Unmark
|
// Simkl Unmark — try both IDs
|
||||||
const isSimklAuth = await this.simklService.isAuthenticated();
|
const isSimklAuth = await this.simklService.isAuthenticated();
|
||||||
if (isSimklAuth) {
|
if (isSimklAuth) {
|
||||||
await this.simklService.removeFromHistory({ movies: [{ ids: { imdb: imdbId } }] });
|
const simklId = (fallbackImdbId && fallbackImdbId !== imdbId) ? fallbackImdbId : imdbId;
|
||||||
|
await this.simklService.removeFromHistory({ movies: [{ ids: { imdb: simklId } }] });
|
||||||
logger.log(`[WatchedService] Simkl remove request sent for movie`);
|
logger.log(`[WatchedService] Simkl remove request sent for movie`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove local progress
|
// Remove local progress — clear both IDs to be safe
|
||||||
await storageService.removeWatchProgress(imdbId, 'movie');
|
await storageService.removeWatchProgress(imdbId, 'movie');
|
||||||
await mmkvStorage.removeItem(`watched:movie:${imdbId}`);
|
await mmkvStorage.removeItem(`watched:movie:${imdbId}`);
|
||||||
|
if (fallbackImdbId && fallbackImdbId !== imdbId) {
|
||||||
|
await storageService.removeWatchProgress(fallbackImdbId, 'movie');
|
||||||
|
await mmkvStorage.removeItem(`watched:movie:${fallbackImdbId}`);
|
||||||
|
}
|
||||||
await this.removeLocalWatchedItems([
|
await this.removeLocalWatchedItems([
|
||||||
{ content_id: imdbId, season: null, episode: null },
|
{ content_id: imdbId, season: null, episode: null },
|
||||||
]);
|
]);
|
||||||
|
|
@ -622,21 +635,25 @@ class WatchedService {
|
||||||
const isTraktAuth = await this.traktService.isAuthenticated();
|
const isTraktAuth = await this.traktService.isAuthenticated();
|
||||||
let syncedToTrakt = false;
|
let syncedToTrakt = false;
|
||||||
|
|
||||||
|
const fallback = showId !== showImdbId ? showId : undefined;
|
||||||
|
|
||||||
if (isTraktAuth) {
|
if (isTraktAuth) {
|
||||||
syncedToTrakt = await this.traktService.removeEpisodeFromHistory(
|
syncedToTrakt = await this.traktService.removeEpisodeFromHistory(
|
||||||
showImdbId,
|
showImdbId,
|
||||||
season,
|
season,
|
||||||
episode
|
episode,
|
||||||
|
fallback
|
||||||
);
|
);
|
||||||
logger.log(`[WatchedService] Trakt remove result for episode: ${syncedToTrakt}`);
|
logger.log(`[WatchedService] Trakt remove result for episode: ${syncedToTrakt}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simkl Unmark
|
// Simkl Unmark — use best available ID
|
||||||
const isSimklAuth = await this.simklService.isAuthenticated();
|
const isSimklAuth = await this.simklService.isAuthenticated();
|
||||||
if (isSimklAuth) {
|
if (isSimklAuth) {
|
||||||
|
const simklId = showImdbId || showId;
|
||||||
await this.simklService.removeFromHistory({
|
await this.simklService.removeFromHistory({
|
||||||
shows: [{
|
shows: [{
|
||||||
ids: { imdb: showImdbId },
|
ids: { imdb: simklId },
|
||||||
seasons: [{
|
seasons: [{
|
||||||
number: season,
|
number: season,
|
||||||
episodes: [{ number: episode }]
|
episodes: [{ number: episode }]
|
||||||
|
|
@ -679,26 +696,27 @@ class WatchedService {
|
||||||
const isTraktAuth = await this.traktService.isAuthenticated();
|
const isTraktAuth = await this.traktService.isAuthenticated();
|
||||||
let syncedToTrakt = false;
|
let syncedToTrakt = false;
|
||||||
|
|
||||||
|
const fallback = showId !== showImdbId ? showId : undefined;
|
||||||
|
|
||||||
if (isTraktAuth) {
|
if (isTraktAuth) {
|
||||||
// Remove entire season from Trakt
|
// Remove entire season from Trakt
|
||||||
syncedToTrakt = await this.traktService.removeSeasonFromHistory(
|
syncedToTrakt = await this.traktService.removeSeasonFromHistory(
|
||||||
showImdbId,
|
showImdbId,
|
||||||
season
|
season,
|
||||||
|
fallback
|
||||||
);
|
);
|
||||||
logger.log(`[WatchedService] Trakt season removal result: ${syncedToTrakt}`);
|
logger.log(`[WatchedService] Trakt season removal result: ${syncedToTrakt}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync to Simkl
|
// Sync to Simkl — use best available ID
|
||||||
const isSimklAuth = await this.simklService.isAuthenticated();
|
const isSimklAuth = await this.simklService.isAuthenticated();
|
||||||
if (isSimklAuth) {
|
if (isSimklAuth) {
|
||||||
|
const simklId = showImdbId || showId;
|
||||||
const episodes = episodeNumbers.map(num => ({ number: num }));
|
const episodes = episodeNumbers.map(num => ({ number: num }));
|
||||||
await this.simklService.removeFromHistory({
|
await this.simklService.removeFromHistory({
|
||||||
shows: [{
|
shows: [{
|
||||||
ids: { imdb: showImdbId },
|
ids: { imdb: simklId },
|
||||||
seasons: [{
|
seasons: [{ number: season, episodes: episodes }]
|
||||||
number: season,
|
|
||||||
episodes: episodes
|
|
||||||
}]
|
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
logger.log(`[WatchedService] Simkl season removal request sent`);
|
logger.log(`[WatchedService] Simkl season removal request sent`);
|
||||||
|
|
@ -728,18 +746,26 @@ class WatchedService {
|
||||||
/**
|
/**
|
||||||
* Check if a movie is marked as watched (locally)
|
* Check if a movie is marked as watched (locally)
|
||||||
*/
|
*/
|
||||||
public async isMovieWatched(imdbId: string): Promise<boolean> {
|
public async isMovieWatched(imdbId: string, fallbackImdbId?: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const isAuthed = await this.traktService.isAuthenticated();
|
const isAuthed = await this.traktService.isAuthenticated();
|
||||||
|
|
||||||
if (isAuthed) {
|
if (isAuthed) {
|
||||||
const traktWatched =
|
const traktWatched =
|
||||||
await this.traktService.isMovieWatchedAccurate(imdbId);
|
await this.traktService.isMovieWatchedAccurate(imdbId, fallbackImdbId);
|
||||||
if (traktWatched) return true;
|
if (traktWatched) return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const local = await mmkvStorage.getItem(`watched:movie:${imdbId}`);
|
const local = await mmkvStorage.getItem(`watched:movie:${imdbId}`);
|
||||||
return local === 'true';
|
if (local === 'true') return true;
|
||||||
|
|
||||||
|
// Also check under fallback ID locally
|
||||||
|
if (fallbackImdbId && fallbackImdbId !== imdbId) {
|
||||||
|
const localFallback = await mmkvStorage.getItem(`watched:movie:${fallbackImdbId}`);
|
||||||
|
if (localFallback === 'true') return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -752,7 +778,8 @@ class WatchedService {
|
||||||
public async isEpisodeWatched(
|
public async isEpisodeWatched(
|
||||||
showId: string,
|
showId: string,
|
||||||
season: number,
|
season: number,
|
||||||
episode: number
|
episode: number,
|
||||||
|
fallbackImdbId?: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const isAuthed = await this.traktService.isAuthenticated();
|
const isAuthed = await this.traktService.isAuthenticated();
|
||||||
|
|
@ -762,7 +789,8 @@ class WatchedService {
|
||||||
await this.traktService.isEpisodeWatchedAccurate(
|
await this.traktService.isEpisodeWatchedAccurate(
|
||||||
showId,
|
showId,
|
||||||
season,
|
season,
|
||||||
episode
|
episode,
|
||||||
|
fallbackImdbId
|
||||||
);
|
);
|
||||||
if (traktWatched) return true;
|
if (traktWatched) return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue