diff --git a/qbittorrent_performer_sorter.html b/qbittorrent_performer_sorter.html
index 604f7fb..8a9f989 100644
--- a/qbittorrent_performer_sorter.html
+++ b/qbittorrent_performer_sorter.html
@@ -740,6 +740,34 @@
border-color: var(--accent-blue);
color: #fff;
}
+ .playlist-pill {
+ background: var(--bg-card);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ color: var(--text-main);
+ padding: 0.35rem 0.65rem;
+ font-size: 0.72rem;
+ font-family: var(--font-mono);
+ cursor: pointer;
+ white-space: nowrap;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ transition: all 0.15s ease;
+ user-select: none;
+ }
+ .playlist-pill:hover {
+ border-color: var(--accent-cyan);
+ color: var(--accent-cyan);
+ background: rgba(6, 182, 212, 0.08);
+ }
+ .playlist-pill.active {
+ background: rgba(6, 182, 212, 0.22);
+ border-color: var(--accent-cyan);
+ color: var(--accent-cyan);
+ font-weight: 700;
+ box-shadow: 0 0 10px rgba(6, 182, 212, 0.35);
+ }
.video-details-panel {
padding: 1rem 1.25rem;
background: var(--bg-card);
@@ -1391,6 +1419,17 @@
+
+
+
+
+ 🎬 Torrent Video Files (0)
+
+ Click any file to preview:
+
+
+
+
@@ -3389,13 +3428,38 @@ Return ONLY valid JSON matching:
t.innerFiles = await fetchTorrentFiles(hash);
}
+ const videoExts = /\.(mp4|mkv|avi|mov|webm|m4v|ts)$/i;
+
if (t.innerFiles && t.innerFiles.length > 0) {
- list.innerHTML = t.innerFiles.map(f => `
-
- ${escapeHtml(f.name)}
- ${formatBytes(f.size)}
-
- `).join('');
+ list.innerHTML = t.innerFiles.map((f, idx) => {
+ const isVideo = videoExts.test(f.name);
+ return `
+
+
+ ${isVideo ? '🎬' : '📄'}
+ ${escapeHtml(f.name)}
+
+
+ ${formatBytes(f.size)}
+ ${isVideo ? `
+
+ ` : ''}
+
+
+ `;
+ }).join('');
+
+ list.querySelectorAll('.btn-modal-preview-inner-file').forEach(pBtn => {
+ pBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ const targetHash = pBtn.getAttribute('data-hash');
+ const targetFile = pBtn.getAttribute('data-file');
+ document.getElementById('file-modal').classList.remove('open');
+ openVideoPreviewForTorrent(targetHash, targetFile);
+ });
+ });
} else {
list.innerHTML = `No inner files returned or single-file torrent.`;
}
@@ -4269,7 +4333,7 @@ Return ONLY a valid JSON object with an array named "results":
renderCurrentVideoPreview();
}
- async function openVideoPreviewForTorrent(hash) {
+ async function openVideoPreviewForTorrent(hash, targetInnerFile = null) {
const query = searchInput.value.trim().toLowerCase();
const filtered = qbState.torrents.filter(t => {
if (!query) return true;
@@ -4287,6 +4351,7 @@ Return ONLY a valid JSON object with an array named "results":
previewState.activeType = 'qb';
previewState.currentList = filtered;
previewState.currentIndex = idx;
+ previewState.selectedInnerFile = targetInnerFile || null;
renderCurrentVideoPreview();
}
@@ -4301,21 +4366,78 @@ Return ONLY a valid JSON object with an array named "results":
let streamPath = '';
let titleName = item.name;
+ const videoExts = /\.(mp4|mkv|avi|mov|webm|m4v|ts)$/i;
+
+ const playlistBar = document.getElementById('video-playlist-bar');
+ const playlistPills = document.getElementById('video-playlist-pills');
+ const playlistCount = document.getElementById('video-playlist-count');
if (previewState.activeType === 'smb') {
streamPath = item.path;
+ if (playlistBar) playlistBar.style.display = 'none';
} else {
// qb torrent
- if (item.innerFiles && item.innerFiles.length > 0) {
- const videoFile = item.innerFiles.find(f => /\.(mp4|mkv|avi|mov|webm|m4v|ts)$/i.test(f.name));
- if (videoFile) {
- const base = (item.save_path || '/downloads').replace(/\/+$/, '');
- streamPath = `${base}/${videoFile.name}`;
+ const videoFiles = (item.innerFiles || []).filter(f => videoExts.test(f.name));
+
+ if (videoFiles.length > 1) {
+ if (playlistBar) {
+ playlistBar.style.display = 'flex';
+ if (playlistCount) playlistCount.textContent = videoFiles.length;
}
- }
- if (!streamPath) {
+
+ // Determine selected inner file
+ let activeFile = null;
+ if (previewState.selectedInnerFile) {
+ activeFile = videoFiles.find(f => f.name === previewState.selectedInnerFile || f.name.endsWith(previewState.selectedInnerFile));
+ }
+ if (!activeFile) {
+ // Default to largest or first video file
+ let largest = videoFiles[0];
+ for (const vf of videoFiles) {
+ if ((vf.size || 0) > (largest.size || 0)) largest = vf;
+ }
+ activeFile = largest;
+ previewState.selectedInnerFile = activeFile.name;
+ }
+
const base = (item.save_path || '/downloads').replace(/\/+$/, '');
- streamPath = `${base}/${item.name}`;
+ streamPath = `${base}/${activeFile.name}`;
+ const shortFileName = activeFile.name.split('/').pop();
+ titleName = `${item.name} âž” ${shortFileName}`;
+
+ if (playlistPills) {
+ playlistPills.innerHTML = videoFiles.map((vf, vIdx) => {
+ const isActive = vf.name === activeFile.name;
+ const shortName = vf.name.split('/').pop();
+ return `
+
+ `;
+ }).join('');
+
+ playlistPills.querySelectorAll('.playlist-pill').forEach(pill => {
+ pill.addEventListener('click', (e) => {
+ e.preventDefault();
+ const chosenFile = pill.getAttribute('data-file');
+ previewState.selectedInnerFile = chosenFile;
+ renderCurrentVideoPreview();
+ });
+ });
+ }
+ } else {
+ if (playlistBar) playlistBar.style.display = 'none';
+ if (videoFiles.length === 1) {
+ const base = (item.save_path || '/downloads').replace(/\/+$/, '');
+ streamPath = `${base}/${videoFiles[0].name}`;
+ const shortFileName = videoFiles[0].name.split('/').pop();
+ titleName = `${item.name} âž” ${shortFileName}`;
+ } else {
+ const base = (item.save_path || '/downloads').replace(/\/+$/, '');
+ streamPath = `${base}/${item.name}`;
+ }
}
}
@@ -4534,6 +4656,7 @@ Return ONLY a valid JSON object with an array named "results":
btnPreviewPrev.addEventListener('click', () => {
if (previewState.currentIndex > 0) {
previewState.currentIndex--;
+ previewState.selectedInnerFile = null;
renderCurrentVideoPreview();
}
});
@@ -4541,6 +4664,7 @@ Return ONLY a valid JSON object with an array named "results":
btnPreviewNext.addEventListener('click', () => {
if (previewState.currentIndex < previewState.currentList.length - 1) {
previewState.currentIndex++;
+ previewState.selectedInnerFile = null;
renderCurrentVideoPreview();
}
});