rewritten loading categories and items
This commit is contained in:
parent
9bae442352
commit
0c5d5e73da
21
pom.xml
21
pom.xml
@ -42,6 +42,27 @@
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>cz.kamma.xtreamplayer.XtreamPlayerApplication</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
|
||||
@ -267,7 +267,7 @@ final class LibraryRepository {
|
||||
}
|
||||
}
|
||||
|
||||
List<LiveStreamRow> listLiveStreams(String categoryId, String search) {
|
||||
List<LiveStreamRow> listLiveStreams(String categoryId, String search, Integer limit, Integer offset) {
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT stream_id, name, category_id, epg_channel_id FROM live_streams WHERE 1=1");
|
||||
List<String> args = new ArrayList<>();
|
||||
@ -280,6 +280,12 @@ final class LibraryRepository {
|
||||
args.add("%" + search.trim().toLowerCase(Locale.ROOT) + "%");
|
||||
}
|
||||
sql.append(" ORDER BY LOWER(name), name");
|
||||
if (limit != null && limit > 0) {
|
||||
sql.append(" LIMIT ").append(limit);
|
||||
if (offset != null && offset > 0) {
|
||||
sql.append(" OFFSET ").append(offset);
|
||||
}
|
||||
}
|
||||
|
||||
List<LiveStreamRow> rows = new ArrayList<>();
|
||||
try (Connection connection = openConnection();
|
||||
@ -301,7 +307,7 @@ final class LibraryRepository {
|
||||
}
|
||||
}
|
||||
|
||||
List<VodStreamRow> listVodStreams(String categoryId, String search) {
|
||||
List<VodStreamRow> listVodStreams(String categoryId, String search, Integer limit, Integer offset) {
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT stream_id, name, category_id, container_extension FROM vod_streams WHERE 1=1");
|
||||
List<String> args = new ArrayList<>();
|
||||
@ -314,6 +320,12 @@ final class LibraryRepository {
|
||||
args.add("%" + search.trim().toLowerCase(Locale.ROOT) + "%");
|
||||
}
|
||||
sql.append(" ORDER BY LOWER(name), name");
|
||||
if (limit != null && limit > 0) {
|
||||
sql.append(" LIMIT ").append(limit);
|
||||
if (offset != null && offset > 0) {
|
||||
sql.append(" OFFSET ").append(offset);
|
||||
}
|
||||
}
|
||||
|
||||
List<VodStreamRow> rows = new ArrayList<>();
|
||||
try (Connection connection = openConnection();
|
||||
@ -335,7 +347,7 @@ final class LibraryRepository {
|
||||
}
|
||||
}
|
||||
|
||||
List<SeriesItemRow> listSeriesItems(String categoryId, String search) {
|
||||
List<SeriesItemRow> listSeriesItems(String categoryId, String search, Integer limit, Integer offset) {
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT series_id, name, category_id FROM series_items WHERE 1=1");
|
||||
List<String> args = new ArrayList<>();
|
||||
@ -348,6 +360,12 @@ final class LibraryRepository {
|
||||
args.add("%" + search.trim().toLowerCase(Locale.ROOT) + "%");
|
||||
}
|
||||
sql.append(" ORDER BY LOWER(name), name");
|
||||
if (limit != null && limit > 0) {
|
||||
sql.append(" LIMIT ").append(limit);
|
||||
if (offset != null && offset > 0) {
|
||||
sql.append(" OFFSET ").append(offset);
|
||||
}
|
||||
}
|
||||
|
||||
List<SeriesItemRow> rows = new ArrayList<>();
|
||||
try (Connection connection = openConnection();
|
||||
@ -408,14 +426,42 @@ final class LibraryRepository {
|
||||
}
|
||||
}
|
||||
|
||||
List<FavoriteRow> listFavorites() {
|
||||
List<FavoriteRow> listFavorites(String searchRaw, int limitRaw, int offsetRaw) {
|
||||
String search = searchRaw == null ? "" : searchRaw.trim().toLowerCase(Locale.ROOT);
|
||||
int limit = Math.max(1, limitRaw);
|
||||
int offset = Math.max(0, offsetRaw);
|
||||
List<FavoriteRow> rows = new ArrayList<>();
|
||||
StringBuilder sql = new StringBuilder("""
|
||||
SELECT favorite_key, mode, ref_id, ext, title, category_id, series_id, season, episode, url, created_at
|
||||
FROM favorites
|
||||
""");
|
||||
List<Object> args = new ArrayList<>();
|
||||
if (!search.isBlank()) {
|
||||
sql.append("""
|
||||
WHERE LOWER(title) LIKE ?
|
||||
OR LOWER(mode) LIKE ?
|
||||
OR LOWER(url) LIKE ?
|
||||
OR LOWER(favorite_key) LIKE ?
|
||||
""");
|
||||
String value = "%" + search + "%";
|
||||
args.add(value);
|
||||
args.add(value);
|
||||
args.add(value);
|
||||
args.add(value);
|
||||
}
|
||||
sql.append(" ORDER BY created_at DESC, favorite_key LIMIT ? OFFSET ?");
|
||||
try (Connection connection = openConnection();
|
||||
PreparedStatement preparedStatement = connection.prepareStatement("""
|
||||
SELECT favorite_key, mode, ref_id, ext, title, category_id, series_id, season, episode, url, created_at
|
||||
FROM favorites
|
||||
ORDER BY created_at DESC, favorite_key
|
||||
""")) {
|
||||
PreparedStatement preparedStatement = connection.prepareStatement(sql.toString())) {
|
||||
args.add(limit);
|
||||
args.add(offset);
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
Object arg = args.get(i);
|
||||
if (arg instanceof Integer intArg) {
|
||||
preparedStatement.setInt(i + 1, intArg);
|
||||
} else {
|
||||
preparedStatement.setString(i + 1, String.valueOf(arg));
|
||||
}
|
||||
}
|
||||
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
||||
while (resultSet.next()) {
|
||||
rows.add(new FavoriteRow(
|
||||
@ -439,6 +485,39 @@ final class LibraryRepository {
|
||||
}
|
||||
}
|
||||
|
||||
int countFavorites(String searchRaw) {
|
||||
String search = searchRaw == null ? "" : searchRaw.trim().toLowerCase(Locale.ROOT);
|
||||
StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM favorites");
|
||||
List<String> args = new ArrayList<>();
|
||||
if (!search.isBlank()) {
|
||||
sql.append("""
|
||||
WHERE LOWER(title) LIKE ?
|
||||
OR LOWER(mode) LIKE ?
|
||||
OR LOWER(url) LIKE ?
|
||||
OR LOWER(favorite_key) LIKE ?
|
||||
""");
|
||||
String value = "%" + search + "%";
|
||||
args.add(value);
|
||||
args.add(value);
|
||||
args.add(value);
|
||||
args.add(value);
|
||||
}
|
||||
try (Connection connection = openConnection();
|
||||
PreparedStatement preparedStatement = connection.prepareStatement(sql.toString())) {
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
preparedStatement.setString(i + 1, args.get(i));
|
||||
}
|
||||
try (ResultSet resultSet = preparedStatement.executeQuery()) {
|
||||
if (resultSet.next()) {
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
throw new IllegalStateException("Unable to count favorites.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
void upsertFavorite(FavoriteRow row) {
|
||||
try (Connection connection = openConnection();
|
||||
PreparedStatement preparedStatement = connection.prepareStatement("""
|
||||
|
||||
@ -144,18 +144,22 @@ final class XtreamLibraryService {
|
||||
return repository.listCategories(type);
|
||||
}
|
||||
|
||||
List<?> listItems(String type, String categoryId, String search) {
|
||||
List<?> listItems(String type, String categoryId, String search, Integer limit, Integer offset) {
|
||||
String normalizedType = type == null ? "" : type.trim().toLowerCase(Locale.ROOT);
|
||||
return switch (normalizedType) {
|
||||
case "live" -> repository.listLiveStreams(categoryId, search);
|
||||
case "vod" -> repository.listVodStreams(categoryId, search);
|
||||
case "series" -> repository.listSeriesItems(categoryId, search);
|
||||
case "live" -> repository.listLiveStreams(categoryId, search, limit, offset);
|
||||
case "vod" -> repository.listVodStreams(categoryId, search, limit, offset);
|
||||
case "series" -> repository.listSeriesItems(categoryId, search, limit, offset);
|
||||
default -> throw new IllegalArgumentException("Unsupported type: " + type);
|
||||
};
|
||||
}
|
||||
|
||||
List<LibraryRepository.FavoriteRow> listFavorites() {
|
||||
return repository.listFavorites();
|
||||
List<LibraryRepository.FavoriteRow> listFavorites(String search, int limit, int offset) {
|
||||
return repository.listFavorites(search, limit, offset);
|
||||
}
|
||||
|
||||
int countFavorites(String search) {
|
||||
return repository.countFavorites(search);
|
||||
}
|
||||
|
||||
LibraryRepository.FavoriteRow saveFavorite(
|
||||
|
||||
@ -472,9 +472,17 @@ public final class XtreamPlayerApplication {
|
||||
String type = query.getOrDefault("type", "");
|
||||
String categoryId = query.getOrDefault("category_id", "");
|
||||
String search = query.getOrDefault("search", "");
|
||||
Integer limit = query.containsKey("limit") ? parseIntOrDefault(query.get("limit"), 0) : null;
|
||||
Integer offset = query.containsKey("offset") ? parseIntOrDefault(query.get("offset"), 0) : null;
|
||||
if (limit != null && limit <= 0) {
|
||||
limit = null;
|
||||
}
|
||||
if (offset != null && offset < 0) {
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("items", libraryService.listItems(type, categoryId, search));
|
||||
out.put("items", libraryService.listItems(type, categoryId, search, limit, offset));
|
||||
writeJsonObject(exchange, 200, out);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
writeJson(exchange, 400, errorJson(exception.getMessage()));
|
||||
@ -546,8 +554,23 @@ public final class XtreamPlayerApplication {
|
||||
|
||||
try {
|
||||
if ("GET".equalsIgnoreCase(method)) {
|
||||
String search = query.getOrDefault("search", "");
|
||||
int limit = parseIntOrDefault(query.get("limit"), 50);
|
||||
int offset = parseIntOrDefault(query.get("offset"), 0);
|
||||
if (limit < 1) {
|
||||
limit = 1;
|
||||
}
|
||||
if (limit > 200) {
|
||||
limit = 200;
|
||||
}
|
||||
if (offset < 0) {
|
||||
offset = 0;
|
||||
}
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("items", libraryService.listFavorites());
|
||||
out.put("items", libraryService.listFavorites(search, limit, offset));
|
||||
out.put("total", libraryService.countFavorites(search));
|
||||
out.put("limit", limit);
|
||||
out.put("offset", offset);
|
||||
writeJsonObject(exchange, 200, out);
|
||||
return;
|
||||
}
|
||||
@ -609,7 +632,7 @@ public final class XtreamPlayerApplication {
|
||||
if ("/".equals(path) || "/index.html".equals(path)) {
|
||||
resourcePath = "/web/index.html";
|
||||
} else if (path.startsWith("/assets/")) {
|
||||
resourcePath = "/web" + path;
|
||||
resourcePath = "/web" + normalizeAssetPath(path);
|
||||
} else {
|
||||
resourcePath = "/web/index.html";
|
||||
}
|
||||
@ -627,6 +650,9 @@ public final class XtreamPlayerApplication {
|
||||
LOGGER.debug("Serving static resource={}", resourcePath);
|
||||
byte[] body = inputStream.readAllBytes();
|
||||
exchange.getResponseHeaders().set("Content-Type", contentType(resourcePath));
|
||||
exchange.getResponseHeaders().set("Cache-Control", "no-store, no-cache, must-revalidate");
|
||||
exchange.getResponseHeaders().set("Pragma", "no-cache");
|
||||
exchange.getResponseHeaders().set("Expires", "0");
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
} finally {
|
||||
@ -1099,6 +1125,17 @@ public final class XtreamPlayerApplication {
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseIntOrDefault(String raw, int defaultValue) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(raw.trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static String configToJson(XtreamConfig config) {
|
||||
return "{"
|
||||
+ "\"serverUrl\":\"" + jsonEscape(config.serverUrl()) + "\","
|
||||
@ -1156,6 +1193,19 @@ public final class XtreamPlayerApplication {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
private static String normalizeAssetPath(String path) {
|
||||
if (path == null || path.isBlank()) {
|
||||
return "/assets/app.js";
|
||||
}
|
||||
if (path.matches("^/assets/app\\.[^.]+\\.js$")) {
|
||||
return "/assets/app.js";
|
||||
}
|
||||
if (path.matches("^/assets/style\\.[^.]+\\.css$")) {
|
||||
return "/assets/style.css";
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private static String urlDecode(String value) {
|
||||
return URLDecoder.decode(value, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
expandedSeasonBySeries: {},
|
||||
customStreams: [],
|
||||
favorites: [],
|
||||
favoriteKeys: new Set(),
|
||||
expandedFavoriteSeriesById: {},
|
||||
favoriteSeriesEpisodesById: {},
|
||||
expandedSeasonByFavoriteSeries: {},
|
||||
@ -27,7 +28,14 @@
|
||||
expandedFavoriteSeriesCategoryById: {},
|
||||
favoriteSeriesCategoryItemsById: {},
|
||||
currentLiveEpgStreamId: null,
|
||||
currentStreamInfo: null
|
||||
currentStreamInfo: null,
|
||||
liveSearchTimer: null,
|
||||
vodSearchTimer: null,
|
||||
seriesSearchTimer: null,
|
||||
favoritesSearchTimer: null,
|
||||
favoritesLimit: 50,
|
||||
favoritesOffset: 0,
|
||||
favoritesTotal: 0
|
||||
};
|
||||
|
||||
const customStorageKey = "xtream_custom_streams_v1";
|
||||
@ -72,6 +80,9 @@
|
||||
customList: document.getElementById("custom-list"),
|
||||
favoritesSearch: document.getElementById("favorites-search"),
|
||||
favoritesList: document.getElementById("favorites-list"),
|
||||
favoritesPrev: document.getElementById("favorites-prev"),
|
||||
favoritesNext: document.getElementById("favorites-next"),
|
||||
favoritesPageInfo: document.getElementById("favorites-page-info"),
|
||||
|
||||
playerTitle: document.getElementById("player-title"),
|
||||
player: document.getElementById("player"),
|
||||
@ -105,12 +116,12 @@
|
||||
bindPlayerActionButtons();
|
||||
|
||||
loadCustomStreams();
|
||||
await loadFavorites();
|
||||
renderCustomStreams();
|
||||
renderFavorites();
|
||||
renderStreamInfo();
|
||||
updateLiveCategoryFavoriteButton();
|
||||
updateVodCategoryFavoriteButton();
|
||||
switchTab("settings");
|
||||
await loadConfig();
|
||||
}
|
||||
|
||||
@ -184,20 +195,35 @@
|
||||
el.panels.forEach((panel) => panel.classList.toggle("active", panel.dataset.panel === tabName));
|
||||
|
||||
if (tabName === "favorites") {
|
||||
renderFavorites();
|
||||
loadFavorites(String(el.favoritesSearch.value || "").trim())
|
||||
.then(() => renderFavorites())
|
||||
.catch(showError);
|
||||
}
|
||||
if (!state.config?.configured || !state.libraryStatus?.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabName === "live" && state.liveStreams.length === 0) {
|
||||
loadLiveData().catch(showError);
|
||||
releaseInactiveTabItems(tabName);
|
||||
if (tabName === "live") {
|
||||
if (state.liveCategories.length === 0) {
|
||||
loadLiveData().catch(showError);
|
||||
} else {
|
||||
loadLiveStreams().catch(showError);
|
||||
}
|
||||
}
|
||||
if (tabName === "vod" && state.vodStreams.length === 0) {
|
||||
loadVodData().catch(showError);
|
||||
if (tabName === "vod") {
|
||||
if (state.vodCategories.length === 0) {
|
||||
loadVodData().catch(showError);
|
||||
} else {
|
||||
loadVodStreams().catch(showError);
|
||||
}
|
||||
}
|
||||
if (tabName === "series" && state.seriesItems.length === 0) {
|
||||
loadSeriesData().catch(showError);
|
||||
if (tabName === "series") {
|
||||
if (state.seriesCategories.length === 0) {
|
||||
loadSeriesData().catch(showError);
|
||||
} else {
|
||||
loadSeriesList().catch(showError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -217,7 +243,7 @@
|
||||
await refreshLibraryStatus();
|
||||
|
||||
if (state.libraryStatus?.ready) {
|
||||
await loadAllLibraryData();
|
||||
renderAllFromState();
|
||||
setSettingsMessage("Settings saved. Using local H2 data.", "ok");
|
||||
} else {
|
||||
renderAllFromState();
|
||||
@ -255,7 +281,7 @@
|
||||
loadLiveStreams().catch(showError);
|
||||
updateLiveCategoryFavoriteButton();
|
||||
});
|
||||
el.liveSearch.addEventListener("input", renderLiveStreams);
|
||||
el.liveSearch.addEventListener("input", scheduleLiveSearch);
|
||||
el.liveRefresh.addEventListener("click", () => loadLiveData().catch(showError));
|
||||
el.liveFavoriteCategory.addEventListener("click", () => {
|
||||
const favorite = selectedLiveCategoryFavorite();
|
||||
@ -273,7 +299,7 @@
|
||||
loadVodStreams().catch(showError);
|
||||
updateVodCategoryFavoriteButton();
|
||||
});
|
||||
el.vodSearch.addEventListener("input", renderVodStreams);
|
||||
el.vodSearch.addEventListener("input", scheduleVodSearch);
|
||||
el.vodRefresh.addEventListener("click", () => loadVodData().catch(showError));
|
||||
el.vodFavoriteCategory.addEventListener("click", () => {
|
||||
const favorite = selectedVodCategoryFavorite();
|
||||
@ -290,7 +316,7 @@
|
||||
loadSeriesList().catch(showError);
|
||||
updateSeriesCategoryFavoriteButton();
|
||||
});
|
||||
el.seriesSearch.addEventListener("input", renderSeriesList);
|
||||
el.seriesSearch.addEventListener("input", scheduleSeriesSearch);
|
||||
el.seriesRefresh.addEventListener("click", () => loadSeriesData().catch(showError));
|
||||
el.seriesFavoriteCategory.addEventListener("click", () => {
|
||||
const favorite = selectedSeriesCategoryFavorite();
|
||||
@ -327,7 +353,26 @@
|
||||
}
|
||||
|
||||
function bindFavoritesTab() {
|
||||
el.favoritesSearch.addEventListener("input", renderFavorites);
|
||||
el.favoritesSearch.addEventListener("input", scheduleFavoritesSearch);
|
||||
el.favoritesPrev.addEventListener("click", () => {
|
||||
if (state.favoritesOffset <= 0) {
|
||||
return;
|
||||
}
|
||||
state.favoritesOffset = Math.max(0, state.favoritesOffset - state.favoritesLimit);
|
||||
loadFavorites(String(el.favoritesSearch.value || "").trim())
|
||||
.then(() => renderFavorites())
|
||||
.catch(showError);
|
||||
});
|
||||
el.favoritesNext.addEventListener("click", () => {
|
||||
if (state.favoritesOffset + state.favoritesLimit >= state.favoritesTotal) {
|
||||
return;
|
||||
}
|
||||
state.favoritesOffset += state.favoritesLimit;
|
||||
loadFavorites(String(el.favoritesSearch.value || "").trim())
|
||||
.then(() => renderFavorites())
|
||||
.catch(showError);
|
||||
});
|
||||
el.favoritesList.addEventListener("click", onFavoritesListClick);
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
@ -342,7 +387,7 @@
|
||||
|
||||
await refreshLibraryStatus();
|
||||
if (state.libraryStatus?.ready) {
|
||||
await loadAllLibraryData();
|
||||
renderAllFromState();
|
||||
} else {
|
||||
clearLibraryState();
|
||||
renderAllFromState();
|
||||
@ -393,7 +438,7 @@
|
||||
await apiJson(`/api/library/load?step=${encodeURIComponent(step.id)}`, {method: "POST"});
|
||||
}
|
||||
await refreshLibraryStatus();
|
||||
await loadAllLibraryData();
|
||||
renderAllFromState();
|
||||
updateProgress(100, `Done. Sources saved in H2 (${new Date().toLocaleString("en-US")}).`);
|
||||
setSettingsMessage(
|
||||
`Sources were loaded and saved to the local H2 database. ${formatSourceCounts(state.libraryStatus?.counts)}`,
|
||||
@ -417,10 +462,6 @@
|
||||
+ `Series: ${seriesCategories} categories / ${seriesItems} series.`;
|
||||
}
|
||||
|
||||
async function loadAllLibraryData() {
|
||||
await Promise.all([loadLiveData(), loadVodData(), loadSeriesData()]);
|
||||
}
|
||||
|
||||
function refreshConfigUi() {
|
||||
const config = state.config || {};
|
||||
el.serverUrl.value = config.serverUrl || "";
|
||||
@ -451,6 +492,65 @@
|
||||
state.favoriteSeriesCategoryItemsById = {};
|
||||
}
|
||||
|
||||
function releaseInactiveTabItems(activeTab) {
|
||||
if (activeTab !== "live") {
|
||||
state.liveStreams = [];
|
||||
}
|
||||
if (activeTab !== "vod") {
|
||||
state.vodStreams = [];
|
||||
}
|
||||
if (activeTab !== "series") {
|
||||
state.seriesItems = [];
|
||||
state.expandedSeriesId = null;
|
||||
state.seriesEpisodesById = {};
|
||||
state.expandedSeasonBySeries = {};
|
||||
}
|
||||
}
|
||||
|
||||
function activeTabName() {
|
||||
const active = el.tabs.querySelector("button[data-tab].active");
|
||||
return String(active?.dataset?.tab || "settings");
|
||||
}
|
||||
|
||||
function scheduleLiveSearch() {
|
||||
if (state.liveSearchTimer) {
|
||||
clearTimeout(state.liveSearchTimer);
|
||||
}
|
||||
state.liveSearchTimer = setTimeout(() => {
|
||||
loadLiveStreams().catch(showError);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function scheduleVodSearch() {
|
||||
if (state.vodSearchTimer) {
|
||||
clearTimeout(state.vodSearchTimer);
|
||||
}
|
||||
state.vodSearchTimer = setTimeout(() => {
|
||||
loadVodStreams().catch(showError);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function scheduleSeriesSearch() {
|
||||
if (state.seriesSearchTimer) {
|
||||
clearTimeout(state.seriesSearchTimer);
|
||||
}
|
||||
state.seriesSearchTimer = setTimeout(() => {
|
||||
loadSeriesList().catch(showError);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function scheduleFavoritesSearch() {
|
||||
if (state.favoritesSearchTimer) {
|
||||
clearTimeout(state.favoritesSearchTimer);
|
||||
}
|
||||
state.favoritesSearchTimer = setTimeout(() => {
|
||||
state.favoritesOffset = 0;
|
||||
loadFavorites(String(el.favoritesSearch.value || "").trim())
|
||||
.then(() => renderFavorites())
|
||||
.catch(showError);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
async function loadLiveData() {
|
||||
ensureLibraryReady();
|
||||
const categoriesPayload = await apiJson("/api/library/categories?type=live");
|
||||
@ -462,10 +562,22 @@
|
||||
|
||||
async function loadLiveStreams() {
|
||||
ensureLibraryReady();
|
||||
const query = new URLSearchParams({type: "live"});
|
||||
if (el.liveCategory.value) {
|
||||
query.set("category_id", el.liveCategory.value);
|
||||
const categoryId = String(el.liveCategory.value || "").trim();
|
||||
const search = String(el.liveSearch.value || "").trim();
|
||||
if (!categoryId && search.length < 2) {
|
||||
state.liveStreams = [];
|
||||
renderLiveStreams("Select Live category or type at least 2 characters.");
|
||||
return;
|
||||
}
|
||||
const query = new URLSearchParams({type: "live"});
|
||||
if (categoryId) {
|
||||
query.set("category_id", categoryId);
|
||||
}
|
||||
if (search) {
|
||||
query.set("search", search);
|
||||
}
|
||||
query.set("limit", "300");
|
||||
query.set("offset", "0");
|
||||
const payload = await apiJson(`/api/library/items?${query.toString()}`);
|
||||
state.liveStreams = sanitizeLiveStreams(payload.items);
|
||||
renderLiveStreams();
|
||||
@ -500,15 +612,11 @@
|
||||
el.liveFavoriteCategory.setAttribute("aria-label", label);
|
||||
}
|
||||
|
||||
function renderLiveStreams() {
|
||||
const search = el.liveSearch.value.trim().toLowerCase();
|
||||
const filtered = state.liveStreams.filter((item) => {
|
||||
const name = String(item.name || "").toLowerCase();
|
||||
return !search || name.includes(search);
|
||||
});
|
||||
function renderLiveStreams(emptyMessage = "No live stream found.") {
|
||||
const filtered = state.liveStreams;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
el.liveList.innerHTML = `<li class="card muted">No live stream found.</li>`;
|
||||
el.liveList.innerHTML = `<li class="card muted">${esc(emptyMessage)}</li>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -539,7 +647,7 @@
|
||||
).catch(showError);
|
||||
});
|
||||
li.querySelector("button[data-action='toggle-favorite']").addEventListener("click", () => {
|
||||
toggleFavorite(favorite).then(() => renderLiveStreams()).catch(showError);
|
||||
toggleFavorite(favorite).then(() => loadLiveStreams()).catch(showError);
|
||||
});
|
||||
el.liveList.appendChild(li);
|
||||
});
|
||||
@ -585,24 +693,32 @@
|
||||
|
||||
async function loadVodStreams() {
|
||||
ensureLibraryReady();
|
||||
const query = new URLSearchParams({type: "vod"});
|
||||
if (el.vodCategory.value) {
|
||||
query.set("category_id", el.vodCategory.value);
|
||||
const categoryId = String(el.vodCategory.value || "").trim();
|
||||
const search = String(el.vodSearch.value || "").trim();
|
||||
if (!categoryId && search.length < 2) {
|
||||
state.vodStreams = [];
|
||||
renderVodStreams("Select VOD category or type at least 2 characters.");
|
||||
return;
|
||||
}
|
||||
const query = new URLSearchParams({type: "vod"});
|
||||
if (categoryId) {
|
||||
query.set("category_id", categoryId);
|
||||
}
|
||||
if (search) {
|
||||
query.set("search", search);
|
||||
}
|
||||
query.set("limit", "300");
|
||||
query.set("offset", "0");
|
||||
const payload = await apiJson(`/api/library/items?${query.toString()}`);
|
||||
state.vodStreams = sanitizeVodStreams(payload.items);
|
||||
renderVodStreams();
|
||||
}
|
||||
|
||||
function renderVodStreams() {
|
||||
const search = el.vodSearch.value.trim().toLowerCase();
|
||||
const filtered = state.vodStreams.filter((item) => {
|
||||
const name = String(item.name || "").toLowerCase();
|
||||
return !search || name.includes(search);
|
||||
});
|
||||
function renderVodStreams(emptyMessage = "No VOD stream found.") {
|
||||
const filtered = state.vodStreams;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
el.vodList.innerHTML = `<li class="card muted">No VOD stream found.</li>`;
|
||||
el.vodList.innerHTML = `<li class="card muted">${esc(emptyMessage)}</li>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -627,7 +743,7 @@
|
||||
.catch(showError);
|
||||
});
|
||||
li.querySelector("button[data-action='toggle-favorite']").addEventListener("click", () => {
|
||||
toggleFavorite(favorite).then(() => renderVodStreams()).catch(showError);
|
||||
toggleFavorite(favorite).then(() => loadVodStreams()).catch(showError);
|
||||
});
|
||||
el.vodList.appendChild(li);
|
||||
});
|
||||
@ -673,10 +789,24 @@
|
||||
|
||||
async function loadSeriesList() {
|
||||
ensureLibraryReady();
|
||||
const query = new URLSearchParams({type: "series"});
|
||||
if (el.seriesCategory.value) {
|
||||
query.set("category_id", el.seriesCategory.value);
|
||||
const categoryId = String(el.seriesCategory.value || "").trim();
|
||||
const search = String(el.seriesSearch.value || "").trim();
|
||||
if (!categoryId && search.length < 2) {
|
||||
state.seriesItems = [];
|
||||
state.expandedSeriesId = null;
|
||||
state.expandedSeasonBySeries = {};
|
||||
renderSeriesList("Select Series category or type at least 2 characters.");
|
||||
return;
|
||||
}
|
||||
const query = new URLSearchParams({type: "series"});
|
||||
if (categoryId) {
|
||||
query.set("category_id", categoryId);
|
||||
}
|
||||
if (search) {
|
||||
query.set("search", search);
|
||||
}
|
||||
query.set("limit", "300");
|
||||
query.set("offset", "0");
|
||||
const payload = await apiJson(`/api/library/items?${query.toString()}`);
|
||||
state.seriesItems = sanitizeSeriesItems(payload.items);
|
||||
if (state.expandedSeriesId
|
||||
@ -687,15 +817,11 @@
|
||||
renderSeriesList();
|
||||
}
|
||||
|
||||
function renderSeriesList() {
|
||||
const search = el.seriesSearch.value.trim().toLowerCase();
|
||||
const filtered = state.seriesItems.filter((item) => {
|
||||
const name = String(item.name || "").toLowerCase();
|
||||
return !search || name.includes(search);
|
||||
});
|
||||
function renderSeriesList(emptyMessage = "No series found.") {
|
||||
const filtered = state.seriesItems;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
el.seriesList.innerHTML = `<li class="card muted">No series found.</li>`;
|
||||
el.seriesList.innerHTML = `<li class="card muted">${esc(emptyMessage)}</li>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -719,7 +845,7 @@
|
||||
toggleSeriesEpisodes(item).catch(showError);
|
||||
});
|
||||
li.querySelector("button[data-action='toggle-favorite']").addEventListener("click", () => {
|
||||
toggleFavorite(favorite).then(() => renderSeriesList()).catch(showError);
|
||||
toggleFavorite(favorite).then(() => loadSeriesList()).catch(showError);
|
||||
});
|
||||
el.seriesList.appendChild(li);
|
||||
|
||||
@ -778,7 +904,7 @@
|
||||
}).catch(showError);
|
||||
});
|
||||
row.querySelector("button[data-action='toggle-favorite']").addEventListener("click", () => {
|
||||
toggleFavorite(episodeFavorite).then(() => renderSeriesList()).catch(showError);
|
||||
toggleFavorite(episodeFavorite).then(() => loadSeriesList()).catch(showError);
|
||||
});
|
||||
seasonList.appendChild(row);
|
||||
});
|
||||
@ -860,22 +986,81 @@
|
||||
localStorage.setItem(customStorageKey, JSON.stringify(state.customStreams));
|
||||
}
|
||||
|
||||
async function loadFavorites() {
|
||||
const payload = await apiJson("/api/favorites");
|
||||
state.favorites = sanitizeFavorites(payload.items);
|
||||
async function loadFavorites(searchRaw = "") {
|
||||
const search = String(searchRaw || "").trim();
|
||||
const query = new URLSearchParams();
|
||||
if (search) {
|
||||
query.set("search", search);
|
||||
}
|
||||
query.set("limit", String(state.favoritesLimit));
|
||||
query.set("offset", String(state.favoritesOffset));
|
||||
const path = query.toString() ? `/api/favorites?${query.toString()}` : "/api/favorites";
|
||||
const payload = await apiJson(path);
|
||||
const list = sanitizeFavorites(payload.items);
|
||||
state.favorites = list;
|
||||
list.forEach((item) => {
|
||||
const key = String(item?.key || "");
|
||||
if (key) {
|
||||
state.favoriteKeys.add(key);
|
||||
}
|
||||
});
|
||||
state.favoritesTotal = Number(payload?.total || 0);
|
||||
state.favoritesLimit = Math.max(1, Number(payload?.limit || state.favoritesLimit || 50));
|
||||
state.favoritesOffset = Math.max(0, Number(payload?.offset || 0));
|
||||
}
|
||||
|
||||
function isFavorite(key) {
|
||||
return state.favorites.some((item) => item?.key === key);
|
||||
return state.favoriteKeys.has(String(key || ""));
|
||||
}
|
||||
|
||||
function findFavoriteByKey(keyRaw) {
|
||||
const key = String(keyRaw || "").trim();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
return state.favorites.find((item) => String(item?.key || "") === key) || null;
|
||||
}
|
||||
|
||||
function clearFavoriteCachesForItem(favorite) {
|
||||
if (!favorite) {
|
||||
return;
|
||||
}
|
||||
const mode = String(favorite?.mode || "");
|
||||
const id = String(favorite?.id || "");
|
||||
if (mode === "series_item") {
|
||||
delete state.expandedFavoriteSeriesById[id];
|
||||
delete state.favoriteSeriesEpisodesById[id];
|
||||
delete state.expandedSeasonByFavoriteSeries[id];
|
||||
}
|
||||
if (mode === "live_category") {
|
||||
delete state.expandedFavoriteLiveCategoryById[id];
|
||||
delete state.favoriteLiveCategoryStreamsById[id];
|
||||
}
|
||||
if (mode === "vod_category") {
|
||||
delete state.expandedFavoriteVodCategoryById[id];
|
||||
delete state.favoriteVodCategoryStreamsById[id];
|
||||
}
|
||||
if (mode === "series_category") {
|
||||
delete state.expandedFavoriteSeriesCategoryById[id];
|
||||
delete state.favoriteSeriesCategoryItemsById[id];
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadFavoritesCurrentPage() {
|
||||
await loadFavorites(String(el.favoritesSearch.value || "").trim());
|
||||
if (state.favorites.length === 0 && state.favoritesOffset > 0) {
|
||||
state.favoritesOffset = Math.max(0, state.favoritesOffset - state.favoritesLimit);
|
||||
await loadFavorites(String(el.favoritesSearch.value || "").trim());
|
||||
}
|
||||
renderFavorites();
|
||||
}
|
||||
|
||||
async function toggleFavorite(favorite) {
|
||||
if (!favorite || !favorite.key) {
|
||||
return;
|
||||
}
|
||||
if (isFavorite(favorite.key)) {
|
||||
await deleteFavoriteByKey(favorite.key);
|
||||
} else {
|
||||
const deleted = await deleteFavoriteByKey(favorite.key);
|
||||
if (!deleted) {
|
||||
const payload = {
|
||||
...favorite,
|
||||
createdAt: Number(favorite?.createdAt || Date.now())
|
||||
@ -891,13 +1076,66 @@
|
||||
}
|
||||
state.favorites = state.favorites.filter((item) => item?.key !== savedItem.key);
|
||||
state.favorites.unshift(savedItem);
|
||||
state.favoriteKeys.add(savedItem.key);
|
||||
}
|
||||
renderFavorites();
|
||||
await reloadFavoritesCurrentPage();
|
||||
updateLiveCategoryFavoriteButton();
|
||||
updateVodCategoryFavoriteButton();
|
||||
updateSeriesCategoryFavoriteButton();
|
||||
}
|
||||
|
||||
async function onFavoritesListClick(event) {
|
||||
const actionNode = event.target.closest("[data-action]");
|
||||
if (!actionNode || !el.favoritesList.contains(actionNode)) {
|
||||
return;
|
||||
}
|
||||
const action = String(actionNode.dataset.action || "");
|
||||
const key = String(actionNode.dataset.key || "");
|
||||
const favorite = findFavoriteByKey(key);
|
||||
if (!favorite && action !== "toggle-favorite-season" && action !== "play-title" && action !== "toggle-favorite") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (action === "toggle-favorite-series" && favorite) {
|
||||
await toggleFavoriteSeriesItem(favorite);
|
||||
return;
|
||||
}
|
||||
if (action === "toggle-favorite-live-category" && favorite) {
|
||||
await toggleFavoriteLiveCategory(favorite);
|
||||
return;
|
||||
}
|
||||
if (action === "toggle-favorite-vod-category" && favorite) {
|
||||
await toggleFavoriteVodCategory(favorite);
|
||||
return;
|
||||
}
|
||||
if (action === "toggle-favorite-series-category" && favorite) {
|
||||
await toggleFavoriteSeriesCategory(favorite);
|
||||
return;
|
||||
}
|
||||
if (action === "open-favorite" && favorite) {
|
||||
await openFavorite(favorite);
|
||||
return;
|
||||
}
|
||||
if (action === "remove-favorite" && favorite) {
|
||||
await deleteFavoriteByKey(favorite.key);
|
||||
clearFavoriteCachesForItem(favorite);
|
||||
await reloadFavoritesCurrentPage();
|
||||
if (activeTabName() === "live") {
|
||||
await loadLiveStreams();
|
||||
}
|
||||
if (activeTabName() === "vod") {
|
||||
await loadVodStreams();
|
||||
}
|
||||
if (activeTabName() === "series") {
|
||||
await loadSeriesList();
|
||||
}
|
||||
renderCustomStreams();
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderFavorites() {
|
||||
const search = el.favoritesSearch.value.trim().toLowerCase();
|
||||
const matchesText = (value) => String(value || "").toLowerCase().includes(search);
|
||||
@ -929,6 +1167,11 @@
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const from = state.favoritesTotal === 0 ? 0 : state.favoritesOffset + 1;
|
||||
const to = Math.min(state.favoritesOffset + state.favoritesLimit, state.favoritesTotal);
|
||||
el.favoritesPageInfo.textContent = `Showing ${from}-${to} of ${state.favoritesTotal}`;
|
||||
el.favoritesPrev.disabled = state.favoritesOffset <= 0;
|
||||
el.favoritesNext.disabled = state.favoritesOffset + state.favoritesLimit >= state.favoritesTotal;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
el.favoritesList.innerHTML = `<li class="card muted">No favorites yet.</li>`;
|
||||
@ -941,6 +1184,9 @@
|
||||
const favoriteTone = favoriteToneClass(favorite);
|
||||
const favoriteBadge = favoriteBadgeLabel(favorite);
|
||||
li.className = `stream-item favorite-item ${favoriteTone}`;
|
||||
li.dataset.favoriteKey = String(favorite?.key || "");
|
||||
li.dataset.favoriteMode = String(favorite?.mode || "");
|
||||
li.dataset.favoriteId = String(favorite?.id || "");
|
||||
const isSeriesItem = favorite?.mode === "series_item";
|
||||
const isLiveCategory = favorite?.mode === "live_category";
|
||||
const isVodCategory = favorite?.mode === "vod_category";
|
||||
@ -957,99 +1203,52 @@
|
||||
li.innerHTML = isSeriesItem
|
||||
? `
|
||||
<div>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-series">${isSeriesItemExpanded ? "Hide" : "Show"} ${esc(favorite.title || "Untitled")}</button>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-series" data-key="${esc(favorite.key)}">${isSeriesItemExpanded ? "Hide" : "Show"} ${esc(favorite.title || "Untitled")}</button>
|
||||
<div class="stream-meta"><span class="fav-type-badge">${esc(favoriteBadge)}</span>${esc(favoriteSummary(favorite))}</div>
|
||||
</div>
|
||||
<div class="stream-actions">
|
||||
<button type="button" data-action="remove-favorite" class="danger">Remove</button>
|
||||
<button type="button" data-action="remove-favorite" data-key="${esc(favorite.key)}" class="danger">Remove</button>
|
||||
</div>
|
||||
`
|
||||
: isLiveCategory
|
||||
? `
|
||||
<div>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-live-category">${isLiveCategoryExpanded ? "Hide" : "Show"} ${esc(favorite.title || "Untitled")}</button>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-live-category" data-key="${esc(favorite.key)}">${isLiveCategoryExpanded ? "Hide" : "Show"} ${esc(favorite.title || "Untitled")}</button>
|
||||
<div class="stream-meta"><span class="fav-type-badge">${esc(favoriteBadge)}</span>${esc(favoriteSummary(favorite))}</div>
|
||||
</div>
|
||||
<div class="stream-actions">
|
||||
<button type="button" data-action="remove-favorite" class="danger">Remove</button>
|
||||
<button type="button" data-action="remove-favorite" data-key="${esc(favorite.key)}" class="danger">Remove</button>
|
||||
</div>
|
||||
`
|
||||
: isVodCategory
|
||||
? `
|
||||
<div>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-vod-category">${isVodCategoryExpanded ? "Hide" : "Show"} ${esc(favorite.title || "Untitled")}</button>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-vod-category" data-key="${esc(favorite.key)}">${isVodCategoryExpanded ? "Hide" : "Show"} ${esc(favorite.title || "Untitled")}</button>
|
||||
<div class="stream-meta"><span class="fav-type-badge">${esc(favoriteBadge)}</span>${esc(favoriteSummary(favorite))}</div>
|
||||
</div>
|
||||
<div class="stream-actions">
|
||||
<button type="button" data-action="remove-favorite" class="danger">Remove</button>
|
||||
<button type="button" data-action="remove-favorite" data-key="${esc(favorite.key)}" class="danger">Remove</button>
|
||||
</div>
|
||||
`
|
||||
: isSeriesCategory
|
||||
? `
|
||||
<div>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-series-category">${isSeriesCategoryExpanded ? "Hide" : "Show"} ${esc(favorite.title || "Untitled")}</button>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-series-category" data-key="${esc(favorite.key)}">${isSeriesCategoryExpanded ? "Hide" : "Show"} ${esc(favorite.title || "Untitled")}</button>
|
||||
<div class="stream-meta"><span class="fav-type-badge">${esc(favoriteBadge)}</span>${esc(favoriteSummary(favorite))}</div>
|
||||
</div>
|
||||
<div class="stream-actions">
|
||||
<button type="button" data-action="remove-favorite" class="danger">Remove</button>
|
||||
<button type="button" data-action="remove-favorite" data-key="${esc(favorite.key)}" class="danger">Remove</button>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div>
|
||||
<button type="button" class="stream-title stream-link" data-action="open-favorite">${esc(favorite.title || "Untitled")}</button>
|
||||
<button type="button" class="stream-title stream-link" data-action="open-favorite" data-key="${esc(favorite.key)}">${esc(favorite.title || "Untitled")}</button>
|
||||
<div class="stream-meta"><span class="fav-type-badge">${esc(favoriteBadge)}</span>${esc(favoriteSummary(favorite))}</div>
|
||||
</div>
|
||||
<div class="stream-actions">
|
||||
<button type="button" data-action="remove-favorite" class="danger">Remove</button>
|
||||
<button type="button" data-action="remove-favorite" data-key="${esc(favorite.key)}" class="danger">Remove</button>
|
||||
</div>
|
||||
`;
|
||||
if (isSeriesItem) {
|
||||
li.querySelector("button[data-action='toggle-favorite-series']").addEventListener("click", () => {
|
||||
toggleFavoriteSeriesItem(favorite).catch(showError);
|
||||
});
|
||||
} else if (isLiveCategory) {
|
||||
li.querySelector("button[data-action='toggle-favorite-live-category']").addEventListener("click", () => {
|
||||
toggleFavoriteLiveCategory(favorite).catch(showError);
|
||||
});
|
||||
} else if (isVodCategory) {
|
||||
li.querySelector("button[data-action='toggle-favorite-vod-category']").addEventListener("click", () => {
|
||||
toggleFavoriteVodCategory(favorite).catch(showError);
|
||||
});
|
||||
} else if (isSeriesCategory) {
|
||||
li.querySelector("button[data-action='toggle-favorite-series-category']").addEventListener("click", () => {
|
||||
toggleFavoriteSeriesCategory(favorite).catch(showError);
|
||||
});
|
||||
} else {
|
||||
li.querySelector("button[data-action='open-favorite']").addEventListener("click", () => {
|
||||
openFavorite(favorite).catch(showError);
|
||||
});
|
||||
}
|
||||
li.querySelector("button[data-action='remove-favorite']").addEventListener("click", () => {
|
||||
deleteFavoriteByKey(favorite.key).then(() => {
|
||||
if (favorite?.mode === "series_item") {
|
||||
delete state.expandedFavoriteSeriesById[seriesId];
|
||||
delete state.favoriteSeriesEpisodesById[seriesId];
|
||||
delete state.expandedSeasonByFavoriteSeries[seriesId];
|
||||
}
|
||||
if (favorite?.mode === "live_category") {
|
||||
delete state.expandedFavoriteLiveCategoryById[liveCategoryId];
|
||||
delete state.favoriteLiveCategoryStreamsById[liveCategoryId];
|
||||
}
|
||||
if (favorite?.mode === "vod_category") {
|
||||
delete state.expandedFavoriteVodCategoryById[vodCategoryId];
|
||||
delete state.favoriteVodCategoryStreamsById[vodCategoryId];
|
||||
}
|
||||
if (favorite?.mode === "series_category") {
|
||||
delete state.expandedFavoriteSeriesCategoryById[seriesCategoryId];
|
||||
delete state.favoriteSeriesCategoryItemsById[seriesCategoryId];
|
||||
}
|
||||
renderFavorites();
|
||||
renderLiveStreams();
|
||||
renderVodStreams();
|
||||
renderSeriesList();
|
||||
renderCustomStreams();
|
||||
}).catch(showError);
|
||||
});
|
||||
el.favoritesList.appendChild(li);
|
||||
|
||||
if (!isSeriesItem && !isLiveCategory && !isVodCategory && !isSeriesCategory) {
|
||||
@ -1754,13 +1953,15 @@
|
||||
async function deleteFavoriteByKey(keyRaw) {
|
||||
const key = String(keyRaw || "").trim();
|
||||
if (!key) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
await apiJson(`/api/favorites?key=${encodeURIComponent(key)}`, {method: "DELETE"});
|
||||
const response = await apiJson(`/api/favorites?key=${encodeURIComponent(key)}`, {method: "DELETE"});
|
||||
state.favorites = state.favorites.filter((item) => item?.key !== key);
|
||||
state.favoriteKeys.delete(key);
|
||||
updateLiveCategoryFavoriteButton();
|
||||
updateVodCategoryFavoriteButton();
|
||||
updateSeriesCategoryFavoriteButton();
|
||||
return Boolean(response?.deleted);
|
||||
}
|
||||
|
||||
function renderCustomStreams() {
|
||||
@ -2513,7 +2714,20 @@
|
||||
}
|
||||
|
||||
async function apiJson(url, options = {}) {
|
||||
const response = await fetch(url, options);
|
||||
const fetchOptions = {...options};
|
||||
if (!fetchOptions.cache) {
|
||||
fetchOptions.cache = "no-store";
|
||||
}
|
||||
const headers = new Headers(fetchOptions.headers || {});
|
||||
if (!headers.has("Cache-Control")) {
|
||||
headers.set("Cache-Control", "no-cache");
|
||||
}
|
||||
if (!headers.has("Pragma")) {
|
||||
headers.set("Pragma", "no-cache");
|
||||
}
|
||||
fetchOptions.headers = headers;
|
||||
|
||||
const response = await fetch(url, fetchOptions);
|
||||
const text = await response.text();
|
||||
let parsed;
|
||||
try {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Xtream Player</title>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<link rel="stylesheet" href="/assets/style.20260304b.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-glow"></div>
|
||||
@ -148,6 +148,13 @@
|
||||
Search
|
||||
<input id="favorites-search" type="search" placeholder="Filter favorites">
|
||||
</label>
|
||||
<div class="card controls">
|
||||
<div class="actions">
|
||||
<button id="favorites-prev" type="button">Previous</button>
|
||||
<button id="favorites-next" type="button">Next</button>
|
||||
</div>
|
||||
<div id="favorites-page-info" class="muted">Page 1</div>
|
||||
</div>
|
||||
<ul id="favorites-list" class="stream-list"></ul>
|
||||
</article>
|
||||
</section>
|
||||
@ -198,6 +205,6 @@
|
||||
</main>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.18/dist/hls.min.js"></script>
|
||||
<script src="/assets/app.js"></script>
|
||||
<script src="/assets/app.20260304b.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user