Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- """
- imdb_csv_importer.py
- Watches a folder for CSV exports whose filenames contain “feature”, “tv series” or “movies”.
- On creation or move, it copies your Chrome URL, sanitizes it into a list name,
- prompts you (Continue/Clear/Cancel/List), lets you preview/reorder/delete
- via a full-screen dark modal with clickable title-type filters at top
- (including a “Fixed Order” toggle and per-type delete “✕”), then renames/
- moves the file, runs npm sync, and restores your config.json.
- Supports click-to-dim and drag-to-reorder on the title-type filters.
- """
- import sys, subprocess, time, shutil, json, re, queue, csv, ctypes
- from pathlib import Path
- # ---- Configuration ----
- CLEAN_CLIPBOARD_RULES = True
- RENAME_PREFIX = ".IMDb "
- # ---- Ensure required packages ----
- def ensure(name):
- try:
- __import__(name)
- except ImportError:
- subprocess.check_call([sys.executable, "-m", "pip", "install", name])
- ensure('watchdog')
- ensure('pyperclip')
- import pyperclip
- from watchdog.observers import Observer
- from watchdog.events import FileSystemEventHandler
- # ---- GUI imports (root created below) ----
- import tkinter as tk
- import tkinter.font as tkfont
- import tkinter.ttk as ttk
- from tkinter import messagebox, filedialog
- root = None # will be set in __main__
- # -------------------------------------------------------------------
- # Overlay: small status popup in bottom-right
- # -------------------------------------------------------------------
- class Overlay(tk.Toplevel):
- def __init__(self, master, fg='white'):
- super().__init__(master)
- self.withdraw()
- self.overrideredirect(True)
- self.attributes('-topmost', True, '-alpha', 0.8)
- self.config(bg='#111111', bd=0, highlightthickness=0)
- self.label = tk.Label(self, text="", bg='#111111', fg=fg,
- font=('Arial',14), justify='left', wraplength=500)
- self.label.pack(padx=12, pady=8)
- def update_text(self, msg: str):
- self.label.config(text=msg)
- self.update_idletasks()
- w = self.label.winfo_reqwidth() + 20
- h = self.label.winfo_reqheight() + 16
- sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
- x = sw - w - 20
- y = sh - h - 60
- self.geometry(f"{w}x{h}+{x}+{y}")
- self.deiconify()
- self.lift()
- self.update()
- # -------------------------------------------------------------------
- # Chrome URL copy via keystrokes
- # -------------------------------------------------------------------
- VK_CONTROL, VK_L, VK_C, VK_ESCAPE = 0x11, 0x4C, 0x43, 0x1B
- KEYUP = 0x0002
- _user32 = ctypes.WinDLL('user32', use_last_error=True)
- def copy_chrome_url():
- for vk,flag in [(VK_CONTROL,0),(VK_L,0),(VK_L,KEYUP),
- (VK_C,0),(VK_C,KEYUP),
- (VK_CONTROL,KEYUP),
- (VK_ESCAPE,0),(VK_ESCAPE,KEYUP)]:
- _user32.keybd_event(vk,0,flag,0)
- time.sleep(0.05)
- # -------------------------------------------------------------------
- # Clipboard helper (pyperclip only)
- # -------------------------------------------------------------------
- def get_clipboard_text():
- try: return pyperclip.paste().strip()
- except: return ""
- # -------------------------------------------------------------------
- # Filename sanitization
- # -------------------------------------------------------------------
- def sanitize_filename(name:str)->str:
- name = re.sub(r'[\\/:*?"<>|\r\n]+','_', name)
- return re.sub(r'_+','_', name).strip('_')[:255]
- def clean_clipboard_text(raw:str)->str:
- patterns = [
- 'title_type=', '&genres=', 'genres=', 'countries=',
- '&primary_language=', '&num_', '&release_date=',
- 'keywords=', 'country_of_origin='
- ]
- for p in patterns:
- raw = raw.replace(p,' ')
- raw = raw.replace(',',' ')
- return re.sub(r'\s+',' ', raw).strip()
- # -------------------------------------------------------------------
- # CSVViewer – full-screen modal with filters, delete, fixed order,
- # sorting, drag-and-drop rows, click-to-dim + drag-to-reorder filters
- # -------------------------------------------------------------------
- class CSVViewer(tk.Toplevel):
- TYPE_COLORS = {
- 'Movie':'#AEC6CF','TV Series':'#77DD77','Short':'#FDFD96',
- 'TV Episode':'#FFB7CE','TV Mini Series':'#CDB4DB','TV Movie':'#FFB347',
- 'TV Special':'#98FF98','TV Short':'#B3FFFF','Video Game':'#B39EB5',
- 'Video':'#FF6961','Music Video':'#BFFF00',
- 'Podcast Series':'#ADD8E6','Podcast Episode':'#FFB6C1'
- }
- DRAG_BLUE = '#5577bb'
- def __init__(self, master=None, *, csv_path:Path):
- super().__init__(master)
- self.csv_path = Path(csv_path)
- # ─── STATE ─────────────────────────────────────────
- self.sort_states = {}
- self.undo_stack = []
- self.selection = set()
- self.fixed_order_active = False
- # filter-click & drag state
- self._filter_press_widget = None
- self._filter_press_xy = (0,0)
- self._filter_drag_active = False
- self._dragging_type = None
- self._drag_target_type = None
- # delete-click & drag state
- self._maybe_delete = False
- self._delete_dragging = False
- self._delete_start_idx = None
- # row-drag state
- self._hover_row = None
- self._scroll_dir = 0
- self._scroll_speed = 1.0
- self._auto_job = None
- self._drag_item = None
- self._target_item = None
- self._press = (0,0,None,None)
- # ──────────────────────────────────────────────────
- # fullscreen setup
- self.overrideredirect(True)
- self.attributes('-topmost', True)
- self.withdraw(); self.update_idletasks()
- sw,sh = self.winfo_screenwidth(), self.winfo_screenheight()
- self.geometry(f"{sw}x{sh}+0+0")
- self.config(bg='#111111')
- self.deiconify()
- # load CSV and backup
- with self.csv_path.open(newline='', encoding='utf-8') as f:
- rows = list(csv.DictReader(f))
- self._backup_data = []
- for i,r in enumerate(rows, start=1):
- row = r.copy(); row['Position'] = str(i)
- self._backup_data.append(row)
- self.data = list(self._backup_data)
- # extract types
- self._backup_types = sorted({r['Title Type'] for r in self._backup_data})
- self.all_types = list(self._backup_types)
- self.visible_types = set(self._backup_types)
- self.deleted_types = set()
- # fonts
- hf,cf = ('Arial',16),('Arial',14)
- fh = tkfont.Font(family=hf[0], size=hf[1])
- fc = tkfont.Font(family=cf[0], size=cf[1])
- italic_fc = tkfont.Font(family=cf[0], size=cf[1], slant='italic')
- # compute column widths
- self.columns = ["Position","Const","Title Type","Title","IMDb Rating","Year","Delete"]
- cw = {}
- for col in self.columns[:-1]:
- texts = [r[col] for r in self.data] + [col]
- longest = max(texts, key=len)
- cw[col] = fc.measure(longest + ' '*10)
- cw['Delete'] = max(fh.measure('✕'), fh.measure('Delete')) + 30
- sb_w = 12
- box_w = sum(cw.values()) + sb_w
- box_h = sh
- # filter frame
- frm = tk.Frame(self, bg='#222222')
- frm.place(x=(sw-box_w)//2, y=0, width=box_w, height=box_h)
- self._filter_parent = frm
- self._filter_font = fc
- self._filter_italic_font = italic_fc
- self._filter_box_w = box_w
- self._filter_tag_h = fc.metrics("linespace") + 8
- self._filter_labels = {}
- self.fixed_label = None
- self._filter_rows = 0
- self._render_type_filters()
- # table offset
- header_h = fh.metrics("linespace") + 4
- y_off = self._filter_rows * self._filter_tag_h + header_h
- # treeview styling
- style = ttk.Style(frm)
- style.theme_use('clam')
- style.configure("VT.Treeview",
- background="#222222", fieldbackground="#222222",
- foreground="white", rowheight=34, font=fc, borderwidth=0)
- style.configure("VT.Treeview.Heading",
- background="#222222", foreground="white",
- font=hf, pady=4, borderwidth=0)
- style.map("VT.Treeview.Heading",
- background=[('active','#88aacc')])
- style.configure("VT.Vertical.TScrollbar",
- troughcolor="#222222", background="#88aacc",
- arrowcolor="#88aacc", bordercolor="#222222",
- lightcolor="#222222", darkcolor="#222222")
- # tree + scrollbar
- self.tree = ttk.Treeview(frm, columns=self.columns,
- show='headings', style="VT.Treeview",
- selectmode='none')
- for col in self.columns:
- self.tree.heading(col, text=col,
- command=lambda c=col: self.sort_by(c))
- self.tree.column(col, width=cw[col], anchor='center', stretch=False)
- for t,color in CSVViewer.TYPE_COLORS.items():
- if t in self.all_types:
- self.tree.tag_configure(t, foreground=color)
- self.tree.tag_configure('selected', background=CSVViewer.DRAG_BLUE)
- self.tree.tag_configure('drag', background=CSVViewer.DRAG_BLUE)
- self.tree.tag_configure('hover', background='#333333')
- vsb = ttk.Scrollbar(frm, orient='vertical', command=self.tree.yview,
- style="VT.Vertical.TScrollbar")
- self.tree.configure(yscrollcommand=vsb.set)
- # show only full rows
- row_h = 34
- avail_h = box_h - 40 - y_off
- disp_h = (avail_h // row_h) * row_h
- self.tree.place(x=0, y=y_off,
- width=box_w - sb_w, height=disp_h)
- vsb.place(x=box_w - sb_w, y=y_off,
- width=sb_w, height=disp_h)
- # bindings
- self.refresh_tree()
- self.tree.bind('<Motion>', self.on_hover, add='+')
- self.tree.bind('<Leave>', self.on_leave, add='+')
- self.tree.bind('<ButtonPress-1>', self.on_press, add='+')
- self.tree.bind('<B1-Motion>', self.on_motion, add='+')
- self.tree.bind('<ButtonRelease-1>', self.on_release)
- self.bind("<Button-1>", self.on_bg_click, add='+')
- # bottom buttons
- btnf = tk.Frame(frm, bg='#222222')
- btnf.place(x=0, y=sh-40, width=box_w, height=40)
- for txt,cmd in (
- ("Save", self.on_save),
- ("Cancel", self.destroy),
- ("Delete", self.on_delete_selected),
- ("Undo", self.on_undo),
- ("Default", self.on_default),
- ("Unselect",self.on_unselect),
- ("Close", self.destroy),
- ):
- tk.Button(btnf, text=txt, bg='#222222', fg='#777777',
- font=fc, bd=0, highlightthickness=1,
- highlightbackground='grey',
- activebackground='#222222',
- activeforeground='#777777',
- relief='flat', command=cmd).pack(side='left', padx=8, pady=4)
- # ─── FILTER RENDER ────────────────────────────────────────────────
- def _render_type_filters(self):
- for lbl in self._filter_labels.values():
- lbl.destroy()
- self._filter_labels.clear()
- if self.fixed_label:
- self.fixed_label.destroy()
- cx,row,spacing = 0,0,10
- # Fixed Order toggle
- wfixed = self._filter_font.measure("Fixed Order")+20
- fg = "white" if self.fixed_order_active else "#777777"
- self.fixed_label = tk.Label(self._filter_parent, text="Fixed Order",
- bg='#222222', fg=fg,
- font=self._filter_italic_font,
- cursor='hand2')
- self.fixed_label.place(x=cx, y=row*self._filter_tag_h,
- width=wfixed, height=self._filter_tag_h)
- self.fixed_label.bind('<Button-1>',
- lambda e: self._toggle_fixed())
- cx += wfixed + spacing
- for t in self.all_types:
- if t in self.deleted_types: continue
- text_w = self._filter_font.measure(t)
- x_w = self._filter_font.measure('✕')
- wlbl = text_w + 5 + x_w + 20
- if cx + wlbl > self._filter_box_w:
- row += 1; cx = 0
- frm = tk.Frame(self._filter_parent, bg='#222222')
- frm.type_name = t
- frm.place(x=cx, y=row*self._filter_tag_h,
- width=wlbl, height=self._filter_tag_h)
- txt_lbl = tk.Label(frm, text=t, bg='#222222',
- fg=(CSVViewer.TYPE_COLORS[t]
- if t in self.visible_types else '#777777'),
- font=self._filter_font, cursor='hand2')
- txt_lbl.pack(side='left', padx=(5,0))
- txt_lbl.type_name = t
- txt_lbl.bind('<ButtonPress-1>', self.on_filter_type_press)
- txt_lbl.bind('<B1-Motion>', self.on_filter_type_motion)
- txt_lbl.bind('<ButtonRelease-1>', self.on_filter_type_release)
- x_lbl = tk.Label(frm, text='✕', bg='#222222',
- fg=(CSVViewer.TYPE_COLORS[t]
- if t in self.visible_types else '#777777'),
- font=self._filter_font, cursor='hand2')
- x_lbl.pack(side='left', padx=(5,5))
- x_lbl.bind('<Button-1>',
- lambda e, t=t: self._delete_type(t))
- self._filter_labels[t] = frm
- cx += wlbl + spacing
- self._filter_rows = row + 1
- # ─── FILTER CLICK & DRAG HANDLERS ─────────────────────────────────
- def on_filter_type_press(self, event):
- self._filter_press_widget = event.widget
- self._filter_press_xy = (event.x_root, event.y_root)
- self._filter_drag_active = False
- self._dragging_type = event.widget.type_name
- event.widget.config(relief='raised')
- def on_filter_type_motion(self, event):
- dx = event.x_root - self._filter_press_xy[0]
- dy = event.y_root - self._filter_press_xy[1]
- if not self._filter_drag_active and abs(dx)<5 and abs(dy)<5:
- return
- self._filter_drag_active = True
- target = self._filter_parent.winfo_containing(event.x_root, event.y_root)
- widget = target
- while widget and not hasattr(widget, 'type_name'):
- widget = widget.master
- if getattr(widget,'type_name',None) in self.all_types:
- self._drag_target_type = widget.type_name
- for frm in self._filter_labels.values():
- frm.config(highlightthickness=0)
- self._filter_labels[self._drag_target_type].config(
- highlightthickness=2,
- highlightbackground=CSVViewer.TYPE_COLORS[self._dragging_type]
- )
- else:
- self._drag_target_type = None
- def on_filter_type_release(self, event):
- dx = event.x_root - self._filter_press_xy[0]
- dy = event.y_root - self._filter_press_xy[1]
- if not self._filter_drag_active and abs(dx)<5 and abs(dy)<5:
- if self._filter_press_widget:
- self._toggle_type(self._filter_press_widget.type_name)
- else:
- src, dst = self._dragging_type, self._drag_target_type
- if self._filter_drag_active and dst and dst!=src:
- self.all_types.remove(src)
- idx = self.all_types.index(dst)
- moved_right = (event.x_root > self._filter_press_xy[0])
- insert_idx = idx+1 if moved_right else idx
- self.all_types.insert(insert_idx, src)
- self._render_type_filters()
- self.refresh_tree()
- for frm in self._filter_labels.values():
- frm.config(relief='flat', highlightthickness=0)
- self._filter_press_widget = None
- self._filter_drag_active = False
- self._dragging_type = None
- self._drag_target_type = None
- # ─── TOGGLE & DELETE FILTERS ───────────────────────────────────────
- def _toggle_fixed(self):
- self.fixed_order_active = not self.fixed_order_active
- self.fixed_label.config(
- fg="white" if self.fixed_order_active else "#777777"
- )
- self.refresh_tree()
- def _toggle_type(self, t):
- if t in self.visible_types:
- self.visible_types.remove(t)
- else:
- self.visible_types.add(t)
- self._render_type_filters()
- self.refresh_tree()
- def _delete_type(self, t):
- self.deleted_types.add(t)
- self.visible_types.discard(t)
- if t in self.all_types:
- self.all_types.remove(t)
- self.data = [r for r in self.data if r['Title Type']!=t]
- self._render_type_filters()
- self.refresh_tree()
- # ─── ROW INTERACTIONS ────────────────────────────────────────────
- def on_hover(self, ev):
- row = self.tree.identify_row(ev.y)
- if row==self._hover_row: return
- if self._hover_row:
- tags = list(self.tree.item(self._hover_row,'tags'))
- if 'hover' in tags:
- tags.remove('hover')
- self.tree.item(self._hover_row,tags=tags)
- self._hover_row = row
- if row:
- vals = self.tree.item(row,'values')
- ttype = vals[2]
- base = CSVViewer.TYPE_COLORS.get(ttype,'#ffffff')
- tint = self._overlay_color(base,0.10)
- self.tree.tag_configure('hover', background=tint)
- tags = list(self.tree.item(row,'tags'))
- if 'hover' not in tags:
- tags.append('hover')
- self.tree.item(row, tags=tags)
- def on_leave(self, ev):
- if self._hover_row:
- tags = list(self.tree.item(self._hover_row,'tags'))
- if 'hover' in tags:
- tags.remove('hover')
- self.tree.item(self._hover_row,tags=tags)
- self._hover_row = None
- def on_bg_click(self, ev):
- if ev.widget is self:
- self.selection.clear()
- self.refresh_tree()
- def _start_scroll(self, d):
- if self._scroll_dir!=d:
- self._scroll_dir=d; self._scroll_speed=1.0
- self._schedule_scroll()
- def _stop_scroll(self):
- self._scroll_dir=0
- if self._auto_job:
- self.after_cancel(self._auto_job)
- self._auto_job=None
- def _schedule_scroll(self):
- if not self._scroll_dir: return
- units = max(1,int(self._scroll_speed))
- self.tree.yview_scroll(self._scroll_dir*units,'units')
- self._scroll_speed*=1.011
- self._auto_job=self.after(50,self._schedule_scroll)
- def on_press(self, ev):
- x,y = ev.x, ev.y
- row = self.tree.identify_row(y)
- col = self.tree.identify_column(x)
- self._press = (x,y,row,col)
- delcol = f"#{len(self.columns)}"
- if col==delcol and row:
- # prepare single-click or drag deletion
- self._maybe_delete = True
- self._delete_dragging = False
- iids = list(self.tree.get_children())
- self._delete_start_idx = iids.index(row) if row in iids else None
- self.selection.clear()
- elif row:
- self._maybe_delete=False
- self._drag_item = row
- tags = list(self.tree.item(row,'tags'))
- if 'drag' not in tags:
- tags.append('drag')
- self.tree.item(row,tags=tags)
- else:
- self._maybe_delete=False
- self._drag_item=None
- def _update_delete_selection(self, start_idx, end_idx):
- iids = list(self.tree.get_children())
- lo,hi = sorted((start_idx, end_idx))
- sel = set(int(iid) for iid in iids[lo:hi+1])
- self.selection = sel
- self.refresh_tree()
- def on_motion(self, ev):
- h,margin = self.tree.winfo_height(),20
- if ev.y<margin: self._start_scroll(-1)
- elif ev.y>h-margin: self._start_scroll(1)
- else: self._stop_scroll()
- x0,y0,row0,col0 = self._press
- delcol = f"#{len(self.columns)}"
- dx,dy = abs(ev.x-x0), abs(ev.y-y0)
- # start drag-range if moved beyond threshold
- if self._maybe_delete and not self._delete_dragging and col0==delcol and row0:
- if dx>5 or dy>5:
- self._delete_dragging = True
- if self._delete_dragging:
- over = self.tree.identify_row(ev.y)
- iids = list(self.tree.get_children())
- if over in iids and self._delete_start_idx is not None:
- idx = iids.index(over)
- self._update_delete_selection(self._delete_start_idx, idx)
- elif self._drag_item:
- over=self.tree.identify_row(ev.y)
- if self._target_item and self._target_item!=self._drag_item:
- if self.tree.exists(self._target_item):
- tags=list(self.tree.item(self._target_item,'tags'))
- if 'drag' in tags:
- tags.remove('drag')
- self.tree.item(self._target_item,tags=tags)
- self._target_item=over
- if over and over!=self._drag_item and self.tree.exists(over):
- tags=list(self.tree.item(over,'tags'))
- if 'drag' not in tags:
- tags.append('drag')
- self.tree.item(over,tags=tags)
- def on_release(self, ev):
- self._stop_scroll()
- x0,y0,row0,col0 = self._press
- dx,dy = abs(ev.x-x0), abs(ev.y-y0)
- delcol = f"#{len(self.columns)}"
- # single-click delete
- if self._maybe_delete and not self._delete_dragging and col0==delcol and row0 and dx<5 and dy<5:
- idx = int(row0)
- self.undo_stack.append(list(self.data))
- self.data.pop(idx)
- for i,r in enumerate(self.data, start=1):
- r['Position'] = str(i)
- self.selection.clear()
- self.refresh_tree()
- self._maybe_delete = False
- return
- # finish delete-range drag
- if self._delete_dragging:
- self._maybe_delete = False
- self._delete_dragging = False
- return
- # row-drag reorder
- if self._drag_item:
- drop=self.tree.identify_row(ev.y)
- if dx<5 and dy<5 and row0 and col0!=delcol:
- idx=int(row0)
- if idx in self.selection: self.selection.remove(idx)
- else: self.selection.add(idx)
- elif drop and drop!=self._drag_item:
- self.undo_stack.append(list(self.data))
- i1,i2=int(self._drag_item),int(drop)
- rowo=self.data.pop(i1)
- self.data.insert(i2,rowo)
- for i,r in enumerate(self.data, start=1):
- r['Position'] = str(i)
- self._drag_item=None
- self.refresh_tree()
- def on_delete_selected(self):
- if self.selection:
- self.undo_stack.append(list(self.data))
- for idx in sorted(self.selection, reverse=True):
- self.data.pop(idx)
- for i,r in enumerate(self.data, start=1):
- r['Position'] = str(i)
- self.selection.clear()
- self.refresh_tree()
- def on_undo(self):
- if self.undo_stack:
- self.data = self.undo_stack.pop()
- for i,r in enumerate(self.data, start=1):
- r['Position'] = str(i)
- self.selection.clear()
- self.refresh_tree()
- def on_default(self):
- self.fixed_order_active = False
- self.visible_types = set(self._backup_types)
- self.deleted_types = set()
- self.all_types = list(self._backup_types)
- self.data = list(self._backup_data)
- self.sort_states.clear()
- self.selection.clear()
- for i,r in enumerate(self.data, start=1):
- r['Position'] = str(i)
- self._render_type_filters()
- self.refresh_tree()
- def on_unselect(self):
- self.selection.clear()
- self.refresh_tree()
- def sort_by(self, col):
- st = self.sort_states.get(col)
- seq = {
- 'Position':['desc','asc',None],
- 'Const':['desc','asc',None],
- 'Title Type':['asc','desc',None],
- 'Title':['asc','desc',None],
- 'IMDb Rating':['desc','asc',None],
- 'Year':['desc','asc',None],
- }
- nxt = seq[col][(seq[col].index(st)+1)%3]
- self.sort_states[col] = nxt
- for c in self.columns:
- self.tree.heading(c, text=c)
- arrow = '▼' if nxt=='desc' else '▲' if nxt=='asc' else ''
- self.tree.heading(col, text=col+(' '+arrow if arrow else ''))
- def keyf(r):
- v=r[col]
- if col=='IMDb Rating':
- try: return (0,float(v))
- except: return (0,-1.0)
- if col=='Year':
- try: return (0,int(v))
- except: return (0,1900)
- try: return (0,float(v))
- except: return (1,v.lower())
- if col!='Title Type' and self.sort_states.get('Title Type'):
- order,new=[],[]
- for r in self.data:
- t=r['Title Type']
- if t not in order: order.append(t)
- rev=(nxt=='desc')
- for grp in order:
- grp_rows=[r for r in self.data if r['Title Type']==grp]
- grp_rows.sort(key=keyf, reverse=rev)
- new.extend(grp_rows)
- self.data=new
- else:
- if nxt is None:
- self.data = list(self._backup_data)
- else:
- self.data.sort(key=keyf, reverse=(nxt=='desc'))
- for i,r in enumerate(self.data, start=1):
- r['Position'] = str(i)
- self.refresh_tree()
- def _overlay_color(self, hexcol, alpha=0.10):
- bg=(34,34,34)
- h=hexcol.lstrip('#')
- br,bgc,bb = int(h[0:2],16),int(h[2:4],16),int(h[4:6],16)
- nr=int(br*alpha+bg[0]*(1-alpha))
- ng=int(bgc*alpha+bg[1]*(1-alpha))
- nb=int(bb*alpha+bg[2]*(1-alpha))
- return f'#{nr:02x}{ng:02x}{nb:02x}'
- def refresh_tree(self):
- self.tree.delete(*self.tree.get_children())
- if self.fixed_order_active:
- display=[]
- for t in self.all_types:
- if t not in self.visible_types: continue
- display += [r for r in self.data if r['Title Type']==t]
- else:
- display=[r for r in self.data if r['Title Type'] in self.visible_types]
- for disp_idx, r in enumerate(display, start=1):
- iid = str(self.data.index(r))
- tags=[r['Title Type']]
- if int(iid) in self.selection: tags.append('selected')
- vals = [
- str(disp_idx),
- r['Const'], r['Title Type'], r['Title'],
- r['IMDb Rating'], r['Year'],
- '✕' if int(iid) in self.selection else ''
- ]
- self.tree.insert('', 'end', iid=iid, values=vals, tags=tags)
- def on_save(self):
- # apply current display ordering
- if self.fixed_order_active:
- ordered=[]
- for t in self.all_types:
- if t not in self.visible_types: continue
- ordered += [r for r in self.data if r['Title Type']==t]
- else:
- ordered=[r for r in self.data if r['Title Type'] in self.visible_types]
- for i,r in enumerate(ordered, start=1):
- r['Position']=str(i)
- self.data = ordered
- try:
- with self.csv_path.open(newline='',encoding='utf-8') as f:
- orig=list(csv.DictReader(f))
- fields=orig[0].keys() if orig else []
- lookup={r['Const']:r for r in orig}
- reordered=[ lookup[r['Const']] for r in self.data ]
- with self.csv_path.open('w',newline='',encoding='utf-8') as f:
- w=csv.DictWriter(f,fieldnames=fields)
- w.writeheader()
- for row in reordered:
- w.writerow(row)
- except Exception as e:
- messagebox.showerror("Error",f"Save failed:\n{e}")
- self.destroy()
- # -------------------------------------------------------------------
- # PromptOverlay: Continue/Clear/Cancel/List
- # -------------------------------------------------------------------
- class PromptOverlay(tk.Toplevel):
- def __init__(self, master, default, csv_path):
- super().__init__(master)
- self.overrideredirect(True)
- self.attributes('-topmost', True, '-alpha', 0.8)
- self.config(bg='#111111')
- self.cancelled = False
- self.csv_path = csv_path
- tk.Label(self, text="List Name:", bg='#111111', fg='white',
- font=('Arial',14)).pack(anchor='w', padx=12, pady=(12,0))
- self.var = tk.StringVar(value=default)
- char_width = max(75, len(default) + 10)
- entry = tk.Entry(self, textvariable=self.var,
- width=char_width,
- bg='#111111', fg='white', insertbackground='white',
- font=('Arial',14), bd=0, highlightthickness=0)
- entry.pack(fill='x', padx=12, pady=8)
- btnf = tk.Frame(self, bg='#111111')
- btnf.pack(fill='x', padx=12, pady=(0,12))
- for txt, cmd in (
- ("Continue", self.on_continue),
- ("Clear", self.on_clear),
- ("Cancel", self.on_cancel),
- ("List", self.on_list),
- ):
- tk.Button(
- btnf, text=txt, bg='#111111', fg='#777777',
- font=('Arial',14), bd=0, highlightthickness=1,
- highlightbackground='grey',
- activebackground='#111111',
- activeforeground='#777777',
- relief='flat', command=cmd
- ).pack(side='left', padx=(0,10))
- self.update_idletasks()
- W,H = self.winfo_reqwidth(), self.winfo_reqheight()
- x,y = self.winfo_pointerx(), self.winfo_pointery()
- self.geometry(f"{W}x{H}+{x}+{y}")
- self.deiconify(); self.lift()
- entry.focus_set(); entry.icursor(tk.END)
- def on_continue(self):
- self.destroy()
- def on_clear(self):
- self.var.set(RENAME_PREFIX)
- def on_cancel(self):
- self.cancelled = True
- self.destroy()
- def on_list(self):
- CSVViewer(master=self.master, csv_path=self.csv_path)
- # -------------------------------------------------------------------
- class IMDbHandler(FileSystemEventHandler):
- # Only trigger when the CSV's header starts with "Position,"
- def on_created(self, evt):
- p = Path(evt.src_path)
- if p.suffix.lower() == '.csv':
- try:
- with p.open('rb') as f:
- header = f.read(9) # b"Position,"
- if header == b'Position,':
- event_queue.put(p)
- except Exception:
- pass
- def on_moved(self, evt):
- p = Path(evt.dest_path)
- if p.suffix.lower() == '.csv':
- try:
- with p.open('rb') as f:
- header = f.read(9)
- if header == b'Position,':
- event_queue.put(p)
- except Exception:
- pass
- def choose_watch_folder():
- default=Path.home()/'Downloads'
- use_default=messagebox.askyesno(
- "Watch Folder",f"Use Downloads folder?\n\n{default}",parent=root)
- if use_default: return default
- d=filedialog.askdirectory(
- title="Select watch folder",initialdir=default,parent=root)
- return Path(d) if d else default
- def process_imdb_list(csv_path, master):
- log=Overlay(master)
- def L(m): print(m); log.update_text(m)
- L(f"[import] Starting: {csv_path.name}"); time.sleep(0.05)
- copy_chrome_url()
- raw=get_clipboard_text(); L(f"[import] URL: {raw}")
- query=raw.split('?',1)[1] if '?' in raw else raw
- cleaned=clean_clipboard_text(query) if CLEAN_CLIPBOARD_RULES else query
- default=RENAME_PREFIX+cleaned
- prm=PromptOverlay(master,default,csv_path)
- master.wait_window(prm)
- if getattr(prm,'cancelled',False):
- L("[import] Cancelled"); log.destroy(); return
- log.destroy()
- fileov=Overlay(master)
- def L2(m): print(m); fileov.update_text(m)
- newname=sanitize_filename(prm.var.get().strip() or default)+csv_path.suffix
- L2(f"[import] Moving to: {newname}"); time.sleep(0.05)
- dest=Path(__file__).parent/newname
- shutil.move(str(csv_path),str(dest)); L2(f"[import] Moved to: {dest.name}")
- time.sleep(0.05)
- cfg=Path(__file__).parent.parent/"config.json"
- orig_cfg=None
- if cfg.exists():
- try: orig_cfg=json.load(cfg.open("r",encoding="utf-8"))
- except: pass
- new_cfg=[{
- "name":dest.stem,"imdbId":dest.name,
- "tmdbId":None,"sortField":None,"isReverse":False
- }]
- with cfg.open("w",encoding="utf-8") as f:
- json.dump(new_cfg,f,indent=4,ensure_ascii=False)
- L2("[import] Config written"); time.sleep(0.05)
- subprocess.run("npm run sync",shell=True,cwd=str(cfg.parent))
- L2("[import] Sync complete"); time.sleep(0.05)
- if orig_cfg is None:
- cfg.write_text("",encoding="utf-8")
- else:
- with cfg.open("w",encoding="utf-8") as f:
- json.dump(orig_cfg,f,indent=4,ensure_ascii=False)
- L2("[import] Config restored")
- fileov.destroy()
- if __name__=="__main__":
- root=tk.Tk(); root.withdraw()
- watch_folder=choose_watch_folder()
- if not watch_folder.exists():
- messagebox.showwarning(
- "Folder Missing",
- f"{watch_folder} not found; falling back to Downloads.",
- parent=root
- )
- watch_folder=Path.home()/'Downloads'
- event_queue=queue.Queue()
- handler=IMDbHandler()
- obs=Observer()
- obs.schedule(handler,str(watch_folder),recursive=False)
- obs.start()
- print(f"Watching {watch_folder} for IMDB CSVs… (CTRL-C to quit)")
- try:
- while True:
- try:
- p=event_queue.get(timeout=0.1)
- process_imdb_list(p,root)
- except queue.Empty:
- pass
- except Exception as ex:
- print("Error:",ex)
- except KeyboardInterrupt:
- obs.stop()
- root.destroy()
- obs.join()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement