Removed the old script
This commit is contained in:
@@ -1,226 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fuzzy search artists in drift.db and show their similar artists."""
|
||||
|
||||
import curses
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_db():
|
||||
"""Find drift.db in XDG data dir."""
|
||||
data_home = os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")
|
||||
p = Path(data_home) / "drift" / "drift.db"
|
||||
if p.exists():
|
||||
return str(p)
|
||||
print(f"Could not find {p}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def load_artists(db_path):
|
||||
conn = sqlite3.connect(db_path)
|
||||
rows = conn.execute(
|
||||
"SELECT a.mbid, COALESCE(a.name, a.mbid) FROM artists a "
|
||||
"LEFT JOIN tracks t ON t.artist_mbid = a.mbid "
|
||||
"GROUP BY a.mbid ORDER BY COUNT(t.path) DESC, a.name"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return rows # [(mbid, display_name), ...]
|
||||
|
||||
|
||||
def get_similar(db_path, mbid):
|
||||
conn = sqlite3.connect(db_path)
|
||||
rows = conn.execute(
|
||||
"SELECT similar_name, match_score FROM similar_artists "
|
||||
"WHERE artist_mbid = ?1 ORDER BY match_score DESC",
|
||||
(mbid,),
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return rows
|
||||
|
||||
|
||||
def get_top_tracks(db_path, mbid):
|
||||
conn = sqlite3.connect(db_path)
|
||||
rows = conn.execute(
|
||||
"SELECT t.path, tt.playcount FROM tracks t "
|
||||
"JOIN top_tracks tt ON tt.artist_mbid = t.artist_mbid "
|
||||
" AND (LOWER(t.title) = tt.name_lower "
|
||||
" OR t.recording_mbid = tt.recording_mbid) "
|
||||
"WHERE t.artist_mbid = ?1 "
|
||||
"ORDER BY tt.playcount DESC",
|
||||
(mbid,),
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return rows
|
||||
|
||||
|
||||
def get_local_track_count(db_path, mbid):
|
||||
conn = sqlite3.connect(db_path)
|
||||
total = conn.execute(
|
||||
"SELECT COUNT(*) FROM tracks WHERE artist_mbid = ?1", (mbid,)
|
||||
).fetchone()[0]
|
||||
matched = conn.execute(
|
||||
"SELECT COUNT(*) FROM tracks t "
|
||||
"JOIN top_tracks tt ON tt.artist_mbid = t.artist_mbid "
|
||||
" AND (LOWER(t.title) = tt.name_lower "
|
||||
" OR t.recording_mbid = tt.recording_mbid) "
|
||||
"WHERE t.artist_mbid = ?1",
|
||||
(mbid,),
|
||||
).fetchone()[0]
|
||||
conn.close()
|
||||
return total, matched
|
||||
|
||||
|
||||
def fuzzy_match(query, name):
|
||||
"""Simple fuzzy: all query chars appear in order in name."""
|
||||
name_lower = name.lower()
|
||||
qi = 0
|
||||
for ch in name_lower:
|
||||
if qi < len(query) and ch == query[qi]:
|
||||
qi += 1
|
||||
return qi == len(query)
|
||||
|
||||
|
||||
def run_tui(stdscr, db_path):
|
||||
curses.curs_set(0)
|
||||
curses.use_default_colors()
|
||||
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
||||
curses.init_pair(2, curses.COLOR_CYAN, -1)
|
||||
curses.init_pair(3, curses.COLOR_WHITE, -1)
|
||||
|
||||
artists = load_artists(db_path)
|
||||
query = ""
|
||||
selected = 0
|
||||
scroll = 0
|
||||
|
||||
while True:
|
||||
stdscr.erase()
|
||||
h, w = stdscr.getmaxyx()
|
||||
|
||||
# filter
|
||||
q = query.lower()
|
||||
if q:
|
||||
filtered = [(m, n) for m, n in artists if fuzzy_match(q, n)]
|
||||
else:
|
||||
filtered = artists
|
||||
|
||||
selected = max(0, min(selected, len(filtered) - 1))
|
||||
|
||||
# prompt
|
||||
prompt = f" > {query}"
|
||||
stdscr.addnstr(0, 0, prompt, w, curses.color_pair(2) | curses.A_BOLD)
|
||||
count_str = f" {len(filtered)}/{len(artists)}"
|
||||
if len(prompt) + len(count_str) < w:
|
||||
stdscr.addstr(0, len(prompt), count_str, curses.color_pair(3))
|
||||
|
||||
# artist list
|
||||
list_h = h - 1
|
||||
if list_h < 1:
|
||||
stdscr.refresh()
|
||||
continue
|
||||
|
||||
if selected < scroll:
|
||||
scroll = selected
|
||||
if selected >= scroll + list_h:
|
||||
scroll = selected - list_h + 1
|
||||
|
||||
for i in range(list_h):
|
||||
idx = scroll + i
|
||||
if idx >= len(filtered):
|
||||
break
|
||||
_, name = filtered[idx]
|
||||
attr = curses.color_pair(1) if idx == selected else curses.A_NORMAL
|
||||
stdscr.addnstr(i + 1, 0, f" {name}", w, attr)
|
||||
|
||||
stdscr.refresh()
|
||||
|
||||
key = stdscr.get_wch()
|
||||
|
||||
if key == "\x1b": # Esc
|
||||
return
|
||||
elif key == curses.KEY_UP or key == "\x10": # Up / Ctrl-P
|
||||
selected = max(0, selected - 1)
|
||||
elif key == curses.KEY_DOWN or key == "\x0e": # Down / Ctrl-N
|
||||
selected = min(len(filtered) - 1, selected + 1)
|
||||
elif key == "\n" or key == curses.KEY_ENTER:
|
||||
if filtered:
|
||||
show_similar(stdscr, db_path, filtered[selected])
|
||||
elif key in (curses.KEY_BACKSPACE, "\x7f", "\x08"):
|
||||
query = query[:-1]
|
||||
selected = 0
|
||||
scroll = 0
|
||||
elif isinstance(key, str) and key.isprintable():
|
||||
query += key
|
||||
selected = 0
|
||||
scroll = 0
|
||||
|
||||
|
||||
def show_similar(stdscr, db_path, artist):
|
||||
mbid, name = artist
|
||||
similar = get_similar(db_path, mbid)
|
||||
top = get_top_tracks(db_path, mbid)
|
||||
total_local, matched_local = get_local_track_count(db_path, mbid)
|
||||
|
||||
curses.curs_set(0)
|
||||
stdscr.erase()
|
||||
h, w = stdscr.getmaxyx()
|
||||
|
||||
mid = w // 2
|
||||
|
||||
title_l = f" Similar to {name}"
|
||||
title_r = f" Top tracks ({matched_local}/{total_local} matched)"
|
||||
stdscr.addnstr(0, 0, title_l, mid, curses.color_pair(2) | curses.A_BOLD)
|
||||
stdscr.addnstr(0, mid, title_r, w - mid, curses.color_pair(2) | curses.A_BOLD)
|
||||
stdscr.addnstr(h - 1, 0, " [q] back", w, curses.color_pair(3))
|
||||
|
||||
scroll_l = 0
|
||||
scroll_r = 0
|
||||
list_h = h - 2
|
||||
|
||||
while True:
|
||||
# Left pane: similar artists
|
||||
for i in range(list_h):
|
||||
stdscr.move(i + 1, 0)
|
||||
stdscr.clrtoeol()
|
||||
idx = scroll_l + i
|
||||
if idx < len(similar):
|
||||
sname, score = similar[idx]
|
||||
line = f" {score:5.2f} {sname}"
|
||||
stdscr.addnstr(i + 1, 0, line, mid)
|
||||
|
||||
# Right pane: top tracks
|
||||
for i in range(list_h):
|
||||
idx = scroll_r + i
|
||||
if idx < len(top):
|
||||
path, playcount = top[idx]
|
||||
# Show just the filename without extension
|
||||
fname = Path(path).stem
|
||||
# Strip "Artist - " prefix if present
|
||||
if " - " in fname:
|
||||
fname = fname.split(" - ", 1)[1]
|
||||
line = f" {playcount:>8} {fname}"
|
||||
stdscr.addnstr(i + 1, mid, line, w - mid)
|
||||
|
||||
stdscr.refresh()
|
||||
key = stdscr.get_wch()
|
||||
|
||||
if key in ("q", "Q", "\x1b"):
|
||||
return
|
||||
elif key == curses.KEY_UP or key == "\x10":
|
||||
scroll_l = max(0, scroll_l - 1)
|
||||
scroll_r = max(0, scroll_r - 1)
|
||||
elif key == curses.KEY_DOWN or key == "\x0e":
|
||||
if scroll_l + list_h < len(similar):
|
||||
scroll_l += 1
|
||||
if scroll_r + list_h < len(top):
|
||||
scroll_r += 1
|
||||
|
||||
|
||||
def main():
|
||||
db_path = find_db()
|
||||
curses.wrapper(run_tui, db_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user