휘난새
The gates to paradise are locked. It became private property. But we can go round the back and hop the fence.
왓챠피디아 데이터 추출 및 레터박스 옮기기 (최신)
2026.03.30

원본 코드:

https://github.com/erinyskim/watchapedia-export (kimdonhyun/erinyskim)

 

GitHub - erinyskim/watchapedia-export

Contribute to erinyskim/watchapedia-export development by creating an account on GitHub.

github.com

현재 코드가 변경되어 실행 불가합니다. 저는 야매로 수정해서 썼습니다.

 

변경 사항:

  1. 헤더: x-watcha-* → x-frograms-*
  2. 엔드포인트 경로: /api/users/{id}/ratings/movie (기존: /contents/movies/ratings)
  3. (26.04.26. 패치) 재시도 로직 수정: 평가 개수가 많을수록 시간이 오래 걸릴 수 있습니다. 콘솔에 "429 발생, 10초 후 재시도..." 메시지가 떠도 정상 진행 중이니 창을 닫지 말고 기다려 주세요.

 

수정 코드:

!async function(){
  const userId = document.querySelector('a[href*="/users/"]').href.replace(/^.*\/users\/(.*)$/, "$1"),
  deviceId = document.cookie.split('; ').find(r => r.startsWith('_c_pdi='))?.split('=')[1] || '',
  sleep = ms => new Promise(res => setTimeout(res, ms)),
  request = async (url) => {
    while (true) {
      await sleep(500);
      const res = await fetch(`https://pedia.watcha.com${url}`, {
        credentials: "same-origin",
        headers: {
          "accept": "application/vnd.frograms+json;version=2.1.0",
          "x-frograms-app-code": "Galaxy",
          "x-frograms-client": "Galaxy-Web-App",
          "x-frograms-client-version": "2.1.0",
          "x-frograms-version": "2.1.0",
          "x-frograms-device-identifier": deviceId,
          "x-frograms-galaxy-language": "ko",
          "x-frograms-galaxy-region": "KR"
        }
      });
      if (res.status === 429) {
        console.log(`429 발생, 10초 후 재시도...`);
        await sleep(10000);
        continue;
      }
      const json = await res.json();
      if (!json.result) {
        console.log(`result 없음, 5초 후 재시도...`, json);
        await sleep(5000);
        continue;
      }
      return json;
    }
  },
  fetchAll = async (url, onProgress) => {
    const items = [];
    let next = url;
    while (next) {
      const json = await request(next);
      items.push(...json.result.result);
      onProgress && onProgress(json.result.result.length);
      next = json.result.next_uri;
    }
    return items;
  },
  merge = (ratings, comments, type) => ratings.map(e => {
    const c = comments.find(c => c.content_code === e.content.code),
    code = e.content.code,
    watchedAt = (() => {
      const d = new Date(c?.watched_at || e.user_content_action.watched_at);
      return isNaN(d) ? '' : `${d.getFullYear()}-${`0${d.getMonth()+1}`.slice(-2)}-${`0${d.getDate()}`.slice(-2)}`;
    })(),
    directors = (e.content.director_names || c?.content?.director_names || []).join(),
    review = c?.text?.trim().replace(/"/g, '""') || '';
    return [
      code,
      `https://pedia.watcha.com/ko-KR/contents/${code}`,
      e.content.title.replace(/"/g, ""),
      type,
      e.content.year,
      directors,
      watchedAt,
      +e.user_content_action.rating / 2,
      review,
      c?.spoiler ?? ''
    ];
  });

  await (async () => {
    const bar = Object.assign(document.createElement("div"), { style: "position:fixed;left:0;top:0;right:0;height:59px;z-index:60;display:flex;align-items:center;background-color:#fff" }),
    inner = Object.assign(document.createElement("div"), { style: "max-width:1320px;width:100%;margin:0 auto;padding:0 32px;" }),
    msg = Object.assign(document.createElement("p"), { style: "color:#7e7e7e;font-size:15px;letter-spacing:-0.3px;" }),
    pct = Object.assign(document.createElement("span"), { style: "font-weight:bold" });
    msg.innerText = "리뷰 다운로드 중입니다. ";
    msg.appendChild(pct);
    inner.appendChild(msg);
    bar.appendChild(inner);
    document.body.appendChild(bar);

    try {
      const totalCount = (await request(`/api/users/${userId}`)).result?.ratings_count || 0,
      counted = { n: 0 },
      progress = (n) => { counted.n += n; pct.innerText = totalCount ? `${(counted.n / totalCount * 100).toFixed(0)}%` : ''; },
      [mRatings, mComments, tRatings, tComments] = await Promise.all([
        fetchAll(`/api/users/${userId}/contents/movies/ratings`, progress),
        fetchAll(`/api/users/${userId}/contents/movies/comments`, progress),
        fetchAll(`/api/users/${userId}/contents/tv_seasons/ratings`, progress),
        fetchAll(`/api/users/${userId}/contents/tv_seasons/comments`, progress),
      ]),
      rows = [
        ...merge(mRatings, mComments, "MOVIE"),
        ...merge(tRatings, tComments, "TV")
      ],
      csv = `ID,URL,Title,Type,Year,Directors,WatchedAt,Rating,Review,Spoiler,\n` + rows.map(r => `"${r.join('","')}"`).join("\n"),
      blob = new Blob(["\ufeff" + csv], { type: "text/csv;charset=utf-8;" }),
      link = document.createElement("a");
      link.href = URL.createObjectURL(blob);
      link.download = `${userId}-watcha.csv`;
      link.click();
    } catch(err) { console.error(err) }
    bar.remove();
  })()
}();

 

사용 방법:

  1. 크롬에서 왓챠피디아에 로그인 (주소 최하단 ko-KR 환경)
  2. F12 혹은 ctrl+shift+i 로 개발자 도구 열기
  3. console 열고 위 코드 복붙 후 엔터 (*콘솔에서 최초 복붙 시 복붙 허용 커맨더 입력할 것)
  4. letterboxd에서 csv 파일 import
    1. https://letterboxd.com/import/
    2. hide successful match 토글을 눌러 등록되지 않은 데이터를 확인할 수 있음.
      1. 감독판 등 부가 정보가 포함되거나 한국어 제목이 등록되지 않은 경우: 원제를 검색해서 match해 주세요.
      2. TV의 경우, letterboxd에서는 리미티드 시리즈만 취급하므로 없을 가능성이 높음.
    3. 최하단에서 import films까지 해야 등록됩니다.