이 레포가 무엇을 하는 물건인가.
당신이 VLC 같은 플레이어에 링크 하나(https://iptv-org.github.io/iptv/index.m3u)만 붙여넣으면, 전 세계 수천 개 무료 채널 목록이 펼쳐진다. iptv-org/iptv는 이 거대한 "방송 채널 주소록"을 만들고 유지하는 프로젝트다.
중요한 건 이게 단순한 텍스트 파일 묶음이 아니라는 점이다. 새 채널은 GitHub 이슈(제보) → 봇이 자동 수집 → 중복·DMCA·DB 검증 → 포맷 정리 → 공개 재생목록 생성 → GitHub Pages 발행이라는 컨베이어 벨트를 매일 자정 한 번씩 자동으로 돈다. 즉 "수만 개 데이터를 사람과 봇이 함께 굴리는 시스템"이 진짜 알맹이다.
iptv-org/iptv는 m3u 재생목록(playlist) 큐레이션 프로젝트다. 원천 데이터(채널 메타데이터)는 자매 레포 iptv-org/database에서, EPG(편성표)는 iptv-org/epg에서 가져온다. 이 레포가 책임지는 것은 "스트림 URL을 채널에 연결하고, 그 묶음을 국가·카테고리·언어·지역별 m3u로 발행하는 일"이다. 영상 자체는 저장하지 않으며, 라이선스도 데이터 공유에 어울리는 CC0(퍼블릭 도메인)다.
#EXTM3U로 시작하는 단순 텍스트 포맷으로, "이 채널 이름 = 이 스트림 주소"를 줄줄이 나열한 목록이다. 미디어 플레이어가 이 파일을 읽어 채널 메뉴를 만든다. iptv-org가 발행하는 결과물이 바로 이 m3u 파일들이다. (.m3u8은 UTF-8 인코딩 버전으로, HLS 스트리밍에서 흔히 쓴다.)트렌딩 이유 · 다른 IPTV 목록 대비 장점.
인터넷에는 "IPTV m3u 목록"이 수없이 떠돈다. 그런데 대부분은 관리되지 않는 죽은 링크 덩어리이거나, 출처가 불분명한 불법 스트림을 섞어둔 것이다. iptv-org/iptv가 12만 스타를 모은 이유는 단순 "링크가 많아서"가 아니라, 데이터를 유지·검증·발행하는 엔지니어링이 진짜 오픈소스 프로젝트처럼 돌아가기 때문이다.
| 비교 항목 | 흔한 m3u 목록 | iptv-org/iptv |
|---|---|---|
| 데이터 규모 | 수백~수천, 들쭉날쭉 | 약 15,250 스트림 · 국가별 324개 m3u |
| 갱신 방식 | 가끔 수동 커밋 | 매일 자정 봇이 자동 수집·발행(cron) |
| 품질 검증 | 거의 없음 | 중복·URL유효성·DB대조·DMCA/NSFW 차단목록 자동 검사 |
| 기여 절차 | 제각각 | 구조화된 이슈 템플릿 → 봇 자동 처리 → PR/CI 검증 |
| 분류 | 대충 섞임 | 국가·카테고리·언어·지역·도시 등 다축으로 자동 생성 |
| 법적 입장 | 모호 | 영상 미저장 · CC0 · 저작권 클레임 절차 명문화 |
| 생태계 | 단독 파일 | database·epg·api·sdk·awesome-iptv 레포 군집 |
스트림 URL은 수명이 짧다. 방송사가 CDN을 바꾸거나 서비스를 닫으면 링크가 죽는다. 수동 목록은 곧 절반이 재생 불가가 되고, 중복 링크가 쌓이며, 누군가 슬쩍 불법/저작권 침해/성인 스트림을 끼워 넣어도 걸러지지 않는다. 결국 "규모가 커질수록 손으로 관리가 불가능해지는" 전형적 데이터 부패에 빠진다.
iptv-org는 큐레이션의 반복·기계적 부분을 전부 자동화했다. 사람은 GitHub 이슈로 "이 채널 추가/수정/문제"만 구조화된 양식으로 제보하고, 봇이 그걸 파싱해 스트림에 반영한다. 그다음 린트(m3u 형식)·검증(중복/URL/DB대조/차단목록)·포맷·생성·발행이 CI에서 자동으로 돈다. "사람의 판단이 필요한 곳에만 사람을, 나머지는 봇에게" — 대규모 데이터 큐레이션의 모범 답안이다.
데이터 처리 엔진(스크립트)·발행(GitHub Pages)·자동화(CI) 각각.
이 레포에는 "서버"가 없다. 대신 한 번 실행하고 끝나는 CLI 스크립트 묶음이 데이터를 가공한다. 전부 TypeScript이고, tsx로 컴파일 없이 바로 실행한다. 명령은 commander로 정의되고 package.json의 playlist:* 스크립트로 호출된다.
| 요소 | 역할 |
|---|---|
| Node.js 22 + TypeScript | 전체 스크립트 런타임/언어. CI도 node 22 고정. |
tsx | TS를 빌드 없이 즉시 실행(tsx scripts/commands/...). |
commander | 각 명령의 인자·옵션 정의(예: 변경된 파일만 검증). |
iptv-playlist-parser | m3u 텍스트 → 스트림 객체로 파싱하는 핵심 라이브러리. |
@iptv-org/sdk | 자매 데이터(channels·blocklist·guides 등) 모델·로더. |
m3u-linter | m3u 형식 규칙 검사(태그 순서·필드 등). 설정은 m3u-linter.json. |
hls-parser / mpd-parser / mediainfo.js | 실제 스트림 재생 가능성·해상도 등 테스트(playlist:test). |
@octokit/* | GitHub API로 이슈·디스커션을 읽어 봇이 처리. |
axios / socks-proxy-agent | 스트림 요청 · 프록시 경유 테스트. |
@freearhey/core · es-toolkit | Collection·Dictionary·Logger 등 유틸 · 함수형 헬퍼. |
| 요소 | 역할 |
|---|---|
| 공개 m3u 파일 | playlist:generate가 만드는 결과물. 국가/카테고리/언어/지역/도시별로 분리. |
| GitHub Pages | iptv-org.github.io/iptv/...로 m3u를 정적 호스팅(gh-pages 브랜치). |
.api/streams.json | playlist:export 결과 → 자매 레포 iptv-org/api로 배포(JSON API). |
| PLAYLISTS.md | 각 m3u 링크와 채널 수를 담은 거대 표. readme:update가 자동 생성. |
| HTML 리포트 | report:create + core/htmlTable.ts — 테스트 결과 표. |
| 워크플로 | 역할 |
|---|---|
update.yml | ★ 핵심. 매일 cron: 0 0 * * * + 수동. 이슈 반영→검증→생성→발행 전체를 돌리고 봇 계정으로 커밋·푸시·Pages 배포. |
check.yml | PR이 올라오면 바뀐 streams 파일만 골라 lint+validate. 통과 못 하면 머지 불가. |
format.yml | 내부 재생목록을 표준 형식으로 일괄 정리 후 커밋. |
| 봇 토큰 | tibdex/github-app-token으로 GitHub App 토큰 발급 → iptv-bot[bot] 명의 커밋. |
| 로컬 CI 재현 | gh act(act:update 등)로 Actions를 로컬에서 그대로 돌려봄. |
0 0 * * *)
0 0 * * *은 "매일 0시 0분"을 뜻한다. GitHub Actions의 schedule 트리거에 이걸 걸면, 사람이 버튼을 누르지 않아도 매일 자정에 update 파이프라인이 스스로 돈다. 이것이 "매일 갱신"의 정체다.제보(이슈) → 봇 수집 → 검증 → 생성 → 발행, 데이터 큐레이션 파이프라인.
이 레포의 본질은 "한 방향으로 흐르는 데이터 컨베이어 벨트"다. 입력은 두 가지: ① 사람이 올린 GitHub 이슈(채널 추가/수정/신고), ② 자매 레포의 원천 데이터(channels·blocklist). 이걸 봇이 매일 빨아들여 내부 streams/*.m3u를 갱신하고, 검증을 통과하면 공개용 m3u를 새로 찍어 GitHub Pages와 API로 발행한다.
이 파이프라인의 심장은 validate.ts다. 단순히 "파일이 m3u 형식인가"를 넘어, 데이터의 의미적 정합성을 검사한다. 핵심 통찰은 오류(error)와 경고(warning)를 구분한다는 점이다 — error는 파이프라인을 멈추지만, warning은 흐름은 보내되 사람에게 알린다.
| 검사 | 판정 | 의미 |
|---|---|---|
| 중복 URL | error | 같은 스트림이 다른 파일에 이미 있으면 거부(어느 파일에 있는지까지 표시). |
| URL 유효성 | error | isURI()로 형식 검사. 깨진 주소 차단. |
| DB 미등록 채널 | warning | 채널 id가 database에 없으면 경고(데이터 정렬 유도). |
| DMCA 차단목록 | error | 저작권자 클레임이 있는 채널은 발행 거부. |
| NSFW 차단목록 | error | 2024년부터 성인 채널 배포 중단 정책을 코드로 집행. |
// 1) 모든 스트림을 url 키로 버퍼에 모아 "중복"을 감지
const buffer = new Dictionary<Stream>()
streams.forEach(s => { if (!selectedFiles.includes(s.filepath)) buffer.set(s.url, s) })
// 2) 검증 대상 스트림을 하나씩 게이트에 통과
if (buffer.has(stream.url)) log.add({ type:'error', ... }) // 중복
if (!isURI(stream.url)) log.add({ type:'error', ... }) // 잘못된 URL
if (!data.channelsKeyById.get(id)) log.add({ type:'warning', ... })// DB에 없음
// 3) 차단목록(블록리스트) 대조 — DMCA / NSFW
blocklistRecords.forEach(r => {
if (r.reason === 'dmca') log.add({ type:'error', ... })
if (r.reason === 'nsfw') log.add({ type:'error', ... })
})
여기서 배울 점: "검증은 별도 데이터 소스(database·blocklist)와의 대조"라는 발상이다. m3u 파일 자체만 보지 않고, 외부 진실원과 교차 검증해 정합성을 지킨다.
| 패턴 | iptv-org에서의 구현 |
|---|---|
| 단일 진실원 분리 | 채널 메타데이터=database, 스트림 링크=이 레포, 편성표=epg. 책임이 레포별로 쪼개짐. |
| 이슈 주도 워크플로 | 구조화된 이슈 템플릿(1_streams_add.yml 등)이 곧 데이터 입력 폼. 봇이 파싱. |
| 봇 자동화 (GitOps) | 모든 변경이 봇 명의 커밋으로 git에 남음 → 이력·되돌리기·감사 가능. |
| 생성기(Generator) 패턴 | 같은 스트림 데이터에서 14종 generator가 축(국가/언어/카테고리…)별 m3u를 찍어냄. |
| 린트 + 검증 분리 | 형식(lint)과 의미(validate)를 따로. CI는 둘 다 통과해야 머지. |
| 증분 검증 | PR에서는 바뀐 파일만 검사(git diff --name-only)해 CI 속도 확보. |
| 모델/표현 분리 | Stream/Playlist 모델 → m3u·JSON·HTML·README 표로 각각 직렬화. |
이 레포를 "전 세계에서 책을 기증받는 거대 공공 도서관"으로 생각하자. 시민(사람)은 정해진 기증 양식(이슈 템플릿)으로 책을 보낸다. 사서 로봇(봇)은 매일 밤 들어온 책을 받아 중복인지, 페이지가 멀쩡한지, 분실·금지 도서 목록(DMCA/NSFW)에 있는지 검사한다. 통과한 책만 서가에 꽂고(streams 갱신), 그걸 다시 주제·언어·국가별 추천 목록(공개 m3u)으로 묶어 출입구(GitHub Pages)에 비치한다. 사람은 "판단"만, 로봇은 "반복 검수·정리"를 맡는 분업이 핵심이다.
데이터(streams)·엔진(scripts)·자동화(.github)의 3구역.
① README.md + .github/ISSUE_TEMPLATE/로 "데이터가 어떻게 들어오나" → ② scripts/commands/playlist/update.ts로 "이슈→데이터 반영" → ③ validate.ts로 "어떻게 검증하나" → ④ generators/ 한두 개로 "어떻게 m3u를 찍나" → ⑤ .github/workflows/update.yml로 "전체를 어떻게 매일 자동으로 묶나". 이 다섯 곳에 큐레이션 파이프라인의 전부가 있다.
대규모 데이터 큐레이션 · CI 자동화에서 무엇을 배우나.
가장 큰 교훈은 기여의 진입장벽을 코드가 아니라 "양식"으로 낮춘 것이다. 기여자는 TypeScript를 몰라도 이슈 템플릿만 채우면 된다. 봇이 그걸 파싱해 데이터에 반영하고, 검증·정리·발행은 전부 자동이다. 커뮤니티 데이터 프로젝트를 키우는 핵심 설계다.
.github/ISSUE_TEMPLATE/*.yml)을 하나 만들어, 비개발자도 구조화된 데이터를 제출하게 해 보라.데이터가 깨지지 않게 지키는 비결은 "검증을 머지 조건으로 만드는 것"이다. check.yml은 PR에서 바뀐 파일만 골라 lint+validate를 돌리고, 실패하면 머지를 막는다. 사람의 선의에 기대지 않고 품질을 자동으로 집행한다. error/warning 구분, 외부 진실원(database·blocklist)과의 교차 검증도 그대로 배울 패턴이다.
update.yml의 cron: 0 0 * * * + GitHub App 토큰 + 봇 명의 커밋 조합은 "사람이 손대지 않아도 매일 스스로 갱신·발행되는 저장소"를 만든다. 모든 변경이 git 이력에 남아 추적·되돌리기가 된다(GitOps). 이건 데이터뿐 아니라 가격·환율·통계 같은 어떤 주기적 데이터에도 그대로 적용된다.
같은 Stream 데이터에서 14종 generator가 국가·언어·카테고리·지역·도시별 m3u를, 또 export가 JSON을, readme:update가 마크다운 표를 찍어낸다. "데이터 모델과 출력 형식의 분리"가 명확해서, 새 분류 축을 추가하려면 generator 하나만 더 만들면 된다.
쓰는 사람 vs 개발/기여하는 사람으로 나뉜다.
| 대상 | 요구사항 |
|---|---|
| 단순 시청자 | 아무것도 설치 안 함. VLC 등 플레이어에 m3u 링크 붙여넣기가 전부. |
| 로컬 개발/기여 | Node.js 22 + npm. npm install(postinstall이 자매 데이터 자동 다운로드). |
| 주요 명령 | npm run playlist:validate / :generate / :format / :test. |
| CI 로컬 재현 | gh(GitHub CLI) + act. npm run act:update로 워크플로 로컬 실행. |
| 봇 운영(메인테이너) | GitHub App(APP_ID·PRIVATE_KEY 시크릿) — 봇 명의 커밋·Pages 배포용. |
| 네트워크 | 스트림 테스트(playlist:test) 시 외부 접속 + 선택적 프록시(socks). |
README가 명시하듯 이 저장소에는 영상 파일이 전혀 없다. 사용자 제보로 모인, 저작권자가 의도적으로 공개한 것으로 보이는 공개 스트림 URL만 담는다. 그럼에도 특정 링크가 저작권을 침해한다면 전용 이슈 템플릿(6_copyright-claim.yml)으로 제거를 요청할 수 있다.
다만 링크를 목록에서 지운다고 그 콘텐츠가 웹에서 사라지지는 않는다(링크 제공은 복제가 아니므로 그 자체가 저작권 직접 침해는 아니라는 것이 프로젝트 입장). 실제 콘텐츠 제거는 GitHub이나 메인테이너가 아니라 실제 호스팅 업체에 요청해야 한다. 시청·재배포 시 본인 관할지의 방송·저작권 법규를 준수할 책임은 사용자에게 있다. 본 문서는 데이터 큐레이션 아키텍처 학습용이다.
난이도별 5단계 — 데이터 큐레이션·CI 자동화 체득.
streams/에서 한 국가 파일(예: kr.m3u)을 열어 #EXTM3U·#EXTINF·tvg-id·스트림 URL의 구조를 직접 읽고, "한 채널 = 메타데이터 한 줄 + URL 한 줄"이 어떻게 표현되는지 도식으로 정리한다.
레포를 클론하고 npm install 후 npm run playlist:validate를 돌려, error/warning이 어떻게 출력되는지 본다. 일부러 m3u에 중복 URL이나 깨진 URL을 한 줄 넣어보고 검증이 그걸 잡아내는지 확인한다.
임의의 데이터 파일(CSV/JSON)에 대해 "중복 키 검사 + 필수 필드 검사 + 금지값(블록리스트) 대조"를 하는 검증 스크립트를 쓴다. iptv처럼 error는 종료코드 1, warning은 출력만 하도록 구분해 본다.
GitHub Actions에서 schedule: cron으로 매일 공개 데이터 API를 호출 → 가공 → 결과를 레포에 봇 명의로 커밋·푸시하는 워크플로를 만든다. iptv의 update.yml을 참고해 "변경이 있을 때만 커밋" 조건까지 넣어 본다.
하나의 입력 데이터 모델에서 "분류 축별"로 여러 출력 파일을 찍어내는 generator 구조를 직접 설계한다. iptv의 generators/처럼 공통 베이스 클래스 + 축별 서브클래스로 짜고, 새 축을 추가할 때 기존 코드 수정 없이 generator만 더하면 되도록 확장성을 확보한다.
6주 코스 — 데이터 포맷부터 CI 자동 발행까지.
| 주차 | 주제 | 실습 · 참고 |
|---|---|---|
| 1주차 | 미디어 재생목록 포맷 — m3u/m3u8·HLS 기본 | streams/*.m3u 분석 · iptv-playlist-parser 사용 |
| 2주차 | TypeScript CLI 만들기 — commander·tsx | scripts/commands/ 정독 · 미니 CLI 작성 |
| 3주차 | 데이터 검증 설계 — 정합성·중복·교차검증 | validate.ts · error/warning 분리 구현 |
| 4주차 | Generator/모델 분리 — 하나의 모델, 여러 표현 | generators/ · models/Stream·Playlist |
| 5주차 | GitHub Actions — PR 게이트·증분 검사 | check.yml · git diff --name-only 활용 |
| 6주차 | 봇 자동화·GitOps — cron·App 토큰·자동 커밋·Pages 배포 | update.yml 통째 분석 · 직접 scheduled workflow 구축 |
본문·소스에 나온 용어 빠른 참조.
| 용어 | 의미 |
|---|---|
| IPTV | 일반 인터넷망으로 실시간 TV를 전송하는 방식. |
| m3u / m3u8 | "채널명 = 스트림 주소"를 나열한 텍스트 재생목록 포맷. 이 레포의 발행물. |
| HLS / DASH(MPD) | 인터넷 실시간 스트리밍 방식. hls-parser/mpd-parser로 분석. |
| 스트림(stream) | 한 채널의 실제 재생 URL + 메타데이터. 이 레포가 큐레이션하는 단위. |
| 데이터 큐레이션 | 흩어진 데이터를 수집·검증·정리·발행해 가치 있게 유지하는 일. |
| 이슈 템플릿 | 구조화된 GitHub 이슈 폼. 비개발자의 데이터 입력 창구. |
| validate(검증) | 중복·URL유효성·DB대조·차단목록 검사. 의미적 정합성 게이트. |
| lint | m3u 형식 규칙 검사(m3u-linter). 의미가 아닌 형식 점검. |
| blocklist(차단목록) | DMCA·NSFW 등 발행하면 안 되는 채널 목록. database에서 가져옴. |
| generator | 스트림 데이터에서 국가/언어/카테고리별 m3u를 찍어내는 생성기. |
| cron (0 0 * * *) | "매일 자정 실행" 스케줄. update 파이프라인의 자동 트리거. |
| GitHub App 토큰 | 봇(iptv-bot[bot]) 명의로 커밋·배포하기 위한 인증. |
| GitHub Pages | 공개 m3u를 정적 호스팅하는 곳(iptv-org.github.io/iptv). |
| GitOps | 모든 변경을 git 커밋으로 남겨 추적·되돌리기 가능하게 운영하는 방식. |
| CC0 | 저작권을 포기한 퍼블릭 도메인 라이선스. 이 데이터의 라이선스. |
| EPG | 전자 편성표. 별도 레포 iptv-org/epg가 담당. |
https://iptv-org.github.io/iptv/index.m3u