mirror of
https://github.com/ovosimpatico/xtream2m3u.git
synced 2026-01-15 08:22:56 -03:00
Add an API URL Builder on the website
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 12s
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 12s
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user