`;
}
// 툴팁 위치 및 내용 설정
Object.assign(tooltipContainer.style, {
left: `${offsetX}px`,
top: `${offsetY}px`,
display: 'block'
});
tooltipContainer.innerHTML = tooltipContent;
});
element.addEventListener('mouseleave', () => {
tooltipContainer.style.display = 'none';
});
// 이벤트 리스너가 적용되었음을 표시
element.setAttribute('data-tooltip-listener', 'true');
}
});
} catch (error) {
console.error('makeThumbnailTooltip 함수에서 오류가 발생했습니다:', error);
}
}
const showMore = (containerSelector, buttonId, n, fixed_n) => {
const userContainer = document.body.querySelector(containerSelector);
const users = Array.from(userContainer?.querySelectorAll('.user') || []);
const displayPerClick = 10;
// n보다 목록이 적으면 함수를 끝낸다
if (users.length <= fixed_n) return false;
// n개를 넘는 모든 요소를 숨긴다
users.slice(n).forEach(user => user.classList.add('show-more'));
const toggleButton = document.createElement('button');
toggleButton.textContent = users.length > n ? `더 보기 (${users.length - n})` : '접기';
toggleButton.id = buttonId;
toggleButton.title = "우클릭시 접기(초기화)";
userContainer.appendChild(toggleButton);
toggleButton.addEventListener('click', () => {
const hiddenUsers = users.filter(user => user.classList.contains('show-more'));
const hiddenCount = hiddenUsers.length;
if (hiddenCount > 0) {
hiddenUsers.slice(0, displayPerClick).forEach(user => user.classList.remove('show-more'));
const remainingHidden = hiddenUsers.length - displayPerClick;
toggleButton.textContent = remainingHidden > 0 ? `더 보기 (${remainingHidden})` : '접기';
} else {
users.slice(fixed_n).forEach(user => user.classList.add('show-more'));
toggleButton.textContent = `더 보기 (${users.length - fixed_n})`;
}
});
toggleButton.addEventListener('contextmenu', event => {
event.preventDefault();
users.slice(fixed_n).forEach(user => user.classList.add('show-more'));
toggleButton.textContent = `더 보기 (${users.length - fixed_n})`;
});
}
const removeDuplicates = () => {
const followUsers = Array.from(document.body.querySelectorAll('.users-section.follow > .user'));
const myplusUsers = Array.from(document.body.querySelectorAll('.users-section.myplus > .user'));
// follow 섹션에 유저가 없으면 종료
if (followUsers.length === 0) return;
// myplus 유저를 user_id를 키로 맵핑하여 빠르게 접근 가능하도록 객체로 변환
const myplusUsersMap = new Map(myplusUsers.map(user => [user.getAttribute('user_id'), user]));
// follow 유저들의 ID를 확인하고, 일치하는 myplus 유저를 제거
for (const followUser of followUsers) {
const followUserId = followUser.getAttribute('user_id');
const duplicateUser = myplusUsersMap.get(followUserId);
if (duplicateUser) {
duplicateUser.remove();
}
}
}
const generateBroadcastElements = async (update) => {
console.log(`방송 목록 갱신: ${new Date().toLocaleString()}`);
try {
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://myapi.sooplive.co.kr/api/favorite',
headers: {
'Content-Type': 'application/json',
},
onload: function(response) {
resolve(response.responseText);
},
onerror: function(error) {
reject(error);
}
});
});
const parsedResponse = JSON.parse(response);
// code 값 확인
if (parsedResponse?.code === -10000) {
//console.log('로그인 상태가 아닙니다.');
if (displayTop) insertTopChannels(update);
return;
}
insertFavoriteChannels(parsedResponse, update);
if (myplusPosition) {
if (displayMyplus || displayMyplusvod) insertMyplusChannels(update);
if (displayTop) insertTopChannels(update);
} else {
if (displayTop) insertTopChannels(update);
if (displayMyplus || displayMyplusvod) insertMyplusChannels(update);
}
} catch (error) {
console.error('Error:', error);
}
}
const addModalSettings = () => {
const openModalBtn = document.createElement("div");
openModalBtn.setAttribute("id", "openModalBtn");
const link = document.createElement("button");
link.setAttribute("class", "btn-settings-ui");
openModalBtn.appendChild(link);
const serviceUtilDiv = document.body.querySelector("div.serviceUtil");
serviceUtilDiv.prepend(openModalBtn);
// 모달 컨텐츠를 담고 있는 HTML 문자열
const modalContentHTML = `
×
설정
통합 옵션
메인 페이지 옵션
사이드바 옵션
${displayFollow}
${displayMyplus}
${displayMyplusvod}
${displayTop}
LIVE 플레이어 옵션
VOD 플레이어 옵션
채팅창 옵션
${nicknameWidth}
차단 관리
채널 차단: 본문 방송 목록 -> 점 세개 버튼 -> [이 브라우저에서 ... 숨기기]차단 해제: Tampermonkey 아이콘을 눌러서 가능합니다.단어 등록: Tampermonkey 아이콘을 눌러서 가능합니다.
부가 설명
1) MY 페이지에서 스트리머 고정 버튼(핀 모양)을 누르면 사이드바에 고정이 됩니다.2) 해당 단어를 포함하는 메시지 숨김. 완전 일치할 때만 숨김은 단어 앞에 e:를 붙이기. 예시) ㄱㅇㅇ,ㅔㅔ,e:ㅇㅇ,e:ㅇㅎ,e:극,e:나,e:락버그 신고는 https://greasyfork.org/ko/scripts/484713에서 가능합니다.
`;
// 모달 컨텐츠를 body에 삽입
document.body.insertAdjacentHTML("beforeend", modalContentHTML);
// 모달 열기 버튼에 이벤트 리스너 추가
let isFirstClick = true; // 첫 클릭 여부를 저장하는 변수
openModalBtn.addEventListener("click", () => {
// 모달을 표시
document.getElementById("myModal").style.display = "block";
// 첫 클릭인 경우에만 updateSettingsData 호출
if (isFirstClick) {
updateSettingsData();
isFirstClick = false; // 첫 클릭 후에는 false로 변경
}
});
// 모달 닫기 버튼에 이벤트 리스너 추가
const closeModalBtn = document.body.querySelector(".myModalClose");
closeModalBtn.addEventListener("click", () => {
// 모달을 숨김
const modal = document.getElementById("myModal");
if (modal) {
modal.style.display = "none";
}
});
// 모달 외부를 클릭했을 때 닫기
document.getElementById("myModal").addEventListener("click", (event) => {
const modalContent = document.querySelector('div.modal-content');
const modal = document.getElementById("myModal");
// 모달 콘텐츠가 아닌 곳을 클릭한 경우에만 모달 닫기
if (modal && !modalContent.contains(event.target)) {
modal.style.display = "none";
}
});
}
const updateSettingsData = () => {
const setCheckboxAndSaveValue = (elementId, storageVariable, storageKey) => {
const checkbox = document.getElementById(elementId);
// elementId가 유효한 경우에만 체크박스를 설정
if (checkbox) {
checkbox.checked = (storageVariable === 1);
checkbox.addEventListener("change", (event) => {
GM_setValue(storageKey, event.target.checked ? 1 : 0);
storageVariable = event.target.checked ? 1 : 0;
});
} else {
console.warn(`Checkbox with id "${elementId}" not found.`);
}
}
// 함수를 사용하여 각 체크박스를 설정하고 값을 저장합니다.
setCheckboxAndSaveValue("fixFixedChannel", isPinnedStreamWithPinEnabled, "isPinnedStreamWithPinEnabled");
setCheckboxAndSaveValue("fixNotificationChannel", isPinnedStreamWithNotificationEnabled, "isPinnedStreamWithNotificationEnabled");
setCheckboxAndSaveValue("showBufferTime", isRemainingBufferTimeEnabled, "isRemainingBufferTimeEnabled");
setCheckboxAndSaveValue("mutedInactiveTabs", isAutoChangeMuteEnabled, "isAutoChangeMuteEnabled");
setCheckboxAndSaveValue("popularChannelsFirst", myplusPosition, "myplusPosition");
setCheckboxAndSaveValue("mpSortByViewers", myplusOrder, "myplusOrder");
setCheckboxAndSaveValue("removeDuplicates", isDuplicateRemovalEnabled, "isDuplicateRemovalEnabled");
setCheckboxAndSaveValue("openInNewTab", isOpenNewtabEnabled, "isOpenNewtabEnabled");
setCheckboxAndSaveValue("mouseOverSideBar", showSidebarOnScreenMode, "showSidebarOnScreenMode");
setCheckboxAndSaveValue("chatPosition", isBottomChatEnabled, "isBottomChatEnabled");
setCheckboxAndSaveValue("showPauseButton", isMakePauseButtonEnabled, "isMakePauseButtonEnabled");
setCheckboxAndSaveValue("switchSharpmodeShortcut", isMakeSharpModeShortcutEnabled, "isMakeSharpModeShortcutEnabled");
setCheckboxAndSaveValue("switchLLShortcut", isMakeLowLatencyShortcutEnabled, "isMakeLowLatencyShortcutEnabled");
setCheckboxAndSaveValue("sendLoadBroadCheck", isSendLoadBroadEnabled, "isSendLoadBroadEnabled");
setCheckboxAndSaveValue("selectBestQuality", isSelectBestQualityEnabled, "isSelectBestQualityEnabled");
setCheckboxAndSaveValue("selectHideSupporterBadge", isHideSupporterBadgeEnabled, "isHideSupporterBadgeEnabled");
setCheckboxAndSaveValue("selectHideFanBadge", isHideFanBadgeEnabled, "isHideFanBadgeEnabled");
setCheckboxAndSaveValue("selectHideSubBadge", isHideSubBadgeEnabled, "isHideSubBadgeEnabled");
setCheckboxAndSaveValue("selectHideVIPBadge", isHideVIPBadgeEnabled, "isHideVIPBadgeEnabled");
setCheckboxAndSaveValue("selectHideMngrBadge", isHideManagerBadgeEnabled, "isHideManagerBadgeEnabled");
setCheckboxAndSaveValue("selectHideStreamerBadge", isHideStreamerBadgeEnabled, "isHideStreamerBadgeEnabled");
setCheckboxAndSaveValue("selectBlockWords", isBlockWordsEnabled, "isBlockWordsEnabled");
setCheckboxAndSaveValue("useInterFont", isChangeFontEnabled, "isChangeFontEnabled");
setCheckboxAndSaveValue("autoClaimGem", isAutoClaimGemEnabled, "isAutoClaimGemEnabled");
setCheckboxAndSaveValue("switchVideoSkipHandler", isVideoSkipHandlerEnabled, "isVideoSkipHandlerEnabled");
setCheckboxAndSaveValue("switchSmallUserLayout", isSmallUserLayoutEnabled, "isSmallUserLayoutEnabled");
setCheckboxAndSaveValue("switchChannelFeed", isChannelFeedEnabled, "isChannelFeedEnabled");
setCheckboxAndSaveValue("switchCustomSidebar", isCustomSidebarEnabled, "isCustomSidebarEnabled");
setCheckboxAndSaveValue("switchRemoveCarousel", isRemoveCarouselEnabled, "isRemoveCarouselEnabled");
setCheckboxAndSaveValue("switchDocumentTitleUpdate", isDocumentTitleUpdateEnabled, "isDocumentTitleUpdateEnabled");
setCheckboxAndSaveValue("switchRemoveRedistributionTag", isRemoveRedistributionTagEnabled, "isRemoveRedistributionTagEnabled");
setCheckboxAndSaveValue("switchRemoveWatchLaterButton", isRemoveWatchLaterButtonEnabled, "isRemoveWatchLaterButtonEnabled");
setCheckboxAndSaveValue("switchBroadTitleTextEllipsis", isBroadTitleTextEllipsisEnabled, "isBroadTitleTextEllipsisEnabled");
setCheckboxAndSaveValue("switchRemoveBroadStartTimeTag", isRemoveBroadStartTimeTagEnabled, "isRemoveBroadStartTimeTagEnabled");
setCheckboxAndSaveValue("switchUnlockCopyPaste", isUnlockCopyPasteEnabled, "isUnlockCopyPasteEnabled");
setCheckboxAndSaveValue("switchAlignNicknameRight", isAlignNicknameRightEnabled, "isAlignNicknameRightEnabled");
setCheckboxAndSaveValue("switchPreviewModal", isPreviewModalEnabled, "isPreviewModalEnabled");
setCheckboxAndSaveValue("switchReplaceEmptyThumbnail", isReplaceEmptyThumbnailEnabled, "isReplaceEmptyThumbnailEnabled");
setCheckboxAndSaveValue("switchSharpening", isSharpeningEnabled, "isSharpeningEnabled");
setCheckboxAndSaveValue("switchAutoScreenMode", isAutoScreenModeEnabled, "isAutoScreenModeEnabled");
setCheckboxAndSaveValue("switchAdjustDelayNoGrid", isAdjustDelayNoGridEnabled, "isAdjustDelayNoGridEnabled");
const handleRangeInput = (inputId, displayId, currentValue, storageKey) => {
const input = document.getElementById(inputId);
input.value = currentValue;
input.addEventListener("input", (event) => {
const newValue = parseInt(event.target.value); // event.target.value로 변경
if (newValue !== currentValue) {
GM_setValue(storageKey, newValue);
currentValue = newValue;
document.getElementById(displayId).textContent = newValue;
if (inputId === "nicknameWidthDisplay") setWidthNickname(newValue);
}
});
}
handleRangeInput("favoriteChannelsDisplay", "favoriteChannelsDisplayValue", displayFollow, "displayFollow");
handleRangeInput("myPlusChannelsDisplay", "myPlusChannelsDisplayValue", displayMyplus, "displayMyplus");
handleRangeInput("myPlusVODDisplay", "myPlusVODDisplayValue", displayMyplusvod, "displayMyplusvod");
handleRangeInput("popularChannelsDisplay", "popularChannelsDisplayValue", displayTop, "displayTop");
handleRangeInput("nicknameWidthDisplay", "nicknameWidthDisplayValue", nicknameWidth, "nicknameWidth");
// 입력 상자 가져오기
const inputBox = document.getElementById('blockWordsInput');
// 입력 상자의 내용이 변경될 때마다 설정 저장
inputBox.addEventListener('input', () => {
const inputValue = inputBox.value.trim();
if (inputValue !== '') {
registeredWords = inputValue;
GM_setValue("registeredWords", inputValue);
}
});
}
const checkSidebarVisibility = () => {
let intervalId = null;
let lastExecutionTime = Date.now(); // 마지막 실행 시점 기록
const handleVisibilityChange = () => {
const sidebar = document.querySelector('#sidebar');
const sidebarDisplay = getComputedStyle(sidebar).display;
if (document.visibilityState === 'visible' && sidebarDisplay === 'none') {
return;
}
const currentTime = Date.now();
const timeSinceLastExecution = (currentTime - lastExecutionTime) / 1000; // 초 단위로 변환
if (document.visibilityState === 'visible' && timeSinceLastExecution >= 60) {
// 60초 이상 경과 시 방송 목록 갱신
console.log('탭 활성화됨');
generateBroadcastElements(1);
lastExecutionTime = currentTime; // 갱신 시점 기록
restartInterval(); // 인터벌 재시작
} else if (document.visibilityState === 'visible') {
console.log('60초 미만 경과: 방송 목록 갱신하지 않음');
} else {
console.log(`탭 비활성화됨: 마지막 갱신 = ${timeSinceLastExecution}초 전`);
}
};
const restartInterval = () => {
if (intervalId) clearInterval(intervalId); // 기존 인터벌 중단
intervalId = setInterval(() => {
handleVisibilityChange();
}, 60 * 1000); // 60초마다 실행
};
const observeSidebarVisibility = (selector) => {
const sidebar = document.querySelector(selector);
let hasBeenVisible = false;
const observer = new MutationObserver((mutations) => {
mutations.forEach(({ attributeName }) => {
if (attributeName === 'style') {
const display = getComputedStyle(sidebar).display;
if (display !== 'none' && !hasBeenVisible) {
hasBeenVisible = true;
console.log('#sidebar가 보임!');
handleVisibilityChange();
} else if (display === 'none' && hasBeenVisible) {
hasBeenVisible = false; // 이전에 보였을 때만 변경
console.log('#sidebar가 안 보임!');
}
}
});
});
// 옵저버 시작 (속성 변화를 감지)
observer.observe(sidebar, {
attributes: true,
attributeFilter: ['style']
});
};
waitForElement('#sidebar', function (elementSelector, element) {
console.log('#sidebar가 로드됨!');
observeSidebarVisibility(elementSelector); // 이미 찾은 요소를 넘김
restartInterval(); // 인터벌 시작
document.addEventListener('visibilitychange', handleVisibilityChange);
});
}
const hideUsersSection = () => {
const styles = [
!displayMyplus && '#sidebar .myplus { display: none !important; }',
!displayMyplusvod && '#sidebar .myplusvod { display: none !important; }',
!displayTop && '#sidebar .top { display: none !important; }'
].filter(Boolean).join(' '); // 빈 값 제거 및 합침
if (styles) {
GM_addStyle(styles);
}
}
const removeTargetFromLinks = () => {
try {
const links = document.querySelectorAll('#container a[target], .side_list a[target]');
links.forEach(link => {
link.removeAttribute('target');
});
} catch (error) {
console.error('target 속성 제거 중 오류 발생:', error);
}
}
const runCommonFunctions = () => {
if (isCustomSidebarEnabled) {
hideUsersSection();
generateBroadcastElements(0);
checkSidebarVisibility();
}
GM_addStyle(CommonStyles);
// 본문 방송 목록의 새 탭 열기 방지
if(!isOpenNewtabEnabled){
setInterval(removeTargetFromLinks, 1000);
}
waitForElement('div.serviceUtil', function (elementSelector, element) {
addModalSettings();
manageRedDot();
});
registerMenuBlockingWord();
blockedUsers.forEach(function(user) {
registerUnblockMenu(user);
});
blockedCategories.forEach(function(category) {
registerCategoryUnblockMenu(category);
});
blockedWords.forEach(function(word) {
registerWordUnblockMenu(word);
});
}
//=================================공용 함수 끝=================================//
//=================================메인 페이지 함수=================================//
const getBroadAid = async (id, broadNumber) => {
const requestOptions = {
method: 'GET',
credentials: 'include'
};
try {
const response = await fetch(`https://live.sooplive.co.kr/api/live_status.php?user_id=${id}&broad_no=${broadNumber}&type=play`, requestOptions);
const result = await response.json();
return result.data.aid || null;
} catch (error) {
console.log('오류 발생:', error);
return null;
}
};
const getBroadDomain = async (id, broadNumber) => {
const requestOptions = {
method: 'GET'
};
try {
const response = await fetch(`https://livestream-manager.sooplive.co.kr/broad_stream_assign.html?return_type=gs_cdn_preview&use_cors=true&cors_origin_url=www.sooplive.co.kr&broad_key=${broadNumber}-common-hd-hls`, requestOptions);
const result = await response.json();
return result.view_url || null;
} catch (error) {
console.log('오류 발생:', error);
return null;
}
};
// 최신 프레임 캡처 함수
const captureLatestFrame = (videoElement) => {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 캔버스 크기 설정 (480x270)
const canvasWidth = 480;
const canvasHeight = 270;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// 원본 비디오의 비율을 유지하면서 크기 계산
const videoRatio = videoElement.videoWidth / videoElement.videoHeight;
const canvasRatio = canvasWidth / canvasHeight;
let drawWidth, drawHeight;
let offsetX = 0, offsetY = 0;
if (videoRatio > canvasRatio) {
drawWidth = canvasWidth;
drawHeight = canvasWidth / videoRatio;
offsetY = (canvasHeight - drawHeight) / 2;
} else {
drawHeight = canvasHeight;
drawWidth = canvasHeight * videoRatio;
offsetX = (canvasWidth - drawWidth) / 2;
}
// 배경을 검은색으로 채우기
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// 비디오의 현재 프레임을 캔버스에 그림
ctx.drawImage(videoElement, offsetX, offsetY, drawWidth, drawHeight);
// webp 형식으로 변환 후 반환
const dataURL = canvas.toDataURL('image/webp');
resolve(dataURL); // 데이터 URL 반환
});
};
// id와 broadNumber로 이미지 데이터 캡처
const getLatestFrameData = async (id, broadNumber) => {
const videoElement = document.createElement('video');
videoElement.playbackRate = 16; // 빠른 재생 속도 설정
// 병렬로 broadAid와 broadDomain 가져오기
const [broadAid, broadDomain] = await Promise.all([
getBroadAid(id, broadNumber),
getBroadDomain(id, broadNumber)
]);
const m3u8url = `${broadDomain}?aid=${broadAid}`;
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(m3u8url);
hls.attachMedia(videoElement);
return new Promise((resolve) => {
videoElement.addEventListener('canplay', async () => {
const frameData = await captureLatestFrame(videoElement);
resolve(frameData);
videoElement.pause();
videoElement.src = '';
});
});
} else {
console.error('HLS.js를 지원하지 않는 브라우저입니다.');
return null;
}
};
const replaceThumbnails = (thumbsBoxLinks) => {
for (const thumbsBoxLink of thumbsBoxLinks) {
if (!thumbsBoxLink.classList.contains("thumbnail-checked")) {
thumbsBoxLink.classList.add("thumbnail-checked");
const hrefValue = thumbsBoxLink.getAttribute('href');
if (hrefValue && hrefValue.includes("sooplive.co.kr")) {
const [ , , , id, broadNumber] = hrefValue.split('/');
thumbsBoxLink.dataset.lastMouseEnterTime = 0;
thumbsBoxLink.addEventListener('mouseenter', async function(event) {
event.preventDefault();
event.stopPropagation();
const currentTime = Date.now();
const lastMouseEnterTime = Number(thumbsBoxLink.dataset.lastMouseEnterTime);
if (currentTime - lastMouseEnterTime >= 30000) {
thumbsBoxLink.dataset.lastMouseEnterTime = currentTime;
const frameData = await getLatestFrameData(id, broadNumber);
let imgElement = thumbsBoxLink.querySelector('img');
if (!imgElement) {
imgElement = document.createElement('img');
thumbsBoxLink.appendChild(imgElement);
}
imgElement.src = frameData;
}
});
}
}
}
};
// 모달 생성 함수 먼저 정의
const createModal = () => {
if (!CURRENT_URL.startsWith("https://www.sooplive.co.kr")){
return false;
}
window.onclick = function(event) {
if (event.target === modal) {
closeModal(modal, modalElements.videoPlayer);
}
};
const modal = document.createElement('div');
modal.className = 'preview-modal';
const modalContent = document.createElement('div');
modalContent.className = 'preview-modal-content';
const closeButton = document.createElement('span');
closeButton.className = 'preview-close';
closeButton.innerHTML = '×';
closeButton.onclick = () => closeModal(modal, modalElements.videoPlayer); // closeButton에 클릭 이벤트 설정
const videoPlayer = document.createElement('video');
videoPlayer.controls = true;
const infoContainer = document.createElement('div');
infoContainer.className = 'info';
// 방송 정보 및 태그 추가
const streamerName = document.createElement('div');
streamerName.className = 'streamer-name';
const videoTitle = document.createElement('div');
videoTitle.className = 'video-title';
const tagsContainer = document.createElement('div');
tagsContainer.className = 'tags';
const startButton = document.createElement('a');
startButton.className = 'start-button';
startButton.textContent = '참여하기 >'; // 버튼 텍스트 설정
infoContainer.append(streamerName, tagsContainer, videoTitle, startButton);
modalContent.append(closeButton, videoPlayer, infoContainer);
modal.appendChild(modalContent);
document.body.appendChild(modal);
return { modal, videoPlayer, streamerName, videoTitle, tagsContainer, startButton };
};
// 전역 변수에 modalElements 저장
const modalElements = createModal(); // 모달을 한 번 생성하고 변수에 저장
const makePreviewModalContents = (thumbsBoxLinks) => {
for (const thumbsBoxLink of thumbsBoxLinks) {
if (!thumbsBoxLink.classList.contains("preview-checked")) {
thumbsBoxLink.classList.add("preview-checked");
const hrefValue = thumbsBoxLink.getAttribute('href');
if (hrefValue?.includes("play.sooplive.co.kr")) {
const [ , , , id, broadNumber] = hrefValue.split('/');
thumbsBoxLink.addEventListener('click', async (event) => {
event.preventDefault();
event.stopPropagation();
// 모달이 이미 표시된 경우 클릭 처리하지 않음
if (modalElements.modal.style.display === 'block') return;
await handleLinkClick(id, broadNumber, thumbsBoxLink);
modalElements.modal.style.display = 'block';
});
}
}
}
};
const closeModal = (modal, videoPlayer) => {
modal.style.display = 'none';
videoPlayer.pause(); // 비디오 정지
videoPlayer.src = ''; // 소스 초기화
};
const handleLinkClick = async (id, broadNumber, thumbsBoxLink) => {
const playerLink = `https://play.sooplive.co.kr/${id}/${broadNumber}`;
try {
// 병렬로 broadAid와 broadDomain 가져오기
const [broadAid, broadDomain] = await Promise.all([
getBroadAid(id, broadNumber),
getBroadDomain(id, broadNumber)
]);
if (broadAid && broadDomain) {
const m3u8url = `${broadDomain}?aid=${broadAid}`;
// 내용 업데이트
updateModalContent(m3u8url, playerLink, thumbsBoxLink);
} else {
console.error('Invalid broadAid or broadDomain');
}
} catch (error) {
console.error('Error fetching broadcast information:', error);
}
};
const updateModalContent = (m3u8url, playerLink, thumbsBoxLink) => {
const { videoPlayer, streamerName, videoTitle, tagsContainer, startButton } = modalElements;
const hrefTarget = isOpenNewtabEnabled ? "_blank" : "_self";
// 방송 정보 업데이트
const parent = thumbsBoxLink.parentNode.parentNode;
streamerName.textContent = parent.querySelector('.nick').innerText; // 스트리머 이름
videoTitle.textContent = parent.querySelector('.title a').innerText; // 방송 제목
// 태그 추가
updateTags(tagsContainer, thumbsBoxLink);
// startButton의 href 업데이트
startButton.setAttribute('href', playerLink);
startButton.setAttribute('target', hrefTarget);
// 비디오 표시 및 재생
const playVideo = () => {
videoPlayer.style.display = 'block'; // 비디오 표시
videoPlayer.play(); // 비디오 재생 시작
};
// HLS.js를 사용하여 m3u8 재생 설정
const initializeHLS = () => {
const hls = new Hls();
hls.loadSource(m3u8url);
hls.attachMedia(videoPlayer);
hls.on(Hls.Events.MANIFEST_PARSED, playVideo);
hls.on(Hls.Events.ERROR, (event, data) => {
console.error('HLS error: ', data);
});
};
const handleSafariSupport = () => {
videoPlayer.src = m3u8url;
videoPlayer.addEventListener('loadedmetadata', playVideo);
videoPlayer.addEventListener('error', () => {
console.error('Video playback error');
alert('비디오를 로드하는 데 오류가 발생했습니다.');
});
};
// HLS 지원 여부에 따라 초기화
if (Hls.isSupported()) {
initializeHLS();
} else if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) {
handleSafariSupport();
} else {
console.error('이 브라우저는 HLS를 지원하지 않습니다.');
alert('이 브라우저는 HLS 비디오를 지원하지 않습니다.');
}
};
// 기존 updateTags 함수 유지
const updateTags = (tagsContainer, thumbsBoxLink) => {
const tags = thumbsBoxLink.parentNode.parentNode.querySelectorAll('.tag_wrap a');
tagsContainer.innerHTML = ''; // 이전 태그 제거
tags.forEach(tag => {
const tagElement = document.createElement('a');
tagElement.textContent = tag.innerText;
tagElement.href = tag.getAttribute("class") === "category"
? `https://www.sooplive.co.kr/directory/category/${encodeURIComponent(tag.innerText)}/live`
: `https://www.sooplive.co.kr/search?hash=hashtag&tagname=${encodeURIComponent(tag.innerText)}&hashtype=live&stype=hash&acttype=live&location=live_main&inflow_tab=`;
tagsContainer.appendChild(tagElement);
});
};
const removeUnwantedTags = () =>{
if (isRemoveCarouselEnabled) {
GM_addStyle(`
div[class^="player_player_wrap"] {
display: none !important;
}
`);
}
if (isRemoveRedistributionTagEnabled) {
GM_addStyle(`
[data-type=cBox] .thumbs-box .allow {
display: none !important;
}
`);
}
if (isRemoveWatchLaterButtonEnabled) {
GM_addStyle(`
[data-type=cBox] .thumbs-box .later {
display: none !important;
}
`);
}
if (isRemoveBroadStartTimeTagEnabled) {
GM_addStyle(`
[data-type=cBox] .thumbs-box .time {
display: none !important;
}
`);
}
if (isBroadTitleTextEllipsisEnabled) {
GM_addStyle(`
[data-type=cBox] .cBox-info .title a {
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
}
`);
}
}
const processStreamers = () => {
const processedLayers = new Set(); // 처리된 레이어를 추적
// 버튼 생성 및 클릭 이벤트 처리
const createHideButton = (listItem, optionsLayer) => {
const hideButton = document.createElement('button'); // "숨기기" 버튼 생성
hideButton.type = 'button';
hideButton.innerHTML = '이 브라우저에서 스트리머 숨기기';
// 클릭 이벤트 추가
hideButton.addEventListener('click', () => {
const userNameElement = listItem.querySelector('a.nick > span'); // 사용자 이름 요소
const userIdElement = listItem.querySelector('.cBox-info > a'); // 사용자 ID 요소
if (userNameElement && userIdElement) {
const userId = userIdElement.href.split('/')[3]; // 사용자 ID 추출
const userName = userNameElement.innerText; // 사용자 이름 추출
console.log(`Blocking user: ${userName}, ID: ${userId}`); // 로그 추가
if (userId && userName) {
blockUser(userName, userId); // 사용자 차단 함수 호출
listItem.style.display = 'none';
}
} else {
console.log("User elements not found."); // 요소가 없을 경우 로그 추가
}
});
optionsLayer.appendChild(hideButton); // 옵션 레이어에 버튼 추가
};
const createCategoryHideButton = (listItem, optionsLayer) => {
const hideButton = document.createElement('button'); // "숨기기" 버튼 생성
hideButton.type = 'button';
hideButton.innerHTML = `이 브라우저에서 해당 카테고리 숨기기`;
// 클릭 이벤트 추가 [data-type=cBox] .cBox-info .tag_wrap a.category
hideButton.addEventListener('click', () => {
const categoryElement = listItem.querySelector('.cBox-info .tag_wrap a.category');
if (categoryElement) {
const categoryName = categoryElement.textContent;
const categoryNo = getCategoryNo(categoryName);
if (categoryName && categoryNo) {
blockCategory(categoryName, categoryNo);
}
} else {
console.log("User elements not found."); // 요소가 없을 경우 로그 추가
}
});
optionsLayer.appendChild(hideButton); // 옵션 레이어에 버튼 추가
}
// DOM 변경 감지 및 처리
const handleDOMChange = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
const moreOptionsContainer = document.querySelector('div._moreDot_wrapper'); // 추가 옵션 컨테이너
const optionsLayer = moreOptionsContainer ? moreOptionsContainer.querySelector('div._moreDot_layer') : null; // 옵션 레이어
if (optionsLayer && optionsLayer.style.display !== 'none' && !processedLayers.has(optionsLayer)) {
const activeButton = document.querySelector('button.more_dot.on'); // 활성화된 버튼
const listItem = activeButton.closest('li[data-type="cBox"]'); // 가장 가까운 리스트 아이템 찾기
if (listItem) {
createHideButton(listItem, optionsLayer); // 숨기기 버튼 생성
createCategoryHideButton(listItem, optionsLayer);
processedLayers.add(optionsLayer); // 이미 처리된 레이어로 추가
}
} else if (!optionsLayer) {
processedLayers.clear(); // 요소가 없을 때 처리된 레이어 초기화
}
// cBox-list의 리스트 아이템 처리
const cBoxListItems = document.querySelectorAll('div.cBox-list li[data-type="cBox"]:not(.hide-checked)');
// cBoxListItems를 for...of 루프로 반복
for (const listItem of cBoxListItems) {
listItem.classList.add('hide-checked');
const userIdElement = listItem.querySelector('.cBox-info > a'); // 사용자 ID 요소
const categoryElement = listItem.querySelector('.cBox-info .tag_wrap a.category');
const titleElement = listItem.querySelector('.cBox-info .title a');
if (userIdElement) {
const userId = userIdElement.href.split('/')[3]; // 사용자 ID 추출
// 차단된 사용자일 경우 li 삭제
if (isUserBlocked(userId)) {
listItem.style.display = 'none';
console.log(`Removed blocked user with ID: ${userId}`); // 로그 추가
}
}
if (categoryElement) {
const categoryName = categoryElement.textContent;
if (isCategoryBlocked(getCategoryNo(categoryName))) {
listItem.style.display = 'none';
console.log(`Removed blocked category with Name: ${categoryName}`); // 로그 추가
}
}
if (titleElement) {
const broadTitle = titleElement.textContent;
// blockedWords에 포함된 단어가 broadTitle에 있는지 체크
for (const word of blockedWords) {
if (broadTitle.toLowerCase().includes(word.toLowerCase())) {
listItem.style.display = 'none';
console.log(`Removed item with blocked word in title: ${broadTitle}`); // 로그 추가
break; // 하나의 차단 단어가 발견되면 더 이상 확인할 필요 없음
}
}
}
}
// 프리뷰 모달 사용
if (isPreviewModalEnabled) {
const allThumbsBoxLinks = document.querySelectorAll('[data-type=cBox] .thumbs-box > a[href]:not([href^="https://vod.sooplive.co.kr"])');
if (allThumbsBoxLinks.length) makePreviewModalContents(allThumbsBoxLinks);
}
// 빈 썸네일 대체
if (isReplaceEmptyThumbnailEnabled){
const noThumbsBoxLinks = document.querySelectorAll('[data-type=cBox] .thumbs-box > a[href].thumb-adult:not([href^="https://vod.sooplive.co.kr"])');
if (noThumbsBoxLinks.length) replaceThumbnails(noThumbsBoxLinks);
}
}
}
};
const observer = new MutationObserver(handleDOMChange); // DOM 변경 감지기
// 감지할 옵션 설정
const config = { childList: true, subtree: true };
// 관찰 시작
observer.observe(document.body, config);
};
//=================================메인 페이지 함수 끝=================================//
//=================================플레이어 페이지 함수=================================//
const detectScreenMode = () => {
const target = document.querySelector('body');
const bodyClasses = target.classList;
const observer = new MutationObserver(function(mutations) {
const sidebar = document.getElementById('sidebar');
const webplayer_contents = document.getElementById('webplayer_contents');
const serviceHeader = document.getElementById('serviceHeader');
const left_navbar = document.querySelector('.left_navbar')
// 스크린 모드
if (bodyClasses.contains('screen_mode')){
sidebar.style.display = 'none';
left_navbar.style.display = 'none';
webplayer_contents.style.left = '0';
sidebar.style.top = '0px';
} else { // 스크린 모드 아닐 때
sidebar.style.display = '';
left_navbar.style.display = '';
if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement) {
webplayer_contents.style.left = '0';
} else {
webplayer_contents.style.left = `${document.getElementById('sidebar').offsetWidth}px`;
}
sidebar.style.top = '64px';
}
});
observer.observe(target, {
attributeFilter: ['class']
});
}
const detectFullscreenmode = () => {
const target = document.querySelector('body');
const bodyClasses = target.classList;
const observer = new MutationObserver(function(mutations) {
const sidebar = document.getElementById('sidebar');
const webplayer_contents = document.getElementById('webplayer_contents');
const serviceHeader = document.getElementById('serviceHeader');
const left_navbar = document.querySelector('.left_navbar')
// 풀 스크린 모드
if (bodyClasses.contains('fullScreen_mode')){
sidebar.style.display = 'none';
left_navbar.style.display = 'none';
webplayer_contents.style.left = '0';
sidebar.style.top = '0px';
} else { // 풀 스크린 모드 나갔을 때
sidebar.style.display = '';
left_navbar.style.display = '';
if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement) {
webplayer_contents.style.left = '0';
} else {
webplayer_contents.style.left = `${document.getElementById('sidebar').offsetWidth}px`;
}
sidebar.style.top = '64px';
}
});
observer.observe(target, {
attributeFilter: ['class']
});
}
const showSidebarOnMouseOver = () => {
const sidebar = document.getElementById('sidebar');
const videoLayer = document.getElementById('player');
const webplayerContents = document.getElementById('webplayer');
const body = document.body;
const handleSidebarMouseOver = () => {
if (body.classList.contains('screen_mode') && sidebar.style.display === 'none') {
sidebar.style.display = '';
sidebar.style.top = '0px';
if (webplayerContents.style.left === '0px') {
webplayerContents.style.left = sidebar.offsetWidth + 'px';
}
}
};
const handleSidebarMouseOut = () => {
if (body.classList.contains('screen_mode') && sidebar.style.display !== 'none') {
sidebar.style.top = '64px';
sidebar.style.display = 'none';
webplayerContents.style.left = '0px';
}
};
const mouseMoveHandler = (event) => {
const mouseX = event.clientX;
const mouseY = event.clientY;
if ((mouseX < 52 && mouseY < videoLayer.clientHeight - 150) ||
(mouseX < sidebar.clientWidth && mouseY < sidebar.clientHeight)) {
handleSidebarMouseOver();
} else {
handleSidebarMouseOut();
}
};
const mouseLeaveHandler = () => {
handleSidebarMouseOut();
};
// 이벤트 리스너 등록
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseleave', mouseLeaveHandler);
};
const toggleSharpModeShortcut = () => {
setupInputFocusHandlers();
setupWriteAreaInputHandler();
setupKeydownHandler(69, togglesharpModeCheck); // E 키
updateLabel('clear_screen', '선명한 모드', '선명한 모드(e)');
};
const toggleLowLatencyShortcut = () => {
setupInputFocusHandlers();
setupWriteAreaInputHandler();
setupKeydownHandler(68, toggleDelayCheck); // D 키
updateLabel('delay_check', '시차 단축', '시차 단축(d)');
};
const setupInputFocusHandlers = () => {
document.body.querySelectorAll('input').forEach(input => {
input.addEventListener('focus', () => {
sharpModeCheckEnabled = false;
delayCheckEnabled = false;
});
input.addEventListener('blur', () => {
sharpModeCheckEnabled = true;
delayCheckEnabled = true;
});
});
};
const setupWriteAreaInputHandler = () => {
const writeArea = document.getElementById('write_area');
writeArea.addEventListener('input', () => {
sharpModeCheckEnabled = false;
delayCheckEnabled = false;
});
};
const setupKeydownHandler = (keyCode, toggleFunction) => {
document.addEventListener('keydown', (event) => {
if (event.keyCode === keyCode && document.activeElement.nodeName !== 'INPUT' && document.activeElement.id !== 'write_area') {
toggleFunction();
}
});
};
const updateLabel = (forId, oldText, newText) => {
const labelElement = document.body.querySelector(`#player label[for="${forId}"]`);
if (labelElement) {
labelElement.innerHTML = labelElement.innerHTML.replace(oldText, newText);
} else {
console.error('Label element not found.');
}
};
const togglesharpModeCheck = () => {
const sharpModeCheckElement = document.getElementById('clear_screen');
sharpModeCheckElement.click();
showPlayerBar(69); // E 키
};
const toggleDelayCheck = () => {
if (isAdjustDelayNoGridEnabled) {
moveToLatestBufferedPoint();
} else {
const delayCheckElement = document.getElementById('delay_check');
delayCheckElement.click();
showPlayerBar(68); // D 키
}
};
const showPlayerBar = (keyCode) => {
const player = document.getElementById('player');
player.classList.add('mouseover');
let settingButton, settingBoxOn;
if (keyCode === 69) { // E 키
settingButton = document.body.querySelector('#player button.btn_quality_mode');
settingBoxOn = document.body.querySelector('.quality_box.on');
} else if (keyCode === 68) { // D 키
settingButton = document.body.querySelector('#player button.btn_setting');
settingBoxOn = document.body.querySelector('.setting_box.on');
}
if (settingButton) {
if (!settingBoxOn) {
settingButton.click();
}
setTimeout(() => {
if (settingBoxOn) {
settingButton.click();
}
player.classList.remove('mouseover');
}, 1000); // 1초 후에 mouseover 클래스 제거
} else {
console.error('Setting button not found or not visible.');
}
};
// 비디오의 가장 최신 버퍼링 지점에서 2초 전으로 이동 (현재 지점보다 오래된 지점으로는 이동하지 않음)
const moveToLatestBufferedPoint = () => {
const video = document.querySelector('video');
const buffered = video.buffered;
if (buffered.length > 0) {
// 버퍼링된 구간의 마지막 시간
const bufferedEnd = buffered.end(buffered.length - 1);
const targetTime = bufferedEnd - 2; // 2초 전으로 설정
// targetTime이 현재 시간보다 뒤에 있을 경우에만 이동
if (targetTime > video.currentTime) {
video.currentTime = targetTime;
}
}
}
const checkPlayerPageHeaderAd = () => {
waitForElement('#header_ad', function (elementSelector, element) {
element.remove();
})
}
const extractDateTime = (text) => {
const [dateStr, timeStr] = text.split(' '); // split 한 번으로 날짜와 시간을 동시에 얻기
const dateTimeStr = `${dateStr}T${timeStr}Z`; // 문자열 템플릿 사용
return new Date(dateTimeStr);
}
const getElapsedTime = (broadcastStartTimeText, type) => {
const broadcastStartTime = extractDateTime(broadcastStartTimeText);
broadcastStartTime.setHours(broadcastStartTime.getHours() - 9);
const currentTime = new Date();
const timeDiff = currentTime - broadcastStartTime;
const secondsElapsed = Math.floor(timeDiff / 1000);
const hoursElapsed = Math.floor(secondsElapsed / 3600);
const minutesElapsed = Math.floor((secondsElapsed % 3600) / 60);
const remainingSeconds = secondsElapsed % 60;
let formattedTime = '';
if (type === "HH:MM:SS") {
formattedTime = `${String(hoursElapsed).padStart(2, '0')}:${String(minutesElapsed).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
} else if (type === "HH:MM") {
if (hoursElapsed > 0) {
formattedTime = `${String(hoursElapsed)}시간 `;
}
formattedTime += `${String(minutesElapsed)}분`;
}
return formattedTime;
}
// remainingBufferTime을 계산하여 리턴하는 함수
const getRemainingBufferTime = (video) => {
const buffered = video.buffered;
if (buffered.length > 0) {
// 마지막 버퍼의 끝과 현재 시간의 차이를 계산
const remainingBufferTime = buffered.end(buffered.length - 1) - video.currentTime;
// 0초 또는 정수일 경우 소수점 한 자리로 반환
return remainingBufferTime >= 0
? remainingBufferTime.toFixed(remainingBufferTime % 1 === 0 ? 0 : 1)
: '';
}
return ''; // 버퍼가 없으면 빈 문자열 반환
};
// remainingBufferTime 값을 삽입하는 함수
const insertRemainingBuffer = (element) => {
const video = element;
const emptyChat = document.body.querySelector('#empty_chat');
// video의 onprogress 이벤트 핸들러
video.onprogress = () => {
const remainingBufferTime = getRemainingBufferTime(video); // remainingBufferTime 계산
if (emptyChat && remainingBufferTime !== '') {
emptyChat.innerText = `${remainingBufferTime}s 지연됨`;
}
};
};
let timerId_m;
const handleMuteByVisibility = () => {
const button = document.body.querySelector("#btn_sound");
if (document.hidden) {
// 탭이 비활성화됨
timerId_m = setTimeout(() => {
if (!button.classList.contains("mute")) {
button.click();
// console.log("탭이 비활성화됨, 음소거");
}
}, 1000);
} else {
// 탭이 활성화됨
if (timerId_m) {
clearTimeout(timerId_m);
}
if (button.classList.contains("mute")) {
button.click();
// console.log("탭이 활성화됨, 음소거 해제");
}
}
}
const isVideoInPiPMode = () => {
const videoElement = document.body.querySelector('video');
return videoElement && document.pictureInPictureElement === videoElement;
}
const registerVisibilityChangeHandler = () => {
document.addEventListener('visibilitychange', () => {
if (!isVideoInPiPMode() && isAutoChangeMuteEnabled) {
handleMuteByVisibility();
}
}, true);
}
const appendPauseButton = () => {
try {
const checkInterval = 250;
let elapsedTime = 0;
let intervalId;
let buttonCreated = false; // 버튼 생성 여부 체크 변수 추가
const checkLiveViewStatus = () => {
const closeStreamButton = document.body.querySelector("#closeStream");
const playerDiv = document.body.querySelector("#player");
const isPlayerPresent = !!playerDiv; // null 체크를 간단히
const isMouseoverClass = isPlayerPresent && playerDiv.classList.contains("mouseover");
const isTimeover = isPlayerPresent && (elapsedTime > 30);
if (closeStreamButton) {
// 버튼이 이미 존재하면 체크
if (!isMouseoverClass && !isTimeover) {
// 마우스 오버 상태가 아니고 시간 초과가 아닐 때 버튼 제거
closeStreamButton.remove();
buttonCreated = false; // 버튼이 제거됨
}
} else if ((!closeStreamButton && isMouseoverClass) || isTimeover) {
// 버튼이 없고 마우스 오버 상태이거나 시간 초과일 때만 생성
if (!buttonCreated) {
createCloseStreamButton();
buttonCreated = true; // 버튼이 생성됨
}
}
elapsedTime += checkInterval / 1000; // 초 단위로 변환하여 증가
};
const createCloseStreamButton = () => {
waitForElement('button#time_shift_play', (elementSelector, element) => {
if (window.getComputedStyle(element).display === 'none') { // Time Shift 기능이 비활성화된 경우
const ctrlDiv = document.body.querySelector('div.ctrl');
const newCloseStreamButton = document.createElement("button");
newCloseStreamButton.type = "button";
newCloseStreamButton.id = "closeStream";
newCloseStreamButton.className = "pause on";
const tooltipDiv = document.createElement("div");
tooltipDiv.className = "tooltip";
const spanElement = document.createElement("span");
spanElement.textContent = "일시정지";
tooltipDiv.appendChild(spanElement);
newCloseStreamButton.appendChild(tooltipDiv);
ctrlDiv.insertBefore(newCloseStreamButton, ctrlDiv.firstChild);
newCloseStreamButton.addEventListener("click", (e) => {
e.preventDefault();
toggleStream(newCloseStreamButton, spanElement);
});
}
});
};
const toggleStream = (button, spanElement) => {
try {
if (button.classList.contains("on")) {
livePlayer.closeStreamConnector();
button.classList.remove("on", "pause");
button.classList.add("off", "play");
spanElement.textContent = "재생";
} else {
livePlayer._startBroad();
button.classList.remove("off", "play");
button.classList.add("on", "pause");
spanElement.textContent = "일시정지";
}
} catch (error) {
console.log(error);
}
};
// setInterval을 사용해 일정 간격으로 체크
intervalId = setInterval(checkLiveViewStatus, checkInterval);
} catch (error) {
console.error(error);
}
};
const detectPlayerChangeAndAppendPauseButton = () => {
const updateBjIdIfMismatch = () => {
const currentUrl = window.location.href;
const urlBjId = currentUrl.split('/')[3];
const infoNickName = document.querySelector('#infoNickName');
const dataBjId = infoNickName.getAttribute('data-bj_id');
const streamerNick = document.querySelector('#streamerNick');
if (dataBjId !== urlBjId) {
infoNickName.setAttribute('data-bj_id', urlBjId);
streamerNick.setAttribute('data-bj_id', urlBjId);
}
};
const handleMutations = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
appendPauseButton();
emptyViewStreamer();
updateBjIdIfMismatch();
}
}
};
appendPauseButton();
const targetNode = document.body.querySelector('#infoNickName');
const observer = new MutationObserver(handleMutations);
observer.observe(targetNode, { childList: true, subtree: true });
}
const emptyViewStreamer = () => {
const viewStreamer = document.getElementById('view_streamer');
if (viewStreamer) {
viewStreamer.innerHTML = '';
}
}
const setWidthNickname = (wpx) => {
if (typeof wpx === 'number' && wpx > 0) { // wpx가 유효한 값인지 확인
GM_addStyle(`
.starting-line .chatting-list-item .message-container .username {
width: ${wpx}px !important;
}
`);
} else {
console.warn('Invalid width value provided for setWidthNickname.'); // 유효하지 않은 값 경고
}
}
const hideBadges = () => {
const badgeSettings = [
{ key: 'isHideSupporterBadgeEnabled', className: 'support' },
{ key: 'isHideFanBadgeEnabled', className: 'fan' },
{ key: 'isHideSubBadgeEnabled', className: 'sub' },
{ key: 'isHideVIPBadgeEnabled', className: 'vip' },
{ key: 'isHideManagerBadgeEnabled', className: 'manager' },
{ key: 'isHideStreamerBadgeEnabled', className: 'streamer' }
];
// 각 배지 숨김 설정 값 가져오기
const settings = badgeSettings.map(setting => ({
key: setting.key,
enabled: GM_getValue(setting.key),
className: setting.className
}));
// 모든 배지 숨김 설정이 비활성화된 경우 종료
if (!settings.some(setting => setting.enabled)) {
return;
}
// 배지 숨기기
const elements = document.querySelectorAll('[class^="grade-badge-"]:not(.done)');
elements.forEach(element => {
const className = element.className.split("grade-badge-")[1].split(" ")[0];
const setting = settings.find(s => s.className === className);
if (setting && setting.enabled) {
element.remove();
} else {
element.classList.add('done');
}
});
// 서브 배지 숨기기
if (settings.find(s => s.className === 'sub' && s.enabled)) {
const thumbSpanSelector = CURRENT_URL.startsWith("https://play.sooplive.co.kr/")
? '#chat_area div.username > button > span.thumb'
: '#chatMemo div.username > button > span.thumb';
const thumbSpans = document.querySelectorAll(thumbSpanSelector);
thumbSpans.forEach(span => span.remove());
}
}
const observeChat = (elementSelector,elem) => {
// 페이지 변경 시 이미지 감지 및 숨기기
const observer = new MutationObserver(function(mutations) {
for (const mutation of mutations) {
if(isBlockWordsEnabled) deleteMessages();
hideBadges();
}
});
const config = {
childList: true,
subtree: true
};
observer.observe(elem, config);
}
const deleteMessages = () => {
const messages = document.body.querySelectorAll('div.message-text > p.msg:not(.done)');
const rw = registeredWords ? registeredWords.split(',') : [];
for (const message of messages) {
const messageText = message.textContent.trim();
const emoticons = message.querySelectorAll('img.emoticon');
const shouldRemove = rw.some(word => {
const trimmedWord = word.trim();
// 공백인 경우를 처리
if (trimmedWord.length === 0) {
return false; // 빈 문자열인 경우 false 반환
}
const wordToCheck = word.trim().startsWith("e:") ? word.trim().slice(2) : word.trim();
return (word.trim().startsWith("e:") && messageText === wordToCheck) ||
(!word.trim().startsWith("e:") && messageText.includes(wordToCheck));
});
if (shouldRemove) {
message.closest('.chatting-list-item').classList.add('filtered-message');
message.closest('.chatting-list-item').remove();
} else {
message.classList.add('done');
}
}
}
const autoClaimGem = () => {
const element = document.querySelector('#actionbox > div.ic_gem');
// 요소가 존재하고, display 속성이 'none'이 아닌 경우 클릭
if (element && getComputedStyle(element).display !== 'none') {
element.click();
}
}
// 비디오 재생 건너뛰기 및 입력란 확인 함수
const videoSkipHandler = (e) => {
const activeElement = document.activeElement;
const tagName = activeElement.tagName.toLowerCase();
// 입력란 활성화 여부 체크
const isInputActive = (tagName === 'input') ||
(tagName === 'textarea') ||
(activeElement.id === 'write_area') ||
(activeElement.contentEditable === 'true');
// 입력란이 활성화되어 있지 않은 경우 비디오 제어
if (!isInputActive) {
const video = document.querySelector('video');
if (video) {
switch (e.code) {
case 'ArrowRight':
// 오른쪽 방향키: 동영상을 1초 앞으로 이동
video.currentTime += 1;
break;
case 'ArrowLeft':
// 왼쪽 방향키: 동영상을 1초 뒤로 이동
video.currentTime -= 1;
break;
}
}
}
}
const homePageCurrentTab = () => {
waitForElement('#logo > a', function (elementSelector, element) {
element.removeAttribute("target");
});
}
const useBottomChat = () => {
// 해상도에 따라 bottomChat 클래스를 추가하거나 제거하는 함수
const toggleBottomChat = () => {
const isPortrait = window.innerHeight + 50 > window.innerWidth;
document.body.classList.toggle('bottomChat', window.innerWidth <= 1350 && isPortrait);
};
// debounce 함수 정의
const debounce = (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
// 윈도우 리사이즈 이벤트를 감지하여 toggleBottomChat 함수를 호출합니다.
window.addEventListener('resize', debounce(toggleBottomChat, 100));
// 페이지 로드 시 한 번 실행하여 초기 설정을 수행합니다.
toggleBottomChat();
};
// #nAllViewer의 숫자를 변환하여 리턴하는 함수
const getViewersNumber = (raw = false) => {
const element = document.querySelector('#nAllViewer');
if (!element) return '0';
// 요소의 텍스트에서 쉼표 제거 후 숫자 처리
const rawNumber = element.innerText.replace(/,/g, '').trim();
// raw가 truthy한 값이면 (1, true 등) 숫자를 변환하지 않고 원본 그대로 반환
if (Boolean(raw)) {
return rawNumber;
}
return addNumberSeparator(rawNumber);
};
let previousViewers = 0; // 이전 시청자 수를 저장할 변수
let previousTitle = ''; // 이전 제목을 저장할 변수
// 제목 표시줄을 업데이트하는 함수
const updateTitleWithViewers = () => {
const originalTitle = document.title.split(' ')[0]; // 기존 제목의 첫 번째 단어
const viewers = getViewersNumber(true); // 현재 시청자 수 갱신
const formattedViewers = addNumberSeparatorAll(viewers); // 형식화된 시청자 수
let title = originalTitle;
if (originalTitle !== previousTitle) {
previousViewers = 0; // 제목이 변경되면 이전 시청자 수 초기화
}
if (viewers && previousViewers) {
if (viewers > previousViewers) {
title += ` 📈${formattedViewers}`;
} else if (viewers < previousViewers) {
title += ` 📉${formattedViewers}`;
} else {
title += ` 📺${formattedViewers}`; // 시청자 수가 변동 없을 때
}
} else {
title += ` 📺${formattedViewers}`; // 시청자 수가 변동 없을 때
}
document.title = title; // 제목을 업데이트
previousViewers = viewers; // 이전 시청자 수 업데이트
previousTitle = originalTitle; // 현재 제목을 이전 제목으로 업데이트
};
const unlockCopyPaste = () => {
const writeArea = document.getElementById('write_area');
// 복사 기능
const handleCopy = (event) => {
event.preventDefault(); // 기본 복사 동작 막기
const selectedText = window.getSelection().toString(); // 선택된 텍스트 가져오기
if (selectedText) {
event.clipboardData.setData('text/plain', selectedText); // 클립보드에 텍스트 쓰기
}
};
// 잘라내기 기능
const handleCut = (event) => {
event.preventDefault(); // 기본 잘라내기 동작 막기
const selectedText = window.getSelection().toString(); // 선택된 텍스트 가져오기
if (selectedText) {
event.clipboardData.setData('text/plain', selectedText); // 클립보드에 텍스트 쓰기
document.execCommand("delete"); // 선택된 텍스트 삭제
}
};
// 붙여넣기 기능
const handlePaste = (event) => {
event.preventDefault(); // 기본 붙여넣기 동작 막기
const text = (event.clipboardData || window.clipboardData).getData('text'); // 클립보드에서 텍스트 가져오기
document.execCommand("insertText", false, text); // 텍스트를 수동으로 삽입
};
// 이벤트 리스너 등록
writeArea.addEventListener('copy', handleCopy);
writeArea.addEventListener('cut', handleCut);
writeArea.addEventListener('paste', handlePaste);
}
const alignNicknameRight = () => {
GM_addStyle(`
.starting-line .chatting-list-item .message-container .username > button {
float: right !important;
}
`);
}
//=================================플레이어 페이지 함수 끝=================================//
//============================ VOD 페이지 함수 시작 ============================//
// 미디어 정보 확인 함수
const checkMediaInfo = async (mediaName) => {
if (mediaName !== 'original') { // 원본 화질로 설정되지 않은 경우
const player = await waitForElementAsync('#player');
player.className = 'video mouseover ctrl_output';
// 설정 버튼 클릭
const settingButton = await waitForElementAsync('#player > div.player_ctrlBox > div.ctrlBox > div.right_ctrl .setting_box > button.btn_setting');
settingButton.click();
// 화질 변경 리스트 대기
const settingList = await waitForElementAsync('#player > div.player_ctrlBox > div.ctrlBox > div.right_ctrl .setting_box.on .setting_list');
const spanElement = Array.from(settingList.querySelectorAll('span')).find(el => el.textContent.includes("화질 변경"));
const buttonElement = spanElement.closest('button');
buttonElement.click();
// 두 번째 설정 대기
const resolutionButton = await waitForElementAsync('#player > div.player_ctrlBox > div.ctrlBox > div.right_ctrl .setting_box .setting_list_subLayer ul > li:nth-child(2) > button');
resolutionButton.click();
resolutionButton.className = 'video';
}
};
//============================ VOD 페이지 함수 끝 ============================//
//============================ 메인 페이지 실행 ============================//
if (CURRENT_URL.startsWith("https://www.sooplive.co.kr")) {
if (isPreviewModalEnabled || isReplaceEmptyThumbnailEnabled) {
loadHlsScript();
}
if (isCustomSidebarEnabled) document.body.classList.add('customSidebar');
if (isSharpeningEnabled) document.body.classList.add('sharpening');
GM_addStyle(mainPageCommonStyles);
observeDarkAttributeChange((darkValue) => {
if(darkValue){
GM_addStyle(mainPageDarkmodeStyles);
} else {
GM_addStyle(mainPageWhitemodeStyles);
}
});
waitForElement('#serviceLnb', function (elementSelector, element) {
if (isCustomSidebarEnabled) makeTopNavbarAndSidebar("main");
runCommonFunctions();
});
removeUnwantedTags();
processStreamers();
}
//============================ 플레이어 페이지 실행 ============================//
if (CURRENT_URL.startsWith("https://play.sooplive.co.kr")) {
// Embed 페이지에서는 실행하지 않음
const pattern = /^https:\/\/play.sooplive.co.kr\/.*\/.*\/embed(\?.*)?$/;
if (pattern.test(CURRENT_URL) || CURRENT_URL.includes("vtype=chat")) {
return;
}
if (isCustomSidebarEnabled) document.body.classList.add('customSidebar');
if (isSharpeningEnabled) document.body.classList.add('sharpening');
GM_addStyle(playerCommonStyles);
observeDarkAttributeChange((darkValue) => {
if(darkValue){
GM_addStyle(darkModePlayerStyles);
} else {
GM_addStyle(whiteModePlayerStyles);
}
});
if (isCustomSidebarEnabled) {
makeTopNavbarAndSidebar("player");
insertFoldButton();
detectScreenMode();
detectFullscreenmode();
if(showSidebarOnScreenMode) showSidebarOnMouseOver();
}
if(isBottomChatEnabled) useBottomChat();
if(isMakePauseButtonEnabled) detectPlayerChangeAndAppendPauseButton();
if(isMakeSharpModeShortcutEnabled) toggleSharpModeShortcut();
if(isMakeLowLatencyShortcutEnabled) toggleLowLatencyShortcut();
if(isRemainingBufferTimeEnabled){
waitForElement('#livePlayer', function (elementSelector, element) {
insertRemainingBuffer(element);
});
}
if(isAutoClaimGemEnabled){
setInterval(autoClaimGem, 30000);
}
if(isVideoSkipHandlerEnabled){
waitForElement('#livePlayer', function (elementSelector, element) {
window.addEventListener('keydown', videoSkipHandler);
});
}
registerVisibilityChangeHandler();
checkPlayerPageHeaderAd();
if(!isOpenNewtabEnabled){
homePageCurrentTab();
}
if(isDocumentTitleUpdateEnabled){
setTimeout(updateTitleWithViewers, 10000);
setInterval(updateTitleWithViewers, 60000);
}
runCommonFunctions();
// LIVE 채팅창
waitForElement('#chat_area', function (elementSelector, element) {
observeChat(elementSelector,element);
});
if (isUnlockCopyPasteEnabled) {
waitForElement('#write_area', function (elementSelector, element) {
unlockCopyPaste();
});
}
if (isAlignNicknameRightEnabled) {
alignNicknameRight();
}
if (isAutoScreenModeEnabled) {
waitForElement('#livePlayer', function (elementSelector, element) {
if (!document.body.classList.contains('screen_mode')) {
document.body.querySelector('#player .btn_screen_mode').click();
}
});
}
}
//============================ VOD 페이지 실행 ============================//
if (CURRENT_URL.startsWith("https://vod.sooplive.co.kr/player/")) {
const isBaseUrl = (url) => /https:\/\/vod\.sooplive\.co\.kr\/player\/\d+/.test(url) && !isCatchUrl(url);
const isCatchUrl = (url) => /https:\/\/vod\.sooplive\.co\.kr\/player\/\d+\/catch/.test(url) || /https:\/\/vod\.sooplive\.co\.kr\/player\/catch/.test(url);
// 다시보기 페이지
if (isBaseUrl(CURRENT_URL)) {
// vodCore 변수가 선언될 때까지 대기하는 함수
const waitForVodCore = () => {
const checkVodCore = setInterval(() => {
if (vodCore?.playerController?._currentMediaInfo?.name) {
clearInterval(checkVodCore); // setInterval 정지
checkMediaInfo(vodCore.playerController._currentMediaInfo.name); // vodCore 변수가 정의되면 미디어 정보 확인 함수 호출
}
}, 500); // 500ms 주기로 확인
};
if(isSelectBestQualityEnabled){
waitForVodCore();
}
GM_addStyle(CommonStyles);
// VOD 채팅창
waitForElement('#webplayer_contents', function (elementSelector, element) {
observeChat(elementSelector,element);
});
waitForElement('div.serviceUtil', function (elementSelector, element) {
addModalSettings();
manageRedDot();
});
if (isAlignNicknameRightEnabled) {
alignNicknameRight();
}
// 캐치 페이지
} else if (isCatchUrl(CURRENT_URL)) {
if (isCustomSidebarEnabled) document.body.classList.add('customSidebar');
if (isSharpeningEnabled) document.body.classList.add('sharpening');
GM_addStyle(mainPageCommonStyles);
observeDarkAttributeChange((darkValue) => {
if(darkValue){
GM_addStyle(mainPageDarkmodeStyles);
} else {
GM_addStyle(mainPageWhitemodeStyles);
}
});
waitForElement('#serviceLnb', function (elementSelector, element) {
if (isCustomSidebarEnabled) makeTopNavbarAndSidebar("main");
runCommonFunctions();
});
}
}
})();