Fix op page UI: button underline, tab panel duplication, header spacing
- /op/dashboard: remove underline from <a class="secondaryButton"> by adding
text-decoration:none + inline-flex centering on .primaryButton/.secondaryButton/
.dangerButton, plus margin-left:auto on .dashboardActions so the buttons stick
to the right side of the row even when the header is laid out as flex+wrap.
- /op/list/<pack>: fix the duplicate "save/clear + playlist URL" UI showing in
the active tab. .tabPanel had a baseline `display: block;` that overrode the
browser default `[hidden] { display: none }`. Add an explicit
`.tabPanel[hidden] { display: none !important }` rule so the inactive panel
actually hides.
- /op/list, /op/list/<pack>, /op/datapack: bump the gap between the "돌아가기"
ghost link and the page title from 8px to 20px.
- docs/yt-dlp-setup.md: install guide (single-binary curl + pipx + apt) since
the server doesn't have pip/pipx/yt-dlp installed. Server keeps the graceful
"수동 입력으로 진행" fallback when yt-dlp is missing.
This commit is contained in:
91
docs/yt-dlp-setup.md
Normal file
91
docs/yt-dlp-setup.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# yt-dlp 설치 가이드
|
||||||
|
|
||||||
|
음악퀴즈 관리 사이트(`/op/list/.../playlist`) 기능에서 유튜브 플레이리스트 메타데이터를 가져올 때 서버에 `yt-dlp` 바이너리가 필요합니다.
|
||||||
|
설치돼 있지 않으면 사이트에 `"서버에 yt-dlp가 설치돼 있지 않습니다. (수동 입력으로 진행)"` 라고 표시되고, 사용자가 직접 곡을 추가해야 합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 가장 간단한 방법 — 단일 바이너리 내려받기 (권장)
|
||||||
|
|
||||||
|
Python/pip 없이도 동작하며, 권한도 깔끔합니다. 서버에 SSH로 접속한 뒤:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
|
||||||
|
sudo chmod a+rx /usr/local/bin/yt-dlp
|
||||||
|
yt-dlp --version
|
||||||
|
```
|
||||||
|
|
||||||
|
마지막 줄에서 버전(예: `2025.12.13`)이 출력되면 끝입니다.
|
||||||
|
|
||||||
|
업데이트는 같은 한 줄을 다시 실행하거나 `yt-dlp -U` 로 수행합니다.
|
||||||
|
|
||||||
|
> 만약 `/usr/local/bin` 에 쓰기 권한이 없는 환경이면 `~/.local/bin/yt-dlp` 로 받고
|
||||||
|
> `~/.local/bin` 이 `$PATH` 에 포함돼 있는지 확인하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. pipx 사용 (이미 pipx 가 깔려 있다면)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y pipx
|
||||||
|
pipx ensurepath
|
||||||
|
pipx install yt-dlp
|
||||||
|
```
|
||||||
|
|
||||||
|
업데이트: `pipx upgrade yt-dlp`
|
||||||
|
|
||||||
|
> Ubuntu 24.04 이상은 시스템 파이썬에 `pip install` 이 막혀 있어 (`PEP 668`)
|
||||||
|
> `pipx` 가 사실상 표준입니다. `pip install yt-dlp` 는 권장하지 않습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. apt 패키지 (구버전일 가능성 있음)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y yt-dlp
|
||||||
|
```
|
||||||
|
|
||||||
|
`apt` 의 yt-dlp 는 유튜브 정책 변경을 따라가지 못해 자주 깨집니다. **1번 방법을 추천합니다.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 부가: ffmpeg
|
||||||
|
|
||||||
|
설치기 EXE 에서 음악을 ogg 로 변환할 때 `ffmpeg` 도 필요합니다. 음악퀴즈 관리 사이트 자체는 필요 없지만,
|
||||||
|
설치기를 서버에서 디버깅한다면 함께 깔아두면 편합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install -y ffmpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 동작 확인
|
||||||
|
|
||||||
|
설치 후 관리 사이트 서비스를 재시작할 필요는 **없습니다**. 매 요청마다 `spawn('yt-dlp', ['--version'])` 으로 직접 호출하므로,
|
||||||
|
`PATH` 상에 `yt-dlp` 가 있기만 하면 바로 인식됩니다.
|
||||||
|
|
||||||
|
확인:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -u <서버를_실행하는_사용자> yt-dlp --version
|
||||||
|
```
|
||||||
|
|
||||||
|
예) systemd 로 `minecraft-launcher.service` 가 실행 중이고 사용자가 `claude` 라면:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -u claude yt-dlp --version
|
||||||
|
```
|
||||||
|
|
||||||
|
여기서 버전이 출력되면 관리 사이트의 "플레이리스트 불러오기" 도 정상 동작합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 트러블슈팅
|
||||||
|
|
||||||
|
- **`yt-dlp: command not found`** — `$PATH` 에 설치 디렉터리가 없습니다. `which yt-dlp` 로 위치 확인.
|
||||||
|
- **`ERROR: ... HTTP Error 403`** — yt-dlp 가 너무 오래된 버전입니다. `yt-dlp -U` 로 업데이트.
|
||||||
|
- **`Sign in to confirm you're not a bot`** — 일시적인 IP 제한. 몇 분 후 재시도하거나, 같은 서버에서 다른 영상 재생을 시도해본 적이 있다면 IP 가 풀릴 때까지 기다려야 합니다.
|
||||||
|
- **systemd 서비스에서만 안 됨** — `PATH` 환경변수가 다를 수 있음. 서비스 유닛에 `Environment=PATH=/usr/local/bin:/usr/bin:/bin` 추가.
|
||||||
@@ -171,9 +171,16 @@ body.siteBody.centerLayout {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboardHeader > div { min-width: 0; }
|
||||||
.dashboardHeader h1 { margin: 0; font-size: 24px; }
|
.dashboardHeader h1 { margin: 0; font-size: 24px; }
|
||||||
|
|
||||||
.dashboardActions { display: flex; gap: 8px; }
|
.dashboardActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-left: auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.inlineForm { margin: 0; }
|
.inlineForm { margin: 0; }
|
||||||
|
|
||||||
@@ -193,6 +200,10 @@ body.siteBody.centerLayout {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primaryButton:hover { background: var(--accent-hover); }
|
.primaryButton:hover { background: var(--accent-hover); }
|
||||||
@@ -205,6 +216,10 @@ body.siteBody.centerLayout {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondaryButton:hover { border-color: var(--accent); }
|
.secondaryButton:hover { border-color: var(--accent); }
|
||||||
@@ -218,6 +233,10 @@ body.siteBody.centerLayout {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dangerButton:hover { background: #d73a48; }
|
.dangerButton:hover { background: #d73a48; }
|
||||||
@@ -370,6 +389,7 @@ body.siteBody.centerLayout {
|
|||||||
.tabBtn.active { color: var(--text); border-bottom-color: var(--accent); }
|
.tabBtn.active { color: var(--text); border-bottom-color: var(--accent); }
|
||||||
|
|
||||||
.tabPanel { display: block; }
|
.tabPanel { display: block; }
|
||||||
|
.tabPanel[hidden] { display: none !important; }
|
||||||
|
|
||||||
.listActionsRow { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
|
.listActionsRow { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
|
||||||
.statusText { font-size: 13px; color: var(--text-muted); margin-left: 8px; }
|
.statusText { font-size: 13px; color: var(--text-muted); margin-left: 8px; }
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<section class="dashboardHeader">
|
<section class="dashboardHeader">
|
||||||
<div>
|
<div>
|
||||||
<a class="ghostLink" href="/op/dashboard">← 돌아가기</a>
|
<a class="ghostLink" href="/op/dashboard">← 돌아가기</a>
|
||||||
<h1 style="margin-top:8px;">데이터팩 수정</h1>
|
<h1 style="margin-top:20px;">데이터팩 수정</h1>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<section class="dashboardHeader">
|
<section class="dashboardHeader">
|
||||||
<div>
|
<div>
|
||||||
<a class="ghostLink" href="/op/dashboard">← 돌아가기</a>
|
<a class="ghostLink" href="/op/dashboard">← 돌아가기</a>
|
||||||
<h1 style="margin-top:8px;">음악목록 수정</h1>
|
<h1 style="margin-top:20px;">음악목록 수정</h1>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<section class="dashboardHeader">
|
<section class="dashboardHeader">
|
||||||
<div>
|
<div>
|
||||||
<a class="ghostLink" href="/op/list">← 돌아가기</a>
|
<a class="ghostLink" href="/op/list">← 돌아가기</a>
|
||||||
<h1 style="margin-top:8px;"><%= pack.name %></h1>
|
<h1 style="margin-top:20px;"><%= pack.name %></h1>
|
||||||
<p class="muted"><%= packKey %>.json</p>
|
<p class="muted"><%= packKey %>.json</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user