Add episode thumbnails and playlist player

This commit is contained in:
Dymas
2026-09-15 16:17:57 +02:00
parent 55954376e2
commit 2597aeaf1d
8 changed files with 261 additions and 55 deletions
+83 -10
View File
@@ -7,12 +7,20 @@ const state = {
const els = {
libraryBadge: document.querySelector("#libraryBadge"),
libraryPath: document.querySelector("#libraryPath"),
backBtn: document.querySelector("#backBtn"),
rescanBtn: document.querySelector("#rescanBtn"),
searchInput: document.querySelector("#searchInput"),
details: document.querySelector("#details"),
titleCardTemplate: document.querySelector("#titleCardTemplate")
titleCardTemplate: document.querySelector("#titleCardTemplate"),
playerOverlay: document.querySelector("#playerOverlay"),
playerVideo: document.querySelector("#playerVideo"),
closePlayerBtn: document.querySelector("#closePlayerBtn"),
playerNowPlaying: document.querySelector("#playerNowPlaying")
};
const player = {
playlist: [],
index: 0
};
function bytes(value) {
@@ -39,7 +47,6 @@ async function api(path, options = {}) {
async function loadLibrary() {
const data = await api("/api/library");
state.titles = data.titles;
els.libraryPath.textContent = data.libraryDir;
els.libraryBadge.textContent = `${state.titles.length} title${state.titles.length === 1 ? "" : "s"}`;
if (!state.titles.some((title) => title.id === state.activeId)) {
state.activeId = "";
@@ -123,6 +130,7 @@ function renderDetails(title) {
<button id="editMetaBtn" type="button">Edit</button>
<button id="fetchMetadataBtn" type="button">Metadata</button>
<button id="makeThumbBtn" type="button">Thumbnail</button>
<button id="playAllBtn" class="primary" type="button">Play all</button>
</div>
</div>
<div id="metaView" class="meta-view">
@@ -139,10 +147,16 @@ function renderDetails(title) {
</div>
`;
wireEpisodeCards(title);
document.querySelector("#editMetaBtn").addEventListener("click", () => {
renderMetadataForm(title);
});
document.querySelector("#playAllBtn").addEventListener("click", () => {
startPlaylist(flattenVideos(title), 0);
});
document.querySelector("#makeThumbBtn").addEventListener("click", async (event) => {
event.currentTarget.disabled = true;
event.currentTarget.textContent = "Working";
@@ -220,19 +234,65 @@ function renderSeason(season) {
<h3>${escapeHtml(season.name)}</h3>
<div class="episodes">
${season.videos.map((video) => `
<article class="episode">
<div>
<strong>${escapeHtml(video.name)}</strong>
<span>${escapeHtml(video.relativePath)} · ${bytes(video.size)}</span>
</div>
<video controls preload="metadata" src="/video/${encodeURIComponent(video.id)}"></video>
</article>
<button class="episode" type="button" data-video-id="${escapeAttr(video.id)}">
<span class="episode__thumb">
<img src="${escapeAttr(video.thumbnail)}" alt="" loading="lazy" onerror="this.hidden=true">
<span class="episode__play">Play</span>
</span>
<span class="episode__name">${escapeHtml(video.name)}</span>
<span class="episode__meta">${escapeHtml(video.relativePath)} · ${bytes(video.size)}</span>
</button>
`).join("")}
</div>
</section>
`;
}
function flattenVideos(title) {
return title.seasons.flatMap((season) => season.videos);
}
function wireEpisodeCards(title) {
const videos = flattenVideos(title);
document.querySelectorAll("[data-video-id]").forEach((node) => {
node.addEventListener("click", () => {
const index = videos.findIndex((video) => video.id === node.dataset.videoId);
startPlaylist(videos, Math.max(index, 0));
});
});
}
function startPlaylist(videos, index = 0) {
if (!videos.length) return;
player.playlist = videos;
player.index = index;
els.playerOverlay.classList.remove("hidden");
playCurrent();
const fullscreenTarget = els.playerOverlay;
if (fullscreenTarget.requestFullscreen) {
fullscreenTarget.requestFullscreen().catch(() => {});
}
}
function playCurrent() {
const video = player.playlist[player.index];
if (!video) {
closePlayer();
return;
}
els.playerVideo.src = `/video/${encodeURIComponent(video.id)}`;
els.playerNowPlaying.textContent = `${video.name} · ${player.index + 1} / ${player.playlist.length}`;
els.playerVideo.play().catch(() => {});
}
function closePlayer() {
els.playerVideo.pause();
els.playerVideo.removeAttribute("src");
els.playerVideo.load();
els.playerOverlay.classList.add("hidden");
if (document.fullscreenElement) document.exitFullscreen().catch(() => {});
}
function initials(value) {
return value.split(/\s+/).slice(0, 2).map((part) => part[0] || "").join("").toUpperCase();
}
@@ -262,6 +322,19 @@ els.backBtn.addEventListener("click", () => {
render();
});
els.closePlayerBtn.addEventListener("click", closePlayer);
els.playerVideo.addEventListener("ended", () => {
player.index += 1;
playCurrent();
});
document.addEventListener("fullscreenchange", () => {
if (!document.fullscreenElement && !els.playerOverlay.classList.contains("hidden")) {
closePlayer();
}
});
els.rescanBtn.addEventListener("click", async () => {
els.rescanBtn.disabled = true;
els.rescanBtn.textContent = "Scanning";