feat: on-demand karaoke processing — button next to random triggers vocal isolation
Karaoke button (🎤) now always visible for audio/video products when the
shop has unsandbox API keys configured. Clicking it triggers on-demand
processing via POST /karaoke/{product_id} if tracks don't exist yet.
Server forks a detached child process (survives uWSGI recycling) to run
process_karaoke, then the existing 10s watch_json refresh picks up the
new URLs when processing completes. Button shows hourglass during
processing and auto-switches to instrumentals on completion.
Changes:
- New route + view: POST /karaoke/{product_id} (on-demand processing)
- watch_json + content.py: add karaoke_eligible flag
- content.j2: data-karaoke-eligible attribute on media container
- watch.js: show button when eligible, trigger processing, detect
completion via URL refresh, auto-switch to instrumentals
- related_content.j2: mic emoji button, JS controls visibility
- CSS: disabled state for processing button
- 8 new functional tests covering eligibility, processing, edge cases
This commit is contained in:
parent
924921e232
commit
4a12722b4d
8 changed files with 392 additions and 9 deletions
|
|
@ -188,6 +188,7 @@ def includeme(config):
|
|||
config.add_route("player", "/player/{product_id}")
|
||||
config.add_route("player_json", "/player/{product_id}/json")
|
||||
config.add_route("watch_json", "/watch/{product_id}/json")
|
||||
config.add_route("karaoke_process", "/karaoke/{product_id}")
|
||||
config.add_route("random", "/random")
|
||||
config.add_route("tv", "/tv")
|
||||
|
||||
|
|
|
|||
|
|
@ -3045,6 +3045,10 @@ textarea {
|
|||
color: white;
|
||||
border-color: var(--blue-color, #5871ad);
|
||||
}
|
||||
.ring-karaoke-btn:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.related-content-row-current {
|
||||
background-color: var(--blue-color, #5871ad);
|
||||
color: white;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@
|
|||
var karaokeMode = 0; // 0=original, 1=instrumentals, 2=vocals
|
||||
var KARAOKE_LABELS = ['Original', 'Instrumentals', 'Vocals'];
|
||||
var karaokeUrls = {original: null, instrumentals: null, vocals: null};
|
||||
var karaokeEligible = false;
|
||||
var karaokeProcessing = false;
|
||||
|
||||
// --- Ring state (persisted in localStorage) ---
|
||||
var ringProductIds = [];
|
||||
|
|
@ -240,6 +242,8 @@
|
|||
karaokeUrls.instrumentals = container ? container.getAttribute('data-instrumentals-url') : null;
|
||||
karaokeUrls.vocals = container ? container.getAttribute('data-vocals-url') : null;
|
||||
karaokeUrls.original = activeMedia ? activeMedia.src : null;
|
||||
karaokeEligible = container ? container.getAttribute('data-karaoke-eligible') === '1' : false;
|
||||
karaokeProcessing = false;
|
||||
karaokeMode = 0;
|
||||
updateKaraokeButton();
|
||||
}
|
||||
|
|
@ -248,12 +252,29 @@
|
|||
var btn = document.getElementById('karaoke-toggle-btn');
|
||||
if (!btn) return;
|
||||
var hasKaraoke = !!(karaokeUrls.instrumentals || karaokeUrls.vocals);
|
||||
btn.style.display = hasKaraoke ? '' : 'none';
|
||||
btn.textContent = KARAOKE_LABELS[karaokeMode];
|
||||
if (karaokeMode === 0) {
|
||||
btn.classList.remove('active');
|
||||
} else {
|
||||
// Show button when tracks exist OR when eligible for on-demand processing
|
||||
btn.style.display = (hasKaraoke || karaokeEligible) ? '' : 'none';
|
||||
|
||||
if (karaokeProcessing) {
|
||||
btn.textContent = '\u231B'; // hourglass
|
||||
btn.title = 'Processing karaoke tracks\u2026';
|
||||
btn.classList.add('active');
|
||||
btn.disabled = true;
|
||||
} else if (hasKaraoke) {
|
||||
btn.textContent = KARAOKE_LABELS[karaokeMode];
|
||||
btn.title = 'Cycle: Original / Instrumentals / Vocals';
|
||||
btn.disabled = false;
|
||||
if (karaokeMode === 0) {
|
||||
btn.classList.remove('active');
|
||||
} else {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
} else {
|
||||
// Eligible but no tracks yet — show mic icon
|
||||
btn.textContent = '\uD83C\uDFA4'; // 🎤
|
||||
btn.title = 'Generate karaoke tracks';
|
||||
btn.classList.remove('active');
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -267,9 +288,45 @@
|
|||
return modes[(idx + 1) % modes.length];
|
||||
}
|
||||
|
||||
function triggerKaraokeProcessing() {
|
||||
if (karaokeProcessing || !currentProductId) return;
|
||||
karaokeProcessing = true;
|
||||
updateKaraokeButton();
|
||||
|
||||
fetch('/karaoke/' + currentProductId, {method: 'POST'})
|
||||
.then(function(resp) { return resp.json(); })
|
||||
.then(function(data) {
|
||||
if (data.status === 'ready') {
|
||||
// Tracks already exist — refresh to get URLs
|
||||
karaokeProcessing = false;
|
||||
fetchWatchData(currentProductId).then(function(fresh) {
|
||||
if (fresh && fresh.instrumentals_url) {
|
||||
karaokeUrls.instrumentals = fresh.instrumentals_url;
|
||||
karaokeUrls.vocals = fresh.vocals_url;
|
||||
karaokeUrls.original = fresh.media_url;
|
||||
updateKaraokeButton();
|
||||
// Auto-switch to instrumentals
|
||||
cycleKaraoke();
|
||||
}
|
||||
});
|
||||
}
|
||||
// status === 'processing' — polling via URL refresh will pick it up
|
||||
})
|
||||
.catch(function() {
|
||||
karaokeProcessing = false;
|
||||
updateKaraokeButton();
|
||||
});
|
||||
}
|
||||
|
||||
function cycleKaraoke() {
|
||||
if (!activeMedia) return;
|
||||
var hasKaraoke = !!(karaokeUrls.instrumentals || karaokeUrls.vocals);
|
||||
|
||||
// No tracks yet but eligible — trigger on-demand processing
|
||||
if (!hasKaraoke && karaokeEligible) {
|
||||
triggerKaraokeProcessing();
|
||||
return;
|
||||
}
|
||||
if (!hasKaraoke) return;
|
||||
|
||||
var nextMode = getNextKaraokeMode();
|
||||
|
|
@ -678,6 +735,8 @@
|
|||
|
||||
// Reset karaoke state for the new product
|
||||
karaokeMode = 0;
|
||||
karaokeProcessing = false;
|
||||
karaokeEligible = !!data.karaoke_eligible;
|
||||
karaokeUrls.original = data.media_url || null;
|
||||
karaokeUrls.instrumentals = data.instrumentals_url || null;
|
||||
karaokeUrls.vocals = data.vocals_url || null;
|
||||
|
|
@ -903,7 +962,7 @@
|
|||
+ '<input type="checkbox" id="watch-autoplay-toggle"' + autoplayChecked + ' />'
|
||||
+ '<span class="autoplay-slider"></span>'
|
||||
+ '</label>'
|
||||
+ '<button id="karaoke-toggle-btn" class="ring-karaoke-btn mps-button js-only" style="display:' + (karaokeUrls.instrumentals || karaokeUrls.vocals ? '' : 'none') + '" title="Cycle: Original / Instrumentals / Vocals">' + KARAOKE_LABELS[karaokeMode] + '</button>'
|
||||
+ '<button id="karaoke-toggle-btn" class="ring-karaoke-btn mps-button js-only" style="display:' + (karaokeUrls.instrumentals || karaokeUrls.vocals || karaokeEligible ? '' : 'none') + '" title="Karaoke"></button>'
|
||||
+ '<button id="skip-random-btn" class="skip-next-btn mps-button js-only" title="Jump to random item">🎲</button>'
|
||||
+ '<button id="skip-next-btn" class="skip-next-btn mps-button js-only" title="Skip to next unwatched">Next ▶</button>'
|
||||
+ '</div>'
|
||||
|
|
@ -1567,9 +1626,19 @@
|
|||
if (data.product_id !== currentProductId) { scheduleUrlRefresh(); return; }
|
||||
|
||||
// Refresh karaoke URLs from the fresh response
|
||||
var hadKaraoke = !!(karaokeUrls.instrumentals || karaokeUrls.vocals);
|
||||
karaokeUrls.original = data.media_url;
|
||||
if (data.instrumentals_url) karaokeUrls.instrumentals = data.instrumentals_url;
|
||||
if (data.vocals_url) karaokeUrls.vocals = data.vocals_url;
|
||||
if (data.karaoke_eligible !== undefined) karaokeEligible = !!data.karaoke_eligible;
|
||||
|
||||
// Processing just completed — tracks appeared
|
||||
if (!hadKaraoke && karaokeProcessing && (data.instrumentals_url || data.vocals_url)) {
|
||||
karaokeProcessing = false;
|
||||
updateKaraokeButton();
|
||||
// Auto-switch to instrumentals
|
||||
cycleKaraoke();
|
||||
}
|
||||
|
||||
var wasPlaying = !activeMedia.paused;
|
||||
var savedTime = activeMedia.currentTime;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
{% if request.shop.watch_mode_enabled and product.extensions.get("product") in video_extensions %}
|
||||
{# Watch mode: direct video render with autoplay #}
|
||||
{% set watch_video_url = request.shop_cdn_endpoint ~ "/" ~ product.s3_path ~ "/product" %}
|
||||
<div id="watch-media-container"{% if instrumentals_url %} data-instrumentals-url="{{ instrumentals_url }}"{% endif %}{% if vocals_url %} data-vocals-url="{{ vocals_url }}"{% endif %}>
|
||||
<div id="watch-media-container"{% if instrumentals_url %} data-instrumentals-url="{{ instrumentals_url }}"{% endif %}{% if vocals_url %} data-vocals-url="{{ vocals_url }}"{% endif %}{% if karaoke_eligible %} data-karaoke-eligible="1"{% endif %}>
|
||||
<div class="watch-video-container">
|
||||
<video id="watch-video" src="{{ watch_video_url }}" autoplay controls playsinline class="product-main"></video>
|
||||
<button class="unmute-overlay">Tap to unmute</button>
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
{% elif request.shop.watch_mode_enabled and product.extensions.get("product") in audio_extensions %}
|
||||
{# Watch mode: audio with album art #}
|
||||
{% set watch_audio_url = request.shop_cdn_endpoint ~ "/" ~ product.s3_path ~ "/product" %}
|
||||
<div id="watch-media-container"{% if instrumentals_url %} data-instrumentals-url="{{ instrumentals_url }}"{% endif %}{% if vocals_url %} data-vocals-url="{{ vocals_url }}"{% endif %}>
|
||||
<div id="watch-media-container"{% if instrumentals_url %} data-instrumentals-url="{{ instrumentals_url }}"{% endif %}{% if vocals_url %} data-vocals-url="{{ vocals_url }}"{% endif %}{% if karaoke_eligible %} data-karaoke-eligible="1"{% endif %}>
|
||||
<div class="watch-audio-container">
|
||||
{% if "thumbnail1" in product.extensions %}
|
||||
<img src="{{ request.shop_cdn_endpoint }}/{{ product.s3_path }}/thumbnail1?ts={{ product.updated_timestamp }}" class="product-main audio-cover" />
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
<input type="checkbox" id="watch-autoplay-toggle" checked />
|
||||
<span class="autoplay-slider"></span>
|
||||
</label>
|
||||
<button id="karaoke-toggle-btn" class="ring-karaoke-btn mps-button js-only" style="display:none" title="Cycle: Original / Instrumentals / Vocals">Original</button>
|
||||
<button id="karaoke-toggle-btn" class="ring-karaoke-btn mps-button js-only" style="display:none" title="Karaoke: play instrumental version">🎤</button>
|
||||
<button id="skip-random-btn" class="skip-next-btn mps-button js-only" title="Jump to random item">🎲</button>
|
||||
<button id="skip-next-btn" class="skip-next-btn mps-button js-only" title="Skip to next unwatched">Next ▶</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3148,6 +3148,122 @@ class AuthenticatedFunctionalTests(_AuthenticatedBase):
|
|||
self.assertNotIn("data-instrumentals-url", body)
|
||||
self.assertNotIn("data-vocals-url", body)
|
||||
|
||||
def test_watch_json_karaoke_eligible_with_keys(self):
|
||||
"""Watch JSON returns karaoke_eligible=True when shop has unsandbox keys."""
|
||||
product_id, _ = self._create_content_with_metadata(
|
||||
"karaoke-elig-shop", "Eligible Song", "Has keys",
|
||||
{"extensions": {"product": "mp3"}, "file_bytes": {"product": 5000000}},
|
||||
)
|
||||
|
||||
# Set unsandbox keys on the shop
|
||||
from ..models.product import Product
|
||||
product = self.dbsession.query(Product).get(product_id)
|
||||
product.shop.unsandbox_public_key = "pk_test_123"
|
||||
product.shop.unsandbox_secret_key = "sk_test_456"
|
||||
self.dbsession.flush()
|
||||
transaction.commit()
|
||||
|
||||
res = self.testapp.get(f"/watch/{product_id}/json", status=200)
|
||||
data = res.json
|
||||
self.assertTrue(data["karaoke_eligible"])
|
||||
# No tracks yet — URLs should be null
|
||||
self.assertIsNone(data["instrumentals_url"])
|
||||
self.assertIsNone(data["vocals_url"])
|
||||
|
||||
def test_watch_json_karaoke_not_eligible_without_keys(self):
|
||||
"""Watch JSON returns karaoke_eligible=False when shop has no unsandbox keys."""
|
||||
product_id, _ = self._create_content_with_metadata(
|
||||
"karaoke-noelig-shop", "No Keys Song", "No unsandbox",
|
||||
{"extensions": {"product": "mp3"}, "file_bytes": {"product": 5000000}},
|
||||
)
|
||||
|
||||
res = self.testapp.get(f"/watch/{product_id}/json", status=200)
|
||||
data = res.json
|
||||
self.assertFalse(data["karaoke_eligible"])
|
||||
|
||||
def test_content_page_karaoke_eligible_attr(self):
|
||||
"""Content page embeds data-karaoke-eligible when shop has unsandbox keys."""
|
||||
product_id, product_slug = self._create_content_with_metadata(
|
||||
"karaoke-elig-content-shop", "Eligible Content", "Has unsandbox",
|
||||
{"extensions": {"product": "mp3"}, "file_bytes": {"product": 5000000}},
|
||||
)
|
||||
|
||||
from ..models.product import Product
|
||||
product = self.dbsession.query(Product).get(product_id)
|
||||
product.shop.unsandbox_public_key = "pk_test_123"
|
||||
product.shop.unsandbox_secret_key = "sk_test_456"
|
||||
self.dbsession.flush()
|
||||
transaction.commit()
|
||||
|
||||
res = self.testapp.get(f"/c/{product_id}/{product_slug}", status=200)
|
||||
body = res.body.decode()
|
||||
self.assertIn('data-karaoke-eligible="1"', body)
|
||||
|
||||
def test_content_page_no_karaoke_eligible_without_keys(self):
|
||||
"""Content page omits karaoke-eligible attr when no unsandbox keys."""
|
||||
product_id, product_slug = self._create_content_with_metadata(
|
||||
"karaoke-noelig-content-shop", "No Keys Content", "No unsandbox",
|
||||
{"extensions": {"product": "mp3"}, "file_bytes": {"product": 5000000}},
|
||||
)
|
||||
|
||||
res = self.testapp.get(f"/c/{product_id}/{product_slug}", status=200)
|
||||
body = res.body.decode()
|
||||
self.assertNotIn("data-karaoke-eligible", body)
|
||||
|
||||
def test_karaoke_process_not_found(self):
|
||||
"""POST /karaoke/{bad_id} returns 404."""
|
||||
res = self.testapp.post("/karaoke/nonexistent-id", expect_errors=True)
|
||||
self.assertEqual(res.status_int, 404)
|
||||
|
||||
def test_karaoke_process_no_keys(self):
|
||||
"""POST /karaoke/{id} returns 400 when shop has no unsandbox keys."""
|
||||
product_id, _ = self._create_content_with_metadata(
|
||||
"karaoke-proc-nokeys-shop", "No Keys Proc", "No unsandbox",
|
||||
{"extensions": {"product": "mp3"}, "file_bytes": {"product": 5000000}},
|
||||
)
|
||||
|
||||
res = self.testapp.post(f"/karaoke/{product_id}", expect_errors=True)
|
||||
self.assertEqual(res.status_int, 400)
|
||||
self.assertIn("not configured", res.json["error"])
|
||||
|
||||
def test_karaoke_process_already_has_tracks(self):
|
||||
"""POST /karaoke/{id} returns ready when tracks already exist."""
|
||||
product_id, _ = self._create_content_with_metadata(
|
||||
"karaoke-proc-ready-shop", "Already Done", "Has tracks",
|
||||
{
|
||||
"extensions": {"product": "mp3", "instrumentals": "wav", "vocals": "wav"},
|
||||
"file_bytes": {"product": 5000000, "instrumentals": 8000000, "vocals": 8000000},
|
||||
},
|
||||
)
|
||||
|
||||
from ..models.product import Product
|
||||
product = self.dbsession.query(Product).get(product_id)
|
||||
product.shop.unsandbox_public_key = "pk_test_123"
|
||||
product.shop.unsandbox_secret_key = "sk_test_456"
|
||||
self.dbsession.flush()
|
||||
transaction.commit()
|
||||
|
||||
res = self.testapp.post(f"/karaoke/{product_id}", status=200)
|
||||
self.assertEqual(res.json["status"], "ready")
|
||||
|
||||
def test_karaoke_process_image_not_eligible(self):
|
||||
"""POST /karaoke/{id} returns 400 for image products."""
|
||||
product_id, _ = self._create_content_with_metadata(
|
||||
"karaoke-proc-img-shop", "An Image", "Not audio",
|
||||
{"extensions": {"product": "jpg"}, "file_bytes": {"product": 500000}},
|
||||
)
|
||||
|
||||
from ..models.product import Product
|
||||
product = self.dbsession.query(Product).get(product_id)
|
||||
product.shop.unsandbox_public_key = "pk_test_123"
|
||||
product.shop.unsandbox_secret_key = "sk_test_456"
|
||||
self.dbsession.flush()
|
||||
transaction.commit()
|
||||
|
||||
res = self.testapp.post(f"/karaoke/{product_id}", expect_errors=True)
|
||||
self.assertEqual(res.status_int, 400)
|
||||
self.assertIn("Not audio or video", res.json["error"])
|
||||
|
||||
def test_rss_autodiscovery_links(self):
|
||||
"""Test that RSS and Atom autodiscovery links are present in shop pages."""
|
||||
shop = self._create_shop_helper(
|
||||
|
|
|
|||
|
|
@ -85,9 +85,12 @@ def content(request):
|
|||
# Generate CDN URLs for karaoke tracks if available
|
||||
instrumentals_url = None
|
||||
vocals_url = None
|
||||
karaoke_eligible = False
|
||||
extension = product.extensions.get("product")
|
||||
media_type = get_media_type(extension) if extension else None
|
||||
if media_type in ("video", "audio"):
|
||||
shop = product.shop
|
||||
karaoke_eligible = bool(shop.unsandbox_public_key and shop.unsandbox_secret_key)
|
||||
cdn_base = request.shop_cdn_endpoint
|
||||
for track_name in ("instrumentals", "vocals"):
|
||||
if track_name in product.extensions:
|
||||
|
|
@ -106,4 +109,5 @@ def content(request):
|
|||
"related_products": related_products,
|
||||
"instrumentals_url": instrumentals_url,
|
||||
"vocals_url": vocals_url,
|
||||
"karaoke_eligible": karaoke_eligible,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import fcntl
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pyramid.view import view_config
|
||||
|
||||
from ..models.product import get_media_type, get_related_products, get_ring_related_products
|
||||
|
|
@ -5,6 +9,8 @@ from ..models.shop import get_shop_by_id
|
|||
from ..lib.currency import cents_to_dollars
|
||||
from ..lib.time_funcs import timestamp_to_ago_string, timestamp_to_datetime
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@view_config(route_name="watch_json", renderer="json")
|
||||
def watch_json(request):
|
||||
|
|
@ -68,7 +74,10 @@ def watch_json(request):
|
|||
# Generate presigned URLs for karaoke tracks if available
|
||||
instrumentals_url = None
|
||||
vocals_url = None
|
||||
karaoke_eligible = False
|
||||
if media_type in ("video", "audio"):
|
||||
shop = request.shop
|
||||
karaoke_eligible = bool(shop.unsandbox_public_key and shop.unsandbox_secret_key)
|
||||
for track_name in ("instrumentals", "vocals"):
|
||||
if track_name in product.extensions:
|
||||
track_ext = product.extensions.get(track_name)
|
||||
|
|
@ -223,6 +232,7 @@ def watch_json(request):
|
|||
"comments_enabled": request.shop.comments_enabled,
|
||||
"instrumentals_url": instrumentals_url,
|
||||
"vocals_url": vocals_url,
|
||||
"karaoke_eligible": karaoke_eligible,
|
||||
}
|
||||
|
||||
if price_history:
|
||||
|
|
@ -231,6 +241,185 @@ def watch_json(request):
|
|||
return result
|
||||
|
||||
|
||||
@view_config(route_name="karaoke_process", renderer="json", request_method="POST")
|
||||
def karaoke_process(request):
|
||||
"""On-demand karaoke processing for a single product.
|
||||
|
||||
Forks a detached child process (survives uWSGI recycling) that runs
|
||||
process_karaoke and updates the DB. Returns immediately with status.
|
||||
The watch_json 10s refresh loop picks up the new URLs when done.
|
||||
"""
|
||||
product_id = request.matchdict.get("product_id")
|
||||
from ..models.product import Product
|
||||
try:
|
||||
product = request.dbsession.query(Product).get(product_id)
|
||||
except Exception:
|
||||
product = None
|
||||
|
||||
if not product:
|
||||
request.response.status_int = 404
|
||||
return {"error": "Product not found"}
|
||||
|
||||
shop = product.shop
|
||||
|
||||
if not shop.unsandbox_public_key or not shop.unsandbox_secret_key:
|
||||
request.response.status_int = 400
|
||||
return {"error": "Karaoke not configured for this shop"}
|
||||
|
||||
# Determine which file to process
|
||||
if product.is_sellable:
|
||||
file_key = "preview"
|
||||
else:
|
||||
file_key = "product"
|
||||
|
||||
extension = product.extensions.get(file_key)
|
||||
if not extension:
|
||||
request.response.status_int = 400
|
||||
return {"error": "No media file"}
|
||||
|
||||
media_type = get_media_type(extension)
|
||||
if media_type not in ("video", "audio"):
|
||||
request.response.status_int = 400
|
||||
return {"error": "Not audio or video"}
|
||||
|
||||
# Already has tracks?
|
||||
if "instrumentals" in product.extensions and "vocals" in product.extensions:
|
||||
return {"status": "ready"}
|
||||
|
||||
# One-at-a-time guard per product
|
||||
lockfile = f"/tmp/karaoke_{product_id}.lock"
|
||||
try:
|
||||
check_fd = open(lockfile, "w")
|
||||
fcntl.flock(check_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
fcntl.flock(check_fd, fcntl.LOCK_UN)
|
||||
check_fd.close()
|
||||
except (IOError, OSError):
|
||||
return {"status": "processing"}
|
||||
|
||||
# Capture values before fork
|
||||
db_url = str(request.dbsession.get_bind().url)
|
||||
s3_path = product.s3_path
|
||||
s3_key = f"{s3_path}/{file_key}"
|
||||
is_video = media_type == "video"
|
||||
pk = shop.unsandbox_public_key
|
||||
sk = shop.unsandbox_secret_key
|
||||
app_settings = request.registry.settings
|
||||
has_mirror = shop.has_s3_mirror
|
||||
shop_id = shop.id
|
||||
|
||||
# BYOB credentials
|
||||
if shop.has_primary_s3:
|
||||
s3_region = shop.primary_s3_region
|
||||
s3_endpoint = shop.primary_s3_endpoint
|
||||
s3_access = shop.primary_s3_access_key
|
||||
s3_secret = shop.primary_s3_secret_key
|
||||
bucket = shop.primary_s3_bucket
|
||||
else:
|
||||
s3_region = app_settings["bucket.secure_uploads.region"]
|
||||
s3_endpoint = app_settings["bucket.secure_uploads.post_endpoint"]
|
||||
s3_access = app_settings["bucket.secure_uploads.access_key"]
|
||||
s3_secret = app_settings["bucket.secure_uploads.secret_key"]
|
||||
bucket = app_settings["bucket.secure_uploads"]
|
||||
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
os.waitpid(pid, 0)
|
||||
return {"status": "processing"}
|
||||
|
||||
# Intermediate child: detach from uWSGI
|
||||
os.setsid()
|
||||
pid2 = os.fork()
|
||||
if pid2 > 0:
|
||||
os._exit(0)
|
||||
|
||||
# --- Grandchild: fully detached ---
|
||||
import resource
|
||||
maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
|
||||
if maxfd == resource.RLIM_INFINITY:
|
||||
maxfd = 1024
|
||||
for fd in range(3, maxfd):
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
lock_fd = None
|
||||
try:
|
||||
lock_fd = open(lockfile, "w")
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
lock_fd.write(str(os.getpid()))
|
||||
lock_fd.flush()
|
||||
|
||||
import boto3
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session as SASession
|
||||
from ..lib.karaoke import process_karaoke
|
||||
|
||||
engine = create_engine(db_url)
|
||||
session = SASession(bind=engine)
|
||||
|
||||
s3 = boto3.session.Session().client(
|
||||
"s3",
|
||||
region_name=s3_region,
|
||||
endpoint_url=s3_endpoint,
|
||||
aws_access_key_id=s3_access,
|
||||
aws_secret_access_key=s3_secret,
|
||||
)
|
||||
|
||||
log.info("On-demand karaoke: product=%s key=%s", product_id, s3_key)
|
||||
|
||||
sizes = process_karaoke(
|
||||
s3, bucket, s3_key, s3_path, is_video, extension,
|
||||
public_key=pk, secret_key=sk,
|
||||
)
|
||||
|
||||
if sizes:
|
||||
product = session.get(Product, product_id)
|
||||
if product:
|
||||
track_ext = extension if is_video else "wav"
|
||||
for track_name in ("instrumentals", "vocals"):
|
||||
product.set_file_metadata(
|
||||
track_name, track_ext, f"{track_name}.{track_ext}"
|
||||
)
|
||||
tmp = product.file_bytes
|
||||
tmp.update(sizes)
|
||||
product.file_bytes = tmp
|
||||
session.add(product)
|
||||
session.commit()
|
||||
product.update_s3_acls(s3, bucket)
|
||||
log.info("On-demand karaoke done: product=%s inst=%dB vox=%dB",
|
||||
product_id, sizes["instrumentals"], sizes["vocals"])
|
||||
|
||||
if has_mirror:
|
||||
from ..models.shop import Shop as ShopModel
|
||||
shop_obj = session.get(ShopModel, shop_id)
|
||||
if shop_obj:
|
||||
from ..lib.s3_mirror import mirror_keys_async
|
||||
mirror_keys_async(
|
||||
s3, bucket,
|
||||
[f"{s3_path}/instrumentals", f"{s3_path}/vocals"],
|
||||
shop_obj,
|
||||
)
|
||||
else:
|
||||
log.warning("On-demand karaoke failed: product=%s", product_id)
|
||||
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
except Exception:
|
||||
log.exception("On-demand karaoke child failed: product=%s", product_id)
|
||||
finally:
|
||||
if lock_fd:
|
||||
try:
|
||||
lock_fd.close()
|
||||
os.unlink(lockfile)
|
||||
except OSError:
|
||||
pass
|
||||
os._exit(0)
|
||||
|
||||
|
||||
@view_config(route_name="discovery_ring_json", renderer="json")
|
||||
def discovery_ring_json(request):
|
||||
"""Public JSON endpoint to inspect the discovery ring for a shop."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue