영상 정보 불러오는 중...
+diff --git a/chrome_extension/README.md b/chrome_extension/README.md new file mode 100644 index 0000000..822622c --- /dev/null +++ b/chrome_extension/README.md @@ -0,0 +1,40 @@ +# GDM YouTube Downloader Chrome Extension + +YouTube 영상을 FlaskFarm GDM(gommi_downloader_manager)으로 전송하여 다운로드하는 Chrome 확장프로그램입니다. + +## 설치 방법 + +1. Chrome에서 `chrome://extensions/` 접속 +2. 우측 상단 **개발자 모드** 활성화 +3. **압축해제된 확장 프로그램을 로드합니다** 클릭 +4. 이 `chrome_extension` 폴더 선택 + +## 사용 방법 + +### 팝업 UI +1. YouTube 영상 페이지에서 확장 아이콘 클릭 +2. **GDM 서버** 주소 입력 (예: `http://192.168.1.100:9099`) +3. 원하는 **품질** 선택 +4. **다운로드 시작** 클릭 + +### 페이지 버튼 (선택) +- YouTube 동영상 페이지에서 자동으로 **GDM** 버튼이 추가됩니다 +- 버튼 클릭 시 최고 품질로 바로 다운로드 전송 + +### 우클릭 메뉴 +- YouTube 페이지에서 우클릭 → **GDM으로 다운로드** + +## API 엔드포인트 + +확장에서 사용하는 GDM API: + +| 엔드포인트 | 용도 | +|-----------|------| +| `GET /gommi_downloader_manager/ajax/queue/youtube_formats?url=...` | 품질 목록 조회 | +| `POST /gommi_downloader_manager/ajax/queue/youtube_add` | 다운로드 추가 | + +## 요구사항 + +- Chrome 88+ (Manifest V3) +- FlaskFarm + gommi_downloader_manager 플러그인 +- yt-dlp 설치됨 diff --git a/chrome_extension/background.js b/chrome_extension/background.js new file mode 100644 index 0000000..827cad2 --- /dev/null +++ b/chrome_extension/background.js @@ -0,0 +1,85 @@ +// GDM YouTube Downloader - Background Service Worker +// Handles extension lifecycle and context menu integration + +// Context menu setup +chrome.runtime.onInstalled.addListener(() => { + chrome.contextMenus.create({ + id: 'gdm-download', + title: 'GDM으로 다운로드', + contexts: ['page', 'link'], + documentUrlPatterns: ['https://www.youtube.com/*', 'https://youtu.be/*'] + }); +}); + +// Context menu click handler +chrome.contextMenus.onClicked.addListener(async (info, tab) => { + if (info.menuItemId === 'gdm-download') { + const url = info.linkUrl || tab.url; + + // Open popup or send directly + const stored = await chrome.storage.local.get(['serverUrl']); + const serverUrl = (stored.serverUrl || 'http://localhost:9099').replace(/\/$/, ''); + + try { + const response = await fetch( + `${serverUrl}/gommi_downloader_manager/ajax/queue/youtube_add`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: url, + format: 'bestvideo+bestaudio/best' + }) + } + ); + + const data = await response.json(); + + if (data.ret === 'success') { + // Show notification + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icons/icon128.png', + title: 'GDM 다운로드', + message: '다운로드가 추가되었습니다!' + }); + } + } catch (error) { + console.error('GDM download error:', error); + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icons/icon128.png', + title: 'GDM 오류', + message: '서버 연결 실패: ' + error.message + }); + } + } +}); + +// Message handler for content script +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'download') { + handleDownload(request.url, request.format).then(sendResponse); + return true; // Async response + } +}); + +async function handleDownload(url, format = 'bestvideo+bestaudio/best') { + const stored = await chrome.storage.local.get(['serverUrl']); + const serverUrl = (stored.serverUrl || 'http://localhost:9099').replace(/\/$/, ''); + + try { + const response = await fetch( + `${serverUrl}/gommi_downloader_manager/ajax/queue/youtube_add`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url, format }) + } + ); + + return await response.json(); + } catch (error) { + return { ret: 'error', msg: error.message }; + } +} diff --git a/chrome_extension/content.css b/chrome_extension/content.css new file mode 100644 index 0000000..e2e0482 --- /dev/null +++ b/chrome_extension/content.css @@ -0,0 +1,38 @@ +/* GDM YouTube Downloader - Content Script Styles */ + +.gdm-yt-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + margin-left: 8px; + background: linear-gradient(135deg, #3b82f6, #2563eb); + border: none; + border-radius: 18px; + color: white; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + font-family: 'Roboto', Arial, sans-serif; +} + +.gdm-yt-btn:hover { + background: linear-gradient(135deg, #60a5fa, #3b82f6); + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); +} + +.gdm-yt-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +.gdm-yt-btn .gdm-icon { + font-size: 14px; +} + +.gdm-yt-btn .gdm-text { + font-weight: 500; +} diff --git a/chrome_extension/content.js b/chrome_extension/content.js new file mode 100644 index 0000000..20110c7 --- /dev/null +++ b/chrome_extension/content.js @@ -0,0 +1,69 @@ +// GDM YouTube Downloader - Content Script +// Optional: Inject download button on YouTube page + +(function() { + 'use strict'; + + // Check if we're on a YouTube video page + if (!window.location.href.includes('youtube.com/watch')) { + return; + } + + // Wait for YouTube player to load + const observer = new MutationObserver((mutations, obs) => { + const actionBar = document.querySelector('#top-level-buttons-computed'); + if (actionBar && !document.getElementById('gdm-download-btn')) { + injectButton(actionBar); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + function injectButton(container) { + const btn = document.createElement('button'); + btn.id = 'gdm-download-btn'; + btn.className = 'gdm-yt-btn'; + btn.innerHTML = ` + + GDM + `; + btn.title = 'GDM으로 다운로드'; + + btn.addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + + btn.disabled = true; + btn.innerHTML = '전송중'; + + try { + const response = await chrome.runtime.sendMessage({ + action: 'download', + url: window.location.href + }); + + if (response && response.ret === 'success') { + btn.innerHTML = '완료'; + setTimeout(() => { + btn.innerHTML = 'GDM'; + btn.disabled = false; + }, 2000); + } else { + throw new Error(response?.msg || 'Unknown error'); + } + } catch (error) { + btn.innerHTML = '실패'; + console.error('GDM Error:', error); + setTimeout(() => { + btn.innerHTML = 'GDM'; + btn.disabled = false; + }, 2000); + } + }); + + container.appendChild(btn); + } +})(); diff --git a/chrome_extension/icons/icon128.png b/chrome_extension/icons/icon128.png new file mode 100644 index 0000000..fff8b8f Binary files /dev/null and b/chrome_extension/icons/icon128.png differ diff --git a/chrome_extension/icons/icon16.png b/chrome_extension/icons/icon16.png new file mode 100644 index 0000000..5f7d0d0 Binary files /dev/null and b/chrome_extension/icons/icon16.png differ diff --git a/chrome_extension/icons/icon48.png b/chrome_extension/icons/icon48.png new file mode 100644 index 0000000..6938720 Binary files /dev/null and b/chrome_extension/icons/icon48.png differ diff --git a/chrome_extension/manifest.json b/chrome_extension/manifest.json new file mode 100644 index 0000000..04f0ec7 --- /dev/null +++ b/chrome_extension/manifest.json @@ -0,0 +1,39 @@ +{ + "manifest_version": 3, + "name": "GDM YouTube Downloader", + "version": "1.0.0", + "description": "YouTube 영상을 GDM(gommi_downloader_manager)으로 전송하여 다운로드", + "permissions": [ + "activeTab", + "storage" + ], + "host_permissions": [ + "https://www.youtube.com/*", + "https://youtu.be/*", + "http://localhost:*/*", + "http://127.0.0.1:*/*" + ], + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } + }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "background": { + "service_worker": "background.js" + }, + "content_scripts": [ + { + "matches": ["https://www.youtube.com/*"], + "js": ["content.js"], + "css": ["content.css"] + } + ] +} diff --git a/chrome_extension/popup.css b/chrome_extension/popup.css new file mode 100644 index 0000000..8895c69 --- /dev/null +++ b/chrome_extension/popup.css @@ -0,0 +1,263 @@ +/* GDM YouTube Downloader - Popup Styles */ + +:root { + --primary: #3b82f6; + --primary-hover: #2563eb; + --success: #10b981; + --error: #ef4444; + --bg-dark: #0f172a; + --bg-card: #1e293b; + --text: #e2e8f0; + --text-muted: #94a3b8; + --border: rgba(255, 255, 255, 0.1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + width: 360px; + min-height: 400px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg-dark); + color: var(--text); +} + +.container { + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +header { + display: flex; + align-items: center; + gap: 8px; +} + +header h1 { + font-size: 18px; + font-weight: 600; +} + +.section { + display: flex; + flex-direction: column; + gap: 12px; +} + +.hidden { + display: none !important; +} + +/* Loading */ +.spinner { + width: 40px; + height: 40px; + border: 3px solid var(--border); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 20px auto; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Video Info */ +.video-info { + display: flex; + gap: 12px; + padding: 12px; + background: var(--bg-card); + border-radius: 12px; + border: 1px solid var(--border); +} + +.video-info img { + width: 120px; + height: 68px; + object-fit: cover; + border-radius: 8px; +} + +.video-meta { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.video-meta h3 { + font-size: 14px; + font-weight: 500; + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.duration { + font-size: 12px; + color: var(--text-muted); +} + +/* Quality Section */ +.quality-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.quality-section label { + font-size: 13px; + font-weight: 600; + color: var(--text-muted); +} + +.quality-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +.quality-option { + padding: 10px 12px; + background: var(--bg-card); + border: 2px solid var(--border); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + text-align: center; +} + +.quality-option:hover { + border-color: var(--primary); + background: rgba(59, 130, 246, 0.1); +} + +.quality-option.selected { + border-color: var(--primary); + background: rgba(59, 130, 246, 0.2); +} + +.quality-option .label { + font-size: 13px; + font-weight: 600; +} + +.quality-option .note { + font-size: 11px; + color: var(--text-muted); +} + +/* Server Section */ +.server-section { + display: flex; + flex-direction: column; + gap: 6px; +} + +.server-section label { + font-size: 13px; + font-weight: 600; + color: var(--text-muted); +} + +.server-section input { + padding: 10px 12px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + font-size: 13px; +} + +.server-section input:focus { + outline: none; + border-color: var(--primary); +} + +.server-section small { + font-size: 11px; + color: var(--text-muted); +} + +/* Buttons */ +.btn { + padding: 12px 16px; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + color: white; +} + +.btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); +} + +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.btn-secondary { + background: var(--bg-card); + color: var(--text); + border: 1px solid var(--border); +} + +/* Status */ +.status { + padding: 10px 12px; + border-radius: 8px; + font-size: 13px; + text-align: center; +} + +.status.success { + background: rgba(16, 185, 129, 0.2); + color: var(--success); +} + +.status.error { + background: rgba(239, 68, 68, 0.2); + color: var(--error); +} + +.status.info { + background: rgba(59, 130, 246, 0.2); + color: var(--primary); +} + +/* Error */ +.error-text { + color: var(--error); + text-align: center; +} + +footer { + text-align: center; + color: var(--text-muted); + padding-top: 8px; + border-top: 1px solid var(--border); +} diff --git a/chrome_extension/popup.html b/chrome_extension/popup.html new file mode 100644 index 0000000..90f9f7b --- /dev/null +++ b/chrome_extension/popup.html @@ -0,0 +1,76 @@ + + +
+ +영상 정보 불러오는 중...
+