like-surgeon은 조용히 망가진 YouTube Music 라이브러리를 고치는 명령줄 도구다. 재생이 안 되는 좋아요 곡, 다른 음원으로 바뀐 곡, 두 번씩 나오는 곡이 대상이다. YouTube 좋아요와 YT Music 좋아요 목록을 스캔해 SQLite에 넣고, 곡마다 어느 YouTube 좋아요를 보여 주는지 알아낸 다음, 증명할 수 있는 것만 고친다. 이 글은 그걸 가능하게 하는 모델 이야기다.

relink는 이렇게 보인다

좋아요한 곡의 원본 영상이 내려가거나 지역 차단이 걸리면, YT Music은 그 곡을 좋아요 목록에 그대로 두고 다른 영상으로 재생하는 경우가 많다. 대개 같은 곡의 다른 음원이다. 그래서 YT Music이 좋아요를 새 트랙으로 옮긴 것처럼 보인다. YouTube 좋아요에는 여전히 죽은 옛 영상이 남아 있으니, 두 목록이 서로 어긋난 것처럼 보인다.

사라진 곡

나도 처음에는 그렇게 믿고 relink를 당연해 보이는 방식으로 고쳤다. YouTube에서 새 트랙 B에 좋아요를 누르고 죽은 영상 A를 취소하는 방식이다. 이렇게 고친 38곡 중 2곡은 YouTube가 모든 호출에 2xx로 답했는데도 한쪽 쓰기가 조용히 반영되지 않았다. 그중 한 곡은 A 취소만 반영되고 B 좋아요는 빠졌는데, 그러자 곡이 YT Music에서 사라졌다. YouTube에서 A에 좋아요를 다시 누르자 돌아왔다.

YT Music이 좋아요를 B로 옮겼다면 A의 YouTube 좋아요는 상관이 없어야 한다. 그런데 B를 보여 주던 항목은 A의 좋아요와 함께 사라졌다가 함께 돌아왔다. B를 보여 주던 항목이 바로 A의 좋아요였다.

목록은 하나, 보여 주는 곳은 둘

맞는 모델은 이렇다. 좋아요가 저장되는 곳은 YouTube 좋아요 목록 하나뿐이다. YT Music 좋아요 목록은 두 번째 목록이 아니라 첫 번째 목록을 보여 주는 화면이다. 같은 좋아요를 같은 순서로 띄우되 YT Music에 없는 건 빼고, 항목마다 재생할 수 있는 트랙을 붙인다. 그 트랙의 영상은 내가 좋아요를 누른 영상과 다를 수 있다. 코드에서는 두 목록을 플레이리스트 id를 따서 LL, LM이라고 부른다. 모델 전체가 “LM은 LL을 보여 주는 화면” 한 줄로 끝난다.

왼쪽에 YouTube 좋아요 목록, 오른쪽에 YT Music 좋아요 목록을 한 줄씩 맞춘 다이어그램. Song 1은 그대로 보인다. Song 2의 죽은 옛 업로드는 새 음원으로 보이고, like-surgeon이 좋아요를 새 음원으로 옮긴다. 좋아요한 Song 3 뮤직비디오는 역시 좋아요한 음원 트랙으로 보여서 곡이 두 번 나온다. 삭제된 영상은 아예 보이지 않고 보고서에만 남는다.

relink는 A의 좋아요가 B로 보이는 현상이다. 좋아요한 뮤직비디오는 음원 트랙으로 보일 수 있고, 음원에도 좋아요를 눌렀다면 그 곡이 두 번 나온다. YT Music의 좋아요 버튼도 따로 저장되지 않는다. YT Music에서 곡에 좋아요를 누르면 그 영상에 YouTube 좋아요가 생긴다.

버그처럼 보이던 현상도 이걸로 설명된다.

  • relink를 고치려고 YouTube에서 B에 좋아요를 누르면 B로 보이는 좋아요가 하나 더 생겨서 곡이 두 번 나온다.
  • 그 중복을 YT Music에서 취소하면 방금 누른 B의 좋아요가 지워져서 처음 상태로 돌아간다.

연결 고리는 순서뿐이다

이 모델에서는 항목마다 어느 좋아요를 보여 주는지 알아내야 하는데, YT Music은 그걸 알려 주지 않는다. 항목마다 feedback token이 붙어 있지만 내용을 알 수 없다. 대신 순서는 지킨다. 내 라이브러리에서 한 번만 나오고 영상 자체에 YouTube 좋아요가 있는 LM 항목 1,142개가 전부 LL과 같은 순서였다. 무작위로 섞으면 62개쯤만 순서가 맞는다.

compare-likes가 두 목록을 위치로 맞추는 이유다. 영상 자체에 YouTube 좋아요가 있는 항목이 기준점이 되고, 대부분의 곡이 여기에 해당한다. 기준점 두 개 사이에는 짝을 못 찾은 LM 항목 몇 개와 YouTube 좋아요 몇 개가 남는다. 개수가 같으면 순서대로 짝을 짓는다. 남은 첫 번째 좋아요를 남은 첫 번째 항목과 묶는 식이다. 개수가 다르면 짝을 짓지 않고 보고만 한다. 기준점은 위치의 최장 증가 부분 수열(LIS)로 고른다. 앞에서부터 그리디하게 고르면 실제 데이터에서는 기준점이 몇백 개로 줄어들었다.

짝을 정할 때 제목은 쓰지 않는다. 제목이 나오는 곳은 딱 하나다. repoint 전에 두 영상이 같은 녹음으로 보이는지 확인하는데, 길이 차이가 3초 이내이고 채널이 같거나 한쪽 제목이 다른 쪽 제목을 포함해야 한다. 고친 결과를 확인할 수 있었던 24쌍 중 23쌍이 순서로 맞춘 결과와 같았고, 제목으로 짐작했을 때 엉뚱한 곡에 붙었던 한 쌍도 순서로 맞추니 제대로 나왔다.

쓰기 동작 두 가지와 안전장치

항목마다 어느 좋아요를 보여 주는지 알면 수정은 두 가지로 줄어든다. 둘 다 YouTube에서만 한다.

  • repoint: 죽은 A가 B로 보이고 B에는 좋아요가 없을 때. B에 좋아요를 누르고 getRating으로 확인한 뒤, A를 취소하고 다시 확인한다.
  • unlike shadow: A가 B로 보이는데 B에도 좋아요가 있을 때. A를 취소하면 곡이 한 번만 나온다.

앞에서 본 곡이 사라지는 일은 두 수정이 꼭 잡아내야 하는 실패이기도 하다. 짝이 틀렸다면 A를 취소하는 순간 엉뚱한 곡이 YT Music에서 사라지는데, YouTube 응답에는 아무 표시도 없다. 그래서 sync는 좋아요를 하나 취소할 때마다 LM 전체를 다시 읽는다. repoint 뒤에는 모든 곡이 전과 같은 횟수로 나와야 하고, unlike shadow 뒤에는 B가 정확히 하나 줄어야 한다. 15초 뒤에 다시 읽어도 맞지 않으면 A에 좋아요를 다시 누르고, 그 repoint가 B에 누른 좋아요는 되돌리고, 실행을 멈춘다. 결과가 어긋났다면 그 스캔에서 나온 짝은 전부 믿을 수 없기 때문이다.

내 라이브러리에서

도구에 넣기 전에 내 라이브러리의 좋아요 27개로 모델을 먼저 확인했다. 첫 실제 실행에서는 마지막 중복 두 건을 지웠다. 녹음 현장 영상과 팬 업로드가 내가 따로 좋아요한 공식 트랙으로 보이던 경우였고, 취소할 때마다 항목이 정확히 하나씩 줄었다. 이제 LM의 1,172곡은 전부 그 곡 영상에 직접 눌린 YouTube 좋아요다.

증명할 수 없는 건 보고만 한다. YT Music에 아예 보이지 않는 재생 불가 영상의 좋아요가 그런 경우라 직접 정리했다. 삭제된 영상에서는 특이한 점이 하나 더 나왔다. YouTube Data API로 좋아요를 취소하면 404가 나지만, YT Music 평가 API로는 문제없이 취소된다.

like-surgeon은 PyPI에 올라가 있고(uv tool install likesurgeon), sync --dry-run으로 아무것도 바꾸지 않고 무엇을 고칠지 먼저 볼 수 있다. 근거는 설계 문서에 더 자세히 정리해 두었다.