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
|
@@ -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.
|