Greasy Fork

TMDB 한국 지원 강화

TMDB 영화/TV 시리즈 페이지에 한국어, 영어, 원어 제목 추가, 개별 클립보드 복사 기능, 한국 시청등급 및 제작국 표시

目前为 2024-12-27 提交的版本。查看 最新版本

// ==UserScript==
// @name         TMDB 한국 지원 강화
// @namespace    http://tampermonkey.net/
// @version      1.3.8
// @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(`
        .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;
        }
.original-title-label {
    cursor: pointer;
    transition: color 0.3s;
}
.original-title-label:hover {
    color: blue !important;
}
        .title-label {
            cursor: pointer;
            transition: color 0.3s;
        }
        .title-label:hover {
            color: blue !important;
        }
        .ko-title-text {
            cursor: pointer;
            text-decoration: none;
            transition: color 0.3s;
        }
        .ko-title-text:hover {
            color: blue !important;
        }
        .main-link {
            cursor: pointer;
            text-decoration: none;
            transition: color 0.3s;
        }
        .main-link:hover {
            color: blue !important;
        }
        .en-original-link {
            cursor: pointer;
            text-decoration: none;
            transition: color 0.3s;
        }
        .en-original-link:hover {
            color: blue !important;
        }
        .type-link {
            cursor: pointer;
            text-decoration: none;
            transition: color 0.3s;
            margin: 0 5px;
        }
        .type-link:hover {
            color: blue !important;
        }
        .language-link {
            cursor: pointer;
            text-decoration: none;
            transition: color 0.3s;
            margin: 0 5px;
        }
        .language-link:hover {
            color: blue !important;
        }
        .plus-en-link, .plus-original-link, .plus-tmdb-link {
            cursor: pointer;
            text-decoration: none;
            transition: color 0.3s;
            margin: 0 5px;
        }
        .plus-en-link:hover, .plus-original-link:hover, .plus-tmdb-link:hover {
            color: blue !important;
        }
        .year-link {
            cursor: pointer;
            text-decoration: none;
            transition: color 0.3s;
            margin: 0 5px;
        }
        .year-link:hover {
            color: blue !important;
        }
    .tmdb-id-link {
        cursor: pointer;
        text-decoration: none;
        transition: color 0.3s;
    }
    .tmdb-id-link:hover {
        color: blue !important;
    }
    `);

    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 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;

        if (currentUrl.startsWith('https://www.themoviedb.org/tv/')) {
            const match = currentUrl.match(/\/tv\/(\d+)/);
            if (match) {
                mainUrl = `https://www.themoviedb.org/tv/${match[1]}`;
            }
        } else if (currentUrl.startsWith('https://www.themoviedb.org/movie/')) {
            const match = currentUrl.match(/\/movie\/(\d+)/);
            if (match) {
                mainUrl = `https://www.themoviedb.org/movie/${match[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 getCountryPrefix = (originCountry, productionCountry) => {
    const country = originCountry || productionCountry;
    const countryName = translateCountry(country);
    if (['뉴질랜드', '남아프리카', '미국', '캐나다', '호주'].includes(countryName)) return '영';
    if (['대만', '홍콩'].includes(countryName)) return '중';
    if (['멕시코', '아르헨티나', '콜롬비아'].includes(countryName)) return '스';
    if (countryName === '벨기에') return '프';
    if (countryName === '브라질') return '포';
    if (countryName === '스웨덴') return '웨';
    if (countryName === '인도') return '힌';
    return countryName.charAt(0);
};

    const displayTitles = (koTitle, enTitle, originalTitle, type, id, koreanRating, originCountry, productionCountry, year) => {
        const titleElement = document.querySelector('.title h2') || document.querySelector('.header .title h2');
        if (!titleElement) return;

        // 변수 정의
        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';

titleContainer.innerHTML = `
    <span class="main-link" style="color: ${titleColor};">메인</span> /
    <span class="tmdb-id-link" style="color: ${titleColor};">${id}</span> /
    <span class="type-link" style="color: ${titleColor};">${typeText}</span>
    ${type === 'movie' ?
        `<span class="plus-tmdb-link" style="color: ${titleColor};">+TMDB</span>
        <span class="plus-en-link" style="color: ${titleColor};">+영</span>
        <span class="plus-original-link" style="color: ${titleColor};">+원</span>` :
        `<span class="plus-tmdb-link" style="color: ${titleColor};">+TMDB</span>
        <span class="plus-en-link" style="color: ${titleColor};">+영</span>
        <span class="plus-original-link" style="color: ${titleColor};">+원</span>`
    } /
    <span class="ko-title-text" style="color: ${titleColor};">한제</span>:
    <span class="additional-title ko-title">${KoTitle}</span>
    <span class="year-link ko-year" style="color: ${titleColor};">(${year})</span> /
    <span class="en-original-link" style="color: ${titleColor};">영제</span>:
    <span class="additional-title en-title">${EnTitle}</span>
    <span class="year-link en-year" style="color: ${titleColor};">(${year})</span> /
    <span class="original-title-label" style="color: ${titleColor};">원제</span>:
    <span class="additional-title original-title">${OriginalTitle}</span>
    <span class="year-link original-year" style="color: ${titleColor};">(${year})</span>
`;

titleContainer.querySelector('.tmdb-id-link').addEventListener('click', function() {
    copyToClipboard(id);
});

        titleElement.parentNode.insertBefore(titleContainer, titleElement);

        titleContainer.querySelector('.main-link').addEventListener('click', goToMainPage);

// MOVIE/TV 클릭 이벤트
titleContainer.querySelector('.type-link').addEventListener('click', function() {
    const countryPrefix = getCountryPrefix(originCountry, productionCountry);
    let copyText = '';
    if (type === 'movie') {
        copyText = `${ChangedKoTitle} (${year}) {tmdb-${id}} ${countryPrefix}A !${koreanRating} $${translateCountry(originCountry || productionCountry)}`;
    } else {
        copyText = `${ChangedKoTitle} (${year}) ${countryPrefix}A !${koreanRating} $${translateCountry(originCountry || productionCountry)}`;
    }
    copyToClipboard(copyText);
});

// +TMDB 링크 이벤트
titleContainer.querySelector('.plus-tmdb-link').addEventListener('click', function() {
    const countryPrefix = getCountryPrefix(originCountry, productionCountry);
    let copyText = `${ChangedKoTitle} (${year}) {tmdb-${id}} !${koreanRating} $${translateCountry(originCountry || productionCountry)}`;
    copyToClipboard(copyText);
});

        // +영 링크 이벤트
        titleContainer.querySelector('.plus-en-link').addEventListener('click', function() {
            let copyText = `${ChangedKoTitle} (${year}) [${ChangedEnTitle}] {tmdb-${id}} !${koreanRating} $${translateCountry(originCountry || productionCountry)}`;
            copyToClipboard(copyText);
        });

        // +원 링크 이벤트
        titleContainer.querySelector('.plus-original-link').addEventListener('click', function() {
            let copyText = `${ChangedKoTitle} (${year}) [${ChangedEnTitle}] [${ChangedOriginalTitle}] {tmdb-${id}} !${koreanRating} $${translateCountry(originCountry || productionCountry)}`;
            copyToClipboard(copyText);
        });

// 연도 클릭 이벤트
        titleContainer.querySelector('.ko-year').addEventListener('click', function() {
            copyToClipboard(`${KoTitle} ${year}`);
        });

        titleContainer.querySelector('.en-year').addEventListener('click', function() {
            copyToClipboard(`${EnTitle} ${year}`);
        });

        titleContainer.querySelector('.original-year').addEventListener('click', function() {
            copyToClipboard(`${OriginalTitle} ${year}`);
        });

// 타이틀 클릭 이벤트
titleContainer.querySelector('.ko-title-text').addEventListener('click', function() {
    copyToClipboard(`${KoTitle} (${year})`);
});

titleContainer.querySelector('.ko-title').addEventListener('click', function() {
    copyToClipboard(KoTitle);
});

titleContainer.querySelector('.en-original-link').addEventListener('click', function() {
    copyToClipboard(`[${EnTitle}]`);
});

titleContainer.querySelector('.en-title').addEventListener('click', function() {
    copyToClipboard(EnTitle);
});

titleContainer.querySelector('.original-title-label').addEventListener('click', function() {
    copyToClipboard(`[${OriginalTitle}]`);
});

titleContainer.querySelector('.original-title').addEventListener('click', function() {
    copyToClipboard(OriginalTitle);
});

        // 언어 변경 이벤트 리스너 추가
        titleContainer.querySelectorAll('.language-link').forEach(link => {
            link.addEventListener('click', function() {
                const lang = this.getAttribute('onclick').match(/'([^']+)'/)[1];
                changeLanguage(lang);
            });
        });
    };

    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) {
            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 = (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-eng,kor/imdbid-${imdbIdNum}" target="_blank">OS(IMDB)</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">OS(Title)</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="production-countries">• 원작국: ${originCountryText} / 제작국: ${productionCountryText}</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> ` : ''}
                    ${searchLinks}
                </div>
            `;

            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 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);
            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;
})();