206 lines
7.1 KiB
JavaScript
206 lines
7.1 KiB
JavaScript
|
|
import { createRequire } from 'module';
|
||
|
|
const require = createRequire(import.meta.url);
|
||
|
|
import playwright from 'playwright-extra';
|
||
|
|
const { chromium } = playwright;
|
||
|
|
import fs from 'fs';
|
||
|
|
import path from 'path';
|
||
|
|
import AdmZip from 'adm-zip';
|
||
|
|
import { parse } from 'csv-parse/sync';
|
||
|
|
|
||
|
|
// JSON 파일에서 세션 상태를 불러오기
|
||
|
|
const loadSession = async (filePath) => {
|
||
|
|
const jsonData = fs.readFileSync(filePath, 'utf-8');
|
||
|
|
return JSON.parse(jsonData);
|
||
|
|
};
|
||
|
|
|
||
|
|
function ensureDirExists(dirPath) {
|
||
|
|
if (!fs.existsSync(dirPath)) {
|
||
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function ensureChecked(dlg,page, label) {
|
||
|
|
const cb = dlg.getByRole('checkbox', { name: label, exact: true });
|
||
|
|
await cb.scrollIntoViewIfNeeded();
|
||
|
|
|
||
|
|
let state = await cb.getAttribute('aria-checked');
|
||
|
|
if (state !== 'true') {
|
||
|
|
await cb.click({ force: true });
|
||
|
|
// 재확인 루프
|
||
|
|
for (let i = 0; i < 5; i++) {
|
||
|
|
await page.waitForTimeout(100);
|
||
|
|
state = await cb.getAttribute('aria-checked');
|
||
|
|
if (state === 'true') break;
|
||
|
|
}
|
||
|
|
if (state !== 'true') throw new Error(`체크 실패: ${label}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
const loginWithJson = async () => {
|
||
|
|
const optionsBrowser = {
|
||
|
|
headless: false,
|
||
|
|
args: [
|
||
|
|
'--disable-blink-features=AutomationControlled',
|
||
|
|
'--no-sandbox',
|
||
|
|
'--disable-web-security',
|
||
|
|
'--disable-infobars',
|
||
|
|
'--disable-extensions',
|
||
|
|
'--start-maximized',
|
||
|
|
'--window-size=1280,720',
|
||
|
|
],
|
||
|
|
};
|
||
|
|
|
||
|
|
const browser = await chromium.launch(optionsBrowser);
|
||
|
|
|
||
|
|
const sessionData = await loadSession('everlogin.json');
|
||
|
|
|
||
|
|
const optionsContext = {
|
||
|
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
|
||
|
|
locale: 'ko-KR',
|
||
|
|
viewport: { width: 1280, height: 720 },
|
||
|
|
deviceScaleFactor: 1,
|
||
|
|
acceptDownloads: true,
|
||
|
|
storageState: sessionData,
|
||
|
|
extraHTTPHeaders: {
|
||
|
|
'sec-ch-ua': '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
|
||
|
|
'sec-ch-ua-arch': '"arm"',
|
||
|
|
'sec-ch-ua-bitness': '"64"',
|
||
|
|
'sec-ch-ua-form-factors': '"Desktop"',
|
||
|
|
'sec-ch-ua-full-version': '"139.0.7258.154"',
|
||
|
|
'sec-ch-ua-full-version-list': '"Not;A=Brand";v="99.0.0.0", "Google Chrome";v="139.0.7258.154", "Chromium";v="139.0.7258.154"',
|
||
|
|
'sec-ch-ua-mobile': '?0',
|
||
|
|
'sec-ch-ua-model': '""',
|
||
|
|
'sec-ch-ua-platform': '"macOS"',
|
||
|
|
'sec-ch-ua-platform-version': '"15.6.1"',
|
||
|
|
'sec-ch-ua-wow64': '?0',
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const context = await browser.newContext(optionsContext);
|
||
|
|
const page = await context.newPage();
|
||
|
|
|
||
|
|
await page.goto('https://studio.youtube.com/');
|
||
|
|
|
||
|
|
await page.locator('ytcp-navigation-drawer').getByRole('button', { name: '분석', exact: true }).click();
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
await page.getByRole('link', { name: '고급 모드', exact: true }).click();
|
||
|
|
|
||
|
|
|
||
|
|
//await page.locator('yta-explore-date-picker-dropdown ytcp-dropdown-trigger[role="button"]').click();
|
||
|
|
|
||
|
|
// 1) id=picker-trigger 컨테이너 내의 드롭다운 버튼
|
||
|
|
await page.locator('yta-time-picker #picker-trigger ytcp-dropdown-trigger[role="button"]').click();
|
||
|
|
|
||
|
|
await page.locator('tp-yt-paper-item[test-id="week"]').click();
|
||
|
|
|
||
|
|
await page.locator('yta-time-picker #picker-trigger ytcp-dropdown-trigger[role="button"]').click();
|
||
|
|
|
||
|
|
await page.locator('tp-yt-paper-item[test-id="fixed"]').click();
|
||
|
|
|
||
|
|
const caldlg = page.locator('tp-yt-paper-dialog:has(ytcp-date-period-picker)');
|
||
|
|
await caldlg.waitFor({ state: 'visible' });
|
||
|
|
|
||
|
|
// 끝날 값 읽기
|
||
|
|
const endInput = caldlg.locator('#end-date input');
|
||
|
|
await endInput.waitFor({ state: 'visible' });
|
||
|
|
const endVal = await endInput.inputValue();
|
||
|
|
|
||
|
|
// 시작날을 끝날과 동일하게 설정
|
||
|
|
const startInput = caldlg.locator('#start-date input');
|
||
|
|
await startInput.fill(endVal);
|
||
|
|
|
||
|
|
// 적용 클릭 (활성 버튼만)
|
||
|
|
await caldlg.locator('#apply-button[aria-disabled="false"] button').click();
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
// title="측정항목" 이 붙은 드롭다운 버튼
|
||
|
|
await page.locator('yta-explore-column-picker-dropdown[title="측정항목"] ytcp-dropdown-trigger').click();
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
const dlg = page.getByRole('dialog', { name: '측정항목' });
|
||
|
|
|
||
|
|
|
||
|
|
await page.locator('h2.picker-text', { hasText: 'Premium' }).click();
|
||
|
|
|
||
|
|
|
||
|
|
// 1) 전체 선택 해제
|
||
|
|
await dlg.getByRole('button', { name: '전체 선택 해제' }).click();
|
||
|
|
// 3) 필요한 항목들 체크
|
||
|
|
await ensureChecked(dlg,page, '조회수'); // id=EXTERNAL_VIEWS
|
||
|
|
await ensureChecked(dlg,page, '유효 조회수'); // id=ENGAGED_VIEWS
|
||
|
|
await ensureChecked(dlg,page,'시청 시간(단위: 시간)'); // id=EXTERNAL_WATCH_TIME
|
||
|
|
await ensureChecked(dlg,page,'YouTube Premium 조회수'); // id=EXTERNAL_YOUTUBE_RED_VIEWS
|
||
|
|
|
||
|
|
// 4) 적용
|
||
|
|
await dlg.getByRole('button', { name: '적용' }).click();
|
||
|
|
|
||
|
|
const downloadDir = path.resolve(process.cwd(), 'downloads');
|
||
|
|
ensureDirExists(downloadDir);
|
||
|
|
|
||
|
|
const [download] = await Promise.all([
|
||
|
|
page.waitForEvent('download'),
|
||
|
|
(async () => {
|
||
|
|
await page.locator('ytcp-icon-button#export-button, ytcp-icon-button[aria-label="현재 화면 내보내기"]').click();
|
||
|
|
await page.locator('tp-yt-paper-item[test-id="CSV"]').click();
|
||
|
|
})()
|
||
|
|
]);
|
||
|
|
|
||
|
|
const suggested = download.suggestedFilename();
|
||
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
|
|
const zipPath = path.join(downloadDir, `${timestamp}-${suggested || 'export.zip'}`);
|
||
|
|
await download.saveAs(zipPath);
|
||
|
|
console.log(`ZIP 저장: ${zipPath}`);
|
||
|
|
|
||
|
|
const unzipDir = path.join(downloadDir, `${timestamp}-unzipped`);
|
||
|
|
ensureDirExists(unzipDir);
|
||
|
|
const zip = new AdmZip(zipPath);
|
||
|
|
zip.extractAllTo(unzipDir, true);
|
||
|
|
console.log(`압축 해제: ${unzipDir}`);
|
||
|
|
|
||
|
|
const files = fs.readdirSync(unzipDir).filter(f => f.toLowerCase().endsWith('.csv'));
|
||
|
|
if (files.length === 0) {
|
||
|
|
throw new Error('압축 해제 폴더에 CSV가 없습니다.');
|
||
|
|
}
|
||
|
|
const targetCsvPattern = /표 데이터\.csv$/; // '표 데이터.csv' 우선 선택
|
||
|
|
const targetFile = targetCsvPattern ? (files.find(f => targetCsvPattern.test(f)) || files[0]) : files[0];
|
||
|
|
const targetPath = path.join(unzipDir, targetFile);
|
||
|
|
console.log(`선택된 CSV: ${targetPath}`);
|
||
|
|
|
||
|
|
const csvContent = fs.readFileSync(targetPath, 'utf-8');
|
||
|
|
const records = parse(csvContent, {
|
||
|
|
columns: true,
|
||
|
|
skip_empty_lines: true,
|
||
|
|
bom: true,
|
||
|
|
relax_column_count: true,
|
||
|
|
trim: true,
|
||
|
|
});
|
||
|
|
console.log(JSON.stringify(records, null, 2));
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
const waitAfterLoginMs = 60_000;
|
||
|
|
console.log(`로그인 후 ${waitAfterLoginMs / 1000}초 대기합니다...`);
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, waitAfterLoginMs));
|
||
|
|
|
||
|
|
|
||
|
|
try {
|
||
|
|
await browser.close();
|
||
|
|
} catch (e) {
|
||
|
|
console.log('브라우저가 이미 종료되어 close를 건너뜁니다.');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
loginWithJson();
|