Deepfake Authenticator commited on
Commit Β·
6c71866
1
Parent(s): 0f27de7
feat: auto-analyze + YouTube support in browser extension
Browse filesExtension:
- Auto-analyze: popup detects video and starts analysis immediately (400ms delay)
- Comprehensive video detection: YouTube watch/shorts/youtu.be, Twitter/X,
Instagram, Facebook, generic <video>, og:video meta tags
- YouTube sends URL to /analyze-url backend endpoint
- Direct URLs fetched by background service worker
- Results saved to chrome.storage for popup last-result display
Backend:
- New POST /analyze-url endpoint for browser extension
- yt-dlp integration: downloads YouTube/Twitter/Instagram/TikTok videos
- httpx fallback for direct .mp4/.webm URLs
- Auto-cleanup of temp files
- Added yt-dlp and httpx to requirements.txt
- backend/main.py +65 -0
- backend/requirements.txt +4 -0
- extension/background.js +36 -9
- extension/content.js +16 -10
- extension/popup.js +103 -35
backend/main.py
CHANGED
|
@@ -75,6 +75,71 @@ async def health():
|
|
| 75 |
}
|
| 76 |
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
@app.post("/analyze")
|
| 79 |
async def analyze_video(file: UploadFile = File(...)):
|
| 80 |
"""
|
|
|
|
| 75 |
}
|
| 76 |
|
| 77 |
|
| 78 |
+
@app.post("/analyze-url")
|
| 79 |
+
async def analyze_from_url(payload: dict):
|
| 80 |
+
"""Download a video from a URL and analyze it. Used by the browser extension."""
|
| 81 |
+
if not authenticator:
|
| 82 |
+
raise HTTPException(status_code=503, detail="Server is still initializing")
|
| 83 |
+
|
| 84 |
+
video_url = payload.get("url", "").strip()
|
| 85 |
+
if not video_url:
|
| 86 |
+
raise HTTPException(status_code=400, detail="No URL provided")
|
| 87 |
+
|
| 88 |
+
tmp_path = UPLOAD_DIR / f"ext_{uuid.uuid4().hex}.mp4"
|
| 89 |
+
downloaded = False
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
# Try yt-dlp first (YouTube, Twitter, Instagram, TikTok, etc.)
|
| 93 |
+
try:
|
| 94 |
+
import yt_dlp
|
| 95 |
+
ydl_opts = {
|
| 96 |
+
"format": "bestvideo[ext=mp4][height<=720]+bestaudio[ext=m4a]/best[ext=mp4][height<=720]/best",
|
| 97 |
+
"outtmpl": str(tmp_path),
|
| 98 |
+
"quiet": True,
|
| 99 |
+
"no_warnings": True,
|
| 100 |
+
"merge_output_format": "mp4",
|
| 101 |
+
}
|
| 102 |
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
| 103 |
+
ydl.download([video_url])
|
| 104 |
+
downloaded = tmp_path.exists() and tmp_path.stat().st_size > 1000
|
| 105 |
+
if downloaded:
|
| 106 |
+
logger.info(f"yt-dlp downloaded {tmp_path.stat().st_size // 1024}KB")
|
| 107 |
+
except ImportError:
|
| 108 |
+
logger.info("yt-dlp not installed β trying direct HTTP fetch")
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.warning(f"yt-dlp failed ({e}) β trying direct fetch")
|
| 111 |
+
|
| 112 |
+
# Fallback: direct HTTP fetch for plain .mp4/.webm URLs
|
| 113 |
+
if not downloaded:
|
| 114 |
+
try:
|
| 115 |
+
import httpx
|
| 116 |
+
async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client:
|
| 117 |
+
r = await client.get(video_url, headers={"User-Agent": "Mozilla/5.0"})
|
| 118 |
+
if r.status_code == 200:
|
| 119 |
+
tmp_path.write_bytes(r.content)
|
| 120 |
+
downloaded = tmp_path.exists() and tmp_path.stat().st_size > 1000
|
| 121 |
+
except Exception as e:
|
| 122 |
+
logger.warning(f"Direct fetch failed: {e}")
|
| 123 |
+
|
| 124 |
+
if not downloaded:
|
| 125 |
+
raise HTTPException(
|
| 126 |
+
status_code=400,
|
| 127 |
+
detail="Could not download video. For YouTube, install yt-dlp: pip install yt-dlp"
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
result = authenticator.analyze(str(tmp_path))
|
| 131 |
+
return result
|
| 132 |
+
|
| 133 |
+
except HTTPException:
|
| 134 |
+
raise
|
| 135 |
+
except Exception as e:
|
| 136 |
+
logger.exception(f"analyze-url failed: {e}")
|
| 137 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 138 |
+
finally:
|
| 139 |
+
if tmp_path.exists():
|
| 140 |
+
tmp_path.unlink()
|
| 141 |
+
|
| 142 |
+
|
| 143 |
@app.post("/analyze")
|
| 144 |
async def analyze_video(file: UploadFile = File(...)):
|
| 145 |
"""
|
backend/requirements.txt
CHANGED
|
@@ -16,3 +16,7 @@ torchaudio
|
|
| 16 |
moviepy>=1.0.3
|
| 17 |
librosa>=0.10.0
|
| 18 |
soundfile>=0.12.1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
moviepy>=1.0.3
|
| 17 |
librosa>=0.10.0
|
| 18 |
soundfile>=0.12.1
|
| 19 |
+
|
| 20 |
+
# Browser extension support
|
| 21 |
+
yt-dlp>=2024.1.0
|
| 22 |
+
httpx>=0.27.0
|
extension/background.js
CHANGED
|
@@ -49,9 +49,23 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
|
| 49 |
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
| 50 |
if (msg.type === 'ANALYZE_URL') {
|
| 51 |
analyzeVideoUrl(msg.url)
|
| 52 |
-
.then(result =>
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
|
| 57 |
if (msg.type === 'CHECK_HEALTH') {
|
|
@@ -63,26 +77,39 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
| 63 |
}
|
| 64 |
|
| 65 |
if (msg.type === 'ANALYZE_FILE_BYTES') {
|
| 66 |
-
// Receive base64 video bytes from content script, send to backend
|
| 67 |
analyzeBase64Video(msg.data, msg.filename, msg.mimeType)
|
| 68 |
-
.then(result =>
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
| 70 |
return true;
|
| 71 |
}
|
| 72 |
});
|
| 73 |
|
| 74 |
// ββ Analyze a video by URL ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 75 |
async function analyzeVideoUrl(videoUrl) {
|
| 76 |
-
// First fetch the video as a blob
|
| 77 |
const response = await fetch(videoUrl);
|
| 78 |
if (!response.ok) throw new Error(`Failed to fetch video: ${response.status}`);
|
| 79 |
-
|
| 80 |
const blob = await response.blob();
|
| 81 |
const filename = videoUrl.split('/').pop().split('?')[0] || 'video.mp4';
|
| 82 |
-
|
| 83 |
return sendToBackend(blob, filename);
|
| 84 |
}
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
// ββ Analyze base64 video data βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 87 |
async function analyzeBase64Video(base64Data, filename, mimeType) {
|
| 88 |
const byteString = atob(base64Data);
|
|
|
|
| 49 |
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
| 50 |
if (msg.type === 'ANALYZE_URL') {
|
| 51 |
analyzeVideoUrl(msg.url)
|
| 52 |
+
.then(result => {
|
| 53 |
+
chrome.storage.local.set({ lastResult: { ...result, file: msg.url.split('/').pop().slice(0,40) } });
|
| 54 |
+
sendResponse({ ok: true, result });
|
| 55 |
+
})
|
| 56 |
+
.catch(err => sendResponse({ ok: false, error: err.message }));
|
| 57 |
+
return true;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if (msg.type === 'ANALYZE_YOUTUBE') {
|
| 61 |
+
// Send YouTube URL directly to backend β backend handles yt-dlp download
|
| 62 |
+
analyzeYouTubeUrl(msg.url)
|
| 63 |
+
.then(result => {
|
| 64 |
+
chrome.storage.local.set({ lastResult: { ...result, file: 'YouTube video' } });
|
| 65 |
+
sendResponse({ ok: true, result });
|
| 66 |
+
})
|
| 67 |
+
.catch(err => sendResponse({ ok: false, error: err.message }));
|
| 68 |
+
return true;
|
| 69 |
}
|
| 70 |
|
| 71 |
if (msg.type === 'CHECK_HEALTH') {
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
if (msg.type === 'ANALYZE_FILE_BYTES') {
|
|
|
|
| 80 |
analyzeBase64Video(msg.data, msg.filename, msg.mimeType)
|
| 81 |
+
.then(result => {
|
| 82 |
+
chrome.storage.local.set({ lastResult: { ...result, file: msg.filename } });
|
| 83 |
+
sendResponse({ ok: true, result });
|
| 84 |
+
})
|
| 85 |
+
.catch(err => sendResponse({ ok: false, error: err.message }));
|
| 86 |
return true;
|
| 87 |
}
|
| 88 |
});
|
| 89 |
|
| 90 |
// ββ Analyze a video by URL ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 91 |
async function analyzeVideoUrl(videoUrl) {
|
|
|
|
| 92 |
const response = await fetch(videoUrl);
|
| 93 |
if (!response.ok) throw new Error(`Failed to fetch video: ${response.status}`);
|
|
|
|
| 94 |
const blob = await response.blob();
|
| 95 |
const filename = videoUrl.split('/').pop().split('?')[0] || 'video.mp4';
|
|
|
|
| 96 |
return sendToBackend(blob, filename);
|
| 97 |
}
|
| 98 |
|
| 99 |
+
// ββ Analyze YouTube URL via backend ββββββββββββββββββββββββββββββββββββββββββ
|
| 100 |
+
async function analyzeYouTubeUrl(youtubeUrl) {
|
| 101 |
+
const res = await fetch(`${API_BASE}/analyze-url`, {
|
| 102 |
+
method: 'POST',
|
| 103 |
+
headers: { 'Content-Type': 'application/json' },
|
| 104 |
+
body: JSON.stringify({ url: youtubeUrl }),
|
| 105 |
+
});
|
| 106 |
+
if (!res.ok) {
|
| 107 |
+
const err = await res.json().catch(() => ({}));
|
| 108 |
+
throw new Error(err.detail || `Backend error ${res.status}`);
|
| 109 |
+
}
|
| 110 |
+
return res.json();
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
// ββ Analyze base64 video data βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 114 |
async function analyzeBase64Video(base64Data, filename, mimeType) {
|
| 115 |
const byteString = atob(base64Data);
|
extension/content.js
CHANGED
|
@@ -122,20 +122,26 @@ async function startAnalysis(videoUrl) {
|
|
| 122 |
try {
|
| 123 |
// Check if server is running first
|
| 124 |
const health = await fetch('http://localhost:8000/health').catch(() => null);
|
| 125 |
-
if (!health?.ok)
|
| 126 |
-
throw new Error('SERVER_OFFLINE');
|
| 127 |
-
}
|
| 128 |
|
| 129 |
-
|
| 130 |
-
const response = await chrome.runtime.sendMessage({
|
| 131 |
-
type: 'ANALYZE_URL',
|
| 132 |
-
url: videoUrl,
|
| 133 |
-
});
|
| 134 |
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
|
|
|
| 137 |
if (!response.ok) throw new Error(response.error || 'Analysis failed');
|
| 138 |
-
|
| 139 |
renderResult(response.result, videoUrl);
|
| 140 |
} catch (err) {
|
| 141 |
stopStepAnimation();
|
|
|
|
| 122 |
try {
|
| 123 |
// Check if server is running first
|
| 124 |
const health = await fetch('http://localhost:8000/health').catch(() => null);
|
| 125 |
+
if (!health?.ok) throw new Error('SERVER_OFFLINE');
|
|
|
|
|
|
|
| 126 |
|
| 127 |
+
const isYouTube = videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
+
let response;
|
| 130 |
+
if (isYouTube) {
|
| 131 |
+
// For YouTube, send the URL to backend's /analyze-url endpoint
|
| 132 |
+
response = await chrome.runtime.sendMessage({
|
| 133 |
+
type: 'ANALYZE_YOUTUBE',
|
| 134 |
+
url: videoUrl,
|
| 135 |
+
});
|
| 136 |
+
} else {
|
| 137 |
+
response = await chrome.runtime.sendMessage({
|
| 138 |
+
type: 'ANALYZE_URL',
|
| 139 |
+
url: videoUrl,
|
| 140 |
+
});
|
| 141 |
+
}
|
| 142 |
|
| 143 |
+
stopStepAnimation();
|
| 144 |
if (!response.ok) throw new Error(response.error || 'Analysis failed');
|
|
|
|
| 145 |
renderResult(response.result, videoUrl);
|
| 146 |
} catch (err) {
|
| 147 |
stopStepAnimation();
|
extension/popup.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
| 1 |
/**
|
| 2 |
* Authrix Extension β Popup Script
|
|
|
|
| 3 |
*/
|
| 4 |
|
| 5 |
const API_BASE = 'http://localhost:8000';
|
| 6 |
|
| 7 |
// ββ Init ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 8 |
document.addEventListener('DOMContentLoaded', async () => {
|
| 9 |
-
await checkHealth();
|
| 10 |
-
await detectPageVideo();
|
| 11 |
loadLastResult();
|
| 12 |
wireButtons();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
});
|
| 14 |
|
| 15 |
-
// ββ Health check βββββββββββββββββββββββββββββββββββββ
|
| 16 |
async function checkHealth() {
|
| 17 |
-
const badge
|
| 18 |
const modelInfo = document.getElementById('modelInfo');
|
| 19 |
try {
|
| 20 |
const res = await fetch(`${API_BASE}/health`);
|
|
@@ -23,16 +32,18 @@ async function checkHealth() {
|
|
| 23 |
badge.textContent = 'ONLINE';
|
| 24 |
badge.classList.remove('offline');
|
| 25 |
modelInfo.textContent = (d.model || 'VIT ENSEMBLE').toUpperCase().slice(0, 22);
|
| 26 |
-
|
| 27 |
-
badge.textContent = 'LOADING';
|
| 28 |
}
|
|
|
|
|
|
|
| 29 |
} catch {
|
| 30 |
badge.textContent = 'OFFLINE';
|
| 31 |
badge.classList.add('offline');
|
|
|
|
| 32 |
}
|
| 33 |
}
|
| 34 |
|
| 35 |
-
// ββ Detect video on current tab ββββββββββββββββββββββ
|
| 36 |
async function detectPageVideo() {
|
| 37 |
const btn = document.getElementById('btnAnalyzePage');
|
| 38 |
const title = document.getElementById('pageTitle');
|
|
@@ -40,7 +51,7 @@ async function detectPageVideo() {
|
|
| 40 |
|
| 41 |
try {
|
| 42 |
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
| 43 |
-
if (!tab?.id) return;
|
| 44 |
|
| 45 |
const results = await chrome.scripting.executeScript({
|
| 46 |
target: { tabId: tab.id },
|
|
@@ -49,54 +60,113 @@ async function detectPageVideo() {
|
|
| 49 |
|
| 50 |
const found = results?.[0]?.result;
|
| 51 |
if (found?.url) {
|
| 52 |
-
title.textContent = found.title || 'Video detected';
|
| 53 |
sub.textContent = truncateUrl(found.url);
|
| 54 |
-
btn.disabled
|
| 55 |
-
btn.dataset.url
|
|
|
|
| 56 |
} else {
|
| 57 |
title.textContent = 'No video detected';
|
| 58 |
sub.textContent = 'Try right-clicking a video element';
|
| 59 |
-
btn.disabled
|
|
|
|
| 60 |
}
|
| 61 |
} catch {
|
| 62 |
title.textContent = 'Cannot access this page';
|
| 63 |
sub.textContent = 'Extension pages are restricted';
|
|
|
|
| 64 |
}
|
| 65 |
}
|
| 66 |
|
| 67 |
-
// ββ Runs in page context βββββββββββββββββββββ
|
| 68 |
function detectVideosOnPage() {
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
const videos = Array.from(document.querySelectorAll('video'));
|
| 71 |
for (const v of videos) {
|
| 72 |
-
|
| 73 |
-
if (src && src.startsWith('http')) {
|
| 74 |
-
return { url: src, title: document.title };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
}
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
| 81 |
}
|
|
|
|
| 82 |
return null;
|
| 83 |
}
|
| 84 |
|
| 85 |
// ββ Wire buttons ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 86 |
function wireButtons() {
|
| 87 |
-
// Analyze page video
|
| 88 |
document.getElementById('btnAnalyzePage').addEventListener('click', async () => {
|
| 89 |
-
const
|
| 90 |
-
|
| 91 |
-
if (!url) return;
|
| 92 |
-
await injectAndAnalyze(url);
|
| 93 |
});
|
| 94 |
|
| 95 |
-
// Manual URL
|
| 96 |
document.getElementById('btnGo').addEventListener('click', async () => {
|
| 97 |
const url = document.getElementById('urlInput').value.trim();
|
| 98 |
-
if (
|
| 99 |
-
await injectAndAnalyze(url);
|
| 100 |
});
|
| 101 |
|
| 102 |
document.getElementById('urlInput').addEventListener('keydown', async (e) => {
|
|
@@ -107,7 +177,7 @@ function wireButtons() {
|
|
| 107 |
});
|
| 108 |
}
|
| 109 |
|
| 110 |
-
// ββ Inject overlay
|
| 111 |
async function injectAndAnalyze(url) {
|
| 112 |
try {
|
| 113 |
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
@@ -123,8 +193,7 @@ async function injectAndAnalyze(url) {
|
|
| 123 |
args: [url],
|
| 124 |
});
|
| 125 |
|
| 126 |
-
//
|
| 127 |
-
window.close();
|
| 128 |
} catch (err) {
|
| 129 |
console.error('[Authrix popup]', err);
|
| 130 |
}
|
|
@@ -154,12 +223,11 @@ function loadLastResult() {
|
|
| 154 |
});
|
| 155 |
}
|
| 156 |
|
| 157 |
-
// ββ Utilities ββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½ββββββββββββ
|
| 158 |
function truncateUrl(url) {
|
| 159 |
try {
|
| 160 |
const u = new URL(url);
|
| 161 |
-
return u.hostname + u.pathname.slice(0,
|
| 162 |
} catch {
|
| 163 |
-
return url.slice(0,
|
| 164 |
}
|
| 165 |
}
|
|
|
|
| 1 |
/**
|
| 2 |
* Authrix Extension β Popup Script
|
| 3 |
+
* Auto-detects and auto-analyzes videos on the current page.
|
| 4 |
*/
|
| 5 |
|
| 6 |
const API_BASE = 'http://localhost:8000';
|
| 7 |
|
| 8 |
// ββ Init ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
document.addEventListener('DOMContentLoaded', async () => {
|
| 10 |
+
const [healthOk, found] = await Promise.all([checkHealth(), detectPageVideo()]);
|
|
|
|
| 11 |
loadLastResult();
|
| 12 |
wireButtons();
|
| 13 |
+
|
| 14 |
+
// AUTO-ANALYZE: if server is online and a video was found, start immediately
|
| 15 |
+
if (healthOk && found) {
|
| 16 |
+
const btn = document.getElementById('btnAnalyzePage');
|
| 17 |
+
if (!btn.disabled) {
|
| 18 |
+
// Small delay so user sees the popup before it closes
|
| 19 |
+
setTimeout(() => btn.click(), 400);
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
});
|
| 23 |
|
| 24 |
+
// ββ Health check β returns true if ready βββββββββββββββββββββββββββββββββββββ
|
| 25 |
async function checkHealth() {
|
| 26 |
+
const badge = document.getElementById('statusBadge');
|
| 27 |
const modelInfo = document.getElementById('modelInfo');
|
| 28 |
try {
|
| 29 |
const res = await fetch(`${API_BASE}/health`);
|
|
|
|
| 32 |
badge.textContent = 'ONLINE';
|
| 33 |
badge.classList.remove('offline');
|
| 34 |
modelInfo.textContent = (d.model || 'VIT ENSEMBLE').toUpperCase().slice(0, 22);
|
| 35 |
+
return true;
|
|
|
|
| 36 |
}
|
| 37 |
+
badge.textContent = 'LOADING';
|
| 38 |
+
return false;
|
| 39 |
} catch {
|
| 40 |
badge.textContent = 'OFFLINE';
|
| 41 |
badge.classList.add('offline');
|
| 42 |
+
return false;
|
| 43 |
}
|
| 44 |
}
|
| 45 |
|
| 46 |
+
// ββ Detect video on current tab β returns true if found ββββββββββββββββββββββ
|
| 47 |
async function detectPageVideo() {
|
| 48 |
const btn = document.getElementById('btnAnalyzePage');
|
| 49 |
const title = document.getElementById('pageTitle');
|
|
|
|
| 51 |
|
| 52 |
try {
|
| 53 |
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
| 54 |
+
if (!tab?.id) return false;
|
| 55 |
|
| 56 |
const results = await chrome.scripting.executeScript({
|
| 57 |
target: { tabId: tab.id },
|
|
|
|
| 60 |
|
| 61 |
const found = results?.[0]?.result;
|
| 62 |
if (found?.url) {
|
| 63 |
+
title.textContent = found.label || found.title || 'Video detected';
|
| 64 |
sub.textContent = truncateUrl(found.url);
|
| 65 |
+
btn.disabled = false;
|
| 66 |
+
btn.dataset.url = found.url;
|
| 67 |
+
return true;
|
| 68 |
} else {
|
| 69 |
title.textContent = 'No video detected';
|
| 70 |
sub.textContent = 'Try right-clicking a video element';
|
| 71 |
+
btn.disabled = true;
|
| 72 |
+
return false;
|
| 73 |
}
|
| 74 |
} catch {
|
| 75 |
title.textContent = 'Cannot access this page';
|
| 76 |
sub.textContent = 'Extension pages are restricted';
|
| 77 |
+
return false;
|
| 78 |
}
|
| 79 |
}
|
| 80 |
|
| 81 |
+
// ββ Runs in page context β comprehensive video detection βββββββββββββββββββββ
|
| 82 |
function detectVideosOnPage() {
|
| 83 |
+
const url = location.href;
|
| 84 |
+
const host = location.hostname;
|
| 85 |
+
|
| 86 |
+
// ββ YouTube ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 87 |
+
// youtube.com/watch?v=... or youtu.be/...
|
| 88 |
+
const ytWatch = url.match(/[?&]v=([a-zA-Z0-9_-]{11})/);
|
| 89 |
+
const ytShort = url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
|
| 90 |
+
const ytShorts = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/);
|
| 91 |
+
const ytId = (ytWatch?.[1] || ytShort?.[1] || ytShorts?.[1]);
|
| 92 |
+
if (ytId) {
|
| 93 |
+
return {
|
| 94 |
+
url: `https://www.youtube.com/watch?v=${ytId}`,
|
| 95 |
+
label: 'βΆ YouTube: ' + (document.title.replace(' - YouTube','').slice(0,40)),
|
| 96 |
+
title: document.title,
|
| 97 |
+
type: 'youtube',
|
| 98 |
+
};
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// ββ Twitter / X βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 102 |
+
if (host.includes('twitter.com') || host.includes('x.com')) {
|
| 103 |
+
const twitterVid = document.querySelector('video[src*="video.twimg.com"]');
|
| 104 |
+
if (twitterVid?.src) {
|
| 105 |
+
return { url: twitterVid.src, label: 'βΆ Twitter/X video', title: document.title, type: 'direct' };
|
| 106 |
+
}
|
| 107 |
+
// Try blob β find the highest quality source
|
| 108 |
+
const allVids = Array.from(document.querySelectorAll('video'));
|
| 109 |
+
for (const v of allVids) {
|
| 110 |
+
const sources = Array.from(v.querySelectorAll('source'));
|
| 111 |
+
for (const s of sources) {
|
| 112 |
+
if (s.src && s.src.startsWith('http')) {
|
| 113 |
+
return { url: s.src, label: 'βΆ Twitter/X video', title: document.title, type: 'direct' };
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// ββ Instagram βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 120 |
+
if (host.includes('instagram.com')) {
|
| 121 |
+
const igVid = document.querySelector('video');
|
| 122 |
+
if (igVid?.src && igVid.src.startsWith('http')) {
|
| 123 |
+
return { url: igVid.src, label: 'βΆ Instagram video', title: document.title, type: 'direct' };
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// ββ Facebook ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 128 |
+
if (host.includes('facebook.com') || host.includes('fb.watch')) {
|
| 129 |
+
const fbVid = document.querySelector('video[src*="fbcdn"]');
|
| 130 |
+
if (fbVid?.src) {
|
| 131 |
+
return { url: fbVid.src, label: 'βΆ Facebook video', title: document.title, type: 'direct' };
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// ββ Generic <video> with direct HTTP src βββββββββββββββββββββββββββββββββ
|
| 136 |
const videos = Array.from(document.querySelectorAll('video'));
|
| 137 |
for (const v of videos) {
|
| 138 |
+
// Direct src
|
| 139 |
+
if (v.src && v.src.startsWith('http') && !v.src.startsWith('blob:')) {
|
| 140 |
+
return { url: v.src, label: 'βΆ Video on page', title: document.title, type: 'direct' };
|
| 141 |
+
}
|
| 142 |
+
// <source> children
|
| 143 |
+
const sources = Array.from(v.querySelectorAll('source'));
|
| 144 |
+
for (const s of sources) {
|
| 145 |
+
if (s.src && s.src.startsWith('http')) {
|
| 146 |
+
return { url: s.src, label: 'βΆ Video on page', title: document.title, type: 'direct' };
|
| 147 |
+
}
|
| 148 |
}
|
| 149 |
}
|
| 150 |
+
|
| 151 |
+
// ββ OG / meta video tag βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 152 |
+
const ogVideo = document.querySelector('meta[property="og:video"], meta[property="og:video:url"]');
|
| 153 |
+
if (ogVideo?.content) {
|
| 154 |
+
return { url: ogVideo.content, label: 'βΆ Embedded video', title: document.title, type: 'meta' };
|
| 155 |
}
|
| 156 |
+
|
| 157 |
return null;
|
| 158 |
}
|
| 159 |
|
| 160 |
// ββ Wire buttons ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 161 |
function wireButtons() {
|
|
|
|
| 162 |
document.getElementById('btnAnalyzePage').addEventListener('click', async () => {
|
| 163 |
+
const url = document.getElementById('btnAnalyzePage').dataset.url;
|
| 164 |
+
if (url) await injectAndAnalyze(url);
|
|
|
|
|
|
|
| 165 |
});
|
| 166 |
|
|
|
|
| 167 |
document.getElementById('btnGo').addEventListener('click', async () => {
|
| 168 |
const url = document.getElementById('urlInput').value.trim();
|
| 169 |
+
if (url) await injectAndAnalyze(url);
|
|
|
|
| 170 |
});
|
| 171 |
|
| 172 |
document.getElementById('urlInput').addEventListener('keydown', async (e) => {
|
|
|
|
| 177 |
});
|
| 178 |
}
|
| 179 |
|
| 180 |
+
// ββ Inject overlay and trigger analysis ββββββββββββββββββββββββββββββββββββββ
|
| 181 |
async function injectAndAnalyze(url) {
|
| 182 |
try {
|
| 183 |
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
|
|
| 193 |
args: [url],
|
| 194 |
});
|
| 195 |
|
| 196 |
+
window.close(); // close popup so overlay is visible
|
|
|
|
| 197 |
} catch (err) {
|
| 198 |
console.error('[Authrix popup]', err);
|
| 199 |
}
|
|
|
|
| 223 |
});
|
| 224 |
}
|
| 225 |
|
|
|
|
| 226 |
function truncateUrl(url) {
|
| 227 |
try {
|
| 228 |
const u = new URL(url);
|
| 229 |
+
return u.hostname + u.pathname.slice(0, 28);
|
| 230 |
} catch {
|
| 231 |
+
return url.slice(0, 45);
|
| 232 |
}
|
| 233 |
}
|