Add an API URL Builder on the website
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 12s

This commit is contained in:
2025-12-21 01:53:20 -03:00
parent e727ef659a
commit 773229ee76
3 changed files with 224 additions and 1 deletions

View File

@@ -113,6 +113,9 @@
<button class="btn btn-secondary" onclick="goBackToStep1()"> <button class="btn btn-secondary" onclick="goBackToStep1()">
Back Back
</button> </button>
<button class="btn btn-secondary" onclick="showApiBuilder()">
🛠️ API Builder
</button>
<button class="btn btn-success" onclick="showConfirmation()"> <button class="btn btn-success" onclick="showConfirmation()">
Generate Playlist Generate Playlist
</button> </button>
@@ -141,6 +144,75 @@
</div> </div>
</section> </section>
<!-- API Builder Modal -->
<div class="modal" id="apiBuilderModal">
<div class="modal-content" style="max-width: 700px;">
<div class="modal-header">
<h3>🛠️ API URL Builder</h3>
<p class="subtitle" style="font-size: 0.9rem; margin-bottom: 1rem;">Use this URL to fetch your custom playlist directly from any player.</p>
</div>
<div class="api-options">
<div class="form-group">
<label>Endpoint Type</label>
<div class="filter-mode">
<label>
<input type="radio" name="apiType" value="m3u" checked onchange="updateApiUrl()">
<span>M3U Playlist</span>
</label>
<label>
<input type="radio" name="apiType" value="xmltv" onchange="updateApiUrl()">
<span>XMLTV EPG</span>
</label>
</div>
</div>
<!-- Additional Options (only for M3U) -->
<div id="m3uOptions">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;">
<div class="checkbox-wrapper" title="Disables stream proxying. URLs will point directly to the IPTV provider.">
<label class="checkbox-label" style="padding: 0.5rem;">
<input type="checkbox" id="apiNoStreamProxy" checked onchange="updateApiUrl()">
<div class="checkmark"></div>
<div class="checkbox-text"><small>No Stream Proxy</small></div>
</label>
</div>
<div class="checkbox-wrapper" title="Includes the unique 'channel-id' tag in the playlist. Useful for some players.">
<label class="checkbox-label" style="padding: 0.5rem;">
<input type="checkbox" id="apiIncludeChannelId" onchange="updateApiUrl()">
<div class="checkmark"></div>
<div class="checkbox-text"><small>Include Channel ID</small></div>
</label>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;">
<div class="form-group" style="margin-bottom: 0;" title="Optional: Custom base URL for proxied content. Useful for reverse proxies.">
<label style="font-size: 0.8rem; margin-bottom: 0.25rem;">Proxy URL (Optional)</label>
<input type="text" id="apiProxyUrl" placeholder="http://your-domain.com" oninput="updateApiUrl()" style="padding: 0.5rem; font-size: 0.9rem;">
</div>
<div class="form-group" style="margin-bottom: 0;" title="Optional: Custom tag name for the channel ID (default: channel-id).">
<label style="font-size: 0.8rem; margin-bottom: 0.25rem;">Channel ID Tag (Optional)</label>
<input type="text" id="apiChannelIdTag" placeholder="channel-id" oninput="updateApiUrl()" style="padding: 0.5rem; font-size: 0.9rem;">
</div>
</div>
</div>
</div>
<div class="api-url-container">
<label>Generated URL</label>
<div class="url-display-box">
<code id="generatedApiUrl">https://...</code>
<button class="btn-copy" onclick="copyApiUrl()">📋</button>
</div>
</div>
<div class="modal-actions" style="margin-top: 1.5rem;">
<button class="btn btn-secondary" onclick="closeApiBuilder()">Close</button>
</div>
</div>
</div>
<!-- Confirmation Modal --> <!-- Confirmation Modal -->
<div class="modal" id="confirmationModal"> <div class="modal" id="confirmationModal">
<div class="modal-content"> <div class="modal-content">

View File

@@ -30,7 +30,9 @@ const elements = {
modalSummary: document.getElementById('modalSummary'), modalSummary: document.getElementById('modalSummary'),
results: document.getElementById('results'), results: document.getElementById('results'),
downloadLink: document.getElementById('finalDownloadLink'), downloadLink: document.getElementById('finalDownloadLink'),
searchInput: document.getElementById('categorySearch') searchInput: document.getElementById('categorySearch'),
apiBuilderModal: document.getElementById('apiBuilderModal'),
generatedApiUrl: document.getElementById('generatedApiUrl')
}; };
// Step Navigation // Step Navigation
@@ -330,6 +332,109 @@ function filterCategories(searchTerm) {
}); });
} }
// API Builder
function showApiBuilder() {
elements.apiBuilderModal.classList.add('active');
updateApiUrl();
}
function closeApiBuilder() {
elements.apiBuilderModal.classList.remove('active');
}
function updateApiUrl() {
const apiType = document.querySelector('input[name="apiType"]:checked').value;
const noStreamProxy = document.getElementById('apiNoStreamProxy').checked;
const includeChannelId = document.getElementById('apiIncludeChannelId').checked;
const proxyUrl = document.getElementById('apiProxyUrl').value.trim();
const channelIdTag = document.getElementById('apiChannelIdTag').value.trim();
// Toggle options visibility
const m3uOptions = document.getElementById('m3uOptions');
if (apiType === 'm3u') {
m3uOptions.style.display = 'block';
} else {
m3uOptions.style.display = 'none';
}
const baseUrl = window.location.origin;
const params = new URLSearchParams({
url: state.credentials.url,
username: state.credentials.username,
password: state.credentials.password
});
if (state.credentials.includeVod) {
// Backend expects 'include_vod'
params.append('include_vod', 'true');
}
// Smart filtering: Omit filter params if they result in "All Content"
const categories = Array.from(state.selectedCategories);
const totalCategories = state.categories.length;
// Logic for omitting params:
// If Filter Mode is INCLUDE:
// - If ALL categories are selected -> Omit (Implicitly Include All)
// - If SOME categories are selected -> Include 'wanted_groups'
// - If NO categories are selected -> (Technically this would result in empty playlist, but usually implies 'Select something'.
// However, if we want to follow strict logic: Include 'wanted_groups=' (empty) or just don't append.
// Let's assume user wants *something*. If 0 selected in include mode, the URL will produce nothing anyway.
// If Filter Mode is EXCLUDE:
// - If NO categories are selected -> Omit (Implicitly Exclude None = Include All)
// - If SOME categories are selected -> Include 'unwanted_groups'
if (categories.length > 0) {
if (state.filterMode === 'include') {
// Only append if NOT all are selected
if (categories.length < totalCategories) {
params.append('wanted_groups', categories.join(','));
}
} else {
// Exclude mode: Append unwanted groups
params.append('unwanted_groups', categories.join(','));
}
} else {
// Categories length is 0
if (state.filterMode === 'include') {
// Include mode + 0 selected = Empty playlist?
// Or does user imply "All"? Usually UI starts empty.
// If we omit, it defaults to ALL.
// If user explicitly selected NOTHING in "Include Mode", they probably don't want ALL.
// But for the API URL builder, let's assume if they selected nothing, they haven't configured filters, so defaulting to ALL (omitting) might be safer or adding an empty param.
// But let's stick to the prompt: "we should not need to actually include all the categories, we should be able to just ommit it"
// This implies the user selected ALL.
// So if count == 0 in include mode, maybe they haven't started.
// But if they selected ALL (via Select All), count == total.
// The check `categories.length < totalCategories` above handles the "Selected All" case for Include mode.
}
}
if (apiType === 'm3u') {
if (noStreamProxy) params.append('nostreamproxy', 'true');
if (includeChannelId) params.append('include_channel_id', 'true');
if (proxyUrl) params.append('proxy_url', proxyUrl);
if (channelIdTag) params.append('channel_id_tag', channelIdTag);
elements.generatedApiUrl.textContent = `${baseUrl}/m3u?${params.toString()}`;
} else {
if (proxyUrl) params.append('proxy_url', proxyUrl);
elements.generatedApiUrl.textContent = `${baseUrl}/xmltv?${params.toString()}`;
}
}
function copyApiUrl() {
const url = elements.generatedApiUrl.textContent;
navigator.clipboard.writeText(url).then(() => {
const btn = document.querySelector('.btn-copy');
const originalText = btn.textContent;
btn.textContent = '✅';
setTimeout(() => btn.textContent = originalText, 1500);
});
}
// Confirmation & Generation // Confirmation & Generation
function showConfirmation() { function showConfirmation() {
const count = state.selectedCategories.size; const count = state.selectedCategories.size;
@@ -472,6 +577,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (e.target === elements.confirmationModal) closeModal(); if (e.target === elements.confirmationModal) closeModal();
}); });
elements.apiBuilderModal.addEventListener('click', (e) => {
if (e.target === elements.apiBuilderModal) closeApiBuilder();
});
// Input trim handlers // Input trim handlers
document.querySelectorAll('input').forEach(input => { document.querySelectorAll('input').forEach(input => {
input.addEventListener('blur', (e) => { input.addEventListener('blur', (e) => {

View File

@@ -739,3 +739,45 @@ input::placeholder {
gap: 1rem; gap: 1rem;
justify-content: flex-end; justify-content: flex-end;
} }
/* API Builder Styles */
.api-url-container {
margin-top: 1rem;
}
.url-display-box {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
position: relative;
}
.url-display-box code {
flex: 1;
font-family: monospace;
font-size: 0.85rem;
color: var(--accent-primary);
overflow-x: auto;
white-space: nowrap;
padding-right: 2rem;
}
.btn-copy {
background: transparent;
border: none;
cursor: pointer;
font-size: 1.2rem;
padding: 0.25rem;
border-radius: 4px;
transition: all 0.2s;
color: var(--text-secondary);
}
.btn-copy:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}