karaoke-gen 0.101.0__py3-none-any.whl → 0.103.1__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.
@@ -0,0 +1,392 @@
1
+ """
2
+ Unit tests for rate limits admin API endpoints.
3
+
4
+ Tests the rate limit statistics, blocklist management, and user override endpoints.
5
+ """
6
+ import pytest
7
+ from unittest.mock import Mock, patch, MagicMock
8
+ from fastapi.testclient import TestClient
9
+ from fastapi import FastAPI
10
+ from datetime import datetime, timezone
11
+
12
+ from backend.api.routes.rate_limits import router
13
+ from backend.api.dependencies import require_admin
14
+ from backend.services.auth_service import AuthResult, UserType
15
+
16
+
17
+ # Create a test app with the rate_limits router
18
+ app = FastAPI()
19
+ app.include_router(router, prefix="/api")
20
+
21
+
22
+ def get_mock_admin():
23
+ """Override for require_admin dependency."""
24
+ return AuthResult(
25
+ is_valid=True,
26
+ user_type=UserType.ADMIN,
27
+ remaining_uses=-1,
28
+ message="Admin access granted",
29
+ user_email="admin@example.com",
30
+ is_admin=True,
31
+ )
32
+
33
+
34
+ # Override the require_admin dependency
35
+ app.dependency_overrides[require_admin] = get_mock_admin
36
+
37
+
38
+ @pytest.fixture
39
+ def client():
40
+ """Create a test client."""
41
+ return TestClient(app)
42
+
43
+
44
+ @pytest.fixture
45
+ def mock_settings():
46
+ """Create mock settings."""
47
+ settings = Mock()
48
+ settings.enable_rate_limiting = True
49
+ settings.rate_limit_jobs_per_day = 5
50
+ settings.rate_limit_youtube_uploads_per_day = 10
51
+ settings.rate_limit_beta_ip_per_day = 1
52
+ return settings
53
+
54
+
55
+ class TestGetRateLimitStats:
56
+ """Tests for GET /api/admin/rate-limits/stats endpoint."""
57
+
58
+ def test_returns_stats(self, client, mock_settings):
59
+ """Test successful stats retrieval."""
60
+ with patch('backend.api.routes.rate_limits.get_rate_limit_service') as mock_get_rls, \
61
+ patch('backend.api.routes.rate_limits.get_email_validation_service') as mock_get_evs, \
62
+ patch('backend.api.routes.rate_limits.settings', mock_settings):
63
+
64
+ # Setup rate limit service mock
65
+ mock_rls = Mock()
66
+ mock_rls.get_youtube_uploads_today.return_value = 3
67
+ mock_rls.get_all_overrides.return_value = {"user1@example.com": {}}
68
+ mock_get_rls.return_value = mock_rls
69
+
70
+ # Setup email validation service mock
71
+ mock_evs = Mock()
72
+ mock_evs.get_blocklist_stats.return_value = {
73
+ "disposable_domains_count": 100,
74
+ "blocked_emails_count": 5,
75
+ "blocked_ips_count": 2,
76
+ "default_disposable_domains_count": 130,
77
+ }
78
+ mock_get_evs.return_value = mock_evs
79
+
80
+ response = client.get(
81
+ "/api/admin/rate-limits/stats",
82
+ headers={"Authorization": "Bearer admin-token"}
83
+ )
84
+
85
+ assert response.status_code == 200
86
+ data = response.json()
87
+ assert data["jobs_per_day_limit"] == 5
88
+ assert data["youtube_uploads_per_day_limit"] == 10
89
+ assert data["youtube_uploads_today"] == 3
90
+ assert data["youtube_uploads_remaining"] == 7
91
+ assert data["disposable_domains_count"] == 100
92
+ assert data["total_overrides"] == 1
93
+
94
+
95
+ class TestGetUserRateLimitStatus:
96
+ """Tests for GET /api/admin/rate-limits/users/{email} endpoint."""
97
+
98
+ def test_returns_user_status(self, client, mock_settings):
99
+ """Test successful user status retrieval."""
100
+ with patch('backend.api.routes.rate_limits.get_rate_limit_service') as mock_get_rls, \
101
+ patch('backend.api.routes.rate_limits.settings', mock_settings):
102
+
103
+ mock_rls = Mock()
104
+ mock_rls.get_user_job_count_today.return_value = 2
105
+ mock_rls.get_user_override.return_value = None
106
+ mock_get_rls.return_value = mock_rls
107
+
108
+ response = client.get(
109
+ "/api/admin/rate-limits/users/user@example.com",
110
+ headers={"Authorization": "Bearer admin-token"}
111
+ )
112
+
113
+ assert response.status_code == 200
114
+ data = response.json()
115
+ assert data["email"] == "user@example.com"
116
+ assert data["jobs_today"] == 2
117
+ assert data["jobs_limit"] == 5
118
+ assert data["jobs_remaining"] == 3
119
+ assert data["has_bypass"] is False
120
+
121
+ def test_returns_user_with_bypass(self, client, mock_settings):
122
+ """Test user with bypass override."""
123
+ with patch('backend.api.routes.rate_limits.get_rate_limit_service') as mock_get_rls, \
124
+ patch('backend.api.routes.rate_limits.settings', mock_settings):
125
+
126
+ mock_rls = Mock()
127
+ mock_rls.get_user_job_count_today.return_value = 10
128
+ mock_rls.get_user_override.return_value = {
129
+ "bypass_job_limit": True,
130
+ "reason": "VIP user"
131
+ }
132
+ mock_get_rls.return_value = mock_rls
133
+
134
+ response = client.get(
135
+ "/api/admin/rate-limits/users/vip@example.com",
136
+ headers={"Authorization": "Bearer admin-token"}
137
+ )
138
+
139
+ assert response.status_code == 200
140
+ data = response.json()
141
+ assert data["has_bypass"] is True
142
+ assert data["bypass_reason"] == "VIP user"
143
+ assert data["jobs_remaining"] == -1 # Unlimited
144
+
145
+
146
+ class TestBlocklistEndpoints:
147
+ """Tests for blocklist management endpoints."""
148
+
149
+ def test_get_blocklists(self, client):
150
+ """Test getting all blocklists."""
151
+ with patch('backend.api.routes.rate_limits.get_email_validation_service') as mock_get_evs, \
152
+ patch('backend.services.firestore_service.get_firestore_client') as mock_get_db:
153
+
154
+ mock_evs = Mock()
155
+ mock_evs.get_blocklist_config.return_value = {
156
+ "disposable_domains": {"tempmail.com", "mailinator.com"},
157
+ "blocked_emails": {"spammer@example.com"},
158
+ "blocked_ips": {"192.168.1.100"},
159
+ }
160
+ mock_get_evs.return_value = mock_evs
161
+
162
+ # Mock Firestore for metadata
163
+ mock_db = Mock()
164
+ mock_doc = Mock()
165
+ mock_doc.exists = True
166
+ mock_doc.to_dict.return_value = {
167
+ "updated_at": datetime.now(timezone.utc),
168
+ "updated_by": "admin@example.com"
169
+ }
170
+ mock_db.collection.return_value.document.return_value.get.return_value = mock_doc
171
+ mock_get_db.return_value = mock_db
172
+
173
+ response = client.get(
174
+ "/api/admin/rate-limits/blocklists",
175
+ headers={"Authorization": "Bearer admin-token"}
176
+ )
177
+
178
+ assert response.status_code == 200
179
+ data = response.json()
180
+ assert "tempmail.com" in data["disposable_domains"]
181
+ assert "spammer@example.com" in data["blocked_emails"]
182
+
183
+ def test_add_disposable_domain(self, client):
184
+ """Test adding a disposable domain."""
185
+ with patch('backend.api.routes.rate_limits.get_email_validation_service') as mock_get_evs:
186
+
187
+ mock_evs = Mock()
188
+ mock_evs.add_disposable_domain.return_value = True
189
+ mock_get_evs.return_value = mock_evs
190
+
191
+ response = client.post(
192
+ "/api/admin/rate-limits/blocklists/disposable-domains",
193
+ json={"domain": "newtemp.com"},
194
+ headers={"Authorization": "Bearer admin-token"}
195
+ )
196
+
197
+ assert response.status_code == 200
198
+ data = response.json()
199
+ assert data["success"] is True
200
+ mock_evs.add_disposable_domain.assert_called_once_with("newtemp.com", "admin@example.com")
201
+
202
+ def test_add_disposable_domain_invalid(self, client):
203
+ """Test adding invalid domain."""
204
+ response = client.post(
205
+ "/api/admin/rate-limits/blocklists/disposable-domains",
206
+ json={"domain": "invalid"}, # No dot
207
+ headers={"Authorization": "Bearer admin-token"}
208
+ )
209
+
210
+ assert response.status_code == 400
211
+
212
+ def test_remove_disposable_domain(self, client):
213
+ """Test removing a disposable domain."""
214
+ with patch('backend.api.routes.rate_limits.get_email_validation_service') as mock_get_evs:
215
+
216
+ mock_evs = Mock()
217
+ mock_evs.remove_disposable_domain.return_value = True
218
+ mock_get_evs.return_value = mock_evs
219
+
220
+ response = client.delete(
221
+ "/api/admin/rate-limits/blocklists/disposable-domains/tempmail.com",
222
+ headers={"Authorization": "Bearer admin-token"}
223
+ )
224
+
225
+ assert response.status_code == 200
226
+ mock_evs.remove_disposable_domain.assert_called_once()
227
+
228
+ def test_remove_disposable_domain_not_found(self, client):
229
+ """Test removing non-existent domain."""
230
+ with patch('backend.api.routes.rate_limits.get_email_validation_service') as mock_get_evs:
231
+
232
+ mock_evs = Mock()
233
+ mock_evs.remove_disposable_domain.return_value = False
234
+ mock_get_evs.return_value = mock_evs
235
+
236
+ response = client.delete(
237
+ "/api/admin/rate-limits/blocklists/disposable-domains/notfound.com",
238
+ headers={"Authorization": "Bearer admin-token"}
239
+ )
240
+
241
+ assert response.status_code == 404
242
+
243
+ def test_add_blocked_email(self, client):
244
+ """Test adding a blocked email."""
245
+ with patch('backend.api.routes.rate_limits.get_email_validation_service') as mock_get_evs:
246
+
247
+ mock_evs = Mock()
248
+ mock_evs.add_blocked_email.return_value = True
249
+ mock_get_evs.return_value = mock_evs
250
+
251
+ response = client.post(
252
+ "/api/admin/rate-limits/blocklists/blocked-emails",
253
+ json={"email": "spammer@example.com"},
254
+ headers={"Authorization": "Bearer admin-token"}
255
+ )
256
+
257
+ assert response.status_code == 200
258
+
259
+ def test_add_blocked_ip(self, client):
260
+ """Test adding a blocked IP."""
261
+ with patch('backend.api.routes.rate_limits.get_email_validation_service') as mock_get_evs:
262
+
263
+ mock_evs = Mock()
264
+ mock_evs.add_blocked_ip.return_value = True
265
+ mock_get_evs.return_value = mock_evs
266
+
267
+ response = client.post(
268
+ "/api/admin/rate-limits/blocklists/blocked-ips",
269
+ json={"ip_address": "192.168.1.100"},
270
+ headers={"Authorization": "Bearer admin-token"}
271
+ )
272
+
273
+ assert response.status_code == 200
274
+
275
+
276
+ class TestUserOverrideEndpoints:
277
+ """Tests for user override management endpoints."""
278
+
279
+ def test_get_all_overrides(self, client):
280
+ """Test getting all user overrides."""
281
+ with patch('backend.api.routes.rate_limits.get_rate_limit_service') as mock_get_rls:
282
+
283
+ mock_rls = Mock()
284
+ mock_rls.get_all_overrides.return_value = {
285
+ "vip@example.com": {
286
+ "bypass_job_limit": True,
287
+ "custom_daily_job_limit": None,
288
+ "reason": "VIP user",
289
+ "created_by": "admin@example.com",
290
+ "created_at": datetime.now(timezone.utc),
291
+ }
292
+ }
293
+ mock_get_rls.return_value = mock_rls
294
+
295
+ response = client.get(
296
+ "/api/admin/rate-limits/overrides",
297
+ headers={"Authorization": "Bearer admin-token"}
298
+ )
299
+
300
+ assert response.status_code == 200
301
+ data = response.json()
302
+ assert data["total"] == 1
303
+ assert len(data["overrides"]) == 1
304
+ assert data["overrides"][0]["email"] == "vip@example.com"
305
+
306
+ def test_set_user_override(self, client):
307
+ """Test setting a user override."""
308
+ with patch('backend.api.routes.rate_limits.get_rate_limit_service') as mock_get_rls:
309
+
310
+ mock_rls = Mock()
311
+ mock_get_rls.return_value = mock_rls
312
+
313
+ response = client.put(
314
+ "/api/admin/rate-limits/overrides/user@example.com",
315
+ json={
316
+ "bypass_job_limit": True,
317
+ "reason": "Special access granted"
318
+ },
319
+ headers={"Authorization": "Bearer admin-token"}
320
+ )
321
+
322
+ assert response.status_code == 200
323
+ mock_rls.set_user_override.assert_called_once_with(
324
+ user_email="user@example.com",
325
+ bypass_job_limit=True,
326
+ custom_daily_job_limit=None,
327
+ reason="Special access granted",
328
+ admin_email="admin@example.com",
329
+ )
330
+
331
+ def test_set_user_override_with_custom_limit(self, client):
332
+ """Test setting a user override with custom limit."""
333
+ with patch('backend.api.routes.rate_limits.get_rate_limit_service') as mock_get_rls:
334
+
335
+ mock_rls = Mock()
336
+ mock_get_rls.return_value = mock_rls
337
+
338
+ response = client.put(
339
+ "/api/admin/rate-limits/overrides/user@example.com",
340
+ json={
341
+ "bypass_job_limit": False,
342
+ "custom_daily_job_limit": 20,
343
+ "reason": "High volume user"
344
+ },
345
+ headers={"Authorization": "Bearer admin-token"}
346
+ )
347
+
348
+ assert response.status_code == 200
349
+ mock_rls.set_user_override.assert_called_once()
350
+
351
+ def test_set_user_override_missing_reason(self, client):
352
+ """Test setting override without reason fails."""
353
+ response = client.put(
354
+ "/api/admin/rate-limits/overrides/user@example.com",
355
+ json={
356
+ "bypass_job_limit": True,
357
+ "reason": "ab" # Too short
358
+ },
359
+ headers={"Authorization": "Bearer admin-token"}
360
+ )
361
+
362
+ assert response.status_code == 400
363
+
364
+ def test_remove_user_override(self, client):
365
+ """Test removing a user override."""
366
+ with patch('backend.api.routes.rate_limits.get_rate_limit_service') as mock_get_rls:
367
+
368
+ mock_rls = Mock()
369
+ mock_rls.remove_user_override.return_value = True
370
+ mock_get_rls.return_value = mock_rls
371
+
372
+ response = client.delete(
373
+ "/api/admin/rate-limits/overrides/user@example.com",
374
+ headers={"Authorization": "Bearer admin-token"}
375
+ )
376
+
377
+ assert response.status_code == 200
378
+
379
+ def test_remove_user_override_not_found(self, client):
380
+ """Test removing non-existent override."""
381
+ with patch('backend.api.routes.rate_limits.get_rate_limit_service') as mock_get_rls:
382
+
383
+ mock_rls = Mock()
384
+ mock_rls.remove_user_override.return_value = False
385
+ mock_get_rls.return_value = mock_rls
386
+
387
+ response = client.delete(
388
+ "/api/admin/rate-limits/overrides/notfound@example.com",
389
+ headers={"Authorization": "Bearer admin-token"}
390
+ )
391
+
392
+ assert response.status_code == 404
@@ -483,6 +483,17 @@ class VideoWorkerOrchestrator:
483
483
  """Upload video to YouTube."""
484
484
  self.job_log.info("Uploading to YouTube")
485
485
 
486
+ # Check YouTube upload rate limit (system-wide)
487
+ try:
488
+ from backend.services.rate_limit_service import get_rate_limit_service
489
+ rate_limit_service = get_rate_limit_service()
490
+ allowed, remaining, message = rate_limit_service.check_youtube_upload_limit()
491
+ if not allowed:
492
+ self.job_log.warning(f"YouTube upload skipped: {message}")
493
+ return
494
+ except Exception as e:
495
+ self.job_log.warning(f"Rate limit check failed, proceeding with upload: {e}")
496
+
486
497
  # Find the best video file to upload (prefer MKV for FLAC audio, then lossless MP4)
487
498
  video_to_upload = None
488
499
  if self.result.final_video_mkv and os.path.isfile(self.result.final_video_mkv):
@@ -520,6 +531,21 @@ class VideoWorkerOrchestrator:
520
531
  if video_url:
521
532
  self.result.youtube_url = video_url
522
533
  self.job_log.info(f"Uploaded to YouTube: {video_url}")
534
+
535
+ # Record the upload for rate limiting
536
+ try:
537
+ # Get user_email from job if available
538
+ user_email = "unknown"
539
+ if self.job_manager:
540
+ job = self.job_manager.get_job(self.config.job_id)
541
+ if job and job.user_email:
542
+ user_email = job.user_email
543
+ rate_limit_service.record_youtube_upload(
544
+ job_id=self.config.job_id,
545
+ user_email=user_email
546
+ )
547
+ except Exception as e:
548
+ self.job_log.warning(f"Failed to record YouTube upload for rate limiting: {e}")
523
549
  else:
524
550
  self.job_log.warning("YouTube upload did not return a URL")
525
551
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karaoke-gen
3
- Version: 0.101.0
3
+ Version: 0.103.1
4
4
  Summary: Generate karaoke videos with synchronized lyrics. Handles the entire process from downloading audio and lyrics to creating the final video with title screens.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -8,18 +8,20 @@ backend/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  backend/api/dependencies.py,sha256=-61nHBhiihUDSVMQd3VuHLP7uvKrUbm1y-j9RmV6_zc,16871
9
9
  backend/api/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  backend/api/routes/admin.py,sha256=GI4ANzshBUfAzMdRH3M8cUBnzsCbrL1owRXmIY5HK8A,46506
11
- backend/api/routes/audio_search.py,sha256=CK2lVaM8RDP7i61XEtLeS0AJDfVJSy0ctnduK-ew-hQ,40129
11
+ backend/api/routes/audio_search.py,sha256=QxV3kIm_QYGFzrEdv0nrDIsC-X8ms-3ldOyvMwJ93RM,39267
12
12
  backend/api/routes/auth.py,sha256=G1U2KwS3uqBJpgvg2PIe_mZOWCJKPFhhewrmKbW3Z_s,11531
13
- backend/api/routes/file_upload.py,sha256=nEl06-ZqkKZNzyNZh0RRtuOBox54XvdEOFkYtnSqpv8,95828
13
+ backend/api/routes/file_upload.py,sha256=dGLWC7n3g9bUxwZ1UNxhW4gCEF-sogcIxYP0vByI-e4,93539
14
14
  backend/api/routes/health.py,sha256=iZlhJpmz4vminUy_qrzkyk_1tgG1gmITz4jEWaCWJnM,14010
15
15
  backend/api/routes/internal.py,sha256=yqzd5V8xGJQ_vTZL85vKIMwmBXf1MIp1nhder-kQEuA,15683
16
- backend/api/routes/jobs.py,sha256=7i3uAOL9p4cNk1vJHJIIYCBkcJFaJFl6o3iq92Z9KFo,61218
16
+ backend/api/routes/jobs.py,sha256=FBf5_YojNMFamd51lOd3MDdevfOZefGTyMrtZ9Q8BSU,61241
17
+ backend/api/routes/rate_limits.py,sha256=lda7gwQdT9-U80XKsRiXkdYtB1HfpSfH5EskbB1kCQI,13806
17
18
  backend/api/routes/review.py,sha256=BiaXZs-NXl7AVzGiwBzIhzm60fUslcQagSw4XuUByhU,28842
18
19
  backend/api/routes/tenant.py,sha256=sM0WVXWGKJeyBNMbPDnAKOD9-SkJBmH4RkMXVWz4dlM,4038
19
20
  backend/api/routes/themes.py,sha256=_fPZg9N2KN-yyr1YR2vAy8FIm8PqVowboQ4WEpFTYiQ,4721
20
- backend/api/routes/users.py,sha256=nKJKuGedzSMuJVLMU4-CXNc7GsM5c82JzjFYeZtzgsg,52771
21
- backend/config.py,sha256=-HsMaacaKuRftYXUXZJr8GBHPdmacFbrNnFDMk7DHNI,8187
22
- backend/main.py,sha256=yRIdnkv_iIyOiLc2btqomxlflym13A-b3ei3X8gI8Ac,5944
21
+ backend/api/routes/users.py,sha256=9RSZyQPBF2Zx8rYMp6cJZoZ4zQ2vskcW90lFx4FSegA,55339
22
+ backend/config.py,sha256=Nb0-l0w59ZpWk_SXpLuqCm5ccUAU-ycEtENdv6wl74k,9293
23
+ backend/exceptions.py,sha256=tKu1YeAj0fmZsmGU_ZebHol-JqaGrGfauom8_DCrVys,1944
24
+ backend/main.py,sha256=C-eAFYUPsrO9W5y17AnVlIz_iRZiAz8XBQ4l-o1IiJY,6782
23
25
  backend/middleware/__init__.py,sha256=usnVRHqfGW56bWJFqaIXz49pSW91uyciBfwO7ANCpIk,369
24
26
  backend/middleware/audit_logging.py,sha256=oGdgbfH_M_3hIMAGrS5HpRvri2jjLxGFnFuA6IiU9BU,4170
25
27
  backend/middleware/tenant.py,sha256=a-HEAhyvMUC4uFF6VptaNOO9O6YP64-ZoUitppl79PQ,6373
@@ -43,17 +45,19 @@ backend/services/credential_manager.py,sha256=VA--PwvL3gboJ21orQb7MIUiovX8n66Foh
43
45
  backend/services/discord_service.py,sha256=LnEasOgaSRlmebDKsVT2yKCdWNDmskUToP3o2wPmdus,5082
44
46
  backend/services/dropbox_service.py,sha256=R47MUKCqK2BEXXmQxMLG6zkPWw02htFoES9q9xiaYAw,10717
45
47
  backend/services/email_service.py,sha256=gpufwXhNWmdElpWCmKrTLtm2LB8S-0Y5wcyJ3n1i6Jg,52868
48
+ backend/services/email_validation_service.py,sha256=xblVdzieoNTDbkB8MJLOgV_hPsHP_iwO0m36l42QmQw,19102
46
49
  backend/services/encoding_interface.py,sha256=Y8hPskDyX9DOyAD557F3-SdHjymaBkGXcoC9E_OW7SE,15825
47
50
  backend/services/encoding_service.py,sha256=XAX4hyL2wIHdpBCTc8yglcjVJboZxRVAjc07lUTxgFg,18025
48
- backend/services/firestore_service.py,sha256=--Va-2lawugskepxo2p9w3ON2KsMGqacSOutCmU_CDQ,18801
51
+ backend/services/firestore_service.py,sha256=SQWwIelerFOADRG_ZMVqk8zPPR0-utwWhGXwkjtaPIo,19416
49
52
  backend/services/flacfetch_client.py,sha256=zj4VToKamR8bShXZifLvjDmUVa1HHvDhZuaw_b7g4lU,19901
50
53
  backend/services/gce_encoding/README.md,sha256=frTFrp7-ugFAvUd_K9EgE4ud8Xu4Qf1V7BNirQn6qJk,2515
51
54
  backend/services/gce_encoding/__init__.py,sha256=d2H6zM207NSc5afnfgkd3kwOXUu2gFBYjxBSS7tvX_s,571
52
55
  backend/services/gce_encoding/main.py,sha256=2A0Bd3zYzbV66ikShB3avpqfNoDFBxr8Ja6c36qJhRI,22203
53
56
  backend/services/gce_encoding/requirements.txt,sha256=-hW5ot-PnVoKkxrpY3yV6l68wx7AvzgnLYAWEgDGWNc,408
54
57
  backend/services/gdrive_service.py,sha256=3JTXmPBuxpKopAbWDrh6YhqR5L9BLop1R7caXBnIk2A,13266
58
+ backend/services/job_defaults_service.py,sha256=GHWkBeEsx97K86ffAdkDvw8PunpyIFuT2_ezgEoGbBM,4446
55
59
  backend/services/job_logging.py,sha256=theCkAlwMbqyqAbu7vCCRpVRscaIpkam3UrCs_4Yi1g,8052
56
- backend/services/job_manager.py,sha256=2vBp-a1r3kwOg-CvXRjjlgkBh9U5AlI6yLeCBAjKLMY,33214
60
+ backend/services/job_manager.py,sha256=8kmGIsYUg40HO9CG_zxdLqpOgRZno39O55kAlSAeNL0,35192
57
61
  backend/services/job_notification_service.py,sha256=ywGJUMUDqV23OSDsugOUufpXO6N_24bM9YVaPp8bOk4,9557
58
62
  backend/services/langfuse_preloader.py,sha256=t-Rt_ThjZm4K8W3jlyLn2H3cEy7KNUlhrvOPmdg-gYM,3100
59
63
  backend/services/local_encoding_service.py,sha256=Q6yK2-QL9tHM_pUBoxdyfT7ln2L5xMx82JB2nC5OfNQ,20680
@@ -62,6 +66,7 @@ backend/services/lyrics_cache_service.py,sha256=53EIZuDs5jIKr6QiOhIKJjE2mx1Jd1_x
62
66
  backend/services/metrics.py,sha256=hlEeLG4jOR1i_QbWaNQthoko9CSpe-Zn_LHJ2huEFeY,12728
63
67
  backend/services/nltk_preloader.py,sha256=TZFghQrpDTi6COcW_rkjL15SlzdRNvPmrdy690fq0hw,3579
64
68
  backend/services/packaging_service.py,sha256=MUuBk0jtG3vGQhuOWbDv9u1uIMwbBC4NknLpJYFcWhs,10259
69
+ backend/services/rate_limit_service.py,sha256=KuLF9oWO44v-0zKYL3bFBwVq-_HejsQKbc0ojKJ1JSw,22907
65
70
  backend/services/rclone_service.py,sha256=Q6wRAjlBiMMh9FBbbFRp4SHK1ljOQBed9zUJaJgAuhU,3646
66
71
  backend/services/spacy_preloader.py,sha256=rpsMZB0CQWcoXjq8nyZCwJBkyjGheKaCFrfhxCOln5w,1836
67
72
  backend/services/storage_service.py,sha256=gA7Snz1pihzYpRKZjEZx3eDwyMUBezl3ccLuBdpTwE4,8573
@@ -76,7 +81,7 @@ backend/services/worker_service.py,sha256=WRgh3kconWYmvraEIsx6eUto2frKacauKApFU1
76
81
  backend/services/youtube_service.py,sha256=L2jqcWkw6qIaKh4wEfX894mHaTU0m5qtgIX6TPi6uiQ,3828
77
82
  backend/services/youtube_upload_service.py,sha256=fvo4WxLQaKY_I75ViBMLk6vm87ym2ABHK0Fn_StYN6g,16694
78
83
  backend/tests/__init__.py,sha256=jTjELMqIzpgHG9dcXwwkszTAAC4Wf-c93C6D1k-toZg,44
79
- backend/tests/conftest.py,sha256=c4_jH0FIHsiKeUfo3ECPN78MlbW_IARTWwKr9RKLOSM,7843
84
+ backend/tests/conftest.py,sha256=hsUOHf3VZKX-fMorjjUY6gyTIYzdb9ICrdosYW9yAZI,8122
80
85
  backend/tests/emulator/__init__.py,sha256=yOGPU5e0JPNYyUpy-u13jW4gEfWQs3dCJxfincptaCY,156
81
86
  backend/tests/emulator/conftest.py,sha256=VXhoJ3NZg3uiLzSAsushQtozFS7Lt6lmjk7ym-k6V8o,3844
82
87
  backend/tests/emulator/test_e2e_cli_backend.py,sha256=BTrWDv4sRTeOlheZ-d4R37tZsvJ2MhdFLbDb9tjbF08,41018
@@ -95,7 +100,7 @@ backend/tests/test_api_integration.py,sha256=ps4dFoW69NCKJAWvPSr410fEILPxfQNME_Y
95
100
  backend/tests/test_api_routes.py,sha256=j6alBwXgtKspXxbOiDuNCCVMENk6yxdFJkakbife-rM,3193
96
101
  backend/tests/test_audio_analysis_service.py,sha256=bKIAgyxfdbUOr7atHH-WCbZcZFkWGmGOOn7IfvRF2ms,11373
97
102
  backend/tests/test_audio_editing_service.py,sha256=wXs82PRaxFQIji0CO1eKKYD2skHS4_VLK4aBnfj8CLE,15541
98
- backend/tests/test_audio_search.py,sha256=ukZjzsw7XjVM6jPoJRiLp98TDiBbdYCp22-7v0hqeMU,54285
103
+ backend/tests/test_audio_search.py,sha256=VKVqh52to01AdQp1I1sAIkHIdhSOXgk82utc-oKdheg,54505
99
104
  backend/tests/test_audio_services.py,sha256=mOF15i7IMtjM3mV-BNG0QBoMxW58cNPviEE6wka1pjQ,14607
100
105
  backend/tests/test_auth_firestore.py,sha256=2etzuL47Pu-xOtR1CpCxi075OTFsftqneHa09yNJKEk,10063
101
106
  backend/tests/test_config_extended.py,sha256=U2l9-wF28PmSllmRW5zNqRCNoYbBl3Nixb2SrmfGUOA,2475
@@ -105,9 +110,10 @@ backend/tests/test_discord_service.py,sha256=OU5mVzjukFtJd_H5e_HxJqSsf78B0TrNfjx
105
110
  backend/tests/test_distribution_services.py,sha256=8oB2vMq5T_4rvOs6BURoLLTQiDf0kUJ-jF8dzJzDYRk,33699
106
111
  backend/tests/test_dropbox_service.py,sha256=nDN1NjiCMS_f1bICWfe3maqbKT5FbhNuKCb1zjyjKq0,19198
107
112
  backend/tests/test_email_service.py,sha256=u1VSTBCZl7m3GYR-vKV84tu3q7zhMuIhNA5a4OiCDlE,25290
113
+ backend/tests/test_email_validation_service.py,sha256=AGtusq5fUiNQz4gpiT8DXmroNPD5bcYij0R787NDooE,12472
108
114
  backend/tests/test_emulator_integration.py,sha256=m4teGnKLBKQNe9UdFi0-0iR_unnotWs07VWgSd_atuw,10387
109
115
  backend/tests/test_encoding_interface.py,sha256=7q9mKX4QG8-bfkxQgZkmeirK_ynfL88UTbgWS0f097U,14609
110
- backend/tests/test_file_upload.py,sha256=mWEIVj6cK6ZzKf4X0D0JhSk6pmJeCO0dOawV2GED7tc,68038
116
+ backend/tests/test_file_upload.py,sha256=seFF8viLNTEqiuChRolD5gIiMevhhkawvcXMFPcmCHw,68226
111
117
  backend/tests/test_flacfetch_client.py,sha256=ZquMDLFVgORr9GoNfybOV9Z3eruborRgCvBRY6l-V8A,22614
112
118
  backend/tests/test_gdrive_service.py,sha256=fh02KPaFqLEWfPaDQvSehn6r6RLq0bgt6MdXenF1kxA,19762
113
119
  backend/tests/test_impersonation.py,sha256=vuCJaYmPOObLvFWuzxRvqadeRtFXgsbQxyOKd4TaxHg,9087
@@ -120,10 +126,12 @@ backend/tests/test_job_notification_service.py,sha256=YbXqCWbsK8O5WICItb3VCrEY8q
120
126
  backend/tests/test_jobs_api.py,sha256=z3pGTawTpt75a1bQ5x4ynw3pTlNihLA6vqktTc6mBAU,11092
121
127
  backend/tests/test_local_encoding_service.py,sha256=TbBaFOB-8Re09G-qXIQCl8mS9WlQMFNQF01V64IekyI,14638
122
128
  backend/tests/test_local_preview_encoding_service.py,sha256=VwxpcAjDdYaVauSsrJ609U575VWRZbLsXew8BqBXpJY,20228
123
- backend/tests/test_made_for_you.py,sha256=WNiJhmv_zse96Yg4scEXnpu66MMc0Hi4pNyiadfdlQ8,87628
129
+ backend/tests/test_made_for_you.py,sha256=Eanhp4v_dOgpQTxKQPHxHdLG4XiJ-kruVPWkR0iUnsg,87806
124
130
  backend/tests/test_main.py,sha256=BvzcadLUV0SEUO1ViKm0djgy_TEKXm8PPvgfcCCEJAY,3348
125
131
  backend/tests/test_models.py,sha256=buUjtm3TaiIff58SCpG0Du1Bnm-S9X8LyaW64zEhb4U,39606
126
132
  backend/tests/test_packaging_service.py,sha256=v4Bj8R61Fx0Kng6RoTj3SRN9jmxow5qCVtTJstTWNfE,13916
133
+ backend/tests/test_rate_limit_service.py,sha256=1G-kskzv-5hcG_px78vZ9_J_X2Jj21s7l_wuUOWAOkY,16905
134
+ backend/tests/test_rate_limits_api.py,sha256=eFtdMzDMxhvI3MPhm60nRIWh-MVFCdpG0e-FwIVLn9I,14822
127
135
  backend/tests/test_requests.py,sha256=DI2IG67hHk2rhuzUfz_QNJCLRTy1NZPLsnSdZbIfPA8,6817
128
136
  backend/tests/test_routes_jobs.py,sha256=iqOZ93ACKpizSzLEJUx5hIo0vkKe2oIT7s7iZqQUJaE,11258
129
137
  backend/tests/test_routes_review.py,sha256=ENo6_wFUDcyXZhRy_NBqajan9dvqV_7f1rIQ7LLLS-U,14334
@@ -159,7 +167,7 @@ backend/workers/render_video_worker.py,sha256=L6-VCxSBY6MbXBBm0-wu_DXxcIWKfW6ynT
159
167
  backend/workers/screens_worker.py,sha256=vCS7j4dQRQ_tsWNhz-XH5nau7kGyUSzSp-dzyZpeJEo,21402
160
168
  backend/workers/style_helper.py,sha256=7-PM79dAUAZHTj-bnATy43ImqNdl_00E9pm94CYg1BM,6906
161
169
  backend/workers/video_worker.py,sha256=5r3PcYdoca16fjZtFzCwfKm-G7QjL98ZLMhw6qGVDNE,55670
162
- backend/workers/video_worker_orchestrator.py,sha256=NInZjFwztqcYneuxrkDqEFWV7z5BzGlaaoJxj812KA4,27510
170
+ backend/workers/video_worker_orchestrator.py,sha256=kmkEFUVlcWxuGXXwGmhV8A7NFfu4KfdO2LEhxSYc3RA,28779
163
171
  backend/workers/worker_logging.py,sha256=nlbGsjDjdkv28Fm-95vKBumy6P47dGiNN5Zf3r0A480,9928
164
172
  karaoke_gen/__init__.py,sha256=wHpDbURJxmJAMNZ0uQjISv5MIT7KD9RWYi15xlYgEhU,1351
165
173
  karaoke_gen/audio_fetcher.py,sha256=EuMSO_wG0z0ldfQQ_xGC8kdoUCDB1Rr2_IuWIZ218lA,71380
@@ -450,8 +458,8 @@ lyrics_transcriber/transcribers/whisper.py,sha256=YcCB1ic9H6zL1GS0jD0emu8-qlcH0Q
450
458
  lyrics_transcriber/types.py,sha256=UJjaxhVd2o14AG4G8ToU598p0JeYdiTFjpG38jGCoYQ,27917
451
459
  lyrics_transcriber/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
452
460
  lyrics_transcriber/utils/word_utils.py,sha256=-cMGpj9UV4F6IsoDKAV2i1aiqSO8eI91HMAm_igtVMk,958
453
- karaoke_gen-0.101.0.dist-info/METADATA,sha256=zkFeEtygnskVvZLwCarJ9blhCd4pA60LV75aSZm8RaI,23116
454
- karaoke_gen-0.101.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
455
- karaoke_gen-0.101.0.dist-info/entry_points.txt,sha256=xIyLe7K84ZyjO8L0_AmNectz93QjGSs5AkApMtlAd4g,160
456
- karaoke_gen-0.101.0.dist-info/licenses/LICENSE,sha256=81R_4XwMZDODHD7JcZeUR8IiCU8AD7Ajl6bmwR9tYDk,1074
457
- karaoke_gen-0.101.0.dist-info/RECORD,,
461
+ karaoke_gen-0.103.1.dist-info/METADATA,sha256=EMLjE5yyOq75ZCqVMPagcYppdJYukM-wmX9hgQe56Mg,23116
462
+ karaoke_gen-0.103.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
463
+ karaoke_gen-0.103.1.dist-info/entry_points.txt,sha256=xIyLe7K84ZyjO8L0_AmNectz93QjGSs5AkApMtlAd4g,160
464
+ karaoke_gen-0.103.1.dist-info/licenses/LICENSE,sha256=81R_4XwMZDODHD7JcZeUR8IiCU8AD7Ajl6bmwR9tYDk,1074
465
+ karaoke_gen-0.103.1.dist-info/RECORD,,