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
@@ -0,0 +1,397 @@
1
+ """
2
+ YouTube Data API v3 integration for real video search and recommendations.
3
+
4
+ This module provides YouTube API integration for:
5
+ - Video search with real metadata
6
+ - Related video recommendations
7
+ - Channel information
8
+ - Rate limiting and error handling
9
+ """
10
+
11
+ import os
12
+ import time
13
+ from datetime import datetime, timedelta
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ try:
17
+ import pickle
18
+
19
+ import google.auth
20
+ from google.auth.exceptions import RefreshError
21
+ from google.auth.transport.requests import Request
22
+ from google_auth_oauthlib.flow import InstalledAppFlow
23
+ from googleapiclient.discovery import build
24
+ from googleapiclient.errors import HttpError
25
+
26
+ YOUTUBE_API_AVAILABLE = True
27
+ except ImportError:
28
+ YOUTUBE_API_AVAILABLE = False
29
+
30
+ from core.logger import get_logger
31
+
32
+ # VideoMetadata removed with SSD library
33
+
34
+
35
+ class YouTubeAPIService:
36
+ """YouTube Data API v3 service for video search and recommendations."""
37
+
38
+ def __init__(
39
+ self, api_key: str = None, oauth_credentials: str = None, verbose: bool = False
40
+ ):
41
+ """Initialize YouTube API service with API key or OAuth."""
42
+ self.api_key = api_key
43
+ self.oauth_credentials = oauth_credentials
44
+ self.verbose = verbose
45
+ self.logger = get_logger("YouTubeAPI", verbose=verbose)
46
+
47
+ # Rate limiting
48
+ self.last_request_time = 0
49
+ self.min_request_interval = 0.1 # 100ms between requests
50
+
51
+ # Initialize API client
52
+ self.youtube = None
53
+ self.credentials = None
54
+
55
+ if YOUTUBE_API_AVAILABLE:
56
+ if oauth_credentials:
57
+ self._initialize_oauth()
58
+ elif api_key:
59
+ self._initialize_api_key()
60
+ else:
61
+ self.logger.warning("No authentication method provided")
62
+ else:
63
+ self.logger.warning("YouTube API not available")
64
+
65
+ def _initialize_oauth(self):
66
+ """Initialize OAuth authentication."""
67
+ try:
68
+ # OAuth 2.0 scopes for YouTube
69
+ SCOPES = ["https://www.googleapis.com/auth/youtube.readonly"]
70
+
71
+ # Load credentials from file
72
+ creds = None
73
+ token_file = "config/youtube_token.pickle"
74
+
75
+ # Load existing token
76
+ if os.path.exists(token_file):
77
+ with open(token_file, "rb") as token:
78
+ creds = pickle.load(token)
79
+
80
+ # If no valid credentials, get new ones
81
+ if not creds or not creds.valid:
82
+ if creds and creds.expired and creds.refresh_token:
83
+ creds.refresh(Request())
84
+ else:
85
+ # Load OAuth client credentials
86
+ if os.path.exists(self.oauth_credentials):
87
+ flow = InstalledAppFlow.from_client_secrets_file(
88
+ self.oauth_credentials, SCOPES
89
+ )
90
+ creds = flow.run_local_server(port=0)
91
+ else:
92
+ self.logger.error(
93
+ f"OAuth credentials file not found: {self.oauth_credentials}"
94
+ )
95
+ return
96
+
97
+ # Save credentials for next run
98
+ with open(token_file, "wb") as token:
99
+ pickle.dump(creds, token)
100
+
101
+ self.credentials = creds
102
+ self.youtube = build("youtube", "v3", credentials=creds)
103
+ self.logger.info("YouTube API client initialized with OAuth")
104
+
105
+ except Exception as e:
106
+ self.logger.error(f"Failed to initialize OAuth: {e}")
107
+ self.youtube = None
108
+
109
+ def _initialize_api_key(self):
110
+ """Initialize API key authentication."""
111
+ try:
112
+ self.youtube = build("youtube", "v3", developerKey=self.api_key)
113
+ self.logger.info("YouTube API client initialized with API key")
114
+ except Exception as e:
115
+ self.logger.error(f"Failed to initialize YouTube API: {e}")
116
+ self.youtube = None
117
+
118
+ def _rate_limit(self):
119
+ """Apply rate limiting to API requests."""
120
+ current_time = time.time()
121
+ time_since_last = current_time - self.last_request_time
122
+
123
+ if time_since_last < self.min_request_interval:
124
+ sleep_time = self.min_request_interval - time_since_last
125
+ time.sleep(sleep_time)
126
+
127
+ self.last_request_time = time.time()
128
+
129
+ def _get_video_details(self, video_ids: List[str]) -> Dict[str, Dict]:
130
+ """Get detailed video information for multiple videos."""
131
+ if not self.youtube or not video_ids:
132
+ return {}
133
+
134
+ try:
135
+ self._rate_limit()
136
+
137
+ # Batch request for video details
138
+ details_response = (
139
+ self.youtube.videos()
140
+ .list(part="statistics,contentDetails,snippet", id=",".join(video_ids))
141
+ .execute()
142
+ )
143
+
144
+ details_dict = {}
145
+ for item in details_response.get("items", []):
146
+ video_id = item["id"]
147
+ details_dict[video_id] = item
148
+
149
+ return details_dict
150
+
151
+ except HttpError as e:
152
+ self.logger.error(f"Video details API error: {e}")
153
+ return {}
154
+ except Exception as e:
155
+ self.logger.error(f"Video details error: {e}")
156
+ return {}
157
+
158
+ def _parse_duration(self, duration_iso: str) -> int:
159
+ """Parse ISO 8601 duration to seconds."""
160
+ import re
161
+
162
+ # Parse PT1H2M3S format
163
+ pattern = r"PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?"
164
+ match = re.match(pattern, duration_iso)
165
+
166
+ if not match:
167
+ return 0
168
+
169
+ hours = int(match.group(1) or 0)
170
+ minutes = int(match.group(2) or 0)
171
+ seconds = int(match.group(3) or 0)
172
+
173
+ return hours * 3600 + minutes * 60 + seconds
174
+
175
+ def _format_duration(self, seconds: int) -> str:
176
+ """Format duration in seconds to HH:MM:SS."""
177
+ if not seconds:
178
+ return "0:00"
179
+
180
+ hours = seconds // 3600
181
+ minutes = (seconds % 3600) // 60
182
+ secs = seconds % 60
183
+
184
+ if hours > 0:
185
+ return f"{hours}:{minutes:02d}:{secs:02d}"
186
+ else:
187
+ return f"{minutes}:{secs:02d}"
188
+
189
+ def _calculate_age(self, upload_date: datetime) -> str:
190
+ """Calculate human-readable age from upload date."""
191
+ now = datetime.now(upload_date.tzinfo) if upload_date.tzinfo else datetime.now()
192
+ delta = now - upload_date
193
+
194
+ if delta.days > 365:
195
+ years = delta.days // 365
196
+ return f"{years} year{'s' if years > 1 else ''} ago"
197
+ elif delta.days > 30:
198
+ months = delta.days // 30
199
+ return f"{months} month{'s' if months > 1 else ''} ago"
200
+ elif delta.days > 0:
201
+ return f"{delta.days} day{'s' if delta.days > 1 else ''} ago"
202
+ else:
203
+ return "Today"
204
+
205
+ def _get_category_name(self, category_id: str) -> str:
206
+ """Get category name from ID."""
207
+ categories = {
208
+ "1": "Film & Animation",
209
+ "2": "Autos & Vehicles",
210
+ "10": "Music",
211
+ "15": "Pets & Animals",
212
+ "17": "Sports",
213
+ "19": "Travel & Events",
214
+ "20": "Gaming",
215
+ "22": "People & Blogs",
216
+ "23": "Comedy",
217
+ "24": "Entertainment",
218
+ "25": "News & Politics",
219
+ "26": "Howto & Style",
220
+ "27": "Education",
221
+ "28": "Science & Technology",
222
+ }
223
+ return categories.get(category_id, "Unknown")
224
+
225
+
226
+ def get_youtube_api_key() -> Optional[str]:
227
+ """Get YouTube API key from environment, database, config, or user input."""
228
+ # Try environment variable first
229
+ api_key = os.getenv("YOUTUBE_API_KEY")
230
+ if api_key:
231
+ return api_key
232
+
233
+ # Try database
234
+ api_key = _get_api_key_from_db("youtube")
235
+ if api_key:
236
+ return api_key
237
+
238
+ # Try config file
239
+ try:
240
+ from core.config import Config
241
+
242
+ config = Config()
243
+ # Add YouTube API key to config if needed
244
+ config_key = getattr(config, "youtube_api_key", None)
245
+ if config_key:
246
+ return config_key
247
+ except:
248
+ pass
249
+
250
+ # Prompt user for API key
251
+ try:
252
+ from rich.console import Console
253
+ from rich.prompt import Prompt
254
+
255
+ console = Console()
256
+ console.print("\n[bold blue]YouTube API Integration[/bold blue]")
257
+ console.print(
258
+ "For real video search results, you can provide a YouTube Data API v3 key."
259
+ )
260
+ console.print(
261
+ "Without an API key, the system will use mock data for development."
262
+ )
263
+
264
+ choice = Prompt.ask(
265
+ "Do you want to enter an API key?", choices=["Y", "N"], default="N"
266
+ )
267
+
268
+ if choice.upper() == "Y":
269
+ api_key = Prompt.ask("Enter your YouTube Data API v3 key", password=True)
270
+ if api_key and api_key.strip():
271
+ # Save to database
272
+ _save_api_key_to_db("youtube", api_key.strip())
273
+ # Also save to environment for this session
274
+ os.environ["YOUTUBE_API_KEY"] = api_key.strip()
275
+ console.print("[green]✓ API key saved to database and session[/green]")
276
+ return api_key.strip()
277
+ else:
278
+ console.print("[yellow]No API key provided, using mock data[/yellow]")
279
+ else:
280
+ console.print("[yellow]Using mock data for development[/yellow]")
281
+
282
+ except (EOFError, KeyboardInterrupt):
283
+ # Handle non-interactive environments
284
+ pass
285
+ except Exception as e:
286
+ # Handle any other errors gracefully
287
+ pass
288
+
289
+ return None
290
+
291
+
292
+ def _save_api_key_to_db(service_name: str, api_key: str) -> bool:
293
+ """Save API key to database."""
294
+ try:
295
+ from sqlalchemy.sql import func
296
+
297
+ from core.config import Config
298
+ from database.connection import DatabaseManager
299
+ from database.models import APIKeys
300
+
301
+ config = Config()
302
+ db_manager = DatabaseManager(config)
303
+ db_manager.connect_sqlite()
304
+
305
+ with db_manager.get_session() as session:
306
+ # Check if key already exists
307
+ existing_key = (
308
+ session.query(APIKeys)
309
+ .filter(APIKeys.service_name == service_name, APIKeys.is_active == True)
310
+ .first()
311
+ )
312
+
313
+ if existing_key:
314
+ # Update existing key
315
+ existing_key.key_value = api_key
316
+ existing_key.updated_at = func.now()
317
+ else:
318
+ # Create new key
319
+ new_key = APIKeys(
320
+ service_name=service_name, key_value=api_key, is_active=True
321
+ )
322
+ session.add(new_key)
323
+
324
+ session.commit()
325
+ return True
326
+
327
+ except Exception as e:
328
+ # Log error but don't fail the whole process
329
+ import logging
330
+
331
+ logging.error(f"Failed to save API key to database: {e}")
332
+ return False
333
+
334
+
335
+ def _get_api_key_from_db(service_name: str) -> Optional[str]:
336
+ """Get API key from database."""
337
+ try:
338
+ from sqlalchemy.sql import func
339
+
340
+ from core.config import Config
341
+ from database.connection import DatabaseManager
342
+ from database.models import APIKeys
343
+
344
+ config = Config()
345
+ db_manager = DatabaseManager(config)
346
+ db_manager.connect_sqlite()
347
+
348
+ with db_manager.get_session() as session:
349
+ api_key = (
350
+ session.query(APIKeys)
351
+ .filter(APIKeys.service_name == service_name, APIKeys.is_active == True)
352
+ .first()
353
+ )
354
+
355
+ if api_key:
356
+ return api_key.key_value
357
+
358
+ except Exception as e:
359
+ # Log error but don't fail the whole process
360
+ import logging
361
+
362
+ logging.error(f"Failed to get API key from database: {e}")
363
+
364
+ return None
365
+
366
+
367
+ def create_youtube_service(
368
+ verbose: bool = False, oauth_credentials: str = None
369
+ ) -> Optional[YouTubeAPIService]:
370
+ """Create YouTube API service with proper configuration."""
371
+ logger = get_logger("YouTubeAPI", verbose=verbose)
372
+
373
+ # Debug logging
374
+ logger.info(
375
+ f"create_youtube_service called with oauth_credentials: {oauth_credentials}"
376
+ )
377
+ logger.info(
378
+ f"oauth_credentials exists: {oauth_credentials and os.path.exists(oauth_credentials) if oauth_credentials else False}"
379
+ )
380
+
381
+ # Try OAuth first if credentials provided
382
+ if oauth_credentials and os.path.exists(oauth_credentials):
383
+ logger.info("Using OAuth credentials for YouTube API")
384
+ return YouTubeAPIService(oauth_credentials=oauth_credentials, verbose=verbose)
385
+
386
+ # Fallback to API key
387
+ logger.info("Falling back to API key authentication")
388
+ api_key = get_youtube_api_key()
389
+
390
+ if not api_key:
391
+ logger.warning(
392
+ "No YouTube API key found. Set YOUTUBE_API_KEY environment variable."
393
+ )
394
+ return None
395
+
396
+ logger.info("Using API key for YouTube API")
397
+ return YouTubeAPIService(api_key=api_key, verbose=verbose)
spatelier/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """
2
+ Spatelier package shim.
3
+
4
+ Provides a stable import namespace while the codebase remains in a flat layout.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib
10
+ import sys
11
+ from typing import Iterable
12
+
13
+ __version__ = "0.1.0"
14
+ __author__ = "Galen Spikes"
15
+ __email__ = "galenspikes@gmail.com"
16
+
17
+
18
+ def _alias_modules(module_names: Iterable[str]) -> None:
19
+ for name in module_names:
20
+ module = importlib.import_module(name)
21
+ sys.modules[f"{__name__}.{name}"] = module
22
+
23
+
24
+ _alias_modules(
25
+ [
26
+ "cli",
27
+ "core",
28
+ "database",
29
+ "modules",
30
+ "analytics",
31
+ "utils",
32
+ ]
33
+ )
@@ -0,0 +1,260 @@
1
+ Metadata-Version: 2.4
2
+ Name: spatelier
3
+ Version: 0.3.0
4
+ Summary: Personal tool library for video and music file handling
5
+ Author-email: Galen Spikes <galenspikes@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/galenspikes/spatelier
8
+ Project-URL: Documentation, https://github.com/galenspikes/spatelier#readme
9
+ Project-URL: Repository, https://github.com/galenspikes/spatelier
10
+ Project-URL: Issues, https://github.com/galenspikes/spatelier/issues
11
+ Project-URL: Changelog, https://github.com/galenspikes/spatelier/blob/main/CHANGELOG.md
12
+ Keywords: video,audio,media,youtube,download,transcription,processing,cli,tool
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Multimedia :: Video
21
+ Classifier: Topic :: Multimedia :: Sound/Audio
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: typer[all]>=0.9.0
27
+ Requires-Dist: rich>=13.0.0
28
+ Requires-Dist: click>=8.0.0
29
+ Requires-Dist: pydantic>=2.0.0
30
+ Requires-Dist: pyyaml>=6.0.0
31
+ Requires-Dist: loguru>=0.7.0
32
+ Requires-Dist: yt-dlp>=2023.12.30
33
+ Requires-Dist: ffmpeg-python>=0.2.0
34
+ Requires-Dist: mutagen>=1.47.0
35
+ Requires-Dist: pillow>=10.0.0
36
+ Requires-Dist: faster-whisper>=0.9.0
37
+ Requires-Dist: openai-whisper>=20231117
38
+ Requires-Dist: sqlalchemy>=2.0.0
39
+ Requires-Dist: alembic>=1.12.0
40
+ Provides-Extra: youtube-api
41
+ Requires-Dist: google-api-python-client>=2.0.0; extra == "youtube-api"
42
+ Requires-Dist: google-auth-httplib2>=0.1.0; extra == "youtube-api"
43
+ Requires-Dist: google-auth-oauthlib>=1.0.0; extra == "youtube-api"
44
+ Provides-Extra: analytics
45
+ Requires-Dist: pandas>=2.0.0; extra == "analytics"
46
+ Requires-Dist: matplotlib>=3.7.0; extra == "analytics"
47
+ Requires-Dist: seaborn>=0.12.0; extra == "analytics"
48
+ Requires-Dist: plotly>=5.15.0; extra == "analytics"
49
+ Provides-Extra: mongodb
50
+ Requires-Dist: pymongo>=4.5.0; extra == "mongodb"
51
+ Requires-Dist: motor>=3.3.0; extra == "mongodb"
52
+ Provides-Extra: web
53
+ Requires-Dist: beautifulsoup4>=4.12.0; extra == "web"
54
+ Requires-Dist: playwright>=1.40.0; extra == "web"
55
+ Requires-Dist: requests>=2.31.0; extra == "web"
56
+ Provides-Extra: all
57
+ Requires-Dist: google-api-python-client>=2.0.0; extra == "all"
58
+ Requires-Dist: google-auth-httplib2>=0.1.0; extra == "all"
59
+ Requires-Dist: google-auth-oauthlib>=1.0.0; extra == "all"
60
+ Requires-Dist: pandas>=2.0.0; extra == "all"
61
+ Requires-Dist: matplotlib>=3.7.0; extra == "all"
62
+ Requires-Dist: seaborn>=0.12.0; extra == "all"
63
+ Requires-Dist: plotly>=5.15.0; extra == "all"
64
+ Requires-Dist: pymongo>=4.5.0; extra == "all"
65
+ Requires-Dist: motor>=3.3.0; extra == "all"
66
+ Requires-Dist: beautifulsoup4>=4.12.0; extra == "all"
67
+ Requires-Dist: playwright>=1.40.0; extra == "all"
68
+ Requires-Dist: requests>=2.31.0; extra == "all"
69
+ Provides-Extra: dev
70
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
71
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
72
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
73
+ Requires-Dist: black>=23.0.0; extra == "dev"
74
+ Requires-Dist: isort>=5.12.0; extra == "dev"
75
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
76
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
77
+ Requires-Dist: pre-commit>=3.0.0; extra == "dev"
78
+ Requires-Dist: psutil>=7.2.1; extra == "dev"
79
+ Requires-Dist: alembic>=1.18.1; extra == "dev"
80
+ Requires-Dist: pandas>=2.0.0; extra == "dev"
81
+ Requires-Dist: matplotlib>=3.7.0; extra == "dev"
82
+ Requires-Dist: seaborn>=0.12.0; extra == "dev"
83
+ Requires-Dist: plotly>=5.15.0; extra == "dev"
84
+ Dynamic: license-file
85
+
86
+ # Spatelier
87
+
88
+ A personal tool library for video and music file handling, built with modern Python architecture and inspired by excellent projects like yt-dlp.
89
+
90
+ ## Features
91
+
92
+ - **Video Processing**: Download, transcribe, and process videos with automatic subtitle embedding
93
+ - **Audio Processing**: Extract, convert, and normalize audio from videos
94
+ - **Analytics**: Track usage and generate insights from your media library
95
+ - **Database Management**: SQLite + MongoDB for structured and unstructured data
96
+ - **Batch Operations**: Process multiple files with intelligent resume logic
97
+ - **NAS Support**: Smart handling of Network Attached Storage with local processing
98
+ - **Comprehensive Testing**: Full test suite with unit, integration, and performance tests
99
+ - **Modern CLI**: Beautiful, type-safe command-line interface with Rich output
100
+
101
+ ## Architecture
102
+
103
+ ```
104
+ spatelier/
105
+ ├── cli/ # Command-line interface modules
106
+ ├── core/ # Core functionality and base classes
107
+ ├── modules/ # Feature modules (video, audio, etc.)
108
+ ├── database/ # Database models, connections, and repositories
109
+ ├── analytics/ # Analytics and reporting
110
+ ├── utils/ # Utility functions and helpers
111
+ ├── tests/ # Comprehensive test suite
112
+ └── bin/ # Executable scripts
113
+ ```
114
+
115
+ ## Installation
116
+
117
+ ### Homebrew (Recommended - No pip/venv needed!)
118
+
119
+ **Install with Homebrew - handles everything automatically:**
120
+
121
+ ```bash
122
+ brew install --build-from-source https://raw.githubusercontent.com/galenspikes/spatelier/main/Formula/spatelier.rb
123
+ ```
124
+
125
+ That's it! `spatelier` is now available system-wide. No pip, no venv, no Python management.
126
+
127
+ **Update:**
128
+ ```bash
129
+ brew upgrade spatelier
130
+ ```
131
+
132
+ ### Global Installation with pipx
133
+
134
+ **Want to use `spatelier` from anywhere? Use pipx:**
135
+
136
+ ```bash
137
+ # Install pipx (one-time setup)
138
+ brew install pipx
139
+ pipx ensurepath
140
+
141
+ # Install Spatelier globally
142
+ pipx install -e /path/to/spatelier
143
+
144
+ # Or from PyPI (once published)
145
+ pipx install spatelier
146
+ ```
147
+
148
+ Now `spatelier` works from any directory! No venv activation needed.
149
+
150
+ **See [GLOBAL_INSTALL.md](GLOBAL_INSTALL.md) for all global installation options.**
151
+
152
+ ### From PyPI
153
+
154
+ ```bash
155
+ # Basic installation
156
+ pip install spatelier
157
+
158
+ # With optional features
159
+ pip install spatelier[transcription,analytics]
160
+ pip install spatelier[all] # All optional features
161
+ ```
162
+
163
+ ### From Source (Development)
164
+
165
+ For development, you'll need to activate the virtual environment:
166
+
167
+ ```bash
168
+ # 1. Clone the repository
169
+ git clone https://github.com/galenspikes/spatelier
170
+ cd spatelier
171
+
172
+ # 2. Activate virtual environment
173
+ source venv/bin/activate
174
+
175
+ # 3. Install in development mode
176
+ pip install -e ".[dev]"
177
+
178
+ # 4. Install pre-commit hooks
179
+ pre-commit install
180
+ ```
181
+
182
+
183
+ ### Standalone Executable
184
+
185
+ Download the latest release from [GitHub Releases](https://github.com/galenspikes/spatelier/releases) and run directly (no Python installation required).
186
+
187
+ ### Troubleshooting
188
+
189
+ If you're having issues with installation, see [GLOBAL_INSTALL.md](GLOBAL_INSTALL.md) for global installation options.
190
+
191
+ ## Usage
192
+
193
+ ```bash
194
+ # List available commands
195
+ spatelier --help
196
+
197
+ # Video processing
198
+ spatelier video download <url> # Download video
199
+ spatelier video download-enhanced <url> # Download with transcription
200
+ spatelier video download-playlist <url> # Download entire playlist (no transcription)
201
+ spatelier video embed-subtitles <video> # Embed subtitles
202
+ spatelier video convert <input> <output> # Convert format
203
+ spatelier video info <video> # Show video info
204
+
205
+ # Audio processing
206
+ spatelier audio convert <input> <output> # Convert audio format
207
+ spatelier audio info <audio> # Show audio info
208
+
209
+ # Batch operations
210
+
211
+ # Analytics
212
+ spatelier analytics report # Generate usage reports
213
+
214
+ ```
215
+
216
+ ## Development
217
+
218
+ ```bash
219
+ # Run tests
220
+ pytest # Run all tests
221
+ pytest -m unit # Run unit tests only
222
+ pytest -m integration # Run integration tests only
223
+ pytest -m nas # Run NAS tests (requires NAS)
224
+
225
+ # Format code
226
+ black .
227
+ isort .
228
+
229
+ # Type checking
230
+ mypy .
231
+
232
+ # Linting
233
+ flake8 .
234
+
235
+ # Database migrations
236
+ alembic revision --autogenerate -m "description"
237
+ alembic upgrade head
238
+ ```
239
+
240
+ ## Contributing
241
+
242
+ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
243
+
244
+ 1. Fork the repository
245
+ 2. Create a feature branch
246
+ 3. Make your changes
247
+ 4. Add tests
248
+ 5. Submit a pull request
249
+
250
+ ## Changelog
251
+
252
+ See [CHANGELOG.md](CHANGELOG.md) for a list of changes in each version.
253
+
254
+ ## Security
255
+
256
+ For security concerns, please see [SECURITY.md](SECURITY.md).
257
+
258
+ ## License
259
+
260
+ MIT License - see [LICENSE](LICENSE) file for details.