아카이브 / 개발
flo-genie-playlist-sync — FLO·지니뮤직 플레이리스트 동기화 PoC
목차 — 프로젝트 요약 · 담당 범위 · 문제와 구현 접근 · 이 작업에서 한 일 · 결과물과 원문 · 구현 상세
프로젝트 요약
공개된 플레이리스트 API가 확인되지 않는 FLO와 지니뮤직 사이의 동기화를 위해, 로그인한 브라우저 세션의 요청을 캡처·분석하고 메타데이터로 곡을 매칭해 추가 전용 동기화 계획을 만드는 탐색 단계 도구.
담당 범위
설계·구현을 맡은 개인 프로젝트다. 공개 저장소에 코드와 사용 방법을 남겼다.
문제와 구현 접근
Playwright 전용 브라우저 프로필에서 XHR/fetch 요청을 기록하고, 기록된 요청을 점수화해 플레이리스트 CRUD와 곡 검색 API 후보를 찾는다. 캡처는 로컬에만 두고 git에서 제외하며, 인증 헤더와 민감해 보이는 요청 본문 키는 best-effort로 가린다. 가려지지 않는 항목은 README의 안전 모델(Safety Model) 절에 따로 적어 두었다.
서비스 간 곡 ID가 호환되지 않아 제목·아티스트·앨범을 정규화하고 ISRC·재생 시간을 함께 비교해 후보를 순위화하는 매칭 로직을 테스트와 함께 구현했다. 정규화한 플레이리스트 JSON으로 양방향·추가 전용 병합 계획을 만드는 드라이런 플래너를 두었다.
라이브 어댑터는 아직 스텁이고, 교차 서비스 쓰기는 캡처한 엔드포인트를 검토하기 전까지 의도적으로 막아 두었다. 비공식 연구용 도구로 본인 계정에서만 쓰고, DRM·CAPTCHA·로그인 확인을 우회하지 않으며, 각 서비스의 약관을 따라야 한다는 이용 조건을 달았다.
이 작업에서 한 일
요청 캡처·분류부터 곡 매칭과 드라이런 계획까지 작동하는 탐색 도구를 만들고, 어떤 스크립트가 실제 계정의 플레이리스트를 바꾸는지와 이용 조건을 문서로 남겼다.
결과물과 원문
관련 등록공보·논문·저장소는 아래 링크에서 볼 수 있다.
구현 상세
README공개 저장소의 구조·기능·실행 문서
FLO <-> Genie Playlist Sync
Design docs and local discovery tooling for syncing playlists between FLO and Genie Music.
FLO and Genie do not appear to publish user playlist CRUD APIs (see docs/OFFICIAL_API_CHECK.md). So this project starts with capture, analysis, and dry-run planning. It records the XHR/fetch traffic of a logged-in browser session, scores the captured requests to find playlist and search endpoints, matches tracks across services by metadata, and builds an add-only sync plan. Live mutation stays disabled until the captured endpoint map has been reviewed and configured.
Status
This is a discovery-stage proof of concept, not a finished sync tool.
What works today:
- Network capture of XHR/fetch traffic from a Playwright persistent browser
profile. The JSONL log applies best-effort masking to a fixed list of auth headers and to request-body keys that look sensitive. See the Safety Model section for what is not masked.
- A classifier that scores captured requests as playlist CRUD and
track-search candidates.
- Headless FLO and Genie scripts. Each one searches for a track by title and
artist, ranks the results by metadata, and tries to add the best match to your own playlist. The FLO script then checks the playlist page to confirm the add.
- A track matcher that compares normalized title, artist, and album, plus ISRC
and duration. It scores candidates with token Jaccard and Dice similarity and assigns confidence bands. It is covered by tests.
- A dry-run planner that builds a two-way, add-only merge plan from normalized
playlist JSON.
What does not work yet:
- The adapters in
src/adapters/*are stubs. Every method throws, and live
apply refuses to run.
config/captured-endpoints.sample.jsoncontains onlyTODO_FROM_CAPTURE
placeholders.
- The sync CLI only writes a plan. Asking it for a live run (
--no-dry-run)
fails on purpose.
config/services.json and package.json also contain capture configs for Melon, VIBE, Bugs, Spotify, and YouTube Music. These exist for research only. Their capture: and analyze: commands just record a browser session that you drive by hand. None of these services has its own automation script. Their capture host lists leave out the shared NAVER and Google login hosts (nid.naver.com, accounts.google.com), so traffic to those hosts is not recorded unless you pass --all-hosts.
Disclaimer / Terms of Use
- This is an unofficial project. It is not affiliated with, endorsed by, or
supported by FLO, Genie Music, or any other service named in this repository.
- No official API is used. The tools rely on the first-party websites' own
web traffic and UI, which can change or break without notice.
- This is a personal-use research tool. Use it only with your own accounts and
your own playlist data.
- Each service's terms of use apply, and they may restrict automated access.
You are responsible for complying with them.
- Keep request volume low, and use a temporary playlist for capture runs.
- The tools do not download or stream audio. They do not bypass DRM, CAPTCHA,
phone verification, payment gates, or login checks. You always complete login yourself, on the service's own page.
- Live cross-service sync is disabled.
Requirements
- Node.js 20 or later
- Playwright Chromium (
npx playwright install chromium)
Quick Start
npm install
npx playwright install chromium
npm test
npm run check
npm run sync:dry-run
npm testruns the diff-planner and track-matcher tests.npm run checkrunsnode --checkon every.mjsfile.npm run sync:dry-runbuilds a plan fromexamples/*.jsonand writes it
to state/sync-plan.json.
Capture and analysis need a logged-in browser profile. Before running the FLO headless script, create a temporary playlist in FLO yourself (for example playlist-sync-smoke-YYYYMMDD) and pass its exact name with --playlist:
npm run capture:flo
npm run analyze:flo
npm run capture:flo:crud:headless -- --playlist "playlist-sync-smoke-YYYYMMDD" --title "Ditto" --artist "NewJeans"
npm run auth:genie:login
npm run auth:genie:check
npm run capture:genie:manual
npm run capture:genie:crud:headless -- --title "Ditto" --artist "NewJeans"
npm run capture:genie
npm run analyze:genie
For a first login or an expired session, run npm run capture:flo:manual and log in to FLO manually. After that, npm run capture:flo:crud:headless reuses profiles/flo and runs the FLO search/add/verify capture without opening a visible browser window.
Both headless scripts write to your account. If you pass none of --title, --artist, --album, --isrc, --duration, --duration-ms, or --keyword, they search for the default track Ditto by NewJeans and try to add it. See What the headless scripts change in your account.
The capture profiles are separate from your normal Chrome profile. If you are logged in to Genie or FLO in regular Chrome, the Playwright scripts cannot use that login. Reusing an existing Chrome login would need a Chrome Extension or a local browser bridge, because a hosted web page cannot read another site's Chrome cookies.
For services without OAuth, the connection flow opens the first-party login screen and keeps only the resulting browser session:
npm run auth:flo:login
npm run auth:genie:login
After login, run npm run auth:flo:check or npm run auth:genie:check to check the saved session without opening a visible browser. These commands write only safe status signals under state/auth/. They do not write passwords, cookies, or auth headers.
Track lookup is based on metadata, because a track ID from one service cannot be sent to the other. Use --title, --artist, and optionally --album to find the target service's track ID before adding:
npm run capture:flo:crud:headless -- --playlist "playlist-sync-smoke-YYYYMMDD" --title "Hype Boy" --artist "NewJeans"
What the headless scripts change in your account
The headless scripts write to your account:
capture:flo:crud:headlessadds the matched track to the playlist you name
with --playlist "<name>". If you leave that flag out, or no playlist has exactly that name, it falls back to the first playlist in your FLO library without asking. It never creates a playlist with the --playlist name, so create the temporary playlist in FLO before the run. Only when your library has no playlist at all does it create one; pass --no-create-playlist to prevent this. It then opens the playlist to verify that the track was added.
capture:genie:crud:headlessclicks Genie's add-to-my-album button for the
matched song, but only when the saved profile is logged in. It has no --playlist option. If the profile is not logged in, it captures the search metadata and stops with reauth_required.
- Without track flags, both scripts use the default track
Dittoby
NewJeans.
Manual capture
During manual capture runs, a Chromium window opens. Log in manually, then perform these playlist actions on a temporary playlist:
- list/open playlists
- create playlist
- rename playlist
- search a song
- add a song
- remove a song
- delete the temporary playlist
Press Enter in the terminal to stop capture. Artifacts are written under captures/<service>/<timestamp>/.
Current Architecture
src/capture/browser-capture.mjsrecords XHR/fetch traffic from a headed
or headless Playwright browser session.
scripts/service-login.mjsopens the first-party login page in the saved
profile and writes only session status signals to state/auth/<service>.json.
scripts/flo-headless-crud-capture.mjsreuses the saved FLO profile and
runs a headless title/artist search/add/verify capture.
scripts/genie-headless-crud-capture.mjsreuses the saved Genie profile,
parses title/artist search results from Genie's HTML, and attempts an add only after the profile is logged in.
src/sync/track-match.mjsranks the target service's search candidates, so
that a sync write can map a source track to a target track ID before adding.
src/analyze/classify-playlist-apis.mjsscores captured requests and writes
playlist API candidates.
src/sync/diff.mjscomputes a two-way union sync plan from normalized
playlist state files.
src/adapters/*are placeholders for live adapters, for use once the
captured endpoints are mapped.
docs/PRD.mddescribes the target web settings UI, the authentication
model, and the end-state user stories.
docs/SERVICE_COVERAGE_RESEARCH.mdcovers market priority, GitHub findings,
and capture feasibility for each service.
docs/ADAPTER_ARCHITECTURE.mddescribes the common service adapter and the
capability-gated execution model.
docs/API_STRATEGY_BY_SERVICE.mdcompares official OAuth with browser-capture
adapters for each service.
docs/CAPTURE_CAMPAIGN_PLAN.mddescribes the capture campaign for each
target service, from signup to playlist CRUD.
docs/CAPTURE_RUNBOOK.mdgives the step-by-step capture procedure for FLO
and Genie.
docs/SESSION_AND_SEARCH_ARCHITECTURE.mdcovers session keepalive and
recovery, and the requirements for searching tracks on the target service.
docs/OFFICIAL_API_CHECK.mdrecords the result of the official API check.
Some docs, including docs/PRD.md, are written in Korean.
Safety Model
- Captures stay local, and git ignores them.
- Capture records only XHR/fetch traffic to the hosts listed under
allowedHosts for the service in config/services.json. The --all-hosts flag removes that filter.
- Masking in
network.jsonlis best-effort. Only these request and response
headers are replaced with <redacted>: authorization, cookie, set-cookie, x-csrf-token, x-xsrf-token, x-auth-token, and proxy-authorization. JSON and form-encoded request bodies are masked by key: a key is masked when its name contains authorization, cookie, password, passwd, pwd, token, session, email, phone, tel, name, or birth, or when it is exactly memberId, loginId, userId, user_id, accountId, or account_id (case-insensitive).
- What is not masked: service-specific auth headers with other names (for
example a custom x-...-access-token), body parameters whose names do not match the list above, and the full request and frame URLs, including query strings. Treat every network.jsonl as sensitive and never share it.
- The
capture:*commands (src/capture/browser-capture.mjs) store response
bodies unmasked, as raw text of up to 250 KB each. To skip them, pass --no-response-body, for example npm run capture:flo -- --no-response-body. The headless scripts mask response bodies by key, but those bodies can still contain private account data.
- Every capture run writes
captures/<service>/<timestamp>/storage-state.json.
This applies to the capture: commands and to both headless scripts. The file holds the full live session cookies and site storage. The capture: commands also accept an optional --har flag, which writes network.har with unredacted headers.
profiles/<service>is a logged-in Chromium profile. Anyone who has this
directory can act as you on that service. Deleting it removes the saved login from this machine.
- Never commit or share
captures/,profiles/, orstate/. - Live sync apply is blocked until the endpoint map for each service has been
added and verified.