Files
Gan, Jimmy a0ffa41c9e feat: replace yt-dlp cookies with YouTube Data API v3 OAuth
- Add auth.py: OAuth credential manager with auto-refresh
- Rewrite fetcher.py: use googleapiclient subscriptions/search
- Add google-auth, google-api-python-client dependencies
- Update docker-compose for config volume mount
- Add .gitignore for secrets (client_secret.json, token.pickle)
- Live test: 198 videos ingested from 229 channels
2026-06-24 00:14:16 +08:00

98 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Export YouTube cookies from Chrome. Run: ytc"""
import os, sys, sqlite3, subprocess, hashlib, shutil, tempfile
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
OUTPUT = REPO_ROOT / "config" / "cookies.txt"
COOKIES_DB = Path.home() / "Library/Application Support/Google/Chrome/Default/Cookies"
SALT = b'saltysalt'
KEYLEN = 16 # AES-128
PBKDF2_ITERS = 1003
def get_key():
r = subprocess.run(
['security', 'find-generic-password', '-w', '-s', 'Chrome Safe Storage'],
capture_output=True, text=True, timeout=30
)
if r.returncode != 0:
print("ERROR: Keychain access failed. Approve the dialog.", file=sys.stderr)
sys.exit(1)
return hashlib.pbkdf2_hmac('sha1', r.stdout.strip().encode(), SALT, PBKDF2_ITERS, KEYLEN)
def decrypt(blob, key):
"""Decrypt Chrome v10 encrypted value. Returns str or None."""
if not blob or not blob.startswith(b'v10'):
return None
try:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
iv = blob[3:19]
ct = blob[19:]
c = Cipher(algorithms.AES(key), modes.CBC(iv))
d = c.decryptor()
pt = d.update(ct) + d.finalize()
u = padding.PKCS7(128).unpadder()
return (u.update(pt) + u.finalize()).decode('utf-8', errors='replace')
except Exception as e:
return None
def main():
print("Getting Chrome key from Keychain... (approve dialog)")
key = get_key()
# Copy DB to avoid lock
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
tmp = f.name
shutil.copy2(COOKIES_DB, tmp)
conn = sqlite3.connect(tmp)
conn.text_factory = bytes
rows = conn.execute(
"SELECT host_key, name, encrypted_value, value, path, expires_utc, is_secure, is_httponly "
"FROM cookies WHERE host_key LIKE '%youtube.com' ORDER BY name"
).fetchall()
conn.close()
os.unlink(tmp)
print(f"Found {len(rows)} YouTube cookies")
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
ok, fail, skipped = 0, 0, 0
with open(OUTPUT, 'w') as f:
f.write("# Netscape HTTP Cookie File\n# Extracted from Chrome\n\n")
for hk, name, enc_val, raw_val, path, expires, secure, httponly in rows:
val = decrypt(enc_val, key)
if val is None and raw_val:
val = raw_val.decode('utf-8', errors='replace')
if not val:
n = name.decode() if isinstance(name, bytes) else name
print(f" SKIP: {n} (decrypt failed, no raw value)")
skipped += 1
continue
d = hk.decode() if isinstance(hk, bytes) else hk
n = name.decode() if isinstance(name, bytes) else name
p = path.decode() if isinstance(path, bytes) else path
flag = "TRUE" if d.startswith('.') else "FALSE"
s = "TRUE" if secure else "FALSE"
e = int(expires / 1_000_000) if expires and expires > 0 else 0
if httponly:
f.write(f"#HttpOnly_{d}\t{flag}\t{p}\t{s}\t{e}\t{n}\t{val}\n")
f.write(f"{d}\t{flag}\t{p}\t{s}\t{e}\t{n}\t{val}\n")
ok += 1
print(f"\nExported: {ok} Failed: {fail} Skipped: {skipped}")
print(f"Output: {OUTPUT} ({OUTPUT.stat().st_size} bytes)")
if __name__ == '__main__':
main()