앱
분석 기준: 공식 repository ID 1339970099, 커밋 de22796c04919aaa057d77930c33c81313765569. README·설계·운영 문서와 API 조립·DB·미디어·경로·작업 복구·진단 코드 및 의존성을 읽었습니다. 설치·빌드·외부 사이트 수집·모델 실행은 하지 않았습니다.
Peach는 로컬 디스크와 마운트한 클라우드 드라이브의 영상을 SQLite에 등록하고, 브라우저에서 검색·재생·메타데이터·시청 기록을 관리하는 셀프호스팅 애플리케이션입니다. 기본은 단일 사용자이며 Windows 앱, macOS 앱/소스 실행, Linux Docker 경로가 문서화돼 있습니다.
범용 미디어 기술 위에 성인 영상 컬렉션에 특화한 작품 식별·인물·제작사·취향 기능이 있습니다. README는 명시적으로 18세 이상 사용자의 본인 컬렉션 관리용이라고 설명합니다. 저장소는 영상이나 사이트 데이터를 포함하지 않는다고 밝힙니다. 이 글은 데이터 모델과 파일·작업·플레이어의 구현을 다루며 성인 이미지나 실제 컬렉션을 사용하지 않습니다.
| 이럴 때 검토 | 도입 판단 |
|---|---|
| 여러 디스크·클라우드 폴더 | 영상 파일 위치와 라이브러리 기록을 분리하고, 마운트가 끊겨도 기록을 유지하고 싶을 때 |
| 개인용 브라우저 앱 | NAS나 데스크톱 한 곳에서 관리하고 같은 네트워크의 다른 기기에서 보고 싶을 때 |
| 구현 학습 | FastAPI 모듈 경계·SQLite 단일 writer·HTTP Range·작업 체크포인트를 실제 소스로 배울 때 |
| 확인할 한계 | 다중 사용자·테넌트 격리 없음, 외부 메타데이터/피드의 가용성과 권한에 의존, 모든 데이터가 무조건 기기 안에만 남는 것은 아님 |
저장소가 보여주는 차별점은 파일·작품·개인 기록을 한 행으로 뭉개지 않는 것입니다. 같은 작품의 여러 버전·분할 파일을 묶고, 사라진 파일의 개인 기록을 남기며, 새로 등록된 파일과 재연결하는 흐름이 있습니다. scan.py의 머리 설명은 스캔이 크기·수정 시각·마지막 관측을 갱신하되 검토된 자료 필드를 함부로 덮어쓰지 않는다고 명시합니다.
일반 동영상 플레이어가 파일 하나를 여는 데 집중한다면 Peach는 라이브러리의 장기 상태를 관리합니다. 여러 사용자에게 서비스하는 미디어 서버보다도 개인 컬렉션과 외부 자료의 연결에 초점이 있습니다. 클라우드 통합은 드라이브를 Peach가 전부 직접 구현한다는 뜻이 아닙니다. CloudDrive2 등으로 마운트한 폴더를 파일 시스템으로 읽고, 별도의 클라우드 다운로드 경로를 제공합니다.
README의 115·PikPak 다운로드와 외부 피드 기능도 실제 제품의 일부입니다. resource_search.py에는 사용자 설정 Torznab 인덱서, follow_providers.py에는 공식 공개 피드와 아카이브 사이트가 함께 등록됩니다. 기능이 있다는 사실과 각 콘텐츠를 받을 권리가 있다는 판단은 별개입니다. 외부 연결을 쓰지 않고도 로컬 파일 관리 구조를 살펴볼 수 있습니다.
| 영역 | 고정 소스의 기술 | 역할 |
|---|---|---|
| 백엔드 | Python 3.14+, FastAPI·Starlette·Uvicorn·Pydantic | JSON API·페이지·인증·미디어 응답, 서비스 조립 |
| 영구 저장 | SQLite, repository.py, migrations/ | 라이브러리·개인 기록·자료 출처, 읽기 전용 연결과 쓰기 트랜잭션 경계 |
| 프론트엔드 | React 19·React Router·TanStack Query/Table·TypeScript·Vite·Tailwind v4 | 라우팅·조회 상태·표·상세 화면. web/dist/ 산출물을 서버에서 제공 |
| 플레이어 | Video.js 10, FFmpeg/ffprobe | 브라우저 재생, 미디어 탐색·추출·변환. FFmpeg는 별도 실행 도구 |
| 작업·외부 IO | APScheduler·watchdog·filelock·HTTPX, 일부 curl_cffi·gRPC | 주기 작업·파일 변화·프로세스 간 잠금·외부 소스/CloudDrive2 연결 |
| 데스크톱·컨테이너 | pystray·PyInstaller, macOS Cocoa 확장, Docker Compose | 트레이에서 로컬 서비스를 열거나 NAS에서 브라우저로 접근 |
| 선택 AI | providers.py의 Inference/Agent 구분 | 모델 목록·능력·설정 상태를 분리해 노출. 기본 영상 관리에 모델 실행을 요구하지 않음 |
| 검증 도구 | Python 테스트, Vitest, Node test·Playwright 기반 e2e | 경로·DB·작업·UI 계약 검사. 이 분석에서는 실행하지 않음 |
의존성의 정확한 버전은 Python 쪽 pyproject.toml·uv.lock, UI 쪽 frontend/package.json이 기준입니다. 루트 package.json은 Video.js·폰트·아이콘 등 웹 vendor 자산용으로, 프론트엔드 전체 의존성 목록과 다릅니다.
api.py는 서비스들을 만들고 request.app.state로 공급하는 조립 지점입니다. 라우터를 기능별로 나누지만 routes_api의 /api/{route:path} catch-all은 마지막에 등록해야 합니다. 그렇지 않으면 먼저 처리해야 할 미디어 경로까지 잡아버립니다. “파일을 나눴다”보다 라우팅 우선순위와 의존성의 소유자를 보는 것이 핵심입니다.
# src/peach/repository.py:22-30
def connect(self, *, write: bool = False, timeout: float = 30):
target = (
str(self.db_path) if write
else self.db_path.resolve().as_uri() + "?mode=ro"
)
connection = sqlite3.connect(
target, timeout=timeout, check_same_thread=False, uri=not write,
)
위 코드는 타입 표기를 일부 생략한 발췌입니다. 읽기 경로를 관례로만 유지하지 않고 URI의 mode=ro로 제한합니다. 쓰기는 write_transaction의 프로세스 내 lock과 commit/rollback으로 감싸며, 성공 시 필요할 때만 after_commit을 부릅니다. 주기적인 작업 진행 기록까지 매번 집계 캐시를 지우지 않도록 notify=False를 둘 수 있습니다.
두 기기의 DB 복제는 별개의 계약입니다. 설계 문서는 한 번에 한 기기만 쓰고, 양쪽이 바뀌면 읽기 전용으로 전환해 보존할 DB를 선택하도록 한다고 명시합니다. 이 lock 하나가 여러 기기의 충돌을 자동 해결하는 것은 아닙니다.
DB의 asset.path는 Windows 형태로 유지됩니다. 예를 들어 R:\media\a.mp4를 Linux에서는 /media/a.mp4로 읽습니다. [media.locations]는 DB 접두사, [media.mounts]는 현재 기기의 실제 디렉터리입니다. platform.translate_ledger_path는 등록된 대응이 없으면 존재하지 않는 UNMAPPED_ROOT 아래로 보내 읽기를 거부하게 합니다.
FilesystemBackend.file_for는 변환한 경로가 허용 루트 안에 있는지 검사합니다. MediaOffline과 MediaUnavailable도 구분하므로 마운트 전체가 끊긴 경우와 파일 하나가 없는 경우를 같은 삭제 사건으로 취급하지 않습니다. 스캔 결과·개인 기록·원본 파일의 수명이 다른 이유가 코드에 드러납니다.
MediaEngine.stream_plan의 기본 결과는 HTTP Range입니다. HLS는 명시적으로 요청한 모드이면서 115/PikPak의 MP4/M4V이고 알려진 양수 duration이 있는 경우에 선택합니다. 코드 주석은 HEVC를 그대로 MPEG-TS에 넣었을 때 Chromium MSE에서 조용히 검은 화면이 되는 사례를 이유로 듭니다. 확장자나 스트리밍 방식 하나만 바꾼다고 브라우저 코덱 지원까지 해결되는 것은 아닙니다.
job_recovery.py는 요청·job ID·PID·진행 위치를 로컬 JSON에 원자적으로 저장합니다. resume은 admission lock 안에서 상태와 PID를 확인하고, 살아 있는 다른 프로세스나 현재 프로세스가 이미 시작한 작업을 건너뜁니다. 실패하면 blocked와 오류를 남깁니다. 모든 외부 부작용이 정확히 한 번 실행된다는 보장은 아니며, 재실행 가능한 범위와 결과 저장 위치를 작업별로 봐야 합니다.
peach/
├─ src/peach/
│ ├─ api.py · routes_*.py 앱 조립 / HTTP 경계
│ ├─ repository.py · scan.py SQLite / 파일 관측
│ ├─ platform.py · media.py 경로 대응 / 재생 원본
│ ├─ jobs.py · job_recovery.py 작업 수명 / 복구
│ ├─ follow_*.py · metadata*.py 외부 자료와 피드
│ ├─ remote_reporting.py 기술 진단 선택·전송
│ └─ library_sharing.py 별도의 컬렉션 공유 동의
├─ frontend/ React·플레이어·테스트·Vite
├─ web/ 페이지 자원 / dist 빌드 결과
├─ migrations/ DB 스키마 이력
├─ docs/ 설계·운영·ADR·라이선스 설명
├─ tests/ · scripts/ 회귀 테스트 / 개발·배포 도구
└─ pyproject.toml · uv.lock · compose.yaml
실행 데이터는 별도 peach-data 디렉터리에 DB·캐시·state·secrets·로그 등으로 나뉩니다. Docker는 /data 영구 볼륨, 소스 실행과 데스크톱 앱은 각 경로를 사용합니다. 소스 폴더만 백업했다고 개인 기록과 설정까지 보존되는 것이 아닙니다.
공식 docs/adr/0085-organization-and-three-code-repositories.md는 2026-09-30 GitHub 조직으로 앱을 이전하며 커밋·태그·릴리스·이슈를 보존했다고 기록합니다. 앱은 peach-mitao/peach, 웹사이트와 데모는 별도 저장소입니다. 앱의 기본 브랜치는 master이며 서로 다른 저장소의 산출물을 혼동하지 않아야 합니다.
| 주제 | 읽을 파일 | 자신의 앱에 적용할 질문 |
|---|---|---|
| 단일체의 모듈 경계 | api.py, docs/ARCHITECTURE.md | 서비스 조립·라우팅·영역 로직의 책임이 섞이지 않는가? |
| 개인 기록 보존 | scan.py, repository.py | 파일이 잠시 안 보일 때 사용자가 남긴 기록도 지울 것인가? |
| 경로의 정체 | platform.py, media.py | 저장된 식별자와 이번 기기의 경로를 어떻게 분리하는가? |
| 복구 가능한 작업 | job_recovery.py | 진행 위치를 결과 저장 전후 언제 기록하며 중복 실행은 누가 막는가? |
| 프로토콜과 코덱 | MediaEngine.stream_plan | Range·HLS·컨테이너·코덱을 서로 독립적으로 판단하는가? |
| 동의의 분리 | remote_reporting.py, library_sharing.py | 기술 진단 동의를 개인 데이터 제공 동의로 확대하지 않는가? |
AI 기능을 읽을 때도 능력 탐색과 모델 호출을 구분해야 합니다. providers.py의 OpenCodeGoClient.list_models는 목록을 조회하는 코드이고, registry의 available·configured·experimental은 서로 다른 상태입니다. 실행 파일이 있다는 이유만으로 로그인과 실제 추론 성공을 판정하면 안 됩니다.
| 경로 | 조건·제약 |
|---|---|
| Windows 배포본 | x64 테스트 패키지. 전체 설치본은 미디어 도구 포함, lite는 설치 중 필요 도구 다운로드. 테스트 빌드는 미서명 |
| macOS | arm64/x86_64별 앱·DMG 빌드 경로. 테스트 앱은 공증되지 않았으며 FFmpeg 등 미디어 도구는 별도 설치 |
| 소스 실행 | Git·uv·Python 3.14+. pyproject.toml의 uv 하한은 0.12.13, macOS는 macos extra 필요 |
| 프론트엔드 개발 | Node.js 24+, frontend 의존성. 소스 UI를 수정·재빌드할 때의 조건이며 일반 설치본 사용 조건과 다름 |
| Docker | 문서화된 이미지 대상 linux/amd64, ARM NAS 미검증. 기본 media bind는 읽기 전용, 데이터는 별도 영구 볼륨 |
| 전환·추출 | FFmpeg/ffprobe 필요. CPU·GPU·코덱에 따라 비용이 다르며 보편적 최소 RAM/CPU 수치는 소스에서 확인하지 못함 |
| 외부 서비스 | 클라우드 마운트/다운로드는 해당 계정·도구·권한 필요. 구독·용량·속도 비용은 서비스별 조건을 별도 확인 |
| 라이선스 | AGPL-3.0-or-later. 외부 컴포넌트는 자체 라이선스 유지. 배포된 FFmpeg 등 도구 조건은 docs/REUSE.md 등에서 구분 |
README와 진단 코드는 버전 통계·오류 진단 선택이 기본 켜짐이라고 명시합니다. Sentry 수신점이 설정돼 있을 때 기술 필드를 전송하고 설정에서 끌 수 있습니다. 전체 로그·영상 정보·시청 기록을 기술 진단에 넣지 않는다는 범위가 별도로 설명돼 있습니다.
컬렉션·취향 공유는 library_sharing.py의 별도 동의이며 기본 꺼짐입니다. 켜면 작품·평가·시청 요약·선호 차원의 주간 스냅샷을 보낼 수 있습니다. 식별자를 빼도 상세 취향이 재식별될 수 있고 수신자는 연결 IP를 볼 수 있다고 공식 문서가 명시합니다. 외부 메타데이터·피드 조회와 선택 AI 연결도 네트워크 경로이므로 모든 동작을 완전 오프라인이라고 부를 수 없습니다.
앱은 단일 사용자용이고 공개 인터넷에 직접 포트를 노출하지 않도록 설계돼 있습니다. 기본 Compose는 호스트의 127.0.0.1에만 공개합니다. 여러 사용자의 독립된 권한·라이브러리를 제공해야 한다면 이 구조가 맞는지 먼저 판단해야 합니다.
공식 저장소를 확보한 디렉터리에서 읽기 명령으로 요구사항을 확인합니다.
PIN=de22796c04919aaa057d77930c33c81313765569
git cat-file -t "$PIN"
git show "$PIN:pyproject.toml"
git show "$PIN:frontend/package.json"
git show "$PIN:compose.yaml"
완료 기준: Python 3.14+·Node 24+와 /data·/media의 역할을 설명합니다. read_only: true가 media bind의 속성인지 확인합니다. Python 3.12로 이 앱을 실행할 수 있다고 해석하면 안 됩니다.
git show "$PIN:src/peach/repository.py"
git show "$PIN:src/peach/platform.py"
git show "$PIN:src/peach/media.py"
입력은 가상의 R:\media\sample.mp4, DB 접두사는 R:\media, Linux 마운트는 /media라고 둡니다. 완료 기준: 결과 경로 /media/sample.mp4까지의 변환을 그리고, 마운트 설정이 없는 경우와 허용 루트 밖 경로가 어느 함수에서 거부되는지 적습니다. 실제 DB를 열거나 수정하지 않아도 됩니다.
이 과제는 자신의 테스트 환경에서만 선택합니다. 현재 분석에서는 실행하지 않았습니다. Python 3.14+·uv가 필요하고 의존성 및 인터프리터 다운로드가 발생할 수 있습니다. macOS에서 공식 운영 문서의 소스 실행 경로는 다음과 같습니다.
uv sync --locked --python 3.14 --extra macos
PEACH_DATA_ROOT="$PWD/../peach-lab-data" ./.venv/bin/peach-tray
Windows에서는 uv sync --locked --python 3.14 후 별도 PEACH_DATA_ROOT를 지정해 .venv\Scripts\peach-tray.exe를 실행합니다. 브라우저 설정에서 이 컴퓨터만 접근하도록 하고, 테스트용으로 직접 만든 비민감 영상 사본 한 개가 든 폴더만 추가합니다. 외부 피드·브라우저 기록·클라우드를 연결할 필요는 없습니다. 진단·공유 선택을 확인한 뒤 스캔합니다.
완료 기준: 영상 사본이 라이브러리에 나타나고 재생되는지, 앱을 재시작한 뒤 기록이 남는지 확인합니다. 실패하면 코덱·FFmpeg·실제 마운트·허용 루트·로그를 각각 확인합니다. 일반 원본 디렉터리를 대상으로 이동·삭제·정리 작업을 시험하지 않습니다.
git show "$PIN:src/peach/job_recovery.py"
git show "$PIN:src/peach/remote_reporting.py"
git show "$PIN:src/peach/library_sharing.py"
완료 기준: 작업의 active·살아 있는 PID·현재 PID·blocked 처리를 표로 정리합니다. 이어서 기술 진단은 기록이 없을 때 기본 켜짐, 컬렉션 공유는 명시적 동의가 있을 때만 켜짐이라는 차이를 실제 조건으로 짚습니다. 모델을 실행하거나 개인 기록을 전송하지 않고도 재시작·개인정보 설계의 경계를 평가할 수 있습니다.
| 주차 | 읽기·실습과 산출물 |
|---|---|
| 1주 | docs/ARCHITECTURE.md와 api.py를 읽고 app.state·라우터·영역별 로직의 의존 방향 작성 |
| 2주 | SQLite 읽기/쓰기 경계, 스캔과 개인 기록을 비교하고 “파일 소실·마운트 해제·같은 작품의 새 버전” 처리 표 작성 |
| 3주 | platform.py와 media.py에서 경로·허용 루트·Range/HLS 조건을 따라 가상의 입력별 결과 표 작성 |
| 4주 | 체크포인트·동의·외부 IO를 묶어 재시작 중복 방지와 데이터 전송에 대한 검증 계획 작성. 실제 네트워크·기기 동작은 별도 환경에서 확인 |
| 용어 | 이 프로젝트에서의 의미 |
|---|---|
| Modular monolith | 영역별 코드는 나누되 한 애플리케이션으로 배포하는 구조 |
| Ledger | 파일·작품·개인 기록·자료 출처를 보존하는 SQLite DB. 단순 파일 목록보다 수명이 긴 기록 |
| 마운트 / 경로 매핑 | 외부 저장소를 폴더처럼 제공하고 DB의 경로를 현재 시스템의 디렉터리에 대응시키는 것 |
| HTTP Range | 파일 일부 바이트를 요청하는 방식. 원본을 브라우저가 재생할 때 탐색·이어보기에 활용 |
| HLS | 재생 목록과 작은 미디어 조각을 주고받는 방식. 코덱 지원 여부와는 별도 문제 |
| 체크포인트 | 긴 작업의 요청·진행 위치를 보존해 재시작 후 미완 부분을 이어갈 근거 |
| NFO / Torznab | 미디어 메타데이터 sidecar 파일 / 인덱서 검색 API 규약. 둘은 라이브러리 정리와 외부 검색의 다른 입력 |
| AGPL / ADR | 프로젝트가 선언한 소프트웨어 라이선스 / 설계 결정의 배경·대안·영향을 남긴 문서 |