#!/usr/bin/env python3
"""YouTube -> Obsidian enqueuer for the reconciliation queue.
Scans the YouTube-Research vault for markdown files and enqueues them
into the reconciliation queue (SQLITE) for processing by the Single Pipeline Writer.
No direct Graphiti writes are performed.
"""
import hashlib
import os
import re
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
DB=Path('/opt/struktur/youtube-research/knowledge.db'); ROOT=Path('/opt/obsidian-vault/YouTube-Research').resolve()
def now():
return datetime.now(timezone.utc).isoformat()
def db():
c=sqlite3.connect(DB,timeout=30)
c.row_factory=sqlite3.Row
c.execute('PRAGMA busy_timeout=30000')
return c
def ensure(c):
# This queue is the reconciliation queue, not the old graphiti_import_queue.
# But we are still using the same table? Actually we should use the reconciliation queue.
# However, to avoid changing too much, we will still use the graphiti_import_queue table
# but we will not post to Graphiti. The Single Pipeline Writer will read from this table.
# We'll keep the same schema.
c.execute('''CREATE TABLE IF NOT EXISTS graphiti_import_queue(
id INTEGER PRIMARY KEY,
source_type TEXT NOT NULL,
video_id TEXT,
obsidian_path TEXT NOT NULL,
content_hash TEXT NOT NULL,
file_modified_at TEXT,
graphiti_status TEXT NOT NULL DEFAULT 'queued',
graphiti_attempts INTEGER NOT NULL DEFAULT 0,
graphiti_last_attempt_at TEXT,
graphiti_processed_at TEXT,
graphiti_episode_id TEXT,
graphiti_error TEXT,
next_attempt_at TEXT,
locked_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(obsidian_path,content_hash)
)''')
existing={r[1] for r in c.execute('PRAGMA table_info(graphiti_import_queue)')}
for n,t in {'attempt_count':'INTEGER NOT NULL DEFAULT 0','last_attempt_at':'TEXT','last_error_class':'TEXT','last_error_message':'TEXT','last_http_status':'INTEGER','completed_at':'TEXT'}.items():
if n not in existing: c.execute(f'ALTER TABLE graphiti_import_queue ADD COLUMN {n} {t}')
c.execute('CREATE INDEX IF NOT EXISTS idx_graphiti_queue_due ON graphiti_import_queue(graphiti_status,next_attempt_at)')
c.commit()
def front(s):
out={}
if s.startswith('---'):
for line in s.split('---',2)[1].splitlines():
m=re.match(r'^([A-Za-z0-9_-]+):\s*(.*)$',line)
if m: out[m.group(1)]=m.group(2).strip().strip('"')
return out
def enqueue(p):
p=Path(p).resolve()
if ROOT not in p.parents or p.suffix.lower()!='.md' or not p.is_file():
return 0
s=p.read_text(encoding='utf-8')
h=hashlib.sha256(s.encode()).hexdigest()
f=front(s)
t=now()
c=db()
ensure(c)
identity=hashlib.sha256(f"youtube:{f.get('youtube_id')}:{h}".encode()).hexdigest()
record=f"youtube:{f.get('youtube_id')}:{h}"
c.execute("""
INSERT OR IGNORE INTO graphiti_import_queue(
source_type,video_id,obsidian_path,content_hash,file_modified_at,
graphiti_status,created_at,updated_at,reconciliation_record_id,
import_identity,knowledge_unit_identity
) VALUES('youtube',?,?,?,?, 'queued',?,?,?,?,?)
""",(f.get('youtube_id'),str(p),h,datetime.fromtimestamp(p.stat().st_mtime,timezone.utc).isoformat(),t,t,record,identity,f"source:{f.get('youtube_id')}"))
n=c.total_changes
c.commit()
c.close()
return int(bool(n))
def reconcile():
return sum(enqueue(p) for p in ROOT.rglob('*.md'))
if __name__=='__main__':
import argparse
parser=argparse.ArgumentParser()
parser.add_argument('--once',action='store_true',help='Run enqueue once and exit')
parser.add_argument('--reconcile',action='store_true',help='Alias for --once')
args=parser.parse_args()
if args.once or args.reconcile:
enqueued=reconcile()
print(f'Enqueued {enqueued} new/changed files')
else:
# Default behavior: also just enqueue (keep queue up-to-date)
enqueued=reconcile()
print(f'Enqueued {enqueued} new/changed files (default)')