karaoke-gen 0.90.1__py3-none-any.whl → 0.99.3__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/.coveragerc +20 -0
- backend/.gitignore +37 -0
- backend/Dockerfile +43 -0
- backend/Dockerfile.base +74 -0
- backend/README.md +242 -0
- backend/__init__.py +0 -0
- backend/api/__init__.py +0 -0
- backend/api/dependencies.py +457 -0
- backend/api/routes/__init__.py +0 -0
- backend/api/routes/admin.py +835 -0
- backend/api/routes/audio_search.py +913 -0
- backend/api/routes/auth.py +348 -0
- backend/api/routes/file_upload.py +2112 -0
- backend/api/routes/health.py +409 -0
- backend/api/routes/internal.py +435 -0
- backend/api/routes/jobs.py +1629 -0
- backend/api/routes/review.py +652 -0
- backend/api/routes/themes.py +162 -0
- backend/api/routes/users.py +1513 -0
- backend/config.py +172 -0
- backend/main.py +157 -0
- backend/middleware/__init__.py +5 -0
- backend/middleware/audit_logging.py +124 -0
- backend/models/__init__.py +0 -0
- backend/models/job.py +519 -0
- backend/models/requests.py +123 -0
- backend/models/theme.py +153 -0
- backend/models/user.py +254 -0
- backend/models/worker_log.py +164 -0
- backend/pyproject.toml +29 -0
- backend/quick-check.sh +93 -0
- backend/requirements.txt +29 -0
- backend/run_tests.sh +60 -0
- backend/services/__init__.py +0 -0
- backend/services/audio_analysis_service.py +243 -0
- backend/services/audio_editing_service.py +278 -0
- backend/services/audio_search_service.py +702 -0
- backend/services/auth_service.py +630 -0
- backend/services/credential_manager.py +792 -0
- backend/services/discord_service.py +172 -0
- backend/services/dropbox_service.py +301 -0
- backend/services/email_service.py +1093 -0
- backend/services/encoding_interface.py +454 -0
- backend/services/encoding_service.py +502 -0
- backend/services/firestore_service.py +512 -0
- backend/services/flacfetch_client.py +573 -0
- backend/services/gce_encoding/README.md +72 -0
- backend/services/gce_encoding/__init__.py +22 -0
- backend/services/gce_encoding/main.py +589 -0
- backend/services/gce_encoding/requirements.txt +16 -0
- backend/services/gdrive_service.py +356 -0
- backend/services/job_logging.py +258 -0
- backend/services/job_manager.py +853 -0
- backend/services/job_notification_service.py +271 -0
- backend/services/langfuse_preloader.py +98 -0
- backend/services/local_encoding_service.py +590 -0
- backend/services/local_preview_encoding_service.py +407 -0
- backend/services/lyrics_cache_service.py +216 -0
- backend/services/metrics.py +413 -0
- backend/services/nltk_preloader.py +122 -0
- backend/services/packaging_service.py +287 -0
- backend/services/rclone_service.py +106 -0
- backend/services/spacy_preloader.py +65 -0
- backend/services/storage_service.py +209 -0
- backend/services/stripe_service.py +371 -0
- backend/services/structured_logging.py +254 -0
- backend/services/template_service.py +330 -0
- backend/services/theme_service.py +469 -0
- backend/services/tracing.py +543 -0
- backend/services/user_service.py +721 -0
- backend/services/worker_service.py +558 -0
- backend/services/youtube_service.py +112 -0
- backend/services/youtube_upload_service.py +445 -0
- backend/tests/__init__.py +4 -0
- backend/tests/conftest.py +224 -0
- backend/tests/emulator/__init__.py +7 -0
- backend/tests/emulator/conftest.py +109 -0
- backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
- backend/tests/emulator/test_emulator_integration.py +356 -0
- backend/tests/emulator/test_style_loading_direct.py +436 -0
- backend/tests/emulator/test_worker_logs_direct.py +229 -0
- backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
- backend/tests/requirements-test.txt +10 -0
- backend/tests/requirements.txt +6 -0
- backend/tests/test_admin_email_endpoints.py +411 -0
- backend/tests/test_api_integration.py +460 -0
- backend/tests/test_api_routes.py +93 -0
- backend/tests/test_audio_analysis_service.py +294 -0
- backend/tests/test_audio_editing_service.py +386 -0
- backend/tests/test_audio_search.py +1398 -0
- backend/tests/test_audio_services.py +378 -0
- backend/tests/test_auth_firestore.py +231 -0
- backend/tests/test_config_extended.py +68 -0
- backend/tests/test_credential_manager.py +377 -0
- backend/tests/test_dependencies.py +54 -0
- backend/tests/test_discord_service.py +244 -0
- backend/tests/test_distribution_services.py +820 -0
- backend/tests/test_dropbox_service.py +472 -0
- backend/tests/test_email_service.py +492 -0
- backend/tests/test_emulator_integration.py +322 -0
- backend/tests/test_encoding_interface.py +412 -0
- backend/tests/test_file_upload.py +1739 -0
- backend/tests/test_flacfetch_client.py +632 -0
- backend/tests/test_gdrive_service.py +524 -0
- backend/tests/test_instrumental_api.py +431 -0
- backend/tests/test_internal_api.py +343 -0
- backend/tests/test_job_creation_regression.py +583 -0
- backend/tests/test_job_manager.py +356 -0
- backend/tests/test_job_manager_notifications.py +329 -0
- backend/tests/test_job_notification_service.py +443 -0
- backend/tests/test_jobs_api.py +283 -0
- backend/tests/test_local_encoding_service.py +423 -0
- backend/tests/test_local_preview_encoding_service.py +567 -0
- backend/tests/test_main.py +87 -0
- backend/tests/test_models.py +918 -0
- backend/tests/test_packaging_service.py +382 -0
- backend/tests/test_requests.py +201 -0
- backend/tests/test_routes_jobs.py +282 -0
- backend/tests/test_routes_review.py +337 -0
- backend/tests/test_services.py +556 -0
- backend/tests/test_services_extended.py +112 -0
- backend/tests/test_spacy_preloader.py +119 -0
- backend/tests/test_storage_service.py +448 -0
- backend/tests/test_style_upload.py +261 -0
- backend/tests/test_template_service.py +295 -0
- backend/tests/test_theme_service.py +516 -0
- backend/tests/test_unicode_sanitization.py +522 -0
- backend/tests/test_upload_api.py +256 -0
- backend/tests/test_validate.py +156 -0
- backend/tests/test_video_worker_orchestrator.py +847 -0
- backend/tests/test_worker_log_subcollection.py +509 -0
- backend/tests/test_worker_logging.py +365 -0
- backend/tests/test_workers.py +1116 -0
- backend/tests/test_workers_extended.py +178 -0
- backend/tests/test_youtube_service.py +247 -0
- backend/tests/test_youtube_upload_service.py +568 -0
- backend/utils/test_data.py +27 -0
- backend/validate.py +173 -0
- backend/version.py +27 -0
- backend/workers/README.md +597 -0
- backend/workers/__init__.py +11 -0
- backend/workers/audio_worker.py +618 -0
- backend/workers/lyrics_worker.py +683 -0
- backend/workers/render_video_worker.py +483 -0
- backend/workers/screens_worker.py +535 -0
- backend/workers/style_helper.py +198 -0
- backend/workers/video_worker.py +1277 -0
- backend/workers/video_worker_orchestrator.py +701 -0
- backend/workers/worker_logging.py +278 -0
- karaoke_gen/instrumental_review/static/index.html +7 -4
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
- karaoke_gen/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/METADATA +1 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/RECORD +196 -46
- lyrics_transcriber/correction/agentic/agent.py +17 -6
- lyrics_transcriber/correction/agentic/providers/config.py +9 -5
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -93
- lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
- lyrics_transcriber/correction/anchor_sequence.py +151 -37
- lyrics_transcriber/correction/corrector.py +192 -130
- lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
- lyrics_transcriber/correction/operations.py +24 -9
- lyrics_transcriber/correction/phrase_analyzer.py +18 -0
- lyrics_transcriber/frontend/package-lock.json +2 -2
- lyrics_transcriber/frontend/package.json +1 -1
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
- lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
- lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
- lyrics_transcriber/frontend/src/theme.ts +42 -15
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/vite.config.js +5 -0
- lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/index.html +6 -2
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/output/generator.py +17 -3
- lyrics_transcriber/output/video.py +60 -95
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Email service for sending transactional emails.
|
|
3
|
+
|
|
4
|
+
Supports multiple providers:
|
|
5
|
+
- SendGrid (recommended for production)
|
|
6
|
+
- Console logging (for development/testing)
|
|
7
|
+
|
|
8
|
+
Future providers can be added:
|
|
9
|
+
- Mailgun
|
|
10
|
+
- AWS SES
|
|
11
|
+
- Postmark
|
|
12
|
+
"""
|
|
13
|
+
import html
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from typing import Optional, List
|
|
18
|
+
|
|
19
|
+
from backend.config import get_settings
|
|
20
|
+
from karaoke_gen.utils import sanitize_filename
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EmailProvider(ABC):
|
|
27
|
+
"""Abstract base class for email providers."""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def send_email(
|
|
31
|
+
self,
|
|
32
|
+
to_email: str,
|
|
33
|
+
subject: str,
|
|
34
|
+
html_content: str,
|
|
35
|
+
text_content: Optional[str] = None,
|
|
36
|
+
cc_emails: Optional[List[str]] = None,
|
|
37
|
+
) -> bool:
|
|
38
|
+
"""Send an email. Returns True if successful."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ConsoleEmailProvider(EmailProvider):
|
|
43
|
+
"""
|
|
44
|
+
Development email provider that logs to console.
|
|
45
|
+
|
|
46
|
+
Useful for local development and testing.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def send_email(
|
|
50
|
+
self,
|
|
51
|
+
to_email: str,
|
|
52
|
+
subject: str,
|
|
53
|
+
html_content: str,
|
|
54
|
+
text_content: Optional[str] = None,
|
|
55
|
+
cc_emails: Optional[List[str]] = None,
|
|
56
|
+
) -> bool:
|
|
57
|
+
logger.info("=" * 60)
|
|
58
|
+
logger.info(f"EMAIL TO: {to_email}")
|
|
59
|
+
if cc_emails:
|
|
60
|
+
logger.info(f"CC: {', '.join(cc_emails)}")
|
|
61
|
+
logger.info(f"SUBJECT: {subject}")
|
|
62
|
+
logger.info("-" * 60)
|
|
63
|
+
logger.info(text_content or html_content)
|
|
64
|
+
logger.info("=" * 60)
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PreviewEmailProvider(EmailProvider):
|
|
69
|
+
"""
|
|
70
|
+
Email provider that captures HTML content for previewing.
|
|
71
|
+
|
|
72
|
+
Instead of sending emails, stores the HTML content for later retrieval.
|
|
73
|
+
Useful for generating email previews.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self):
|
|
77
|
+
self.last_html: Optional[str] = None
|
|
78
|
+
self.last_subject: Optional[str] = None
|
|
79
|
+
self.last_to_email: Optional[str] = None
|
|
80
|
+
|
|
81
|
+
def send_email(
|
|
82
|
+
self,
|
|
83
|
+
to_email: str,
|
|
84
|
+
subject: str,
|
|
85
|
+
html_content: str,
|
|
86
|
+
text_content: Optional[str] = None,
|
|
87
|
+
cc_emails: Optional[List[str]] = None,
|
|
88
|
+
) -> bool:
|
|
89
|
+
self.last_to_email = to_email
|
|
90
|
+
self.last_subject = subject
|
|
91
|
+
self.last_html = html_content
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def get_last_html(self) -> Optional[str]:
|
|
95
|
+
"""Get the HTML content from the last 'sent' email."""
|
|
96
|
+
return self.last_html
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SendGridEmailProvider(EmailProvider):
|
|
100
|
+
"""
|
|
101
|
+
SendGrid email provider for production.
|
|
102
|
+
|
|
103
|
+
Requires SENDGRID_API_KEY environment variable.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, api_key: str, from_email: str, from_name: str = "Nomad Karaoke"):
|
|
107
|
+
self.api_key = api_key
|
|
108
|
+
self.from_email = from_email
|
|
109
|
+
self.from_name = from_name
|
|
110
|
+
|
|
111
|
+
def send_email(
|
|
112
|
+
self,
|
|
113
|
+
to_email: str,
|
|
114
|
+
subject: str,
|
|
115
|
+
html_content: str,
|
|
116
|
+
text_content: Optional[str] = None,
|
|
117
|
+
cc_emails: Optional[List[str]] = None,
|
|
118
|
+
) -> bool:
|
|
119
|
+
try:
|
|
120
|
+
# Import here to avoid requiring sendgrid in all environments
|
|
121
|
+
from sendgrid import SendGridAPIClient
|
|
122
|
+
from sendgrid.helpers.mail import Mail, Email, To, Content, Cc
|
|
123
|
+
|
|
124
|
+
sg = SendGridAPIClient(api_key=self.api_key)
|
|
125
|
+
|
|
126
|
+
message = Mail(
|
|
127
|
+
from_email=Email(self.from_email, self.from_name),
|
|
128
|
+
to_emails=To(to_email),
|
|
129
|
+
subject=subject,
|
|
130
|
+
html_content=Content("text/html", html_content)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Add CC recipients if provided (deduplicated and normalized)
|
|
134
|
+
if cc_emails:
|
|
135
|
+
# Normalize emails (lowercase, strip whitespace) and deduplicate
|
|
136
|
+
seen = set()
|
|
137
|
+
unique_cc_emails = []
|
|
138
|
+
for cc_email in cc_emails:
|
|
139
|
+
normalized = cc_email.strip().lower()
|
|
140
|
+
if normalized and normalized not in seen:
|
|
141
|
+
seen.add(normalized)
|
|
142
|
+
unique_cc_emails.append(cc_email.strip())
|
|
143
|
+
|
|
144
|
+
for cc_email in unique_cc_emails:
|
|
145
|
+
message.add_cc(Cc(cc_email))
|
|
146
|
+
|
|
147
|
+
if text_content:
|
|
148
|
+
message.add_content(Content("text/plain", text_content))
|
|
149
|
+
|
|
150
|
+
response = sg.send(message)
|
|
151
|
+
|
|
152
|
+
if response.status_code >= 200 and response.status_code < 300:
|
|
153
|
+
cc_info = f" (CC: {', '.join(cc_emails)})" if cc_emails else ""
|
|
154
|
+
logger.info(f"Email sent to {to_email}{cc_info} via SendGrid")
|
|
155
|
+
return True
|
|
156
|
+
else:
|
|
157
|
+
logger.error(f"SendGrid returned status {response.status_code}")
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
except Exception:
|
|
161
|
+
logger.exception("Failed to send email via SendGrid")
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class EmailService:
|
|
166
|
+
"""
|
|
167
|
+
High-level email service for sending transactional emails.
|
|
168
|
+
|
|
169
|
+
Automatically selects the appropriate provider based on configuration.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
# Brand colors and assets for consistent email styling
|
|
173
|
+
BRAND_PRIMARY = "#ff7acc" # Pink
|
|
174
|
+
BRAND_PRIMARY_HOVER = "#e066b3" # Darker pink for hover states
|
|
175
|
+
BRAND_SECONDARY = "#ffdf6b" # Yellow (accent)
|
|
176
|
+
BRAND_SUCCESS = "#22c55e" # Green for success states (Tailwind green-500)
|
|
177
|
+
LOGO_URL = "https://beveradb.github.io/public-images/Nomad-Karaoke-Logo-small-indexed-websafe-rectangle.gif"
|
|
178
|
+
|
|
179
|
+
def __init__(self):
|
|
180
|
+
self.settings = get_settings()
|
|
181
|
+
self.provider = self._get_provider()
|
|
182
|
+
self.frontend_url = os.getenv("FRONTEND_URL", "https://gen.nomadkaraoke.com")
|
|
183
|
+
# After consolidation, buy URL is the same as frontend URL
|
|
184
|
+
self.buy_url = os.getenv("BUY_URL", self.frontend_url)
|
|
185
|
+
|
|
186
|
+
def _get_provider(self) -> EmailProvider:
|
|
187
|
+
"""Get the configured email provider."""
|
|
188
|
+
sendgrid_api_key = os.getenv("SENDGRID_API_KEY")
|
|
189
|
+
from_email = os.getenv("EMAIL_FROM", "gen@nomadkaraoke.com")
|
|
190
|
+
from_name = os.getenv("EMAIL_FROM_NAME", "Nomad Karaoke")
|
|
191
|
+
|
|
192
|
+
if sendgrid_api_key:
|
|
193
|
+
logger.info("Using SendGrid email provider")
|
|
194
|
+
return SendGridEmailProvider(sendgrid_api_key, from_email, from_name)
|
|
195
|
+
else:
|
|
196
|
+
logger.warning("No email provider configured, using console logging")
|
|
197
|
+
return ConsoleEmailProvider()
|
|
198
|
+
|
|
199
|
+
def is_configured(self) -> bool:
|
|
200
|
+
"""Check if a real email provider is configured (not just console logging)."""
|
|
201
|
+
return isinstance(self.provider, SendGridEmailProvider)
|
|
202
|
+
|
|
203
|
+
def send_magic_link(self, email: str, token: str) -> bool:
|
|
204
|
+
"""
|
|
205
|
+
Send a magic link email for authentication.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
email: User's email address
|
|
209
|
+
token: Magic link token
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
True if email was sent successfully
|
|
213
|
+
"""
|
|
214
|
+
magic_link_url = f"{self.frontend_url}/auth/verify?token={token}"
|
|
215
|
+
|
|
216
|
+
subject = "Sign in to Nomad Karaoke"
|
|
217
|
+
|
|
218
|
+
extra_styles = """
|
|
219
|
+
.warning {
|
|
220
|
+
background-color: #fef3c7;
|
|
221
|
+
border: 1px solid #fcd34d;
|
|
222
|
+
border-radius: 4px;
|
|
223
|
+
padding: 12px;
|
|
224
|
+
margin: 20px 0;
|
|
225
|
+
font-size: 14px;
|
|
226
|
+
}
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
content = f"""
|
|
230
|
+
<p>Hi there,</p>
|
|
231
|
+
|
|
232
|
+
<p>Click the button below to sign in to Nomad Karaoke:</p>
|
|
233
|
+
|
|
234
|
+
<p style="text-align: center;">
|
|
235
|
+
<a href="{magic_link_url}" class="button">Sign In</a>
|
|
236
|
+
</p>
|
|
237
|
+
|
|
238
|
+
<div class="warning">
|
|
239
|
+
⏰ This link expires in 15 minutes and can only be used once.
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<p>If the button doesn't work, copy and paste this link into your browser:</p>
|
|
243
|
+
<p style="word-break: break-all; font-size: 14px; color: #666;">
|
|
244
|
+
{magic_link_url}
|
|
245
|
+
</p>
|
|
246
|
+
|
|
247
|
+
<p>If you didn't request this email, you can safely ignore it.</p>
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
html_content = self._build_email_html(content, extra_styles)
|
|
251
|
+
|
|
252
|
+
text_content = f"""
|
|
253
|
+
Sign in to Nomad Karaoke
|
|
254
|
+
========================
|
|
255
|
+
|
|
256
|
+
Click this link to sign in:
|
|
257
|
+
{magic_link_url}
|
|
258
|
+
|
|
259
|
+
This link expires in 15 minutes and can only be used once.
|
|
260
|
+
|
|
261
|
+
If you didn't request this email, you can safely ignore it.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
© {self._get_year()} Nomad Karaoke
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
return self.provider.send_email(email, subject, html_content, text_content)
|
|
268
|
+
|
|
269
|
+
def send_credits_added(self, email: str, credits: int, total_credits: int) -> bool:
|
|
270
|
+
"""
|
|
271
|
+
Send notification when credits are added to account.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
email: User's email address
|
|
275
|
+
credits: Number of credits added
|
|
276
|
+
total_credits: New total credit balance
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
True if email was sent successfully
|
|
280
|
+
"""
|
|
281
|
+
subject = f"🎉 {credits} credits added to your Nomad Karaoke account"
|
|
282
|
+
|
|
283
|
+
extra_styles = f"""
|
|
284
|
+
.credits-box {{
|
|
285
|
+
background-color: #ecfdf5;
|
|
286
|
+
border: 2px solid {self.BRAND_SUCCESS};
|
|
287
|
+
border-radius: 12px;
|
|
288
|
+
padding: 24px;
|
|
289
|
+
text-align: center;
|
|
290
|
+
margin: 20px 0;
|
|
291
|
+
}}
|
|
292
|
+
.credits-number {{
|
|
293
|
+
font-size: 48px;
|
|
294
|
+
font-weight: bold;
|
|
295
|
+
color: {self.BRAND_SUCCESS};
|
|
296
|
+
}}
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
content = f"""
|
|
300
|
+
<p>Great news!</p>
|
|
301
|
+
|
|
302
|
+
<p><strong>{credits} credits</strong> have been added to your account.</p>
|
|
303
|
+
|
|
304
|
+
<div class="credits-box">
|
|
305
|
+
<div>Your balance:</div>
|
|
306
|
+
<div class="credits-number">{total_credits}</div>
|
|
307
|
+
<div>credits</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<p>Each credit lets you create one professional karaoke video with:</p>
|
|
311
|
+
<ul>
|
|
312
|
+
<li>AI-powered vocal/instrumental separation</li>
|
|
313
|
+
<li>Synchronized lyrics with word-level timing</li>
|
|
314
|
+
<li>4K video output</li>
|
|
315
|
+
<li>YouTube upload</li>
|
|
316
|
+
</ul>
|
|
317
|
+
|
|
318
|
+
<p style="text-align: center;">
|
|
319
|
+
<a href="{self.frontend_url}" class="button">Create Karaoke Now</a>
|
|
320
|
+
</p>
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
html_content = self._build_email_html(content, extra_styles)
|
|
324
|
+
|
|
325
|
+
text_content = f"""
|
|
326
|
+
{credits} credits added to your Nomad Karaoke account!
|
|
327
|
+
|
|
328
|
+
Your new balance: {total_credits} credits
|
|
329
|
+
|
|
330
|
+
Each credit lets you create one professional karaoke video.
|
|
331
|
+
|
|
332
|
+
Start creating: {self.frontend_url}
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
© {self._get_year()} Nomad Karaoke
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
return self.provider.send_email(email, subject, html_content, text_content)
|
|
339
|
+
|
|
340
|
+
def send_welcome_email(self, email: str, credits: int = 0) -> bool:
|
|
341
|
+
"""
|
|
342
|
+
Send welcome email to new users.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
email: User's email address
|
|
346
|
+
credits: Initial credit balance (if any)
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
True if email was sent successfully
|
|
350
|
+
"""
|
|
351
|
+
subject = "Welcome to Nomad Karaoke! 🎤"
|
|
352
|
+
|
|
353
|
+
credits_text = f"You have <strong>{credits} credits</strong> to get started!" if credits > 0 else ""
|
|
354
|
+
|
|
355
|
+
extra_styles = """
|
|
356
|
+
.feature {
|
|
357
|
+
display: flex;
|
|
358
|
+
align-items: flex-start;
|
|
359
|
+
margin: 16px 0;
|
|
360
|
+
}
|
|
361
|
+
.feature-icon {
|
|
362
|
+
font-size: 24px;
|
|
363
|
+
margin-right: 12px;
|
|
364
|
+
}
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
content = f"""
|
|
368
|
+
<p>Welcome to Nomad Karaoke!</p>
|
|
369
|
+
|
|
370
|
+
<p>Turn any song into a professional karaoke video in minutes. {credits_text}</p>
|
|
371
|
+
|
|
372
|
+
<p><strong>Here's how it works:</strong></p>
|
|
373
|
+
|
|
374
|
+
<div class="feature">
|
|
375
|
+
<span class="feature-icon">🎵</span>
|
|
376
|
+
<div>
|
|
377
|
+
<strong>1. Search for a song</strong><br>
|
|
378
|
+
Enter the artist and title, and we'll find high-quality audio.
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<div class="feature">
|
|
383
|
+
<span class="feature-icon">✨</span>
|
|
384
|
+
<div>
|
|
385
|
+
<strong>2. Our system works its magic</strong><br>
|
|
386
|
+
We separate vocals, transcribe lyrics, and sync everything perfectly.
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div class="feature">
|
|
391
|
+
<span class="feature-icon">✏️</span>
|
|
392
|
+
<div>
|
|
393
|
+
<strong>3. Review & customize</strong><br>
|
|
394
|
+
Fine-tune the lyrics if needed, choose your instrumental.
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<div class="feature">
|
|
399
|
+
<span class="feature-icon">🎬</span>
|
|
400
|
+
<div>
|
|
401
|
+
<strong>4. Get your video</strong><br>
|
|
402
|
+
Download your 4K karaoke video or upload directly to YouTube.
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<p style="text-align: center;">
|
|
407
|
+
<a href="{self.frontend_url}" class="button">Get Started</a>
|
|
408
|
+
</p>
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
html_content = self._build_email_html(content, extra_styles)
|
|
412
|
+
|
|
413
|
+
text_content = f"""
|
|
414
|
+
Welcome to Nomad Karaoke!
|
|
415
|
+
|
|
416
|
+
Turn any song into a professional karaoke video in minutes.
|
|
417
|
+
|
|
418
|
+
Here's how it works:
|
|
419
|
+
|
|
420
|
+
1. Search for a song
|
|
421
|
+
Enter the artist and title, and we'll find high-quality audio.
|
|
422
|
+
|
|
423
|
+
2. Our system works its magic
|
|
424
|
+
We separate vocals, transcribe lyrics, and sync everything perfectly.
|
|
425
|
+
|
|
426
|
+
3. Review & customize
|
|
427
|
+
Fine-tune the lyrics if needed, choose your instrumental.
|
|
428
|
+
|
|
429
|
+
4. Get your video
|
|
430
|
+
Download your 4K karaoke video or upload directly to YouTube.
|
|
431
|
+
|
|
432
|
+
Get started: {self.frontend_url}
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
© {self._get_year()} Nomad Karaoke
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
return self.provider.send_email(email, subject, html_content, text_content)
|
|
439
|
+
|
|
440
|
+
def send_beta_welcome_email(self, email: str, credits: int = 1) -> bool:
|
|
441
|
+
"""
|
|
442
|
+
Send welcome email to new beta testers.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
email: User's email address
|
|
446
|
+
credits: Initial free credits granted
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
True if email was sent successfully
|
|
450
|
+
"""
|
|
451
|
+
subject = "Welcome Beta Tester! Free Karaoke Credits Inside 🎤"
|
|
452
|
+
|
|
453
|
+
extra_styles = f"""
|
|
454
|
+
.beta-badge {{
|
|
455
|
+
display: inline-block;
|
|
456
|
+
background: linear-gradient(135deg, {self.BRAND_PRIMARY}, {self.BRAND_SECONDARY});
|
|
457
|
+
color: #333;
|
|
458
|
+
padding: 4px 12px;
|
|
459
|
+
border-radius: 20px;
|
|
460
|
+
font-size: 12px;
|
|
461
|
+
font-weight: bold;
|
|
462
|
+
margin-top: 10px;
|
|
463
|
+
}}
|
|
464
|
+
.credits-box {{
|
|
465
|
+
background-color: #ecfdf5;
|
|
466
|
+
border: 2px solid {self.BRAND_SUCCESS};
|
|
467
|
+
border-radius: 12px;
|
|
468
|
+
padding: 24px;
|
|
469
|
+
text-align: center;
|
|
470
|
+
margin: 20px 0;
|
|
471
|
+
}}
|
|
472
|
+
.credits-number {{
|
|
473
|
+
font-size: 48px;
|
|
474
|
+
font-weight: bold;
|
|
475
|
+
color: {self.BRAND_SUCCESS};
|
|
476
|
+
}}
|
|
477
|
+
.reminder {{
|
|
478
|
+
background-color: #fef3c7;
|
|
479
|
+
border: 1px solid #fcd34d;
|
|
480
|
+
border-radius: 8px;
|
|
481
|
+
padding: 16px;
|
|
482
|
+
margin: 20px 0;
|
|
483
|
+
}}
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
content = f"""
|
|
487
|
+
<p>Thank you for joining our beta program!</p>
|
|
488
|
+
|
|
489
|
+
<p>As promised, here's your free credit to create a karaoke video:</p>
|
|
490
|
+
|
|
491
|
+
<div class="credits-box">
|
|
492
|
+
<div>Your balance:</div>
|
|
493
|
+
<div class="credits-number">{credits}</div>
|
|
494
|
+
<div>free credit{'' if credits == 1 else 's'}</div>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<div class="reminder">
|
|
498
|
+
<strong>📝 Your Promise:</strong><br>
|
|
499
|
+
Remember, as a beta tester you agreed to:
|
|
500
|
+
<ul style="margin: 10px 0; padding-left: 20px;">
|
|
501
|
+
<li>Review and correct any lyrics transcription errors</li>
|
|
502
|
+
<li>Share your honest feedback after trying the tool</li>
|
|
503
|
+
</ul>
|
|
504
|
+
We'll send you a quick feedback form after your job completes!
|
|
505
|
+
</div>
|
|
506
|
+
|
|
507
|
+
<p style="text-align: center;">
|
|
508
|
+
<a href="{self.frontend_url}" class="button">Create Your Karaoke Video</a>
|
|
509
|
+
</p>
|
|
510
|
+
|
|
511
|
+
<p>Thanks for helping us make Nomad Karaoke better!</p>
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
extra_header = '<div><span class="beta-badge">BETA TESTER</span></div>'
|
|
515
|
+
html_content = self._build_email_html(content, extra_styles, extra_header)
|
|
516
|
+
|
|
517
|
+
text_content = f"""
|
|
518
|
+
Welcome Beta Tester!
|
|
519
|
+
|
|
520
|
+
Thank you for joining our beta program!
|
|
521
|
+
|
|
522
|
+
You've been granted {credits} free credit{'' if credits == 1 else 's'} to create karaoke videos.
|
|
523
|
+
|
|
524
|
+
YOUR PROMISE:
|
|
525
|
+
As a beta tester, you agreed to:
|
|
526
|
+
- Review and correct any lyrics transcription errors
|
|
527
|
+
- Share your honest feedback after trying the tool
|
|
528
|
+
|
|
529
|
+
We'll send you a quick feedback form after your job completes!
|
|
530
|
+
|
|
531
|
+
Create your karaoke video: {self.frontend_url}
|
|
532
|
+
|
|
533
|
+
Thanks for helping us make Nomad Karaoke better!
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
© {self._get_year()} Nomad Karaoke
|
|
537
|
+
"""
|
|
538
|
+
|
|
539
|
+
return self.provider.send_email(email, subject, html_content, text_content)
|
|
540
|
+
|
|
541
|
+
def send_feedback_request_email(self, email: str, feedback_url: str, job_title: Optional[str] = None) -> bool:
|
|
542
|
+
"""
|
|
543
|
+
Send feedback request email to beta testers.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
email: User's email address
|
|
547
|
+
feedback_url: URL to the feedback form
|
|
548
|
+
job_title: Optional title of the completed job
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
True if email was sent successfully
|
|
552
|
+
"""
|
|
553
|
+
subject = "Quick feedback on your karaoke experience? 🎤"
|
|
554
|
+
|
|
555
|
+
# Escape job_title to prevent XSS in email clients that render HTML
|
|
556
|
+
safe_job_title = html.escape(job_title) if job_title else ""
|
|
557
|
+
job_context = f" for <strong>{safe_job_title}</strong>" if job_title else ""
|
|
558
|
+
|
|
559
|
+
extra_styles = f"""
|
|
560
|
+
.feedback-box {{
|
|
561
|
+
background: linear-gradient(135deg, #fff0f9, #ffe0f2);
|
|
562
|
+
border: 2px solid {self.BRAND_PRIMARY};
|
|
563
|
+
border-radius: 12px;
|
|
564
|
+
padding: 24px;
|
|
565
|
+
text-align: center;
|
|
566
|
+
margin: 20px 0;
|
|
567
|
+
}}
|
|
568
|
+
.stars {{
|
|
569
|
+
font-size: 36px;
|
|
570
|
+
margin: 10px 0;
|
|
571
|
+
}}
|
|
572
|
+
.time-note {{
|
|
573
|
+
background-color: #fef3c7;
|
|
574
|
+
border-radius: 8px;
|
|
575
|
+
padding: 12px;
|
|
576
|
+
margin: 20px 0;
|
|
577
|
+
font-size: 14px;
|
|
578
|
+
}}
|
|
579
|
+
"""
|
|
580
|
+
|
|
581
|
+
content = f"""
|
|
582
|
+
<p>Hi there!</p>
|
|
583
|
+
|
|
584
|
+
<p>Hope you enjoyed creating your karaoke video{job_context}! As a beta tester, your feedback is super valuable to us.</p>
|
|
585
|
+
|
|
586
|
+
<div class="feedback-box">
|
|
587
|
+
<div class="stars">⭐⭐⭐⭐⭐</div>
|
|
588
|
+
<p><strong>How was your experience?</strong></p>
|
|
589
|
+
<p>Quick 2-minute survey - we'd love to hear your thoughts!</p>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
<p style="text-align: center;">
|
|
593
|
+
<a href="{feedback_url}" class="button">Share Your Feedback</a>
|
|
594
|
+
</p>
|
|
595
|
+
|
|
596
|
+
<div class="time-note">
|
|
597
|
+
⏱️ This takes less than 2 minutes and helps us improve the tool for everyone!
|
|
598
|
+
</div>
|
|
599
|
+
|
|
600
|
+
<p>Specifically, we'd love to know:</p>
|
|
601
|
+
<ul>
|
|
602
|
+
<li>How easy was it to use?</li>
|
|
603
|
+
<li>Were the lyrics accurate?</li>
|
|
604
|
+
<li>How was the correction experience?</li>
|
|
605
|
+
<li>What could we improve?</li>
|
|
606
|
+
</ul>
|
|
607
|
+
|
|
608
|
+
<p>Thanks for being part of making Nomad Karaoke better!</p>
|
|
609
|
+
"""
|
|
610
|
+
|
|
611
|
+
html_content = self._build_email_html(content, extra_styles)
|
|
612
|
+
|
|
613
|
+
text_content = f"""
|
|
614
|
+
Quick feedback on your karaoke experience?
|
|
615
|
+
|
|
616
|
+
Hi there!
|
|
617
|
+
|
|
618
|
+
Hope you enjoyed creating your karaoke video{' for ' + safe_job_title if safe_job_title else ''}!
|
|
619
|
+
|
|
620
|
+
As a beta tester, your feedback is super valuable to us.
|
|
621
|
+
|
|
622
|
+
Share your feedback (2-minute survey): {feedback_url}
|
|
623
|
+
|
|
624
|
+
We'd love to know:
|
|
625
|
+
- How easy was it to use?
|
|
626
|
+
- Were the lyrics accurate?
|
|
627
|
+
- How was the correction experience?
|
|
628
|
+
- What could we improve?
|
|
629
|
+
|
|
630
|
+
Thanks for being part of making Nomad Karaoke better!
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
© {self._get_year()} Nomad Karaoke
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
return self.provider.send_email(email, subject, html_content, text_content)
|
|
637
|
+
|
|
638
|
+
def _get_year(self) -> int:
|
|
639
|
+
"""Get current year for copyright notices."""
|
|
640
|
+
from datetime import datetime
|
|
641
|
+
return datetime.now().year
|
|
642
|
+
|
|
643
|
+
def _get_email_header(self, extra_header_content: str = "") -> str:
|
|
644
|
+
"""Get the standard email header with logo.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
extra_header_content: Optional extra content to add after the logo (e.g., beta badge)
|
|
648
|
+
"""
|
|
649
|
+
return f"""
|
|
650
|
+
<div class="header">
|
|
651
|
+
<a href="https://nomadkaraoke.com"><img src="{self.LOGO_URL}" alt="Nomad Karaoke" /></a>
|
|
652
|
+
{extra_header_content}
|
|
653
|
+
</div>
|
|
654
|
+
"""
|
|
655
|
+
|
|
656
|
+
def _get_email_footer(self) -> str:
|
|
657
|
+
"""Get the standard email footer with support message and signature."""
|
|
658
|
+
return f"""
|
|
659
|
+
<p style="margin-top: 30px; font-style: italic; color: #666;">If anything isn't perfect, just reply to this email and I'll fix it!</p>
|
|
660
|
+
|
|
661
|
+
<div class="signature">
|
|
662
|
+
{self._get_email_signature()}
|
|
663
|
+
</div>
|
|
664
|
+
"""
|
|
665
|
+
|
|
666
|
+
def _get_base_styles(self) -> str:
|
|
667
|
+
"""Get base CSS styles shared by all emails."""
|
|
668
|
+
return f"""
|
|
669
|
+
body {{
|
|
670
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
671
|
+
line-height: 1.6;
|
|
672
|
+
color: #333;
|
|
673
|
+
max-width: 600px;
|
|
674
|
+
margin: 0 auto;
|
|
675
|
+
padding: 20px;
|
|
676
|
+
}}
|
|
677
|
+
.header {{
|
|
678
|
+
text-align: center;
|
|
679
|
+
padding: 20px 0;
|
|
680
|
+
}}
|
|
681
|
+
.header img {{
|
|
682
|
+
max-width: 180px;
|
|
683
|
+
height: auto;
|
|
684
|
+
border-radius: 10px;
|
|
685
|
+
}}
|
|
686
|
+
.button {{
|
|
687
|
+
display: inline-block;
|
|
688
|
+
background-color: {self.BRAND_PRIMARY};
|
|
689
|
+
color: white;
|
|
690
|
+
padding: 14px 28px;
|
|
691
|
+
text-decoration: none;
|
|
692
|
+
border-radius: 8px;
|
|
693
|
+
font-weight: 600;
|
|
694
|
+
margin: 20px 0;
|
|
695
|
+
}}
|
|
696
|
+
.signature {{
|
|
697
|
+
margin-top: 30px;
|
|
698
|
+
padding-top: 20px;
|
|
699
|
+
}}
|
|
700
|
+
"""
|
|
701
|
+
|
|
702
|
+
def _build_email_html(self, content: str, extra_styles: str = "", extra_header_content: str = "") -> str:
|
|
703
|
+
"""Build a complete HTML email with standard header, footer, and styles.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
content: The main body content of the email
|
|
707
|
+
extra_styles: Additional CSS styles specific to this email type
|
|
708
|
+
extra_header_content: Optional extra content to add in header (e.g., beta badge)
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
Complete HTML email string
|
|
712
|
+
"""
|
|
713
|
+
return f"""
|
|
714
|
+
<!DOCTYPE html>
|
|
715
|
+
<html>
|
|
716
|
+
<head>
|
|
717
|
+
<style>
|
|
718
|
+
{self._get_base_styles()}
|
|
719
|
+
{extra_styles}
|
|
720
|
+
</style>
|
|
721
|
+
</head>
|
|
722
|
+
<body>
|
|
723
|
+
{self._get_email_header(extra_header_content)}
|
|
724
|
+
|
|
725
|
+
{content}
|
|
726
|
+
|
|
727
|
+
{self._get_email_footer()}
|
|
728
|
+
</body>
|
|
729
|
+
</html>
|
|
730
|
+
"""
|
|
731
|
+
|
|
732
|
+
def _get_email_signature(self) -> str:
|
|
733
|
+
"""Get the HTML email signature for Nomad Karaoke."""
|
|
734
|
+
return """
|
|
735
|
+
<table cellpadding="0" cellspacing="0" border="0" style="font-size: medium; font-family: Trebuchet MS;">
|
|
736
|
+
<tbody>
|
|
737
|
+
<tr>
|
|
738
|
+
<td>
|
|
739
|
+
<table cellpadding="0" cellspacing="0" border="0" style="font-size: medium; font-family: Trebuchet MS;">
|
|
740
|
+
<tbody>
|
|
741
|
+
<tr>
|
|
742
|
+
<td style="vertical-align: top;">
|
|
743
|
+
<table cellpadding="0" cellspacing="0" border="0"
|
|
744
|
+
style="font-size: medium; font-family: Trebuchet MS;">
|
|
745
|
+
<tbody>
|
|
746
|
+
<tr>
|
|
747
|
+
<td style="text-align: center;"><a
|
|
748
|
+
href="https://www.linkedin.com/in/andrewbeveridge"
|
|
749
|
+
target="_blank"><img
|
|
750
|
+
src="https://beveradb.github.io/public-images/andrew-buildspace-circle-150px.png"
|
|
751
|
+
role="presentation" style="display: block; max-width: 128px;"
|
|
752
|
+
width="130"></a></td>
|
|
753
|
+
</tr>
|
|
754
|
+
<tr>
|
|
755
|
+
<td height="5"></td>
|
|
756
|
+
</tr>
|
|
757
|
+
<tr>
|
|
758
|
+
<td style="text-align: center;"><a href="https://nomadkaraoke.com"
|
|
759
|
+
target="_blank"><img role="presentation" width="130"
|
|
760
|
+
style="display: block; max-width: 130px; border-radius: 7px;"
|
|
761
|
+
src="https://beveradb.github.io/public-images/Nomad-Karaoke-Logo-small-indexed-websafe-rectangle.gif"></a>
|
|
762
|
+
</td>
|
|
763
|
+
</tr>
|
|
764
|
+
</tbody>
|
|
765
|
+
</table>
|
|
766
|
+
</td>
|
|
767
|
+
<td width="10">
|
|
768
|
+
<div></div>
|
|
769
|
+
</td>
|
|
770
|
+
<td style="padding: 0px; vertical-align: middle;">
|
|
771
|
+
<h2 color="#000000"
|
|
772
|
+
style="margin: 0px; font-size: 18px; color: rgb(0, 0, 0); font-weight: 600;">
|
|
773
|
+
<span>Andrew</span><span> </span><span>Beveridge</span>
|
|
774
|
+
</h2>
|
|
775
|
+
<p color="#000000" font-size="medium"
|
|
776
|
+
style="margin: 0px; color: rgb(0, 0, 0); font-size: 14px; line-height: 22px;">
|
|
777
|
+
<span>Founder</span>
|
|
778
|
+
</p>
|
|
779
|
+
<p color="#000000" font-size="medium"
|
|
780
|
+
style="margin: 0px; font-weight: 500; color: rgb(0, 0, 0); font-size: 14px; line-height: 22px;">
|
|
781
|
+
<span>Nomad Karaoke
|
|
782
|
+
LLC</span>
|
|
783
|
+
</p>
|
|
784
|
+
<table cellpadding="0" cellspacing="0" border="0"
|
|
785
|
+
style="width: 100%; font-size: medium; font-family: Trebuchet MS; margin-top: 3px; margin-bottom: 3px;">
|
|
786
|
+
<tbody>
|
|
787
|
+
<tr>
|
|
788
|
+
<td color="#ff7acc" direction="horizontal" width="auto" height="15"
|
|
789
|
+
style="width: 100%; display: block; line-height:0; font-size:0;">
|
|
790
|
+
</td>
|
|
791
|
+
</tr>
|
|
792
|
+
<tr>
|
|
793
|
+
<td color="#ff7acc" direction="horizontal" width="auto" height="1"
|
|
794
|
+
style="width: 100%; border-bottom: 1px solid rgb(255, 122, 204); border-left: medium; display: block; line-height:0; font-size:0;">
|
|
795
|
+
</td>
|
|
796
|
+
</tr>
|
|
797
|
+
</tbody>
|
|
798
|
+
</table>
|
|
799
|
+
|
|
800
|
+
<table cellpadding="0" cellspacing="0" border="0"
|
|
801
|
+
style="width: 100%; font-size: medium; font-family: Trebuchet MS; margin-top: 3px; margin-bottom: 3px;">
|
|
802
|
+
<tbody>
|
|
803
|
+
<tr>
|
|
804
|
+
<td style="text-align: center; line-height:0; font-size:0;">
|
|
805
|
+
<table cellpadding="0" cellspacing="0" border="0"
|
|
806
|
+
style="display: inline-block; font-size: medium; font-family: Trebuchet MS;">
|
|
807
|
+
<tbody>
|
|
808
|
+
<tr style="text-align: center;">
|
|
809
|
+
<td><a href="https://www.youtube.com/@nomadkaraoke"
|
|
810
|
+
color="#230a89"
|
|
811
|
+
style="display: inline-block; padding: 0px; background-color: rgb(35, 10, 137);"
|
|
812
|
+
target="_blank"><img
|
|
813
|
+
src="https://beveradb.github.io/public-images/youtube-icon-2x.png"
|
|
814
|
+
alt="youtube" color="#230a89" width="24"
|
|
815
|
+
style="background-color: rgb(35, 10, 137); max-width: 135px; display: block;"></a>
|
|
816
|
+
</td>
|
|
817
|
+
<td width="5">
|
|
818
|
+
<div></div>
|
|
819
|
+
</td>
|
|
820
|
+
<td><a href="https://github.com/nomadkaraoke"
|
|
821
|
+
color="#230a89"
|
|
822
|
+
style="display: inline-block; padding: 0px; background-color: rgb(35, 10, 137);"
|
|
823
|
+
target="_blank"><img
|
|
824
|
+
src="https://beveradb.github.io/public-images/github-icon-2x.png"
|
|
825
|
+
alt="github" color="#230a89" width="24"
|
|
826
|
+
style="background-color: rgb(35, 10, 137); max-width: 135px; display: block;"></a>
|
|
827
|
+
</td>
|
|
828
|
+
<td width="5">
|
|
829
|
+
<div></div>
|
|
830
|
+
</td>
|
|
831
|
+
<td><a href="https://www.linkedin.com/in/andrewbeveridge"
|
|
832
|
+
color="#230a89"
|
|
833
|
+
style="display: inline-block; padding: 0px; background-color: rgb(35, 10, 137);"
|
|
834
|
+
target="_blank"><img
|
|
835
|
+
src="https://beveradb.github.io/public-images/linkedin-icon-2x.png"
|
|
836
|
+
alt="linkedin" color="#230a89" width="24"
|
|
837
|
+
style="background-color: rgb(35, 10, 137); max-width: 135px; display: block;"></a>
|
|
838
|
+
</td>
|
|
839
|
+
<td width="5">
|
|
840
|
+
<div></div>
|
|
841
|
+
</td>
|
|
842
|
+
<td><a href="https://twitter.com/beveradb" color="#230a89"
|
|
843
|
+
style="display: inline-block; padding: 0px; background-color: rgb(35, 10, 137);"><img
|
|
844
|
+
src="https://beveradb.github.io/public-images/twitter-icon-2x.png"
|
|
845
|
+
alt="twitter" color="#230a89" width="24"
|
|
846
|
+
style="background-color: rgb(35, 10, 137); max-width: 135px; display: block;"></a>
|
|
847
|
+
</td>
|
|
848
|
+
<td width="5">
|
|
849
|
+
<div></div>
|
|
850
|
+
</td>
|
|
851
|
+
<td><a href="https://www.instagram.com/beveradb/"
|
|
852
|
+
color="#230a89"
|
|
853
|
+
style="display: inline-block; padding: 0px; background-color: rgb(35, 10, 137);"><img
|
|
854
|
+
src="https://beveradb.github.io/public-images/instagram-icon-2x.png"
|
|
855
|
+
alt="instagram" color="#230a89" width="24"
|
|
856
|
+
style="background-color: rgb(35, 10, 137); max-width: 135px; display: block;"></a>
|
|
857
|
+
</td>
|
|
858
|
+
<td width="5">
|
|
859
|
+
<div></div>
|
|
860
|
+
</td>
|
|
861
|
+
</tr>
|
|
862
|
+
</tbody>
|
|
863
|
+
</table>
|
|
864
|
+
</td>
|
|
865
|
+
</tr>
|
|
866
|
+
</tbody>
|
|
867
|
+
</table>
|
|
868
|
+
<table cellpadding="0" cellspacing="0" border="0"
|
|
869
|
+
style="width: 100%; font-size: medium; font-family: Trebuchet MS; margin-top: 3px; margin-bottom: 3px;">
|
|
870
|
+
<tbody>
|
|
871
|
+
<tr>
|
|
872
|
+
<td color="#ff7acc" direction="horizontal" width="auto" height="1"
|
|
873
|
+
style="width: 100%; border-bottom: 1px solid rgb(255, 122, 204); border-left: medium; display: block; line-height:0; font-size:0;">
|
|
874
|
+
</td>
|
|
875
|
+
</tr>
|
|
876
|
+
<tr>
|
|
877
|
+
<td color="#ff7acc" direction="horizontal" width="auto" height="15"
|
|
878
|
+
style="width: 100%; display: block;">
|
|
879
|
+
</td>
|
|
880
|
+
</tr>
|
|
881
|
+
</tbody>
|
|
882
|
+
</table>
|
|
883
|
+
<table cellpadding="0" cellspacing="0" border="0"
|
|
884
|
+
style="font-size: medium; font-family: Trebuchet MS;">
|
|
885
|
+
<tbody>
|
|
886
|
+
<tr style="vertical-align: middle;" height="20">
|
|
887
|
+
<td width="30" style="vertical-align: middle;">
|
|
888
|
+
<table cellpadding="0" cellspacing="0" border="0"
|
|
889
|
+
style="font-size: medium; font-family: Trebuchet MS;">
|
|
890
|
+
<tbody>
|
|
891
|
+
<tr>
|
|
892
|
+
<td style="vertical-align: bottom;"><span color="#ff7acc"
|
|
893
|
+
width="11"
|
|
894
|
+
style="display: inline-block; background-color: rgb(255, 122, 204);"><img
|
|
895
|
+
src="https://beveradb.github.io/public-images/phone-icon-2x.png"
|
|
896
|
+
color="#ff7acc" alt="mobilePhone" width="13"
|
|
897
|
+
style="display: block; background-color: rgb(255, 122, 204);"></span>
|
|
898
|
+
</td>
|
|
899
|
+
</tr>
|
|
900
|
+
</tbody>
|
|
901
|
+
</table>
|
|
902
|
+
</td>
|
|
903
|
+
<td style="padding: 0px; color: rgb(0, 0, 0);"><a href="tel:8036363267"
|
|
904
|
+
color="#000000"
|
|
905
|
+
style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>+1 (803) 636-3267</span></a> | <a href="tel:07835171222"
|
|
906
|
+
color="#000000"
|
|
907
|
+
style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>+44 07835171222</span></a></td>
|
|
908
|
+
</tr>
|
|
909
|
+
<tr style="vertical-align: middle;" height="20">
|
|
910
|
+
<td width="30" style="vertical-align: middle;">
|
|
911
|
+
<table cellpadding="0" cellspacing="0" border="0"
|
|
912
|
+
style="font-size: medium; font-family: Trebuchet MS;">
|
|
913
|
+
<tbody>
|
|
914
|
+
<tr>
|
|
915
|
+
<td style="vertical-align: bottom;"><span color="#ff7acc"
|
|
916
|
+
width="11"
|
|
917
|
+
style="display: inline-block; background-color: rgb(255, 122, 204);"><img
|
|
918
|
+
src="https://beveradb.github.io/public-images/email-icon-2x.png"
|
|
919
|
+
color="#ff7acc" alt="emailAddress" width="13"
|
|
920
|
+
style="display: block; background-color: rgb(255, 122, 204);"></span>
|
|
921
|
+
</td>
|
|
922
|
+
</tr>
|
|
923
|
+
</tbody>
|
|
924
|
+
</table>
|
|
925
|
+
</td>
|
|
926
|
+
<td style="padding: 0px;"><a href="mailto:andrew@nomadkaraoke.com"
|
|
927
|
+
color="#000000"
|
|
928
|
+
style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>andrew@nomadkaraoke.com</span></a>
|
|
929
|
+
</td>
|
|
930
|
+
</tr>
|
|
931
|
+
<tr style="vertical-align: middle;" height="20">
|
|
932
|
+
<td width="30" style="vertical-align: middle;">
|
|
933
|
+
<table cellpadding="0" cellspacing="0" border="0"
|
|
934
|
+
style="font-size: medium; font-family: Trebuchet MS;">
|
|
935
|
+
<tbody>
|
|
936
|
+
<tr>
|
|
937
|
+
<td style="vertical-align: bottom;"><span color="#ff7acc"
|
|
938
|
+
width="11"
|
|
939
|
+
style="display: inline-block; background-color: rgb(255, 122, 204);"><img
|
|
940
|
+
src="https://beveradb.github.io/public-images/link-icon-2x.png"
|
|
941
|
+
color="#ff7acc" alt="website" width="13"
|
|
942
|
+
style="display: block; background-color: rgb(255, 122, 204);"></span>
|
|
943
|
+
</td>
|
|
944
|
+
</tr>
|
|
945
|
+
</tbody>
|
|
946
|
+
</table>
|
|
947
|
+
</td>
|
|
948
|
+
<td style="padding: 0px;"><a href="https://nomadkaraoke.com" target="_blank"
|
|
949
|
+
color="#000000"
|
|
950
|
+
style="text-decoration: none; color: rgb(0, 0, 0); font-size: 12px;"><span>nomadkaraoke.com</span></a>
|
|
951
|
+
</td>
|
|
952
|
+
</tr>
|
|
953
|
+
</tbody>
|
|
954
|
+
</table>
|
|
955
|
+
</td>
|
|
956
|
+
</tr>
|
|
957
|
+
</tbody>
|
|
958
|
+
</table>
|
|
959
|
+
</td>
|
|
960
|
+
</tr>
|
|
961
|
+
</tbody>
|
|
962
|
+
</table>
|
|
963
|
+
"""
|
|
964
|
+
|
|
965
|
+
def send_job_completion(
|
|
966
|
+
self,
|
|
967
|
+
to_email: str,
|
|
968
|
+
message_content: str,
|
|
969
|
+
artist: Optional[str] = None,
|
|
970
|
+
title: Optional[str] = None,
|
|
971
|
+
brand_code: Optional[str] = None,
|
|
972
|
+
cc_admin: bool = True,
|
|
973
|
+
) -> bool:
|
|
974
|
+
"""
|
|
975
|
+
Send job completion email with the rendered template content.
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
to_email: User's email address
|
|
979
|
+
message_content: Pre-rendered message content (plain text)
|
|
980
|
+
artist: Artist name for subject line
|
|
981
|
+
title: Song title for subject line
|
|
982
|
+
brand_code: Release ID (e.g., "NOMAD-1178") for subject line
|
|
983
|
+
cc_admin: Whether to CC gen@nomadkaraoke.com
|
|
984
|
+
|
|
985
|
+
Returns:
|
|
986
|
+
True if email was sent successfully
|
|
987
|
+
"""
|
|
988
|
+
# Build subject: "NOMAD-1178: Artist - Title (Your karaoke video is ready!)"
|
|
989
|
+
# Sanitize artist/title to handle Unicode characters (curly quotes, em dashes, etc.)
|
|
990
|
+
# that cause email header encoding issues (MIME headers use latin-1)
|
|
991
|
+
safe_artist = sanitize_filename(artist) if artist else None
|
|
992
|
+
safe_title = sanitize_filename(title) if title else None
|
|
993
|
+
if brand_code and safe_artist and safe_title:
|
|
994
|
+
subject = f"{brand_code}: {safe_artist} - {safe_title} (Your karaoke video is ready!)"
|
|
995
|
+
elif safe_artist and safe_title:
|
|
996
|
+
subject = f"{safe_artist} - {safe_title} (Your karaoke video is ready!)"
|
|
997
|
+
else:
|
|
998
|
+
subject = "Your karaoke video is ready!"
|
|
999
|
+
|
|
1000
|
+
extra_styles = """
|
|
1001
|
+
.content {
|
|
1002
|
+
white-space: pre-wrap;
|
|
1003
|
+
}
|
|
1004
|
+
"""
|
|
1005
|
+
|
|
1006
|
+
content = f"""
|
|
1007
|
+
<div class="content">{html.escape(message_content)}</div>
|
|
1008
|
+
"""
|
|
1009
|
+
|
|
1010
|
+
html_content = self._build_email_html(content, extra_styles)
|
|
1011
|
+
|
|
1012
|
+
cc_emails = ["gen@nomadkaraoke.com"] if cc_admin else None
|
|
1013
|
+
|
|
1014
|
+
return self.provider.send_email(
|
|
1015
|
+
to_email=to_email,
|
|
1016
|
+
subject=subject,
|
|
1017
|
+
html_content=html_content,
|
|
1018
|
+
text_content=message_content,
|
|
1019
|
+
cc_emails=cc_emails,
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
def send_action_reminder(
|
|
1023
|
+
self,
|
|
1024
|
+
to_email: str,
|
|
1025
|
+
message_content: str,
|
|
1026
|
+
action_type: str,
|
|
1027
|
+
artist: Optional[str] = None,
|
|
1028
|
+
title: Optional[str] = None,
|
|
1029
|
+
) -> bool:
|
|
1030
|
+
"""
|
|
1031
|
+
Send action-needed reminder email.
|
|
1032
|
+
|
|
1033
|
+
Args:
|
|
1034
|
+
to_email: User's email address
|
|
1035
|
+
message_content: Pre-rendered message content (plain text)
|
|
1036
|
+
action_type: Type of action needed ("lyrics" or "instrumental")
|
|
1037
|
+
artist: Artist name for subject line
|
|
1038
|
+
title: Song title for subject line
|
|
1039
|
+
|
|
1040
|
+
Returns:
|
|
1041
|
+
True if email was sent successfully
|
|
1042
|
+
"""
|
|
1043
|
+
# Build subject based on action type
|
|
1044
|
+
song_info = f" for {artist} - {title}" if artist and title else ""
|
|
1045
|
+
if action_type == "lyrics":
|
|
1046
|
+
subject = f"Action needed: Review lyrics{song_info}"
|
|
1047
|
+
elif action_type == "instrumental":
|
|
1048
|
+
subject = f"Action needed: Select instrumental{song_info}"
|
|
1049
|
+
else:
|
|
1050
|
+
subject = f"Action needed{song_info}"
|
|
1051
|
+
|
|
1052
|
+
extra_styles = """
|
|
1053
|
+
.alert {
|
|
1054
|
+
background-color: #fef3c7;
|
|
1055
|
+
border: 1px solid #fcd34d;
|
|
1056
|
+
border-radius: 8px;
|
|
1057
|
+
padding: 16px;
|
|
1058
|
+
margin: 20px 0;
|
|
1059
|
+
text-align: center;
|
|
1060
|
+
}
|
|
1061
|
+
.content {
|
|
1062
|
+
white-space: pre-wrap;
|
|
1063
|
+
}
|
|
1064
|
+
"""
|
|
1065
|
+
|
|
1066
|
+
content = f"""
|
|
1067
|
+
<div class="alert">
|
|
1068
|
+
⏰ Your karaoke video needs input from you!
|
|
1069
|
+
</div>
|
|
1070
|
+
|
|
1071
|
+
<div class="content">{html.escape(message_content)}</div>
|
|
1072
|
+
"""
|
|
1073
|
+
|
|
1074
|
+
html_content = self._build_email_html(content, extra_styles)
|
|
1075
|
+
|
|
1076
|
+
return self.provider.send_email(
|
|
1077
|
+
to_email=to_email,
|
|
1078
|
+
subject=subject,
|
|
1079
|
+
html_content=html_content,
|
|
1080
|
+
text_content=message_content,
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
# Global instance
|
|
1085
|
+
_email_service = None
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def get_email_service() -> EmailService:
|
|
1089
|
+
"""Get the global email service instance."""
|
|
1090
|
+
global _email_service
|
|
1091
|
+
if _email_service is None:
|
|
1092
|
+
_email_service = EmailService()
|
|
1093
|
+
return _email_service
|