-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatabase.py
More file actions
191 lines (152 loc) · 5.67 KB
/
Copy pathdatabase.py
File metadata and controls
191 lines (152 loc) · 5.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import sqlite3
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, "sample_archive.db")
SUPPORTED_EXTENSIONS = ('.wav', '.flac', '.mp3', '.ogg', '.aif', '.aiff')
def get_connection():
"""Every caller (UI thread, scan thread) opens its own short-lived connection."""
conn = sqlite3.connect(DB_PATH, timeout=30)
conn.execute("PRAGMA foreign_keys = ON;")
conn.execute("PRAGMA synchronous = NORMAL;") # safe with WAL, much faster commits
return conn
def _ensure_tag_color_column(cursor):
"""Older databases may lack the 'color' column on tags."""
cursor.execute("PRAGMA table_info(tags)")
columns = [col[1] for col in cursor.fetchall()]
if columns and "color" not in columns:
try:
cursor.execute("ALTER TABLE tags ADD COLUMN color TEXT DEFAULT '#255d8f'")
except sqlite3.OperationalError:
pass
def init_db():
conn = get_connection()
cursor = conn.cursor()
# WAL is stored in the database file, so this only needs to succeed once,
# but it is harmless to re-assert on every start.
cursor.execute("PRAGMA journal_mode = WAL;")
cursor.execute("""
CREATE TABLE IF NOT EXISTS samples (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT UNIQUE,
file_name TEXT
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
color TEXT DEFAULT '#255d8f'
)
""")
_ensure_tag_color_column(cursor)
cursor.execute("""
CREATE TABLE IF NOT EXISTS sample_tags (
sample_id INTEGER,
tag_id INTEGER,
PRIMARY KEY (sample_id, tag_id),
FOREIGN KEY (sample_id) REFERENCES samples(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
)
""")
# The primary key covers lookups by sample; this covers lookups/counts by tag.
cursor.execute("CREATE INDEX IF NOT EXISTS idx_sample_tags_tag ON sample_tags(tag_id)")
cursor.execute("""
CREATE TABLE IF NOT EXISTS library_roots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT UNIQUE
)
""")
conn.commit()
conn.close()
def find_audio_files(directory_path, should_cancel=None):
"""Walks a directory tree and returns normalized paths of supported audio files."""
found = []
for root, _, files in os.walk(directory_path):
if should_cancel and should_cancel():
break
for file in files:
if file.lower().endswith(SUPPORTED_EXTENSIONS):
found.append(os.path.normpath(os.path.join(root, file)))
return found
def index_files(paths, progress_callback=None, should_cancel=None, batch_size=500):
"""
Inserts file paths into the samples table in batched transactions.
Returns the number of files processed (existing rows are left untouched).
"""
conn = get_connection()
cursor = conn.cursor()
processed = 0
batch = []
try:
for path in paths:
if should_cancel and should_cancel():
break
batch.append((path, os.path.basename(path)))
processed += 1
if len(batch) >= batch_size:
cursor.executemany(
"INSERT OR IGNORE INTO samples (file_path, file_name) VALUES (?, ?)", batch
)
conn.commit()
batch = []
if progress_callback and processed % 25 == 0:
progress_callback(processed, path)
if batch:
cursor.executemany(
"INSERT OR IGNORE INTO samples (file_path, file_name) VALUES (?, ?)", batch
)
conn.commit()
finally:
conn.close()
return processed
def scan_directory(directory_path, progress_callback=None):
"""Convenience wrapper: walk a folder and index everything found."""
return index_files(find_audio_files(directory_path), progress_callback=progress_callback)
def delete_sample_from_db(file_path):
delete_samples_from_db([file_path])
def delete_samples_from_db(file_paths):
"""Removes many sample records in a single transaction."""
paths = [(os.path.normpath(p),) for p in file_paths]
if not paths:
return
conn = get_connection()
try:
conn.executemany("DELETE FROM samples WHERE file_path = ?", paths)
conn.commit()
finally:
conn.close()
def reset_database():
conn = get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM sample_tags")
cursor.execute("DELETE FROM samples")
cursor.execute("DELETE FROM tags")
cursor.execute("DELETE FROM library_roots")
conn.commit()
# VACUUM requires autocommit mode (no active transaction)
isolation = conn.isolation_level
conn.isolation_level = None
cursor.execute("VACUUM")
conn.isolation_level = isolation
conn.close()
def add_library_root(path):
conn = get_connection()
cursor = conn.cursor()
norm_path = os.path.normpath(path)
cursor.execute("INSERT OR IGNORE INTO library_roots (path) VALUES (?)", (norm_path,))
conn.commit()
conn.close()
def get_library_roots():
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT path FROM library_roots ORDER BY path ASC")
rows = cursor.fetchall()
conn.close()
return [r[0] for r in rows]
def remove_library_root(path):
conn = get_connection()
cursor = conn.cursor()
norm_path = os.path.normpath(path)
cursor.execute("DELETE FROM library_roots WHERE path = ?", (norm_path,))
conn.commit()
conn.close()