spatelier 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- analytics/__init__.py +1 -0
- analytics/reporter.py +497 -0
- cli/__init__.py +1 -0
- cli/app.py +147 -0
- cli/audio.py +129 -0
- cli/cli_analytics.py +320 -0
- cli/cli_utils.py +282 -0
- cli/error_handlers.py +122 -0
- cli/files.py +299 -0
- cli/update.py +325 -0
- cli/video.py +823 -0
- cli/worker.py +615 -0
- core/__init__.py +1 -0
- core/analytics_dashboard.py +368 -0
- core/base.py +303 -0
- core/base_service.py +69 -0
- core/config.py +345 -0
- core/database_service.py +116 -0
- core/decorators.py +263 -0
- core/error_handler.py +210 -0
- core/file_tracker.py +254 -0
- core/interactive_cli.py +366 -0
- core/interfaces.py +166 -0
- core/job_queue.py +437 -0
- core/logger.py +79 -0
- core/package_updater.py +469 -0
- core/progress.py +228 -0
- core/service_factory.py +295 -0
- core/streaming.py +299 -0
- core/worker.py +765 -0
- database/__init__.py +1 -0
- database/connection.py +265 -0
- database/metadata.py +516 -0
- database/models.py +288 -0
- database/repository.py +592 -0
- database/transcription_storage.py +219 -0
- modules/__init__.py +1 -0
- modules/audio/__init__.py +5 -0
- modules/audio/converter.py +197 -0
- modules/video/__init__.py +16 -0
- modules/video/converter.py +191 -0
- modules/video/fallback_extractor.py +334 -0
- modules/video/services/__init__.py +18 -0
- modules/video/services/audio_extraction_service.py +274 -0
- modules/video/services/download_service.py +852 -0
- modules/video/services/metadata_service.py +190 -0
- modules/video/services/playlist_service.py +445 -0
- modules/video/services/transcription_service.py +491 -0
- modules/video/transcription_service.py +385 -0
- modules/video/youtube_api.py +397 -0
- spatelier/__init__.py +33 -0
- spatelier-0.3.0.dist-info/METADATA +260 -0
- spatelier-0.3.0.dist-info/RECORD +59 -0
- spatelier-0.3.0.dist-info/WHEEL +5 -0
- spatelier-0.3.0.dist-info/entry_points.txt +2 -0
- spatelier-0.3.0.dist-info/licenses/LICENSE +21 -0
- spatelier-0.3.0.dist-info/top_level.txt +7 -0
- utils/__init__.py +1 -0
- utils/helpers.py +250 -0
database/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Database modules for data storage and analytics."""
|
database/connection.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database connection management.
|
|
3
|
+
|
|
4
|
+
This module provides database connection management for both SQLite and MongoDB.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, Union
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
|
13
|
+
except ImportError: # pragma: no cover - optional dependency
|
|
14
|
+
AsyncIOMotorClient = None
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from pymongo import MongoClient
|
|
18
|
+
except ImportError: # pragma: no cover - optional dependency
|
|
19
|
+
MongoClient = None
|
|
20
|
+
from sqlalchemy import create_engine
|
|
21
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
22
|
+
|
|
23
|
+
from core.config import Config, get_default_data_dir
|
|
24
|
+
from core.logger import get_logger
|
|
25
|
+
from database.models import Base
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DatabaseManager:
|
|
29
|
+
"""Database connection manager for both SQLite and MongoDB."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, config: Config, verbose: bool = False):
|
|
32
|
+
"""
|
|
33
|
+
Initialize database manager.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
config: Configuration instance
|
|
37
|
+
verbose: Enable verbose logging
|
|
38
|
+
"""
|
|
39
|
+
self.config = config
|
|
40
|
+
self.verbose = verbose
|
|
41
|
+
self.logger = get_logger("DatabaseManager", verbose=verbose)
|
|
42
|
+
|
|
43
|
+
# SQLite connection
|
|
44
|
+
self.sqlite_engine = None
|
|
45
|
+
self.sqlite_session = None
|
|
46
|
+
|
|
47
|
+
# MongoDB connections
|
|
48
|
+
self.mongo_client = None
|
|
49
|
+
self.mongo_async_client = None
|
|
50
|
+
self.mongo_db = None
|
|
51
|
+
self.mongo_async_db = None
|
|
52
|
+
|
|
53
|
+
def connect_sqlite(
|
|
54
|
+
self, database_path: Optional[Union[str, Path]] = None
|
|
55
|
+
) -> Session:
|
|
56
|
+
"""
|
|
57
|
+
Connect to SQLite database.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
database_path: Path to SQLite database file
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
SQLAlchemy session
|
|
64
|
+
"""
|
|
65
|
+
if database_path is None:
|
|
66
|
+
database_path = self.config.database.sqlite_path
|
|
67
|
+
|
|
68
|
+
database_path = Path(database_path)
|
|
69
|
+
database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
# Create SQLite engine
|
|
72
|
+
self.sqlite_engine = create_engine(
|
|
73
|
+
f"sqlite:///{database_path}", echo=self.verbose, pool_pre_ping=True
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Create session factory
|
|
77
|
+
SessionLocal = sessionmaker(
|
|
78
|
+
autocommit=False, autoflush=False, bind=self.sqlite_engine
|
|
79
|
+
)
|
|
80
|
+
self.sqlite_session = SessionLocal()
|
|
81
|
+
|
|
82
|
+
# Create tables
|
|
83
|
+
Base.metadata.create_all(bind=self.sqlite_engine)
|
|
84
|
+
|
|
85
|
+
# Create FTS5 virtual table and triggers for transcriptions (if not exists)
|
|
86
|
+
from sqlalchemy import text
|
|
87
|
+
|
|
88
|
+
with self.sqlite_engine.connect() as conn:
|
|
89
|
+
# Drop existing FTS5 table and triggers if they exist (to allow overwrite)
|
|
90
|
+
conn.execute(text("DROP TABLE IF EXISTS transcriptions_fts"))
|
|
91
|
+
conn.execute(text("DROP TRIGGER IF EXISTS transcriptions_ai"))
|
|
92
|
+
conn.execute(text("DROP TRIGGER IF EXISTS transcriptions_ad"))
|
|
93
|
+
conn.execute(text("DROP TRIGGER IF EXISTS transcriptions_au"))
|
|
94
|
+
conn.commit()
|
|
95
|
+
|
|
96
|
+
# Create FTS5 virtual table
|
|
97
|
+
conn.execute(
|
|
98
|
+
text(
|
|
99
|
+
"""
|
|
100
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS transcriptions_fts USING fts5(
|
|
101
|
+
full_text, content='transcriptions', content_rowid='id'
|
|
102
|
+
)
|
|
103
|
+
"""
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Create triggers for automatic FTS5 updates
|
|
108
|
+
conn.execute(
|
|
109
|
+
text(
|
|
110
|
+
"""
|
|
111
|
+
CREATE TRIGGER IF NOT EXISTS transcriptions_ai AFTER INSERT ON transcriptions BEGIN
|
|
112
|
+
INSERT INTO transcriptions_fts(rowid, full_text) VALUES (new.id, new.full_text);
|
|
113
|
+
END
|
|
114
|
+
"""
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
conn.execute(
|
|
119
|
+
text(
|
|
120
|
+
"""
|
|
121
|
+
CREATE TRIGGER IF NOT EXISTS transcriptions_ad AFTER DELETE ON transcriptions BEGIN
|
|
122
|
+
INSERT INTO transcriptions_fts(transcriptions_fts, rowid, full_text)
|
|
123
|
+
VALUES('delete', old.id, old.full_text);
|
|
124
|
+
END
|
|
125
|
+
"""
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
conn.execute(
|
|
130
|
+
text(
|
|
131
|
+
"""
|
|
132
|
+
CREATE TRIGGER IF NOT EXISTS transcriptions_au AFTER UPDATE ON transcriptions BEGIN
|
|
133
|
+
INSERT INTO transcriptions_fts(transcriptions_fts, rowid, full_text)
|
|
134
|
+
VALUES('delete', old.id, old.full_text);
|
|
135
|
+
INSERT INTO transcriptions_fts(rowid, full_text) VALUES (new.id, new.full_text);
|
|
136
|
+
END
|
|
137
|
+
"""
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
conn.commit()
|
|
142
|
+
|
|
143
|
+
# Populate FTS5 table with existing transcriptions
|
|
144
|
+
conn.execute(
|
|
145
|
+
text(
|
|
146
|
+
"""
|
|
147
|
+
INSERT OR IGNORE INTO transcriptions_fts(rowid, full_text)
|
|
148
|
+
SELECT id, full_text FROM transcriptions
|
|
149
|
+
"""
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
conn.commit()
|
|
153
|
+
|
|
154
|
+
self.logger.info(f"Connected to SQLite database: {database_path}")
|
|
155
|
+
return self.sqlite_session
|
|
156
|
+
|
|
157
|
+
def get_session(self):
|
|
158
|
+
"""Get SQLite session for database operations."""
|
|
159
|
+
if not self.sqlite_session:
|
|
160
|
+
self.connect_sqlite()
|
|
161
|
+
return self.sqlite_session
|
|
162
|
+
|
|
163
|
+
def connect_mongodb(
|
|
164
|
+
self,
|
|
165
|
+
connection_string: Optional[str] = None,
|
|
166
|
+
database_name: Optional[str] = None,
|
|
167
|
+
async_mode: bool = False,
|
|
168
|
+
):
|
|
169
|
+
"""
|
|
170
|
+
Connect to MongoDB.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
connection_string: MongoDB connection string (optional, uses config default)
|
|
174
|
+
database_name: Database name (optional, uses config default)
|
|
175
|
+
async_mode: Whether to use async client
|
|
176
|
+
"""
|
|
177
|
+
if connection_string is None:
|
|
178
|
+
connection_string = self.config.database.mongodb_url
|
|
179
|
+
if database_name is None:
|
|
180
|
+
database_name = self.config.database.mongodb_database
|
|
181
|
+
if async_mode:
|
|
182
|
+
if AsyncIOMotorClient is None:
|
|
183
|
+
raise RuntimeError(
|
|
184
|
+
"MongoDB async client is unavailable. Install the 'mongodb' extra to enable it."
|
|
185
|
+
)
|
|
186
|
+
# Async MongoDB client
|
|
187
|
+
self.mongo_async_client = AsyncIOMotorClient(connection_string)
|
|
188
|
+
self.mongo_async_db = self.mongo_async_client[database_name]
|
|
189
|
+
self.logger.info(f"Connected to MongoDB (async): {database_name}")
|
|
190
|
+
else:
|
|
191
|
+
if MongoClient is None:
|
|
192
|
+
raise RuntimeError(
|
|
193
|
+
"MongoDB client is unavailable. Install the 'mongodb' extra to enable it."
|
|
194
|
+
)
|
|
195
|
+
# Sync MongoDB client
|
|
196
|
+
self.mongo_client = MongoClient(connection_string)
|
|
197
|
+
self.mongo_db = self.mongo_client[database_name]
|
|
198
|
+
self.logger.info(f"Connected to MongoDB (sync): {database_name}")
|
|
199
|
+
|
|
200
|
+
def get_sqlite_session(self) -> Session:
|
|
201
|
+
"""Get SQLite session."""
|
|
202
|
+
if self.sqlite_session is None:
|
|
203
|
+
raise RuntimeError("SQLite not connected. Call connect_sqlite() first.")
|
|
204
|
+
return self.sqlite_session
|
|
205
|
+
|
|
206
|
+
def get_mongo_db(self):
|
|
207
|
+
"""Get MongoDB database (sync)."""
|
|
208
|
+
if self.mongo_db is None:
|
|
209
|
+
raise RuntimeError("MongoDB not connected. Call connect_mongodb() first.")
|
|
210
|
+
return self.mongo_db
|
|
211
|
+
|
|
212
|
+
def get_mongo_async_db(self):
|
|
213
|
+
"""Get MongoDB database (async)."""
|
|
214
|
+
if self.mongo_async_db is None:
|
|
215
|
+
raise RuntimeError(
|
|
216
|
+
"MongoDB async not connected. Call connect_mongodb(async_mode=True) first."
|
|
217
|
+
)
|
|
218
|
+
return self.mongo_async_db
|
|
219
|
+
|
|
220
|
+
def close_connections(self):
|
|
221
|
+
"""Close all database connections."""
|
|
222
|
+
if self.sqlite_session:
|
|
223
|
+
self.sqlite_session.close()
|
|
224
|
+
self.sqlite_session = None
|
|
225
|
+
self.logger.info("SQLite session closed")
|
|
226
|
+
|
|
227
|
+
if self.sqlite_engine:
|
|
228
|
+
self.sqlite_engine.dispose()
|
|
229
|
+
self.sqlite_engine = None
|
|
230
|
+
self.logger.info("SQLite engine disposed")
|
|
231
|
+
|
|
232
|
+
if self.mongo_client:
|
|
233
|
+
self.mongo_client.close()
|
|
234
|
+
self.mongo_client = None
|
|
235
|
+
self.logger.info("MongoDB client closed")
|
|
236
|
+
|
|
237
|
+
if self.mongo_async_client:
|
|
238
|
+
# Note: Motor clients don't need explicit closing in async context
|
|
239
|
+
self.logger.info("MongoDB async client closed")
|
|
240
|
+
|
|
241
|
+
def __enter__(self):
|
|
242
|
+
"""Context manager entry."""
|
|
243
|
+
return self
|
|
244
|
+
|
|
245
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
246
|
+
"""Context manager exit."""
|
|
247
|
+
self.close_connections()
|
|
248
|
+
# Ensure session is properly closed
|
|
249
|
+
if hasattr(self, "sqlite_session") and self.sqlite_session:
|
|
250
|
+
self.sqlite_session.close()
|
|
251
|
+
self.sqlite_session = None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class DatabaseConfig:
|
|
255
|
+
"""Database configuration class."""
|
|
256
|
+
|
|
257
|
+
def __init__(self, config: Config):
|
|
258
|
+
"""Initialize database configuration."""
|
|
259
|
+
self.config = config
|
|
260
|
+
self.sqlite_path = get_default_data_dir() / "spatelier.db"
|
|
261
|
+
self.mongo_connection_string = "mongodb://localhost:27017"
|
|
262
|
+
self.mongo_database = "spatelier"
|
|
263
|
+
self.enable_mongodb = False
|
|
264
|
+
self.enable_analytics = True
|
|
265
|
+
self.retention_days = 365 # Keep analytics data for 1 year
|