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.
Files changed (59) hide show
  1. analytics/__init__.py +1 -0
  2. analytics/reporter.py +497 -0
  3. cli/__init__.py +1 -0
  4. cli/app.py +147 -0
  5. cli/audio.py +129 -0
  6. cli/cli_analytics.py +320 -0
  7. cli/cli_utils.py +282 -0
  8. cli/error_handlers.py +122 -0
  9. cli/files.py +299 -0
  10. cli/update.py +325 -0
  11. cli/video.py +823 -0
  12. cli/worker.py +615 -0
  13. core/__init__.py +1 -0
  14. core/analytics_dashboard.py +368 -0
  15. core/base.py +303 -0
  16. core/base_service.py +69 -0
  17. core/config.py +345 -0
  18. core/database_service.py +116 -0
  19. core/decorators.py +263 -0
  20. core/error_handler.py +210 -0
  21. core/file_tracker.py +254 -0
  22. core/interactive_cli.py +366 -0
  23. core/interfaces.py +166 -0
  24. core/job_queue.py +437 -0
  25. core/logger.py +79 -0
  26. core/package_updater.py +469 -0
  27. core/progress.py +228 -0
  28. core/service_factory.py +295 -0
  29. core/streaming.py +299 -0
  30. core/worker.py +765 -0
  31. database/__init__.py +1 -0
  32. database/connection.py +265 -0
  33. database/metadata.py +516 -0
  34. database/models.py +288 -0
  35. database/repository.py +592 -0
  36. database/transcription_storage.py +219 -0
  37. modules/__init__.py +1 -0
  38. modules/audio/__init__.py +5 -0
  39. modules/audio/converter.py +197 -0
  40. modules/video/__init__.py +16 -0
  41. modules/video/converter.py +191 -0
  42. modules/video/fallback_extractor.py +334 -0
  43. modules/video/services/__init__.py +18 -0
  44. modules/video/services/audio_extraction_service.py +274 -0
  45. modules/video/services/download_service.py +852 -0
  46. modules/video/services/metadata_service.py +190 -0
  47. modules/video/services/playlist_service.py +445 -0
  48. modules/video/services/transcription_service.py +491 -0
  49. modules/video/transcription_service.py +385 -0
  50. modules/video/youtube_api.py +397 -0
  51. spatelier/__init__.py +33 -0
  52. spatelier-0.3.0.dist-info/METADATA +260 -0
  53. spatelier-0.3.0.dist-info/RECORD +59 -0
  54. spatelier-0.3.0.dist-info/WHEEL +5 -0
  55. spatelier-0.3.0.dist-info/entry_points.txt +2 -0
  56. spatelier-0.3.0.dist-info/licenses/LICENSE +21 -0
  57. spatelier-0.3.0.dist-info/top_level.txt +7 -0
  58. utils/__init__.py +1 -0
  59. 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