// ==UserScript==
// @name TMDB 한국 지원 강화
// @namespace http://tampermonkey.net/
// @version 1.2
// @description TMDB 영화/TV 시리즈 페이지에 한국어, 영어, 원어 제목 추가, 개별 클립보드 복사 기능, 한국 시청등급 및 제작국 표시
// @match https://www.themoviedb.org/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// ==/UserScript==
// 주의사항: 아래 YOUR_API_KEY 부분을 실제 TMDB API 키로 교체하는 것을 잊지 마세요.
const apiKey = "YOUR_API_KEY";
(function() {
'use strict';
GM_addStyle(`
.additional-titles { font-size: 1.1em; line-height: 1.4; margin-bottom: 10px; }
.additional-title { cursor: pointer; transition: color 0.3s; }
.additional-title:hover { color: blue !important; }
#additional-info {
margin-top: 10px;
clear: both;
display: flex;
align-items: center;
width: 100%;
}
#production-countries {
font-size: inherit;
margin-right: 20px;
}
#external-links {
font-size: inherit;
}
#external-links a {
margin-right: 10px;
text-decoration: none;
color: inherit;
transition: color 0.3s;
}
#external-links a:hover {
color: blue;
}
`);
const copyToClipboard = text => {
navigator.clipboard.writeText(text).then(() => {
showTemporaryMessage(`${text} 클립보드에 복사됨`);
});
};
const showTemporaryMessage = message => {
const messageElement = document.createElement('div');
Object.assign(messageElement.style, {
position: 'fixed', top: '10px', left: '50%', transform: 'translateX(-50%)',
backgroundColor: 'rgba(0, 0, 0, 0.7)', color: 'white', padding: '10px',
borderRadius: '5px', zIndex: '9999'
});
messageElement.textContent = message;
document.body.appendChild(messageElement);
setTimeout(() => document.body.removeChild(messageElement), 1000);
};
const getIdAndType = () => {
const [, type, id] = window.location.pathname.split('/');
return { id, type };
};
const fetchData = async () => {
const { id, type } = getIdAndType();
if (!id || !type) return;
try {
const [koData, enData, originalData] = await Promise.all([
fetchTMDBData(id, type, 'ko-KR'),
fetchTMDBData(id, type, 'en-US'),
fetchTMDBData(id, type, null)
]);
const koTitle = koData.title || koData.name || '한국어 제목 미기재';
const enTitle = enData.title || enData.name || '영어 제목 미기재';
const originalTitle = originalData.original_title || originalData.original_name || '원어 제목 미기재';
const koreanRating = getKoreanCertification(koData, type);
const productionCountries = getProductionCountries(koData);
const imdbId = koData.imdb_id || koData.external_ids?.imdb_id;
const wikidataId = koData.external_ids?.wikidata_id;
const tvdbId = koData.external_ids?.tvdb_id;
displayTitles(koTitle, enTitle, originalTitle);
displayKoreanRating(koreanRating);
displayAdditionalInfo(productionCountries, koTitle, imdbId, wikidataId, tvdbId);
} catch (error) {
console.error('TMDB API 요청 오류:', error);
}
};
const fetchTMDBData = async (id, type, language) => {
const url = `https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}${language ? `&language=${language}` : ''}&append_to_response=external_ids,release_dates,content_ratings`;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: response => response.status === 200 ? resolve(JSON.parse(response.responseText)) : reject(`API 요청 실패: ${response.status}`),
onerror: reject
});
});
};
const displayTitles = (koTitle, enTitle, originalTitle) => {
const titleElement = document.querySelector('.title h2') || document.querySelector('.header .title h2');
if (!titleElement) return;
const titleContainer = document.createElement('div');
titleContainer.className = 'additional-titles';
titleContainer.innerHTML = `
<span class="label">한제: </span><span class="additional-title ko-title">${koTitle}</span> /
<span class="label">영제: </span><span class="additional-title en-title">${enTitle}</span> /
<span class="label">원제: </span><span class="additional-title original-title">${originalTitle}</span>
`;
// 제목 컨테이너를 h2 요소 위에 삽입
titleElement.parentNode.insertBefore(titleContainer, titleElement);
['ko-title', 'en-title', 'original-title'].forEach(className => {
titleContainer.querySelector(`.${className}`).addEventListener('click', function() {
copyToClipboard(this.textContent);
});
});
};
const getKoreanCertification = (data, type) => {
const ratings = type === 'movie' ? data.release_dates?.results : data.content_ratings?.results;
const koreanRating = ratings?.find(r => r.iso_3166_1 === 'KR')?.release_dates?.[0]?.certification ||
ratings?.find(r => r.iso_3166_1 === 'KR')?.rating;
return koreanRating || '등급미정';
};
const getProductionCountries = (data) => {
return data.production_countries?.map(country => country.name).join(', ') || '정보 없음';
};
const displayKoreanRating = rating => {
const factsElement = document.querySelector('.facts');
if (!factsElement) return;
let koreanRatingElement = document.getElementById('korean-rating');
if (!koreanRatingElement) {
koreanRatingElement = Object.assign(document.createElement('span'), {
id: 'korean-rating',
style: 'font-size: 1em; margin-right: 10px; font-weight: bold;'
});
factsElement.insertBefore(koreanRatingElement, factsElement.firstChild);
}
koreanRatingElement.textContent = rating;
};
const displayAdditionalInfo = (countries, koTitle, imdbId, wikidataId, tvdbId) => {
const factsElement = document.querySelector('.facts');
let additionalInfoContainer = document.getElementById('additional-info');
if (!additionalInfoContainer) {
additionalInfoContainer = document.createElement('div');
additionalInfoContainer.id = "additional-info";
factsElement.parentNode.insertBefore(additionalInfoContainer, factsElement.nextSibling);
additionalInfoContainer.innerHTML = `
<div id="production-countries">• 제작국: ${countries}</div>
<div id="external-links">
<a href="https://m.kinolights.com/search/contents?keyword=${encodeURIComponent(koTitle)}" target="_blank">키노</a>
${imdbId ? `<a href="https://www.imdb.com/title/${imdbId}" target="_blank">IMDB</a>` : ''}
${wikidataId ? `<a href="https://www.wikidata.org/wiki/${wikidataId}" target="_blank">Wikidata</a>` : ''}
${tvdbId ? `<a href="https://www.thetvdb.com/dereferrer/series/${tvdbId}" target="_blank">TVDB</a>` : ''}
</div>
`;
additionalInfoContainer.style.clear = "both";
additionalInfoContainer.style.display = "flex";
additionalInfoContainer.style.alignItems = "center";
additionalInfoContainer.style.justifyContent = "flex-start";
additionalInfoContainer.style.width = "100%";
additionalInfoContainer.querySelector('#production-countries').style.marginRight = "20px";
additionalInfoContainer.querySelector('#external-links').style.fontSize = "inherit";
const titleColor = window.getComputedStyle(document.querySelector('.title h2')).color;
additionalInfoContainer.querySelectorAll('#external-links a').forEach(link => {
link.style.color = titleColor;
link.style.marginRight = "10px";
link.style.textDecoration = "none";
link.addEventListener('mouseover', () => link.style.color = "blue");
link.addEventListener('mouseout', () => link.style.color = titleColor);
});
}
};
const init = () => {
fetchData();
};
window.addEventListener('load', init);
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
init();
}
}).observe(document, {subtree: true, childList: true});
let lastUrl = location.href;
})();