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.
- backend/api/routes/admin.py +512 -1
- backend/api/routes/audio_search.py +17 -34
- backend/api/routes/file_upload.py +60 -84
- backend/api/routes/internal.py +6 -0
- backend/api/routes/jobs.py +11 -3
- backend/api/routes/rate_limits.py +428 -0
- backend/api/routes/review.py +13 -6
- backend/api/routes/tenant.py +120 -0
- backend/api/routes/users.py +229 -247
- backend/config.py +16 -0
- backend/exceptions.py +66 -0
- backend/main.py +30 -1
- backend/middleware/__init__.py +7 -1
- backend/middleware/tenant.py +192 -0
- backend/models/job.py +19 -3
- backend/models/tenant.py +208 -0
- backend/models/user.py +18 -0
- backend/services/email_service.py +253 -6
- backend/services/email_validation_service.py +646 -0
- backend/services/firestore_service.py +27 -0
- backend/services/job_defaults_service.py +113 -0
- backend/services/job_manager.py +73 -3
- backend/services/rate_limit_service.py +641 -0
- backend/services/stripe_service.py +61 -35
- backend/services/tenant_service.py +285 -0
- backend/services/user_service.py +85 -7
- backend/tests/conftest.py +7 -1
- backend/tests/emulator/test_made_for_you_integration.py +167 -0
- backend/tests/test_admin_job_files.py +337 -0
- backend/tests/test_admin_job_reset.py +384 -0
- backend/tests/test_admin_job_update.py +326 -0
- backend/tests/test_audio_search.py +12 -8
- backend/tests/test_email_service.py +233 -0
- backend/tests/test_email_validation_service.py +298 -0
- backend/tests/test_file_upload.py +8 -6
- backend/tests/test_impersonation.py +223 -0
- backend/tests/test_job_creation_regression.py +4 -0
- backend/tests/test_job_manager.py +146 -1
- backend/tests/test_made_for_you.py +2088 -0
- backend/tests/test_models.py +139 -0
- backend/tests/test_rate_limit_service.py +396 -0
- backend/tests/test_rate_limits_api.py +392 -0
- backend/tests/test_tenant_api.py +350 -0
- backend/tests/test_tenant_middleware.py +345 -0
- backend/tests/test_tenant_models.py +406 -0
- backend/tests/test_tenant_service.py +418 -0
- backend/workers/video_worker.py +8 -3
- backend/workers/video_worker_orchestrator.py +26 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/METADATA +1 -1
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/RECORD +55 -33
- lyrics_transcriber/frontend/src/api.ts +13 -5
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +90 -57
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.99.3.dist-info → karaoke_gen-0.103.1.dist-info}/entry_points.txt +0 -0
- {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
|
-
"""
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|