Advertisement
Guest User

imdb_csv_importer.py

a guest
Apr 27th, 2025
14
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.11 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. imdb_csv_importer.py
  4.  
  5. Watches a folder for “Advanced title search…csv” exports from IMDb.
  6. On creation, it copies your Chrome URL, sanitizes it into a list name,
  7. prompts you (Continue/Clear/Cancel/List), lets you preview/reorder/delete
  8. via a full-screen dark modal, then renames/moves the file, runs npm sync,
  9. and restores your config.json.
  10. """
  11.  
  12. import sys, subprocess, time, shutil, json, re, queue, csv, ctypes
  13. from pathlib import Path
  14.  
  15. # ---- Configuration ----
  16. CLEAN_CLIPBOARD_RULES = True
  17. RENAME_PREFIX = ".IMDb "
  18.  
  19. # ---- Ensure required packages ----
  20. def ensure(name):
  21. try: __import__(name)
  22. except ImportError:
  23. subprocess.check_call([sys.executable, "-m", "pip", "install", name])
  24. ensure('watchdog'); ensure('pyperclip')
  25.  
  26. import pyperclip
  27. from watchdog.observers import Observer
  28. from watchdog.events import FileSystemEventHandler
  29.  
  30. # ---- GUI imports & root ----
  31. import tkinter as tk
  32. import tkinter.font as tkfont
  33. import tkinter.ttk as ttk
  34. from tkinter import messagebox, filedialog
  35.  
  36. root = tk.Tk(); root.withdraw()
  37.  
  38. # -------------------------------------------------------------------------------
  39. # Overlay: small status popup in bottom-right
  40. # -------------------------------------------------------------------------------
  41. class Overlay(tk.Toplevel):
  42. def __init__(self, master, fg='white'):
  43. super().__init__(master)
  44. self.withdraw()
  45. self.overrideredirect(True)
  46. self.attributes('-topmost', True, '-alpha', 0.8)
  47. self.config(bg='#111111', bd=0, highlightthickness=0)
  48. self.label = tk.Label(
  49. self, text="", bg='#111111', fg=fg,
  50. font=('Arial',14), justify='left', wraplength=500
  51. )
  52. self.label.pack(padx=12, pady=8)
  53.  
  54. def update_text(self, msg: str):
  55. self.label.config(text=msg)
  56. self.update_idletasks()
  57. w = self.label.winfo_reqwidth() + 20
  58. h = self.label.winfo_reqheight() + 16
  59. sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
  60. x = sw - w - 20
  61. y = sh - h - 60
  62. self.geometry(f"{w}x{h}+{x}+{y}")
  63. self.deiconify(); self.lift(); self.update()
  64.  
  65. # ---- Chrome URL copy via simulated keystrokes ----
  66. VK_CONTROL, VK_L, VK_C, VK_ESCAPE = 0x11, 0x4C, 0x43, 0x1B
  67. KEYUP = 0x0002
  68. _user32 = ctypes.WinDLL('user32', use_last_error=True)
  69. def copy_chrome_url():
  70. _user32.keybd_event(VK_CONTROL,0,0,0)
  71. _user32.keybd_event(VK_L, 0,0,0)
  72. _user32.keybd_event(VK_L, 0,KEYUP,0)
  73. time.sleep(0.05)
  74. _user32.keybd_event(VK_C, 0,0,0)
  75. _user32.keybd_event(VK_C, 0,KEYUP,0)
  76. time.sleep(0.05)
  77. _user32.keybd_event(VK_CONTROL,0,KEYUP,0)
  78. time.sleep(0.05)
  79. _user32.keybd_event(VK_ESCAPE,0,0,0)
  80. _user32.keybd_event(VK_ESCAPE,0,KEYUP,0)
  81. time.sleep(0.05)
  82.  
  83. # ---- Clipboard helper ----
  84. def get_clipboard_text():
  85. try:
  86. t = pyperclip.paste().strip()
  87. if t: return t
  88. except:
  89. pass
  90. r = tk.Tk(); r.withdraw()
  91. try: return r.clipboard_get().strip()
  92. except tk.TclError: return ""
  93. finally: r.destroy()
  94.  
  95. # ---- Sanitization ----
  96. def sanitize_filename(name: str) -> str:
  97. name = re.sub(r'[\\/:*?"<>|\r\n]+', '_', name)
  98. name = re.sub(r'_+', '_', name).strip('_')
  99. return name[:255]
  100.  
  101. def clean_clipboard_text(raw: str) -> str:
  102. patterns = [
  103. 'title_type=', '&genres=', 'genres=', 'countries=',
  104. '&primary_language=', '&num_', '&release_date=',
  105. 'keywords=', 'country_of_origin='
  106. ]
  107. for p in patterns:
  108. raw = raw.replace(p, ' ')
  109. raw = raw.replace(',', ' ')
  110. return re.sub(r'\s+', ' ', raw).strip()
  111.  
  112. # -------------------------------------------------------------------------------
  113. # CSVViewer – full-screen, dark, sortable, deletable, draggable, undo/restore.
  114. # -------------------------------------------------------------------------------
  115. class CSVViewer(tk.Toplevel):
  116. # pastel colours per title type
  117. TYPE_COLORS = {
  118. 'Movie': '#AEC6CF',
  119. 'TV Series': '#77DD77',
  120. 'Short': '#FDFD96',
  121. 'TV Episode': '#FFB7CE',
  122. 'TV Mini Series': '#CDB4DB',
  123. 'TV Movie': '#FFB347',
  124. 'TV Special': '#98FF98',
  125. 'TV Short': '#B3FFFF',
  126. 'Video Game': '#B39EB5',
  127. 'Video': '#FF6961',
  128. 'Music Video': '#BFFF00',
  129. 'Podcast Series': '#ADD8E6',
  130. 'Podcast Episode': '#FFB6C1'
  131. }
  132.  
  133. def __init__(self, master=None, *, csv_path):
  134. super().__init__(master)
  135. self.csv_path = Path(csv_path)
  136. self.overrideredirect(True)
  137. self.attributes('-topmost', True)
  138. self.config(bg='#111111')
  139. self.withdraw(); self.update_idletasks()
  140.  
  141. # State
  142. self.sort_states = {}
  143. self.undo_stack = []
  144. self.selection = set()
  145.  
  146. # Full screen
  147. sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
  148. self.geometry(f"{sw}x{sh}+0+0")
  149. self.deiconify()
  150.  
  151. # Load CSV data
  152. try:
  153. with self.csv_path.open(newline='', encoding='utf-8') as f:
  154. orig = list(csv.DictReader(f))
  155. except Exception as e:
  156. messagebox.showerror("Error", f"Could not load CSV:\n{e}")
  157. self.destroy(); return
  158.  
  159. self.original_data = []
  160. for i,row in enumerate(orig, start=1):
  161. r = row.copy(); r['Position'] = str(i)
  162. self.original_data.append(r)
  163. self.data = list(self.original_data)
  164.  
  165. # Columns & fonts
  166. self.columns = ["Position","Const","Title Type","Title","IMDb Rating","Year","Delete"]
  167. hf, cf = ('Arial',16), ('Arial',14)
  168. fh = tkfont.Font(family=hf[0], size=hf[1])
  169. fc = tkfont.Font(family=cf[0], size=cf[1])
  170.  
  171. # Compute column widths
  172. cw = {}
  173. for col in self.columns[:-1]:
  174. texts = [r[col] for r in self.data] + [col]
  175. longest = max(texts, key=len)
  176. cw[col] = fc.measure(longest + ' '*10)
  177. cw['Delete'] = max(fh.measure('Delete'), fh.measure('✕')) + 30
  178.  
  179. sb_w = 12
  180. box_w = sum(cw[c] for c in self.columns) + sb_w
  181. box_h = sh
  182.  
  183. frm = tk.Frame(self, bg='#222222')
  184. frm.place(x=(sw-box_w)//2, y=0, width=box_w, height=box_h)
  185.  
  186. style = ttk.Style(frm); style.theme_use('clam')
  187. style.configure("VT.Treeview",
  188. background="#222222",
  189. fieldbackground="#222222",
  190. foreground="white",
  191. rowheight=34,
  192. font=fc,
  193. borderwidth=0)
  194. style.configure("VT.Treeview.Heading",
  195. background="#222222",
  196. foreground="white",
  197. font=hf,
  198. pady=4,
  199. borderwidth=0)
  200. style.map("VT.Treeview.Heading",
  201. background=[('active','#88aacc')])
  202. style.configure("VT.Vertical.TScrollbar",
  203. troughcolor="#222222",
  204. background="#88aacc",
  205. arrowcolor="#88aacc",
  206. bordercolor="#222222",
  207. lightcolor="#222222",
  208. darkcolor="#222222")
  209.  
  210. # Treeview
  211. self.tree = ttk.Treeview(
  212. frm,
  213. columns=self.columns,
  214. show='headings',
  215. style="VT.Treeview",
  216. selectmode='none'
  217. )
  218. for col in self.columns:
  219. self.tree.heading(col, text=col,
  220. command=lambda c=col: self.sort_by(c))
  221. self.tree.column(col, width=cw[col], anchor='center', stretch=False)
  222.  
  223. # configure pastel-foreground tags per type
  224. for ttype, col in CSVViewer.TYPE_COLORS.items():
  225. self.tree.tag_configure(ttype, foreground=col)
  226. # selection & drag highlight
  227. self.tree.tag_configure('selected', background='#335599')
  228. self.tree.tag_configure('drag', background='#5577bb')
  229.  
  230. vsb = ttk.Scrollbar(frm,
  231. orient='vertical',
  232. command=self.tree.yview,
  233. style="VT.Vertical.TScrollbar")
  234. self.tree.configure(yscrollcommand=vsb.set)
  235.  
  236. header_h = fh.metrics("linespace") + 4
  237. self.tree.place(x=0, y=header_h,
  238. width=box_w-sb_w,
  239. height=box_h-40-header_h)
  240. vsb.place(x=box_w-sb_w,
  241. y=header_h,
  242. width=sb_w,
  243. height=box_h-40-header_h)
  244.  
  245. self.refresh_tree()
  246.  
  247. # Drag/select support
  248. self._press = self._drag_item = self._target_item = None
  249. self._drag_select = False
  250. self._drag_start_idx = None
  251.  
  252. self.tree.bind('<ButtonPress-1>', self.on_press, add='+')
  253. self.tree.bind('<B1-Motion>', self.on_motion, add='+')
  254. self.tree.bind('<ButtonRelease-1>', self.on_release)
  255.  
  256. # Bottom buttons
  257. btnf = tk.Frame(frm, bg='#222222')
  258. btnf.place(x=0, y=sh-40, width=box_w, height=40)
  259. for txt, cmd in (
  260. ("Save", self.on_save),
  261. ("Cancel", self.destroy),
  262. ("Delete", self.on_delete_selected),
  263. ("Undo", self.on_undo),
  264. ("Default", self.on_default),
  265. ("Unselect",self.on_unselect),
  266. ("Close", self.destroy),
  267. ):
  268. tk.Button(
  269. btnf,
  270. text=txt,
  271. bg='#222222',
  272. fg='#777777',
  273. font=cf,
  274. bd=0,
  275. highlightthickness=1,
  276. highlightbackground='grey',
  277. activebackground='#222222',
  278. activeforeground='#777777',
  279. relief='flat',
  280. command=cmd
  281. ).pack(side='left', padx=8, pady=4)
  282.  
  283. def refresh_tree(self):
  284. self.tree.delete(*self.tree.get_children())
  285. for idx,row in enumerate(self.data):
  286. tags = [ row['Title Type'] ]
  287. if idx in self.selection:
  288. tags.append('selected')
  289. vals = [row[c] for c in self.columns[:-1]]
  290. vals.append('✕' if idx in self.selection else '')
  291. self.tree.insert('', 'end',
  292. iid=str(idx),
  293. values=vals,
  294. tags=tags)
  295.  
  296. def sort_by(self, col):
  297. st = self.sort_states.get(col)
  298. seq = {
  299. 'Position': ['desc','asc',None],
  300. 'Const': ['desc','asc',None],
  301. 'Title Type': ['asc','desc',None],
  302. 'Title': ['asc','desc',None],
  303. 'IMDb Rating': ['desc','asc',None],
  304. 'Year': ['desc','asc',None],
  305. }
  306. nxt = seq[col][(seq[col].index(st)+1) % 3]
  307. self.sort_states[col] = nxt
  308. for c in self.columns:
  309. self.tree.heading(c, text=c)
  310. arrow = '▼' if nxt=='desc' else '▲' if nxt=='asc' else ''
  311. self.tree.heading(col, text=col + (' '+arrow if arrow else ''))
  312.  
  313. def keyf(r):
  314. v = r[col]
  315. # treat missing/non-numeric ratings as -1.0
  316. if col == 'IMDb Rating':
  317. try:
  318. return float(v)
  319. except:
  320. return -1.0
  321. # treat missing/non-numeric years as 1900
  322. if col == 'Year':
  323. try:
  324. return int(v)
  325. except:
  326. return 1900
  327. # fallback: numeric if possible, else lexicographic
  328. try:
  329. return float(v)
  330. except:
  331. return v.lower()
  332.  
  333. if col != 'Title Type' and self.sort_states.get('Title Type'):
  334. group_order = []
  335. for r in self.data:
  336. t = r['Title Type']
  337. if t not in group_order:
  338. group_order.append(t)
  339. new_data = []
  340. rev = (nxt=='desc')
  341. for grp in group_order:
  342. grp_rows = [r for r in self.data if r['Title Type']==grp]
  343. grp_rows.sort(key=keyf, reverse=rev)
  344. new_data.extend(grp_rows)
  345. self.data = new_data
  346. else:
  347. if nxt is None:
  348. self.data = list(self.original_data)
  349. else:
  350. rev = (nxt=='desc')
  351. self.data.sort(key=keyf, reverse=rev)
  352. for i,r in enumerate(self.data, start=1):
  353. r['Position'] = str(i)
  354. self.refresh_tree()
  355.  
  356. def on_press(self, ev):
  357. x,y = ev.x, ev.y
  358. row = self.tree.identify_row(y)
  359. col = self.tree.identify_column(x)
  360. self._press = (x,y,row,col)
  361. if col == f"#{len(self.columns)}" and row:
  362. idx = int(row)
  363. self._drag_select = True
  364. self._drag_start_idx = idx
  365. self.selection = {idx}
  366. self.refresh_tree()
  367. else:
  368. self._drag_item = row
  369. if row:
  370. tags = list(self.tree.item(row,'tags'))
  371. if 'drag' not in tags:
  372. tags.append('drag')
  373. self.tree.item(row, tags=tags)
  374.  
  375. def on_motion(self, ev):
  376. if self._drag_select:
  377. over = self.tree.identify_row(ev.y)
  378. if over:
  379. idx = int(over)
  380. lo,hi = sorted([self._drag_start_idx, idx])
  381. self.selection = set(range(lo,hi+1))
  382. self.refresh_tree()
  383. else:
  384. over = self.tree.identify_row(ev.y)
  385. if over != self._target_item:
  386. if self._target_item and self._target_item != self._drag_item:
  387. tags = list(self.tree.item(self._target_item,'tags'))
  388. if 'drag' in tags:
  389. tags.remove('drag')
  390. self.tree.item(self._target_item, tags=tags)
  391. self._target_item = over
  392. if over and over != self._drag_item:
  393. tags = list(self.tree.item(over,'tags'))
  394. if 'drag' not in tags:
  395. tags.append('drag')
  396. self.tree.item(over, tags=tags)
  397.  
  398. def on_release(self, ev):
  399. x0,y0,row0,col0 = self._press
  400. dx,dy = abs(ev.x-x0), abs(ev.y-y0)
  401. if self._drag_select:
  402. self._drag_select = False
  403. return
  404. for itm in (self._drag_item, self._target_item):
  405. if itm:
  406. tags = list(self.tree.item(itm,'tags'))
  407. if 'drag' in tags:
  408. tags.remove('drag')
  409. self.tree.item(itm, tags=tags)
  410. if dx<5 and dy<5 and row0:
  411. idx = int(row0)
  412. if col0 == f"#{len(self.columns)}":
  413. self.undo_stack.append(list(self.data))
  414. self.data.pop(idx)
  415. self.selection.discard(idx)
  416. for i,r in enumerate(self.data,start=1):
  417. r['Position'] = str(i)
  418. self.refresh_tree()
  419. else:
  420. if idx in self.selection:
  421. self.selection.remove(idx)
  422. else:
  423. self.selection.add(idx)
  424. self.refresh_tree()
  425. else:
  426. drop = self.tree.identify_row(ev.y)
  427. if self._drag_item and drop and drop!=self._drag_item:
  428. self.undo_stack.append(list(self.data))
  429. i1,i2 = int(self._drag_item), int(drop)
  430. rowo = self.data.pop(i1)
  431. self.data.insert(i2,rowo)
  432. for i,r in enumerate(self.data,start=1):
  433. r['Position'] = str(i)
  434. self.refresh_tree()
  435. self._drag_item = None
  436. self._target_item = None
  437.  
  438. def on_delete_selected(self):
  439. if self.selection:
  440. self.undo_stack.append(list(self.data))
  441. for idx in sorted(self.selection, reverse=True):
  442. self.data.pop(idx)
  443. self.selection.clear()
  444. for i,r in enumerate(self.data,start=1):
  445. r['Position'] = str(i)
  446. self.refresh_tree()
  447.  
  448. def on_undo(self):
  449. if self.undo_stack:
  450. self.data = self.undo_stack.pop()
  451. for i,r in enumerate(self.data,start=1):
  452. r['Position'] = str(i)
  453. self.selection.clear()
  454. self.refresh_tree()
  455.  
  456. def on_default(self):
  457. self.data = list(self.original_data)
  458. self.undo_stack.clear()
  459. self.selection.clear()
  460. for i,r in enumerate(self.data,start=1):
  461. r['Position'] = str(i)
  462. self.refresh_tree()
  463.  
  464. def on_unselect(self):
  465. self.selection.clear()
  466. self.refresh_tree()
  467.  
  468. def on_save(self):
  469. try:
  470. with self.csv_path.open(newline='', encoding='utf-8') as f:
  471. orig = list(csv.DictReader(f))
  472. fields = orig[0].keys() if orig else []
  473. lookup = {r['Const']:r for r in orig}
  474. reordered = [lookup[r['Const']] for r in self.data]
  475. with self.csv_path.open('w', newline='', encoding='utf-8') as f:
  476. w = csv.DictWriter(f, fieldnames=fields)
  477. w.writeheader()
  478. for row in reordered:
  479. w.writerow(row)
  480. except Exception as e:
  481. messagebox.showerror("Error", f"Save failed: {e}")
  482. self.destroy()
  483.  
  484. # -------------------------------------------------------------------------------
  485. # PromptOverlay – pops the dark translucent prompt with Continue/Clear/Cancel/List
  486. # -------------------------------------------------------------------------------
  487. class PromptOverlay(tk.Toplevel):
  488. def __init__(self, master, default, csv_path):
  489. super().__init__(master)
  490. self.overrideredirect(True)
  491. self.attributes('-topmost', True, '-alpha', 0.8)
  492. self.config(bg='#111111')
  493. self.cancelled = False
  494. self.csv_path = csv_path
  495.  
  496. tk.Label(
  497. self, text="List Name:", bg='#111111', fg='white',
  498. font=('Arial',14)
  499. ).pack(anchor='w', padx=12, pady=(12,0))
  500.  
  501. self.var = tk.StringVar(value=default)
  502. entry = tk.Entry(
  503. self, textvariable=self.var,
  504. bg='#111111', fg='white', insertbackground='white',
  505. font=('Arial',14), bd=0, highlightthickness=0
  506. )
  507. entry.pack(fill='x', padx=12, pady=8)
  508.  
  509. btnf = tk.Frame(self, bg='#111111')
  510. btnf.pack(fill='x', padx=12, pady=(0,12))
  511. for txt, cmd in (
  512. ("Continue", self.on_continue),
  513. ("Clear", self.on_clear),
  514. ("Cancel", self.on_cancel),
  515. ("List", self.on_list),
  516. ):
  517. b = tk.Button(
  518. btnf, text=txt, bg='#111111', fg='#777777',
  519. font=('Arial',14), bd=0, highlightthickness=1,
  520. highlightbackground='grey',
  521. activebackground='#111111',
  522. activeforeground='#777777',
  523. relief='flat', command=cmd
  524. )
  525. b.pack(side='left', padx=(0,10))
  526.  
  527. self.update_idletasks()
  528. font = tkfont.Font(family='Arial', size=14)
  529. text_px = font.measure(self.var.get())
  530. extra_px = font.measure(' ' * 10)
  531. entry_required = text_px + extra_px
  532. btns_width = sum(w.winfo_reqwidth() for w in btnf.winfo_children())
  533. W = max(int(entry_required) + 24, btns_width + 24)
  534. x, y = self.winfo_pointerx(), self.winfo_pointery()
  535. self.geometry(f"{W}x{self.winfo_reqheight()}+{x}+{y}")
  536. self.deiconify(); self.lift()
  537. entry.focus_set(); entry.icursor(tk.END)
  538.  
  539. def on_continue(self): self.destroy()
  540. def on_clear(self): self.var.set(RENAME_PREFIX)
  541. def on_cancel(self): self.cancelled = True; self.destroy()
  542. def on_list(self):
  543. CSVViewer(master=self.master, csv_path=self.csv_path)
  544.  
  545. # -------------------------------------------------------------------------------
  546. # Watchdog handler & main loop
  547. # -------------------------------------------------------------------------------
  548. class IMDbHandler(FileSystemEventHandler):
  549. def on_created(self, evt):
  550. p = Path(evt.src_path)
  551. if re.match(r"^Advanced title search.*\.csv$", p.name, re.IGNORECASE):
  552. event_queue.put(p)
  553. def on_moved(self, evt):
  554. p = Path(evt.dest_path)
  555. if re.match(r"^Advanced title search.*\.csv$", p.name, re.IGNORECASE):
  556. event_queue.put(p)
  557.  
  558. def choose_watch_folder():
  559. dlg = tk.Tk(); dlg.withdraw()
  560. use_default = messagebox.askyesno(
  561. "Watch Folder",
  562. f"Use Downloads folder?\n\n{Path.home()/'Downloads'}"
  563. )
  564. if use_default:
  565. fld = Path.home()/"Downloads"
  566. else:
  567. sel = filedialog.askdirectory(
  568. title="Select watch folder",
  569. initialdir=Path.home()/"Downloads"
  570. )
  571. fld = Path(sel) if sel else Path.home()/"Downloads"
  572. dlg.destroy()
  573. return fld
  574.  
  575. def process_imdb_list(csv_path: Path, master: tk.Tk):
  576. # PHASE 1: pre-prompt overlay
  577. log = Overlay(master)
  578. def L1(msg):
  579. print(msg); log.update_text(msg)
  580.  
  581. L1(f"[import] Starting: {csv_path.name}")
  582. time.sleep(0.05)
  583.  
  584. copy_chrome_url()
  585. raw = get_clipboard_text()
  586. L1(f"[import] URL: {raw}")
  587.  
  588. query = raw.split('?',1)[1] if '?' in raw else raw
  589. cleaned = clean_clipboard_text(query) if CLEAN_CLIPBOARD_RULES else query
  590. default = RENAME_PREFIX + cleaned
  591.  
  592. prm = PromptOverlay(master, default, csv_path)
  593. master.wait_window(prm)
  594. if getattr(prm, 'cancelled', False):
  595. L1("[import] Cancelled")
  596. log.destroy()
  597. return
  598.  
  599. # PHASE 2: post-prompt overlay
  600. log.destroy()
  601. fileov = Overlay(master)
  602. def L2(msg):
  603. print(msg); fileov.update_text(msg)
  604.  
  605. newname = sanitize_filename(prm.var.get().strip() or default) + csv_path.suffix
  606. L2(f"[import] Moving to: {newname}")
  607. time.sleep(0.05)
  608.  
  609. dest = Path(__file__).parent / newname
  610. shutil.move(str(csv_path), str(dest))
  611. L2(f"[import] Moved to: {dest.name}")
  612. time.sleep(0.05)
  613.  
  614. cfg = Path(__file__).parent.parent / "config.json"
  615. orig_cfg = None
  616. if cfg.exists():
  617. try: orig_cfg = json.load(cfg.open("r", encoding="utf-8"))
  618. except: pass
  619.  
  620. new_cfg = [{
  621. "name": dest.stem,
  622. "imdbId": dest.name,
  623. "tmdbId": None,
  624. "sortField": None,
  625. "isReverse": False
  626. }]
  627. with cfg.open("w", encoding="utf-8") as f:
  628. json.dump(new_cfg, f, indent=4, ensure_ascii=False)
  629. L2("[import] Config written")
  630. time.sleep(0.05)
  631.  
  632. subprocess.run("npm run sync", shell=True, cwd=str(cfg.parent))
  633. L2("[import] Sync complete")
  634. time.sleep(0.05)
  635.  
  636. if orig_cfg is None:
  637. cfg.write_text("", encoding="utf-8")
  638. else:
  639. with cfg.open("w", encoding="utf-8") as f:
  640. json.dump(orig_cfg, f, indent=4, ensure_ascii=False)
  641. L2("[import] Config restored")
  642.  
  643. fileov.destroy()
  644.  
  645. if __name__ == "__main__":
  646. watch_folder = choose_watch_folder()
  647. if not watch_folder.exists():
  648. messagebox.showwarning(
  649. "Folder Missing",
  650. f"{watch_folder} not found; falling back to Downloads."
  651. )
  652. watch_folder = Path.home()/"Downloads"
  653.  
  654. event_queue = queue.Queue()
  655. handler = IMDbHandler()
  656. obs = Observer()
  657. obs.schedule(handler, str(watch_folder), recursive=False)
  658. obs.start()
  659. print(f"Watching {watch_folder} for IMDB CSVs… (CTRL-C to quit)")
  660.  
  661. try:
  662. while True:
  663. try:
  664. p = event_queue.get(timeout=0.1)
  665. process_imdb_list(p, root)
  666. except queue.Empty:
  667. pass
  668. except Exception as ex:
  669. print("Error:", ex)
  670. except KeyboardInterrupt:
  671. obs.stop()
  672. root.destroy()
  673. obs.join()
  674.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement