karaoke-gen 0.99.3__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.
Files changed (55) hide show
  1. backend/api/routes/admin.py +512 -1
  2. backend/api/routes/audio_search.py +17 -34
  3. backend/api/routes/file_upload.py +60 -84
  4. backend/api/routes/internal.py +6 -0
  5. backend/api/routes/jobs.py +11 -3
  6. backend/api/routes/rate_limits.py +428 -0
  7. backend/api/routes/review.py +13 -6
  8. backend/api/routes/tenant.py +120 -0
  9. backend/api/routes/users.py +229 -247
  10. backend/config.py +16 -0
  11. backend/exceptions.py +66 -0
  12. backend/main.py +30 -1
  13. backend/middleware/__init__.py +7 -1
  14. backend/middleware/tenant.py +192 -0
  15. backend/models/job.py +19 -3
  16. backend/models/tenant.py +208 -0
  17. backend/models/user.py +18 -0
  18. backend/services/email_service.py +253 -6
  19. backend/services/email_validation_service.py +646 -0
  20. backend/services/firestore_service.py +27 -0
  21. backend/services/job_defaults_service.py +113 -0
  22. backend/services/job_manager.py +73 -3
  23. backend/services/rate_limit_service.py +641 -0
  24. backend/services/stripe_service.py +61 -35
  25. backend/services/tenant_service.py +285 -0
  26. backend/services/user_service.py +85 -7
  27. backend/tests/conftest.py +7 -1
  28. backend/tests/emulator/test_made_for_you_integration.py +167 -0
  29. backend/tests/test_admin_job_files.py +337 -0
  30. backend/tests/test_admin_job_reset.py +384 -0
  31. backend/tests/test_admin_job_update.py +326 -0
  32. backend/tests/test_audio_search.py +12 -8
  33. backend/tests/test_email_service.py +233 -0
  34. backend/tests/test_email_validation_service.py +298 -0
  35. backend/tests/test_file_upload.py +8 -6
  36. backend/tests/test_impersonation.py +223 -0
  37. backend/tests/test_job_creation_regression.py +4 -0
  38. backend/tests/test_job_manager.py +146 -1
  39. backend/tests/test_made_for_you.py +2088 -0
  40. backend/tests/test_models.py +139 -0
  41. backend/tests/test_rate_limit_service.py +396 -0
  42. backend/tests/test_rate_limits_api.py +392 -0
  43. backend/tests/test_tenant_api.py +350 -0
  44. backend/tests/test_tenant_middleware.py +345 -0
  45. backend/tests/test_tenant_models.py +406 -0
  46. backend/tests/test_tenant_service.py +418 -0
  47. backend/workers/video_worker.py +8 -3
  48. backend/workers/video_worker_orchestrator.py +26 -0
  49. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/METADATA +1 -1
  50. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/RECORD +55 -33
  51. lyrics_transcriber/frontend/src/api.ts +13 -5
  52. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +90 -57
  53. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/WHEEL +0 -0
  54. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/entry_points.txt +0 -0
  55. {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/licenses/LICENSE +0 -0
@@ -14,7 +14,9 @@ import html
14
14
  import logging
15
15
  import os
16
16
  from abc import ABC, abstractmethod
17
+ from datetime import datetime, timedelta, timezone
17
18
  from typing import Optional, List
19
+ from zoneinfo import ZoneInfo
18
20
 
19
21
  from backend.config import get_settings
20
22
  from karaoke_gen.utils import sanitize_filename
@@ -34,8 +36,21 @@ class EmailProvider(ABC):
34
36
  html_content: str,
35
37
  text_content: Optional[str] = None,
36
38
  cc_emails: Optional[List[str]] = None,
39
+ bcc_emails: Optional[List[str]] = None,
40
+ from_email_override: Optional[str] = None,
37
41
  ) -> bool:
38
- """Send an email. Returns True if successful."""
42
+ """
43
+ Send an email. Returns True if successful.
44
+
45
+ Args:
46
+ to_email: Recipient email address
47
+ subject: Email subject
48
+ html_content: HTML content
49
+ text_content: Plain text content (optional)
50
+ cc_emails: CC recipients (optional)
51
+ bcc_emails: BCC recipients (optional)
52
+ from_email_override: Override the default sender email (optional, for multi-tenant)
53
+ """
39
54
  pass
40
55
 
41
56
 
@@ -53,11 +68,17 @@ class ConsoleEmailProvider(EmailProvider):
53
68
  html_content: str,
54
69
  text_content: Optional[str] = None,
55
70
  cc_emails: Optional[List[str]] = None,
71
+ bcc_emails: Optional[List[str]] = None,
72
+ from_email_override: Optional[str] = None,
56
73
  ) -> bool:
57
74
  logger.info("=" * 60)
75
+ if from_email_override:
76
+ logger.info(f"FROM: {from_email_override}")
58
77
  logger.info(f"EMAIL TO: {to_email}")
59
78
  if cc_emails:
60
79
  logger.info(f"CC: {', '.join(cc_emails)}")
80
+ if bcc_emails:
81
+ logger.info(f"BCC: {', '.join(bcc_emails)}")
61
82
  logger.info(f"SUBJECT: {subject}")
62
83
  logger.info("-" * 60)
63
84
  logger.info(text_content or html_content)
@@ -85,6 +106,8 @@ class PreviewEmailProvider(EmailProvider):
85
106
  html_content: str,
86
107
  text_content: Optional[str] = None,
87
108
  cc_emails: Optional[List[str]] = None,
109
+ bcc_emails: Optional[List[str]] = None,
110
+ from_email_override: Optional[str] = None,
88
111
  ) -> bool:
89
112
  self.last_to_email = to_email
90
113
  self.last_subject = subject
@@ -115,16 +138,21 @@ class SendGridEmailProvider(EmailProvider):
115
138
  html_content: str,
116
139
  text_content: Optional[str] = None,
117
140
  cc_emails: Optional[List[str]] = None,
141
+ bcc_emails: Optional[List[str]] = None,
142
+ from_email_override: Optional[str] = None,
118
143
  ) -> bool:
119
144
  try:
120
145
  # Import here to avoid requiring sendgrid in all environments
121
146
  from sendgrid import SendGridAPIClient
122
- from sendgrid.helpers.mail import Mail, Email, To, Content, Cc
147
+ from sendgrid.helpers.mail import Mail, Email, To, Content, Cc, Bcc
123
148
 
124
149
  sg = SendGridAPIClient(api_key=self.api_key)
125
150
 
151
+ # Use override sender if provided (for multi-tenant)
152
+ sender_email = from_email_override or self.from_email
153
+
126
154
  message = Mail(
127
- from_email=Email(self.from_email, self.from_name),
155
+ from_email=Email(sender_email, self.from_name),
128
156
  to_emails=To(to_email),
129
157
  subject=subject,
130
158
  html_content=Content("text/html", html_content)
@@ -144,6 +172,19 @@ class SendGridEmailProvider(EmailProvider):
144
172
  for cc_email in unique_cc_emails:
145
173
  message.add_cc(Cc(cc_email))
146
174
 
175
+ # Add BCC recipients if provided (deduplicated and normalized)
176
+ if bcc_emails:
177
+ seen = set()
178
+ unique_bcc_emails = []
179
+ for bcc_email in bcc_emails:
180
+ normalized = bcc_email.strip().lower()
181
+ if normalized and normalized not in seen:
182
+ seen.add(normalized)
183
+ unique_bcc_emails.append(bcc_email.strip())
184
+
185
+ for bcc_email in unique_bcc_emails:
186
+ message.add_bcc(Bcc(bcc_email))
187
+
147
188
  if text_content:
148
189
  message.add_content(Content("text/plain", text_content))
149
190
 
@@ -151,7 +192,8 @@ class SendGridEmailProvider(EmailProvider):
151
192
 
152
193
  if response.status_code >= 200 and response.status_code < 300:
153
194
  cc_info = f" (CC: {', '.join(cc_emails)})" if cc_emails else ""
154
- logger.info(f"Email sent to {to_email}{cc_info} via SendGrid")
195
+ bcc_info = f" (BCC: {', '.join(bcc_emails)})" if bcc_emails else ""
196
+ logger.info(f"Email sent to {to_email}{cc_info}{bcc_info} via SendGrid")
155
197
  return True
156
198
  else:
157
199
  logger.error(f"SendGrid returned status {response.status_code}")
@@ -200,13 +242,19 @@ class EmailService:
200
242
  """Check if a real email provider is configured (not just console logging)."""
201
243
  return isinstance(self.provider, SendGridEmailProvider)
202
244
 
203
- def send_magic_link(self, email: str, token: str) -> bool:
245
+ def send_magic_link(
246
+ self,
247
+ email: str,
248
+ token: str,
249
+ sender_email: Optional[str] = None,
250
+ ) -> bool:
204
251
  """
205
252
  Send a magic link email for authentication.
206
253
 
207
254
  Args:
208
255
  email: User's email address
209
256
  token: Magic link token
257
+ sender_email: Override sender email address (for multi-tenant)
210
258
 
211
259
  Returns:
212
260
  True if email was sent successfully
@@ -264,7 +312,9 @@ If you didn't request this email, you can safely ignore it.
264
312
  © {self._get_year()} Nomad Karaoke
265
313
  """
266
314
 
267
- return self.provider.send_email(email, subject, html_content, text_content)
315
+ return self.provider.send_email(
316
+ email, subject, html_content, text_content, from_email_override=sender_email
317
+ )
268
318
 
269
319
  def send_credits_added(self, email: str, credits: int, total_credits: int) -> bool:
270
320
  """
@@ -1017,6 +1067,7 @@ Thanks for being part of making Nomad Karaoke better!
1017
1067
  html_content=html_content,
1018
1068
  text_content=message_content,
1019
1069
  cc_emails=cc_emails,
1070
+ bcc_emails=["done@nomadkaraoke.com"],
1020
1071
  )
1021
1072
 
1022
1073
  def send_action_reminder(
@@ -1080,6 +1131,202 @@ Thanks for being part of making Nomad Karaoke better!
1080
1131
  text_content=message_content,
1081
1132
  )
1082
1133
 
1134
+ def send_made_for_you_order_confirmation(
1135
+ self,
1136
+ to_email: str,
1137
+ artist: str,
1138
+ title: str,
1139
+ job_id: str,
1140
+ notes: Optional[str] = None,
1141
+ ) -> bool:
1142
+ """
1143
+ Send order confirmation email to customer for made-for-you orders.
1144
+
1145
+ Args:
1146
+ to_email: Customer's email address
1147
+ artist: Artist name
1148
+ title: Song title
1149
+ job_id: Order/job ID for reference
1150
+ notes: Optional customer notes
1151
+
1152
+ Returns:
1153
+ True if email was sent successfully
1154
+ """
1155
+ subject = f"Order Confirmed: {artist} - {title} | Nomad Karaoke"
1156
+
1157
+ extra_styles = """
1158
+ .order-details {
1159
+ background-color: #f8fafc;
1160
+ border-radius: 8px;
1161
+ padding: 20px;
1162
+ margin: 20px 0;
1163
+ }
1164
+ .order-details h3 {
1165
+ margin-top: 0;
1166
+ color: #1e293b;
1167
+ }
1168
+ .order-details p {
1169
+ margin: 8px 0;
1170
+ color: #475569;
1171
+ }
1172
+ .highlight {
1173
+ color: #059669;
1174
+ font-weight: bold;
1175
+ }
1176
+ """
1177
+
1178
+ notes_html = ""
1179
+ notes_text = ""
1180
+ if notes:
1181
+ notes_html = f"<p><strong>Your Notes:</strong> {html.escape(notes)}</p>"
1182
+ notes_text = f"\nYour Notes: {notes}"
1183
+
1184
+ content = f"""
1185
+ <h2>Thank You for Your Order!</h2>
1186
+
1187
+ <div class="order-details">
1188
+ <h3>Order Details</h3>
1189
+ <p><strong>Order ID:</strong> {html.escape(job_id)}</p>
1190
+ <p><strong>Artist:</strong> {html.escape(artist)}</p>
1191
+ <p><strong>Title:</strong> {html.escape(title)}</p>
1192
+ {notes_html}
1193
+ </div>
1194
+
1195
+ <p>Our team will create your custom karaoke video within <span class="highlight">24 hours</span>.</p>
1196
+
1197
+ <p>You'll receive an email with download links as soon as your video is ready.</p>
1198
+
1199
+ <p><strong>No action needed</strong> - sit back and we'll take care of everything!</p>
1200
+ """
1201
+
1202
+ text_content = f"""Thank You for Your Order!
1203
+
1204
+ Order Details:
1205
+ Order ID: {job_id}
1206
+ Artist: {artist}
1207
+ Title: {title}{notes_text}
1208
+
1209
+ Our team will create your custom karaoke video within 24 hours.
1210
+
1211
+ You'll receive an email with download links as soon as your video is ready.
1212
+
1213
+ No action needed - sit back and we'll take care of everything!
1214
+ """
1215
+
1216
+ html_content = self._build_email_html(content, extra_styles)
1217
+
1218
+ return self.provider.send_email(
1219
+ to_email=to_email,
1220
+ subject=subject,
1221
+ html_content=html_content,
1222
+ text_content=text_content,
1223
+ bcc_emails=["done@nomadkaraoke.com"],
1224
+ )
1225
+
1226
+ def send_made_for_you_admin_notification(
1227
+ self,
1228
+ to_email: str,
1229
+ customer_email: str,
1230
+ artist: str,
1231
+ title: str,
1232
+ job_id: str,
1233
+ admin_login_token: str,
1234
+ notes: Optional[str] = None,
1235
+ audio_source_count: int = 0,
1236
+ ) -> bool:
1237
+ """
1238
+ Send notification email to admin for new made-for-you orders.
1239
+
1240
+ Args:
1241
+ to_email: Admin email address
1242
+ customer_email: Customer's email address
1243
+ artist: Artist name
1244
+ title: Song title
1245
+ job_id: Job ID for reference
1246
+ admin_login_token: Token for one-click admin login
1247
+ notes: Optional customer notes
1248
+ audio_source_count: Number of audio sources found
1249
+
1250
+ Returns:
1251
+ True if email was sent successfully
1252
+ """
1253
+ subject = f"Karaoke Order: {artist} - {title} [ID: {job_id}]"
1254
+
1255
+ extra_styles = """
1256
+ .order-info {
1257
+ background-color: #f8fafc;
1258
+ border-radius: 8px;
1259
+ padding: 20px;
1260
+ margin: 20px 0;
1261
+ }
1262
+ .order-info p {
1263
+ margin: 8px 0;
1264
+ }
1265
+ .action-button {
1266
+ display: inline-block;
1267
+ background-color: #2563eb;
1268
+ color: white !important;
1269
+ padding: 12px 24px;
1270
+ border-radius: 6px;
1271
+ text-decoration: none;
1272
+ font-weight: bold;
1273
+ margin-top: 16px;
1274
+ }
1275
+ """
1276
+
1277
+ notes_html = ""
1278
+ notes_text = ""
1279
+ if notes:
1280
+ notes_html = f"<p><strong>Customer Notes:</strong> {html.escape(notes)}</p>"
1281
+ notes_text = f"\nCustomer Notes: {notes}"
1282
+
1283
+ # Calculate deadline (24 hours from now, converted to Eastern Time)
1284
+ deadline_utc = datetime.now(timezone.utc) + timedelta(hours=24)
1285
+ eastern_tz = ZoneInfo("America/New_York")
1286
+ deadline_eastern = deadline_utc.astimezone(eastern_tz)
1287
+ # Use "ET" (Eastern Time) to be correct for both EST and EDT
1288
+ deadline_str = deadline_eastern.strftime("%B %d, %Y at %I:%M %p") + " ET"
1289
+
1290
+ # Link to /app/ with admin login token for one-click access
1291
+ app_url = f"{self.frontend_url.rstrip('/')}/app/?admin_token={admin_login_token}"
1292
+
1293
+ content = f"""
1294
+ <div class="order-info">
1295
+ <p><strong>Order / Job ID:</strong> {html.escape(job_id)}</p>
1296
+ <p><strong>Customer:</strong> {html.escape(customer_email)}</p>
1297
+ <p><strong>Artist:</strong> {html.escape(artist)}</p>
1298
+ <p><strong>Title:</strong> {html.escape(title)}</p>
1299
+ <p><strong>Audio Sources Found:</strong> {audio_source_count}</p>
1300
+ {notes_html}
1301
+ <p><strong>Deliver By:</strong> {deadline_str}</p>
1302
+ <p style="margin-top: 16px;"><strong>Action Required:</strong> Select an audio source to start processing.</p>
1303
+ <a href="{app_url}" class="action-button">Open Job</a>
1304
+ </div>
1305
+ """
1306
+
1307
+ text_content = f"""New Karaoke Order
1308
+
1309
+ Order / Job ID: {job_id}
1310
+ Customer: {customer_email}
1311
+ Artist: {artist}
1312
+ Title: {title}
1313
+ Audio Sources Found: {audio_source_count}{notes_text}
1314
+ Deliver By: {deadline_str}
1315
+
1316
+ Action Required: Select an audio source to start processing.
1317
+
1318
+ Open Job: {app_url}
1319
+ """
1320
+
1321
+ html_content = self._build_email_html(content, extra_styles)
1322
+
1323
+ return self.provider.send_email(
1324
+ to_email=to_email,
1325
+ subject=subject,
1326
+ html_content=html_content,
1327
+ text_content=text_content,
1328
+ )
1329
+
1083
1330
 
1084
1331
  # Global instance
1085
1332
  _email_service = None