// ==UserScript==// @name TMDB 한국 지원 강화
// @namespace http://tampermonkey.net/
// @version 1.8.6
// @description TMDB 영화/TV 시리즈 페이지에 한국어, 영어, 원어 제목 추가, 개별 클립보드 복사 기능, 한국 시청등급 및 제작국 표시
// @match https://www.themoviedb.org/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @author DongHaerang
// ==/UserScript==
// 주의사항: 아래 YOUR_API_KEY 부분을 실제 TMDB API 키로 교체하는 것을 잊지 마세요.
const apiKey = "YOUR_API_KEY";
(function() {
'use strict';
GM_addStyle(`
#tmdb-info-table {
background-color: rgba(0, 0, 0, 0.3) !important;
}
.clickable-text[data-copyable="true"], #external-links a[data-copyable="true"] {
color: yellow !important;
content: "□";
margin-left: 0px;
display: inline-block;
}
.clickable-text, #external-links a {
cursor: pointer;
text-decoration: none;
transition: color 0.3s;
font-size: 12pt !important;
color: lightskyblue !important;
}
// #external-links a[target="_blank"]::after {
// content: "↗";
// margin-left: 0px;
// display: inline-block;
// }
// #external-links a::after, #korean-rating::after {
// content: "↙";
// margin-left: 0px;
// display: inline-block;
// }
.clickable-text:hover, #external-links a:hover, #korean-rating:hover {
color: blue !important;
}
#external-links a {
margin-right: 0px;
}
.additional-titles {
line-height: 1.4;
margin-bottom: 10px;
font-size: 12pt !important;
}
#additional-info {
margin-top: 10px;
clear: both;
display: flex;
align-items: center;
width: 100%;
font-size: 12pt !important;
}
#production-countries {
margin-right: 20px;
font-size: 12pt !important;
}
#external-links {
font-size: 12pt !important;
}
.right-aligned-links {
float: right;
display: inline-block;
}
`);
const copyToClipboard = (text, clickedText) => {
// MOVIE, TV, +TMDB, +영, +원, 한제, 영제, 원제 클릭시에만 특수문자 변환
const specialClickTargets = ['MOVIE', 'TV', '+TMDB', '+영', '+원', '한제', '영제', '원제', 'TTT'];
text = text.replace(/:/g, ';').replace(/\?/g, '?').replace(/\//g, '/');
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 translateCountry = (countryCode) => {
const countryMap = {
'US': '미국',
'GB': '영국',
'KR': '한국',
'JP': '일본',
'CN': '중국',
'FR': '프랑스',
'DE': '독일',
'IT': '이탈리아',
'ES': '스페인',
'CA': '캐나다',
'AU': '호주',
'RU': '러시아',
'IN': '인도',
'BR': '브라질',
'MX': '멕시코',
'NL': '네덜란드',
'BE': '벨기에',
'SE': '스웨덴',
'DK': '덴마크',
'NO': '노르웨이',
'FI': '핀란드',
'PL': '폴란드',
'TR': '터키',
'TH': '태국',
'VN': '베트남',
'ID': '인도네시아',
'MY': '말레이시아',
'SG': '싱가포르',
'PH': '필리핀',
'TW': '대만',
'HK': '홍콩',
'NZ': '뉴질랜드',
'CO': '콜롬비아',
'AR': '아르헨티나',
'ZA': '남아프리카',
// 국가 전체 이름도 매핑
'United States of America': '미국',
'United Kingdom': '영국',
'South Korea': '한국',
'Japan': '일본',
'China': '중국',
'France': '프랑스',
'Germany': '독일',
'Italy': '이탈리아',
'Spain': '스페인',
'Canada': '캐나다',
'Australia': '호주',
'Russia': '러시아',
'India': '인도',
'Brazil': '브라질',
'Mexico': '멕시코',
'Netherlands': '네덜란드',
'Belgium': '벨기에',
'Sweden': '스웨덴',
'Denmark': '덴마크',
'Norway': '노르웨이',
'Finland': '핀란드',
'Poland': '폴란드',
'Turkey': '터키',
'Thailand': '태국',
'Vietnam': '베트남',
'Indonesia': '인도네시아',
'Malaysia': '말레이시아',
'Singapore': '싱가포르',
'Philippines': '필리핀',
'Taiwan': '대만',
'Hong Kong': '홍콩',
'New Zealand': '뉴질랜드',
'Colombia': '콜롬비아',
'Argentina': '아르헨티나',
'South Africa': '남아프리카'
};
return countryMap[countryCode] || '미기재';
};
const getIdAndType = () => {
const [, type, id] = window.location.pathname.split('/');
return { id: id?.split('-')[0], type };
};
const goToMainPage = () => {
const currentUrl = window.location.href;
let mainUrl;
const tvMatch = currentUrl.match(/\/tv\/(\d+)/);
const movieMatch = currentUrl.match(/\/movie\/(\d+)/);
if (tvMatch) {
mainUrl = `https://www.themoviedb.org/tv/${tvMatch[1]}`;
} else if (movieMatch) {
mainUrl = `https://www.themoviedb.org/movie/${movieMatch[1]}`;
}
if (mainUrl) {
window.location.href = mainUrl;
}
};
const changeLanguage = (lang) => {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('language', lang);
window.location.href = currentUrl.toString();
};
const languageMap = {
'ko': '한국어',
'en': '영어',
'ja': '일본어',
'zh': '중국어',
'es': '스페인어',
'fr': '프랑스어',
'de': '독일어',
'it': '이탈리아어',
'pt': '포르투갈어',
'ru': '러시아어',
'hi': '힌디어',
'ar': '아랍어',
'th': '태국어',
'vi': '베트남어',
'tr': '터키어',
'nl': '네덜란드어',
'pl': '폴란드어',
'sv': '웨덴어',
'da': '덴마크어',
'fi': '핀란드어',
'no': '노르웨이어',
'cs': '체코어',
'el': '그리스어',
'he': '히브리어',
'id': '인도네시아어',
'ms': '말레이어',
'ro': '루마니아어',
'hu': '헝가리어',
'bn': '벵골어',
'uk': '우크라이나어',
'fa': '페르시아어'
};
const getLanguagePrefix = async (id, type) => {
try {
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`,
onload: response => response.status === 200 ? resolve(JSON.parse(response.responseText)) : reject(`API 요청 실패: ${response.status}`),
onerror: reject
});
});
const originalLanguage = response.original_language;
// languageMap에서 해당 언어의 전체 이름을 가져온 후, 첫 글자만 반환
return (languageMap[originalLanguage] || '기타언어').charAt(0);
} catch (error) {
console.error('언어 정보 가져오기 실패:', error);
return '기';
}
};
const displayTitles = async (koTitle, enTitle, originalTitle, type, id, koreanRating, originCountry, productionCountry, year, imdbId, wikidataId, tvdbId) => {
const titleElement = document.querySelector('.title h2') || document.querySelector('.header .title h2');
if (!titleElement) return;
// TMDB API로 대체 제목 가져오기
let koreanAltTitlesText = '';
let englishAltTitlesText = '';
let japaneseAltTitlesText = '';
try {
const alternativeTitles = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.themoviedb.org/3/${type}/${id}/alternative_titles?api_key=${apiKey}`,
onload: response => response.status === 200 ? resolve(JSON.parse(response.responseText)) : reject(`API 요청 실패: ${response.status}`),
onerror: reject
});
});
// 한국 대체 제목 찾기
const koreanAltTitles = type === 'movie'
? alternativeTitles.titles?.filter(title => title.iso_3166_1 === 'KR') || []
: alternativeTitles.results?.filter(title => title.iso_3166_1 === 'KR') || [];
koreanAltTitlesText = koreanAltTitles.length > 0
? ` ${koreanAltTitles.map(t => `<span class="clickable-text" data-copyable="true">[${t.title}]</span>`).join(', ')}`
: '';
// 영어 대체 제목 찾기
const englishAltTitles = type === 'movie'
? alternativeTitles.titles?.filter(title => title.iso_3166_1 === 'US' || title.iso_3166_1 === 'GB') || []
: alternativeTitles.results?.filter(title => title.iso_3166_1 === 'US' || title.iso_3166_1 === 'GB') || [];
englishAltTitlesText = englishAltTitles.length > 0
? ` ${englishAltTitles.map(t => `<span class="clickable-text" data-copyable="true">[${t.title}]</span>`).join(', ')}`
: '';
// 일본 대체 제목 찾기
const japaneseAltTitles = type === 'movie'
? alternativeTitles.titles?.filter(title => title.iso_3166_1 === 'JP') || []
: alternativeTitles.results?.filter(title => title.iso_3166_1 === 'JP') || [];
japaneseAltTitlesText = japaneseAltTitles.length > 0
? ` ${japaneseAltTitles.map(t => `<span class="clickable-text" data-copyable="true">[${t.title}]</span>`).join(', ')}`
: '';
} catch (error) {
console.error('대체 제목 가져오기 실패:', error);
}
// 변수 정의
const KoTitle = koTitle;
const EnTitle = enTitle;
const OriginalTitle = originalTitle;
const ChangedKoTitle = koTitle.replace(/:/g, ';').replace(/\?/g, '?');
const ChangedEnTitle = enTitle.replace(/:/g, ';').replace(/\?/g, '?');
const ChangedOriginalTitle = originalTitle.replace(/:/g, ';').replace(/\?/g, '?');
const titleContainer = document.createElement('div');
titleContainer.className = 'additional-titles';
const titleColor = window.getComputedStyle(titleElement).color;
const typeText = type === 'tv' ? 'TV' : 'MOVIE';
const processedEnTitle = enTitle.replace(/ /g, '+').replace(/[^\w\s+-]/g, '');
const osoSearchUrl = `https://www.opensubtitles.org/ko/search2/moviename-${encodeURIComponent(processedEnTitle)}+${year}/sublanguageid-kor`;
titleContainer.innerHTML = `
<table id="tmdb-info-table" style="width: 100%;">
<tr>
<td>
<div id="external-links">
▷ <span class="clickable-text" data-copyable="true" style="color: ${titleColor};">${id}</span> /
<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">${typeText}</span>
${type === 'movie' ?
`<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">+TMDB</span>
<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">+영</span>
<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">+원</span>` :
`<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">+TMDB</span>
<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">+영</span>
<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">+원</span>`
}
▶ <a href="${window.location.pathname}?language=en-US">🇺🇸</a>
<a href="${window.location.pathname}?language=ja-JP">🇯🇵</a>
<a href="${window.location.pathname}?language=ko-KR">🇰🇷</a>
▶ <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>` : ''}
/ <a href="${osoSearchUrl}" target="_blank">OSo</a>
<a href="https://www.opensubtitles.com/en/ko/search-all/q-${imdbId ? `${imdbId}` : encodeURIComponent(enTitle)}/hearing_impaired-include/machine_translated-/trusted_sources-" target="_blank">OSc</a>
<a href="https://subdl.com/search/${encodeURIComponent(enTitle)}" target="_blank">SUBDL</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_caption&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(koTitle)}%20${year}&sop=and" target="_blank">씨네K</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_caption&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(enTitle)}%20${year}&sop=and" target="_blank">씨네E</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_dramacap&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(koTitle)}%20${year}&sop=and" target="_blank">씨드K</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_dramacap&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(enTitle)}%20${year}&sop=and" target="_blank">씨드E</a>
/ <a href="https://www.google.com/search?q=${encodeURIComponent(koTitle)}+오프닝&tbm=vid" target="_blank">OST🎧️</a>
<a href="https://loaderr.to/ko/" target="_blank">MP3📀</a>
<a href="https://www.y2mate.com/" target="_blank">MP3💿</a><BR>
${window.location.pathname.match(/\/season\/\d+$/) ? `▷ <span class="clickable-text episode-list" style="color: ${titleColor};">에피소드 목록</span>` : ''}
▶ <a href="${window.location.pathname.match(/\/(tv|movie)\/\d+/)[0]}" class="clickable-text" style="color: ${titleColor};">메인</a>
${type === 'movie' ? `<a href="${window.location.pathname.match(/\/(tv|movie)\/\d+/)[0]}/images/posters" style="color: ${titleColor};">🌄</a> / ` : ''}
${window.location.pathname.startsWith('/tv/') ? `<span id="season-list"></span>` : ''}
${type === 'tv' ? `<a href="https://www.google.com/search?q=${encodeURIComponent(koTitle)}+site%3Atv.apple.com" target="_blank">AT🔍️</a>` : ''}
<a href="https://www.google.com/search?q=${encodeURIComponent(koTitle)}%20site:disneyplus.com&tbm=isch" target="_blank">DP1🖼️</a>
<a href="https://www.google.com/search?q=${encodeURIComponent(koTitle)}+디즈니%2B&tbm=isch" target="_blank">DP2🖼️</a>
<a href="https://laftel.net/search?keyword=${encodeURIComponent(koTitle)}" target="_blank">LF🖼️</a>
<a href="https://media.netflix.com/ko/search?countryCode=KR&term=${encodeURIComponent(koTitle)}" target="_blank">NF🖼️</a>
<a href="https://www.tving.com/search?keyword=${encodeURIComponent(koTitle)}" target="_blank">TV🖼️</a>
<a href="https://watcha.com/search?query=${encodeURIComponent(koTitle)}&domain=${type === 'tv' ? 'tv' : 'movie'}" target="_blank">WC🖼️</a>
<a href="https://www.wavve.com/search?searchWord=${encodeURIComponent(koTitle)}" target="_blank">WV🖼️</a>
<a href="https://www.google.com/search?q=${encodeURIComponent(koTitle)}%20${year}&tbm=isch" target="_blank">GG🖼️</a><BR>
▷ <span class="clickable-text" data-copyable="true" style="color: ${titleColor};">한제</span>:
<span class="clickable-text" data-copyable="true">${KoTitle}</span>
<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">(${year})</span> /
<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">영제</span>:
<span class="clickable-text" data-copyable="true">${EnTitle}</span>
<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">(${year})</span> /
<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">원제</span>:
<span class="clickable-text" data-copyable="true">${OriginalTitle}</span>
<span class="clickable-text" data-copyable="true" style="color: ${titleColor};">(${year})</span><BR>
${koreanAltTitlesText ? `▷ <span class="clickable-text" data-copyable="true" style="color: ${titleColor};">한제 대체</span>: ${koreanAltTitlesText}<BR>` : ''}
${englishAltTitlesText ? `▷ <span class="clickable-text" data-copyable="true" style="color: ${titleColor};">영제 대체</span>: ${englishAltTitlesText}<BR>` : ''}
${japaneseAltTitlesText ? `▷ <span class="clickable-text" data-copyable="true" style="color: ${titleColor};">일제 대체</span>: ${japaneseAltTitlesText}<BR>` : ''}
</div>
</td>
</tr>
</table>
`;
// 이벤트 리스너 설정
const elements = {
'메인': () => goToMainPage(),
[id]: () => copyToClipboard(id, id),
[typeText]: async () => {
const languagePrefix = await getLanguagePrefix(id, type);
const copyText = type === 'movie' ?
`${ChangedKoTitle} (${year}) {tmdb-${id}} [${languagePrefix}A !${koreanRating} $${translateCountry(productionCountry || originCountry)}]` :
`${ChangedKoTitle} (${year}) [${languagePrefix}A !${koreanRating} $${translateCountry(productionCountry || originCountry)}]`;
copyToClipboard(copyText, typeText);
},
'+TMDB': () => {
const countryToUse = productionCountry || originCountry; // 제작국가 우선 사용
copyToClipboard(`${ChangedKoTitle} (${year}) {tmdb-${id}} [!${koreanRating} $${translateCountry(countryToUse)}]`, '+TMDB');
},
'+영': () => {
const countryToUse = productionCountry || originCountry; // 제작국가 우선 사용
copyToClipboard(`${ChangedKoTitle} (${year}) [${ChangedEnTitle}] {tmdb-${id}} [!${koreanRating} $${translateCountry(countryToUse)}]`, '+영');
},
'+원': () => {
const countryToUse = productionCountry || originCountry; // 제작국가 우선 사용
copyToClipboard(`${ChangedKoTitle} (${year}) [${ChangedEnTitle}] [${ChangedOriginalTitle}] {tmdb-${id}} [!${koreanRating} $${translateCountry(countryToUse)}]`, '+원');
},
'한제': () => copyToClipboard(`${KoTitle} (${year})`, '한제'),
[KoTitle]: () => copyToClipboard(KoTitle, KoTitle),
'영제': () => copyToClipboard(`[${EnTitle}]`, '영제'),
[EnTitle]: () => copyToClipboard(EnTitle, EnTitle),
'원제': () => copyToClipboard(`[${EnTitle}] [${OriginalTitle}]`, '원제'),
[OriginalTitle]: () => copyToClipboard(OriginalTitle, OriginalTitle),
'한제 대체': async () => {
try {
const { id, type } = getIdAndType();
if (!id || !type) throw new Error('ID 또는 타입이 없습니다.');
const alternativeTitles = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.themoviedb.org/3/${type}/${id}/alternative_titles?api_key=${apiKey}`,
onload: response => response.status === 200 ? resolve(JSON.parse(response.responseText)) : reject(`API 요청 실패: ${response.status}`),
onerror: reject
});
});
const koreanTitles = type === 'movie'
? alternativeTitles.titles?.filter(t => t.iso_3166_1 === 'KR').map(t => t.title) || []
: alternativeTitles.results?.filter(t => t.iso_3166_1 === 'KR').map(t => t.title) || [];
const formattedTitles = koreanTitles.map(title => `[${title}]`).join(' ');
copyToClipboard(formattedTitles, 'TTT');
} catch (error) {
console.error('TTT 복사 실패:', error);
}
},
'영제 대체': async () => {
try {
const { id, type } = getIdAndType();
if (!id || !type) throw new Error('ID 또는 타입이 없습니다.');
const alternativeTitles = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.themoviedb.org/3/${type}/${id}/alternative_titles?api_key=${apiKey}`,
onload: response => response.status === 200 ? resolve(JSON.parse(response.responseText)) : reject(`API 요청 실패: ${response.status}`),
onerror: reject
});
});
const englishTitles = type === 'movie'
? alternativeTitles.titles?.filter(t => t.iso_3166_1 === 'US' || t.iso_3166_1 === 'GB').map(t => t.title) || []
: alternativeTitles.results?.filter(t => t.iso_3166_1 === 'US' || t.iso_3166_1 === 'GB').map(t => t.title) || [];
const formattedTitles = englishTitles.map(title => `[${title}]`).join(' ');
copyToClipboard(formattedTitles, 'TTT');
} catch (error) {
console.error('TTT 복사 실패:', error);
}
},
'일제 대체': async () => {
try {
const { id, type } = getIdAndType();
if (!id || !type) throw new Error('ID 또는 타입이 없습니다.');
const alternativeTitles = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.themoviedb.org/3/${type}/${id}/alternative_titles?api_key=${apiKey}`,
onload: response => response.status === 200 ? resolve(JSON.parse(response.responseText)) : reject(`API 요청 실패: ${response.status}`),
onerror: reject
});
});
const japaneseTitles = type === 'movie'
? alternativeTitles.titles?.filter(t => t.iso_3166_1 === 'JP').map(t => t.title) || []
: alternativeTitles.results?.filter(t => t.iso_3166_1 === 'JP').map(t => t.title) || [];
const formattedTitles = japaneseTitles.map(title => `[${title}]`).join(' ');
copyToClipboard(formattedTitles, 'TTT');
} catch (error) {
console.error('TTT 복사 실패:', error);
}
},
};
titleContainer.querySelectorAll('.clickable-text').forEach(element => {
const text = element.textContent;
if (elements[text]) {
element.addEventListener('click', elements[text]);
} else if (text.match(/^\(\d{4}\)$/)) {
element.addEventListener('click', () => {
const title = element.previousElementSibling.textContent;
copyToClipboard(`${title} ${text.slice(1, -1)}`, text);
});
} else if (text === 'TTT') {
element.addEventListener('click', elements['TTT']);
} else {
// Alternative Titles에 대한 클릭 이벤트 처리
element.addEventListener('click', () => {
copyToClipboard(`${text}`, text);
});
element.addEventListener('dblclick', () => {
const cleanText = text.replace(/^\[|\]$/g, '');
copyToClipboard(cleanText, text);
});
}
});
// 에피소드 목록 이벤트 리스너 추가
titleContainer.querySelectorAll('.episode-list').forEach(element => {
element.addEventListener('click', async () => {
try {
const pathParts = window.location.pathname.split('/');
const tvId = pathParts[2];
const seasonNumber = pathParts[4];
if (!tvId || !seasonNumber) {
throw new Error('TV ID 또는 시즌 번호를 찾을 수 없습니다.');
}
// API 요청에 대한 응답 검증 함수
const validateResponse = (response, type) => {
if (response.status !== 200) {
throw new Error(`${type} API 요청 실패: ${response.status}`);
}
const data = JSON.parse(response.responseText);
if (!data) {
throw new Error(`${type} 데이터가 비어있습니다.`);
}
return data;
};
const [tvInfo, seasonInfo] = await Promise.all([
new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.themoviedb.org/3/tv/${tvId}?api_key=${apiKey}&language=ko-KR`,
onload: response => {
try {
resolve(validateResponse(response, 'TV 정보'));
} catch (error) {
reject(error);
}
},
onerror: error => reject(new Error(`TV 정보 요청 실패: ${error}`))
});
}),
new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.themoviedb.org/3/tv/${tvId}/season/${seasonNumber}?api_key=${apiKey}&language=ko-KR`,
onload: response => {
try {
resolve(validateResponse(response, '시즌 정보'));
} catch (error) {
reject(error);
}
},
onerror: error => reject(new Error(`시즌 정보 요청 실패: ${error}`))
});
})
]);
if (!tvInfo.first_air_date) {
throw new Error('첫 방영일 정보가 없습니다.');
}
const firstAirYear = new Date(tvInfo.first_air_date).getFullYear();
if (!seasonInfo.episodes || !Array.isArray(seasonInfo.episodes) || seasonInfo.episodes.length === 0) {
throw new Error('에피소드 정보가 없습니다.');
}
const episodeList = seasonInfo.episodes.map(ep => {
if (!ep || typeof ep.episode_number === 'undefined') {
console.warn('잘못된 에피소드 데이터:', ep);
return null;
}
const formattedDate = ep.air_date || 'YYYY-MM-DD';
return `${tvInfo.name} (${firstAirYear}) s${seasonNumber.padStart(2, '0')}e${ep.episode_number.toString().padStart(2, '0')}_${formattedDate} ${ep.name || `${ep.episode_number}화`}`;
})
.filter(Boolean) // null 값 제거
.join('\n');
if (!episodeList) {
throw new Error('에피소드 목록을 생성할 수 없습니다.');
}
copyToClipboard(episodeList, '에피소드 목록');
showTemporaryMessage('에피소드 목록이 클립보드에 복사되었습니다.');
} catch (error) {
console.error('에피소드 목록 생성 실패:', error);
showTemporaryMessage(`에피소드 목록 생성 실패: ${error.message}`);
}
});
});
titleElement.parentNode.insertBefore(titleContainer, titleElement);
// 시즌 목록 표시 로직
if (type === 'tv') {
try {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.themoviedb.org/3/tv/${id}?api_key=${apiKey}`,
onload: function(response) {
const tvData = JSON.parse(response.responseText);
const seasonList = document.getElementById('season-list');
const mainTvId = getMainTvId(); // 메인 TV ID 추출
if (seasonList && tvData.seasons) {
let seasonLinks = `<a href="/tv/${mainTvId}/images/posters" style="color: ${titleColor};">🌄</a> / `;
tvData.seasons.forEach(season => {
const seasonNum = season.season_number.toString().padStart(2, '0');
seasonLinks += `<a href="/tv/${mainTvId}/season/${season.season_number}" style="color: ${titleColor};">s${seasonNum}</a> <a href="/tv/${mainTvId}/season/${season.season_number}/images/posters" style="color: ${titleColor};">🌄</a> / `;
});
seasonList.innerHTML = seasonLinks.slice(0, -3);
}
},
onerror: function(error) {
console.error('시즌 정보 가져오기 실패:', error);
}
});
} catch (error) {
console.error('시즌 목록 표시 오류:', error);
}
}
};
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 getOriginCountry = (data) => {
return data.origin_country?.[0] || data.production_countries?.[0]?.iso_3166_1 || null;
};
const getProductionCountry = (data) => {
return data.production_countries?.[0]?.iso_3166_1 || null;
};
const displayKoreanRating = rating => {
if (!rating) return;
const factsElement = document.querySelector('.facts');
if (!factsElement) return;
let koreanRatingElement = document.getElementById('korean-rating');
if (!koreanRatingElement) {
const currentPath = window.location.pathname;
const type = currentPath.includes('/movie/') ? 'movie' : 'tv';
const editUrl = currentPath +
(type === 'movie' ? '/edit?active_nav_item=release_information' : '/edit?active_nav_item=content_ratings');
koreanRatingElement = Object.assign(document.createElement('a'), {
id: 'korean-rating',
className: rating === '등급미정' ? 'unrated' : 'rated',
href: editUrl, // 일반적인 href 링크 사용
style: `
font-size: 1em;
margin-right: 10px;
font-weight: bold;
text-decoration: none;
color: inherit;
cursor: pointer;
`,
textContent: rating
});
factsElement.insertBefore(koreanRatingElement, factsElement.firstChild);
}
koreanRatingElement.textContent = rating;
};
const displayAdditionalInfo = (originCountry, productionCountry, koTitle, enTitle, imdbId, wikidataId, tvdbId, year) => {
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);
const originCountryText = translateCountry(originCountry);
const productionCountryText = translateCountry(productionCountry);
let searchLinks = '';
if (imdbId) {
const imdbIdNum = imdbId.replace('tt', '');
searchLinks = `
▶ <a href="https://www.opensubtitles.org/ko/search/sublanguageid-kor/imdbid-${imdbIdNum}" target="_blank">OSo</a>
<a href="https://www.opensubtitles.com/en/ko/search-all/q-tt${imdbIdNum}/hearing_impaired-include/machine_translated-/trusted_sources-" target="_blank">OSc</a>
<a href="https://subdl.com/search/${encodeURIComponent(enTitle)}%20${year}" target="_blank">SUBDL</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_caption&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(koTitle)}%20${year}&sop=and" target="_blank">씨네(한)</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_caption&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(enTitle)}%20${year}&sop=and" target="_blank">씨네(영)</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_dramacap&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(koTitle)}%20${year}&sop=and" target="_blank">씨드(한)</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_dramacap&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(enTitle)}%20${year}&sop=and" target="_blank">씨드(영)</a>
`;
} else {
searchLinks = `
▶ <a href="https://www.opensubtitles.org/ko/search2/moviename-${encodeURIComponent(enTitle)}%20${year}" target="_blank">OSo</a>
<a href="https://www.opensubtitles.com/en/ko/search-all/q-${encodeURIComponent(enTitle)}/hearing_impaired-include/machine_translated-/trusted_sources-" target="_blank">OSc</a>
<a href="https://subdl.com/search/${encodeURIComponent(enTitle)}%20${year}" target="_blank">SUBDL</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_caption&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(koTitle)}%20${year}&sop=and" target="_blank">씨네(한)</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_caption&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(enTitle)}%20${year}&sop=and" target="_blank">씨네(영)</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_dramacap&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(koTitle)}%20${year}&sop=and" target="_blank">씨드(한)</a>
<a href="https://cineaste.co.kr/bbs/board.php?bo_table=psd_dramacap&sca=&mv_no=&sfl=wr_subject&stx=${encodeURIComponent(enTitle)}%20${year}&sop=and" target="_blank">씨드(영)</a>
`;
}
additionalInfoContainer.innerHTML = `
<div id="external-links">
• 원작국: ${originCountryText} / 제작국: ${productionCountryText}
</div>
`;
const titleColor = window.getComputedStyle(document.querySelector('.title h2')).color;
additionalInfoContainer.querySelectorAll('#external-links a').forEach(link => {
link.style.color = titleColor;
link.style.marginRight = "0px";
});
}
};
const getMainTvId = () => {
const pathParts = window.location.pathname.split('/');
return pathParts[2]; // TV ID 추출
};
const fetchData = async () => {
const { id, type } = getIdAndType();
if (!id || !type) return;
try {
const koResponse = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&language=ko-KR&append_to_response=external_ids,release_dates,content_ratings`,
onload: response => response.status === 200 ? resolve(JSON.parse(response.responseText)) : reject(`API 요청 실패: ${response.status}`),
onerror: reject
});
});
const enResponse = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&language=en-US`,
onload: response => response.status === 200 ? resolve(JSON.parse(response.responseText)) : reject(`API 요청 실패: ${response.status}`),
onerror: reject
});
});
const koTitle = koResponse.title || koResponse.name || '한국어 제목 미기재';
const enTitle = enResponse.title || enResponse.name || '영어 제목 미기재';
const originalTitle = koResponse.original_title || koResponse.original_name || '원어 제목 미기재';
const koreanRating = getKoreanCertification(koResponse, type);
const originCountry = getOriginCountry(koResponse);
const productionCountry = getProductionCountry(koResponse);
const imdbId = koResponse.imdb_id || koResponse.external_ids?.imdb_id;
const wikidataId = koResponse.external_ids?.wikidata_id;
const tvdbId = koResponse.external_ids?.tvdb_id;
const year = new Date(koResponse.release_date || koResponse.first_air_date).getFullYear();
displayTitles(koTitle, enTitle, originalTitle, type, id, koreanRating, originCountry, productionCountry, year, imdbId, wikidataId, tvdbId);
displayKoreanRating(koreanRating);
displayAdditionalInfo(originCountry, productionCountry, koTitle, enTitle, imdbId, wikidataId, tvdbId, year);
} catch (error) {
console.error('TMDB API 요청 오류:', error);
}
};
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;
})();