refactor(logging): replace raw console calls with a leveled logger facade
484 ungated console.* calls across 63 frontend files shipped to end users — 248 console.log occurrences were verified present in the built bundle. The Rust half of the app has used the log crate with a LevelFilter and a RUST_LOG override since the beginning; the frontend had no equivalent. Adds src/lib/utils/logger.ts: four levels, scoped loggers replacing the hand-written "[Scope] " prefixes, debug in dev and warn in production, and a localStorage override so a user can turn verbose logging on in a shipped build to file a bug report. warn and error are never gated away. The sweep itself is mechanical — no control flow, error handling, or message semantics changed. TRACES: | DR-204 | UT-201
This commit is contained in:
@@ -19,6 +19,9 @@ import type {
|
|||||||
PlaylistEntry,
|
PlaylistEntry,
|
||||||
PlaylistCreatedResult,
|
PlaylistCreatedResult,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("RepositoryClient");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository client - thin wrapper over Rust HybridRepository
|
* Repository client - thin wrapper over Rust HybridRepository
|
||||||
@@ -39,14 +42,14 @@ export class RepositoryClient {
|
|||||||
accessToken: string,
|
accessToken: string,
|
||||||
serverId: string
|
serverId: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
console.log("[RepositoryClient] Creating Rust repository...");
|
log.debug("Creating Rust repository...");
|
||||||
this.handle = await commands.repositoryCreate(serverUrl, userId, accessToken, serverId);
|
this.handle = await commands.repositoryCreate(serverUrl, userId, accessToken, serverId);
|
||||||
|
|
||||||
// Store for URL construction
|
// Store for URL construction
|
||||||
this._serverUrl = serverUrl;
|
this._serverUrl = serverUrl;
|
||||||
this._accessToken = accessToken;
|
this._accessToken = accessToken;
|
||||||
|
|
||||||
console.log("[RepositoryClient] Repository created with handle:", this.handle);
|
log.debug("Repository created with handle:", this.handle);
|
||||||
return this.handle;
|
return this.handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
import { haptics } from "$lib/utils/haptics";
|
import { haptics } from "$lib/utils/haptics";
|
||||||
import { toast } from "$lib/stores/toast";
|
import { toast } from "$lib/stores/toast";
|
||||||
import { favoriteOverrides } from "$lib/stores/favorites";
|
import { favoriteOverrides } from "$lib/stores/favorites";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("FavoriteButton");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
itemId: string;
|
itemId: string;
|
||||||
@@ -78,7 +81,7 @@
|
|||||||
isAnimating = false;
|
isAnimating = false;
|
||||||
}, 600);
|
}, 600);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to toggle favorite:", error);
|
log.error("Failed to toggle favorite:", error);
|
||||||
toast.show("Failed to update favorites", "error");
|
toast.show("Failed to update favorites", "error");
|
||||||
isAnimating = false;
|
isAnimating = false;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { downloads, type DownloadInfo } from "$lib/stores/downloads";
|
import { downloads, type DownloadInfo } from "$lib/stores/downloads";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("DownloadItem");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
download: DownloadInfo;
|
download: DownloadInfo;
|
||||||
@@ -69,7 +72,7 @@
|
|||||||
// Refresh to update UI
|
// Refresh to update UI
|
||||||
await downloads.refresh(download.userId);
|
await downloads.refresh(download.userId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to pause download:", error);
|
log.error("Failed to pause download:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +82,7 @@
|
|||||||
// Refresh to update UI
|
// Refresh to update UI
|
||||||
await downloads.refresh(download.userId);
|
await downloads.refresh(download.userId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to resume download:", error);
|
log.error("Failed to resume download:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +92,7 @@
|
|||||||
// Refresh to update UI
|
// Refresh to update UI
|
||||||
await downloads.refresh(download.userId);
|
await downloads.refresh(download.userId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to cancel download:", error);
|
log.error("Failed to cancel download:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +102,7 @@
|
|||||||
// Refresh to update UI
|
// Refresh to update UI
|
||||||
await downloads.refresh(download.userId);
|
await downloads.refresh(download.userId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete download:", error);
|
log.error("Failed to delete download:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { downloads } from "$lib/stores/downloads";
|
import { downloads } from "$lib/stores/downloads";
|
||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import type { MediaItem } from "$lib/api/types";
|
import type { MediaItem } from "$lib/api/types";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("AlbumDownloadButton");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
albumId: string;
|
albumId: string;
|
||||||
@@ -60,7 +63,7 @@
|
|||||||
try {
|
try {
|
||||||
const userId = $auth.user?.id;
|
const userId = $auth.user?.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.error("No user ID found");
|
log.error("No user ID found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +98,7 @@
|
|||||||
await downloads.refresh(userId);
|
await downloads.refresh(userId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Album download operation failed:", error);
|
log.error("Album download operation failed:", error);
|
||||||
} finally {
|
} finally {
|
||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||||
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
|
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
|
||||||
import { favoriteOverrides, resolveIsFavorite } from "$lib/stores/favorites";
|
import { favoriteOverrides, resolveIsFavorite } from "$lib/stores/favorites";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("ArtistDetailView");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
artist: MediaItem;
|
artist: MediaItem;
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
});
|
});
|
||||||
albums = albumsResult.items.filter(item => item.kind === "album");
|
albums = albumsResult.items.filter(item => item.kind === "album");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load albums:", e);
|
log.warn("Failed to load albums:", e);
|
||||||
} finally {
|
} finally {
|
||||||
albumsLoading = false;
|
albumsLoading = false;
|
||||||
}
|
}
|
||||||
@@ -62,7 +65,7 @@
|
|||||||
});
|
});
|
||||||
topTracks = tracksResult.items.filter(item => item.kind === "track");
|
topTracks = tracksResult.items.filter(item => item.kind === "track");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load tracks:", e);
|
log.warn("Failed to load tracks:", e);
|
||||||
} finally {
|
} finally {
|
||||||
tracksLoading = false;
|
tracksLoading = false;
|
||||||
}
|
}
|
||||||
@@ -82,14 +85,14 @@
|
|||||||
.slice(0, 6);
|
.slice(0, 6);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load related artists:", e);
|
log.warn("Failed to load related artists:", e);
|
||||||
} finally {
|
} finally {
|
||||||
artistsLoading = false;
|
artistsLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
singlesLoading = false;
|
singlesLoading = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error loading artist content:", e);
|
log.error("Error loading artist content:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import { isServerReachable } from "$lib/stores/connectivity";
|
import { isServerReachable } from "$lib/stores/connectivity";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("ClearHistoryButton");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Series or season id to clear. */
|
/** Series or season id to clear. */
|
||||||
@@ -51,7 +54,7 @@
|
|||||||
await auth.getRepository().clearWatchHistory(itemId);
|
await auth.getRepository().clearWatchHistory(itemId);
|
||||||
onCleared?.();
|
onCleared?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to clear watch history:", e);
|
log.error("Failed to clear watch history:", e);
|
||||||
alert(
|
alert(
|
||||||
`Could not clear watch history: ${e instanceof Error ? e.message : String(e)}`
|
`Could not clear watch history: ${e instanceof Error ? e.message : String(e)}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
import DownloadButtonCore from "./DownloadButtonCore.svelte";
|
import DownloadButtonCore from "./DownloadButtonCore.svelte";
|
||||||
import type { DownloadState } from "./DownloadButtonCore.svelte";
|
import type { DownloadState } from "./DownloadButtonCore.svelte";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("DownloadButton");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single audio track download button
|
* Single audio track download button
|
||||||
@@ -39,7 +42,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function handleClick() {
|
async function handleClick() {
|
||||||
console.log("🖱️ Download button clicked! Current status:", status);
|
log.debug("🖱️ Download button clicked! Current status:", status);
|
||||||
if (isProcessing) return;
|
if (isProcessing) return;
|
||||||
|
|
||||||
isProcessing = true;
|
isProcessing = true;
|
||||||
@@ -63,25 +66,25 @@
|
|||||||
// Start download
|
// Start download
|
||||||
const userId = $auth.user?.id;
|
const userId = $auth.user?.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.error("No user ID found");
|
log.error("No user ID found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
|
|
||||||
console.log("🎯 Starting download for item:", itemId);
|
log.debug("🎯 Starting download for item:", itemId);
|
||||||
|
|
||||||
// Get stream URL
|
// Get stream URL
|
||||||
const streamUrl = await repo.getAudioStreamUrl(itemId);
|
const streamUrl = await repo.getAudioStreamUrl(itemId);
|
||||||
console.log(" Stream URL obtained:", streamUrl?.substring(0, 50) + "...");
|
log.debug(" Stream URL obtained:", streamUrl?.substring(0, 50) + "...");
|
||||||
if (!streamUrl) {
|
if (!streamUrl) {
|
||||||
throw new Error("Failed to get stream URL");
|
throw new Error("Failed to get stream URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get target directory
|
// Get target directory
|
||||||
const targetDir = await commands.storageGetPath();
|
const targetDir = await commands.storageGetPath();
|
||||||
console.log(" Target directory:", targetDir);
|
log.debug(" Target directory:", targetDir);
|
||||||
|
|
||||||
// Queue and start download in single atomic operation
|
// Queue and start download in single atomic operation
|
||||||
const downloadId = await commands.downloadItemAndStart({
|
const downloadId = await commands.downloadItemAndStart({
|
||||||
@@ -93,16 +96,16 @@
|
|||||||
artistName: artistName || null,
|
artistName: artistName || null,
|
||||||
albumName: albumName || null,
|
albumName: albumName || null,
|
||||||
});
|
});
|
||||||
console.log(" Download queued and started with ID:", downloadId);
|
log.debug(" Download queued and started with ID:", downloadId);
|
||||||
|
|
||||||
// Refresh downloads list
|
// Refresh downloads list
|
||||||
await downloads.refresh(userId);
|
await downloads.refresh(userId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("❌ Failed to start download:", e);
|
log.error("❌ Failed to start download:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Download operation failed:", error);
|
log.error("Download operation failed:", error);
|
||||||
} finally {
|
} finally {
|
||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload";
|
import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload";
|
||||||
import { useOfflineFilterReload } from "$lib/composables/useOfflineFilterReload";
|
import { useOfflineFilterReload } from "$lib/composables/useOfflineFilterReload";
|
||||||
import type { Genre, MediaItem, ItemType } from "$lib/api/types";
|
import type { Genre, MediaItem, ItemType } from "$lib/api/types";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("GenericGenreBrowser");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic genre browser supporting Movies, Music Albums, and TV Series
|
* Generic genre browser supporting Movies, Music Albums, and TV Series
|
||||||
@@ -92,7 +95,7 @@
|
|||||||
genres = result.sort((a, b) => a.name.localeCompare(b.name));
|
genres = result.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
applyFilter();
|
applyFilter();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load genres:", e);
|
log.error("Failed to load genres:", e);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -115,7 +118,7 @@
|
|||||||
});
|
});
|
||||||
genreItems = result.items;
|
genreItems = result.items;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load genre items:", e);
|
log.error("Failed to load genre items:", e);
|
||||||
} finally {
|
} finally {
|
||||||
loadingItems = false;
|
loadingItems = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
import TrackList from "./TrackList.svelte";
|
import TrackList from "./TrackList.svelte";
|
||||||
import AlphabetScrollBar from "./AlphabetScrollBar.svelte";
|
import AlphabetScrollBar from "./AlphabetScrollBar.svelte";
|
||||||
import { excludePodcasts } from "$lib/utils/podcastFilter";
|
import { excludePodcasts } from "$lib/utils/podcastFilter";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("GenericMediaListPage");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic media list page supporting Albums, Artists, Playlists, and Tracks
|
* Generic media list page supporting Albums, Artists, Playlists, and Tracks
|
||||||
@@ -154,7 +157,7 @@
|
|||||||
items = excludePodcasts(result.items);
|
items = excludePodcasts(result.items);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to load ${config.itemType}:`, e);
|
log.error(`Failed to load ${config.itemType}:`, e);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||||
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
|
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
|
||||||
import { favoriteOverrides, resolveIsFavorite } from "$lib/stores/favorites";
|
import { favoriteOverrides, resolveIsFavorite } from "$lib/stores/favorites";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("MediaCard");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: MediaItem | Library;
|
item: MediaItem | Library;
|
||||||
@@ -171,7 +174,7 @@
|
|||||||
media.albumName ?? undefined
|
media.albumName ?? undefined
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[MediaCard] Failed to queue download:", err);
|
log.error("Failed to queue download:", err);
|
||||||
queueError = "Failed to queue";
|
queueError = "Failed to queue";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
import LibraryGrid from "./LibraryGrid.svelte";
|
import LibraryGrid from "./LibraryGrid.svelte";
|
||||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("PersonDetailView");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
person: MediaItem;
|
person: MediaItem;
|
||||||
@@ -34,7 +37,7 @@
|
|||||||
movies = result.items.filter(item => item.kind === "movie");
|
movies = result.items.filter(item => item.kind === "movie");
|
||||||
series = result.items.filter(item => item.kind === "series");
|
series = result.items.filter(item => item.kind === "series");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load filmography:", e);
|
log.error("Failed to load filmography:", e);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
|
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
|
||||||
import { favoriteOverrides } from "$lib/stores/favorites";
|
import { favoriteOverrides } from "$lib/stores/favorites";
|
||||||
import { formatDuration } from "$lib/utils/duration";
|
import { formatDuration } from "$lib/utils/duration";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("PlaylistDetail");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
playlist: MediaItem;
|
playlist: MediaItem;
|
||||||
@@ -40,7 +43,7 @@
|
|||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
entries = await repo.getPlaylistItems(playlist.id);
|
entries = await repo.getPlaylistItems(playlist.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[PlaylistDetail] Failed to load items:", e);
|
log.error("Failed to load items:", e);
|
||||||
toast.error("Failed to load playlist items");
|
toast.error("Failed to load playlist items");
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
@@ -62,7 +65,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[PlaylistDetail] Failed to play all:", e);
|
log.error("Failed to play all:", e);
|
||||||
toast.error("Failed to play playlist");
|
toast.error("Failed to play playlist");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +85,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[PlaylistDetail] Failed to shuffle play:", e);
|
log.error("Failed to shuffle play:", e);
|
||||||
toast.error("Failed to shuffle playlist");
|
toast.error("Failed to shuffle playlist");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +103,7 @@
|
|||||||
playlist.name = trimmed;
|
playlist.name = trimmed;
|
||||||
toast.success("Playlist renamed");
|
toast.success("Playlist renamed");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[PlaylistDetail] Failed to rename:", e);
|
log.error("Failed to rename:", e);
|
||||||
toast.error("Failed to rename playlist");
|
toast.error("Failed to rename playlist");
|
||||||
editName = playlist.name;
|
editName = playlist.name;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -115,7 +118,7 @@
|
|||||||
toast.success("Playlist deleted");
|
toast.success("Playlist deleted");
|
||||||
goto("/library");
|
goto("/library");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[PlaylistDetail] Failed to delete:", e);
|
log.error("Failed to delete:", e);
|
||||||
toast.error("Failed to delete playlist");
|
toast.error("Failed to delete playlist");
|
||||||
} finally {
|
} finally {
|
||||||
showDeleteConfirm = false;
|
showDeleteConfirm = false;
|
||||||
@@ -129,7 +132,7 @@
|
|||||||
entries = entries.filter(e => e.playlistItemId !== entry.playlistItemId);
|
entries = entries.filter(e => e.playlistItemId !== entry.playlistItemId);
|
||||||
toast.success("Track removed");
|
toast.success("Track removed");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[PlaylistDetail] Failed to remove track:", e);
|
log.error("Failed to remove track:", e);
|
||||||
toast.error("Failed to remove track");
|
toast.error("Failed to remove track");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import type { MediaItem, MediaKind, Person } from "$lib/api/types";
|
import type { MediaItem, MediaKind, Person } from "$lib/api/types";
|
||||||
import MediaCard from "./MediaCard.svelte";
|
import MediaCard from "./MediaCard.svelte";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("RelatedItemsSection");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentItemId: string;
|
currentItemId: string;
|
||||||
@@ -57,7 +60,7 @@
|
|||||||
return; // Success - return early
|
return; // Success - return early
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load similar items from API:", e);
|
log.warn("Failed to load similar items from API:", e);
|
||||||
// Fall through to genre-based loading
|
// Fall through to genre-based loading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +81,7 @@
|
|||||||
|
|
||||||
items = result.items.filter(item => item.id !== currentItemId);
|
items = result.items.filter(item => item.id !== currentItemId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load related items by genre:", e);
|
log.warn("Failed to load related items by genre:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +97,7 @@
|
|||||||
const artistAlbums = result.items.filter(item => item.id !== currentItemId);
|
const artistAlbums = result.items.filter(item => item.id !== currentItemId);
|
||||||
items = [...items, ...artistAlbums];
|
items = [...items, ...artistAlbums];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load albums by artist:", e);
|
log.warn("Failed to load albums by artist:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +109,7 @@
|
|||||||
relatedItems = uniqueItems;
|
relatedItems = uniqueItems;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : "Failed to load related items";
|
error = e instanceof Error ? e.message : "Failed to load related items";
|
||||||
console.error("Error loading related items:", e);
|
log.error("Error loading related items:", e);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
|
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("SeasonDownloadButton");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
seasonId: string;
|
seasonId: string;
|
||||||
@@ -46,11 +49,11 @@
|
|||||||
try {
|
try {
|
||||||
const userId = $auth.user?.id;
|
const userId = $auth.user?.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.error("No user ID found");
|
log.error("No user ID found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("📺 Starting season download for:", seasonName, "quality:", quality);
|
log.debug("📺 Starting season download for:", seasonName, "quality:", quality);
|
||||||
|
|
||||||
// Get target directory
|
// Get target directory
|
||||||
const targetDir = await commands.storageGetPath();
|
const targetDir = await commands.storageGetPath();
|
||||||
@@ -67,7 +70,7 @@
|
|||||||
quality
|
quality
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`✅ Queued ${downloadIds.length} episodes for download`);
|
log.debug(`✅ Queued ${downloadIds.length} episodes for download`);
|
||||||
|
|
||||||
// Pin the season item
|
// Pin the season item
|
||||||
await downloads.pinItem(seasonId);
|
await downloads.pinItem(seasonId);
|
||||||
@@ -77,9 +80,9 @@
|
|||||||
// rest as slots free up.
|
// rest as slots free up.
|
||||||
const handle = auth.getRepository().getHandle();
|
const handle = auth.getRepository().getHandle();
|
||||||
await commands.enqueueVideoDownloads(handle, downloadIds, targetDir);
|
await commands.enqueueVideoDownloads(handle, downloadIds, targetDir);
|
||||||
console.log(" Episodes enqueued; backend pump will start them");
|
log.debug(" Episodes enqueued; backend pump will start them");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start season download:", error);
|
log.error("Failed to start season download:", error);
|
||||||
} finally {
|
} finally {
|
||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
|
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("SeriesDownloadButton");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
seriesId: string;
|
seriesId: string;
|
||||||
@@ -40,11 +43,11 @@
|
|||||||
try {
|
try {
|
||||||
const userId = $auth.user?.id;
|
const userId = $auth.user?.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.error("No user ID found");
|
log.error("No user ID found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("📺 Starting series download for:", seriesName, "quality:", quality);
|
log.debug("📺 Starting series download for:", seriesName, "quality:", quality);
|
||||||
|
|
||||||
// Get target directory
|
// Get target directory
|
||||||
const targetDir = await commands.storageGetPath();
|
const targetDir = await commands.storageGetPath();
|
||||||
@@ -59,7 +62,7 @@
|
|||||||
quality
|
quality
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(` Queued ${downloadIds.length} episodes for download`);
|
log.debug(` Queued ${downloadIds.length} episodes for download`);
|
||||||
|
|
||||||
// Pin the series item
|
// Pin the series item
|
||||||
await downloads.pinItem(seriesId);
|
await downloads.pinItem(seriesId);
|
||||||
@@ -69,9 +72,9 @@
|
|||||||
// rest as slots free up.
|
// rest as slots free up.
|
||||||
const handle = auth.getRepository().getHandle();
|
const handle = auth.getRepository().getHandle();
|
||||||
await commands.enqueueVideoDownloads(handle, downloadIds, targetDir);
|
await commands.enqueueVideoDownloads(handle, downloadIds, targetDir);
|
||||||
console.log(" Episodes enqueued; backend pump will start them");
|
log.debug(" Episodes enqueued; backend pump will start them");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start series download:", error);
|
log.error("Failed to start series download:", error);
|
||||||
} finally {
|
} finally {
|
||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
import AddToPlaylistModal from "$lib/components/playlist/AddToPlaylistModal.svelte";
|
import AddToPlaylistModal from "$lib/components/playlist/AddToPlaylistModal.svelte";
|
||||||
import { calculateMenuPosition, type MenuPosition } from "$lib/utils/menuPosition";
|
import { calculateMenuPosition, type MenuPosition } from "$lib/utils/menuPosition";
|
||||||
import { formatDuration } from "$lib/utils/duration";
|
import { formatDuration } from "$lib/utils/duration";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("TrackList");
|
||||||
|
|
||||||
/** Queue context for remote transfer - what type of queue is this? */
|
/** Queue context for remote transfer - what type of queue is this? */
|
||||||
export type QueueContext =
|
export type QueueContext =
|
||||||
@@ -55,7 +58,7 @@
|
|||||||
|
|
||||||
// If this is an album, use the backend album command (more efficient)
|
// If this is an album, use the backend album command (more efficient)
|
||||||
if (context && context.type === "album") {
|
if (context && context.type === "album") {
|
||||||
console.log(`[TrackList] Playing track: "${track.name}" (ID: ${track.id}, index in list: ${index})`);
|
log.debug(`Playing track: "${track.name}" (ID: ${track.id}, index in list: ${index})`);
|
||||||
await playerController.playAlbumTrack({
|
await playerController.playAlbumTrack({
|
||||||
albumId: context.albumId,
|
albumId: context.albumId,
|
||||||
albumName: context.albumName,
|
albumName: context.albumName,
|
||||||
@@ -91,7 +94,7 @@
|
|||||||
// Queue will auto-update from Rust backend event
|
// Queue will auto-update from Rust backend event
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
||||||
console.error("Failed to play track:", errorMessage);
|
log.error("Failed to play track:", errorMessage);
|
||||||
toast.error(`Failed to play track: ${errorMessage}`, 5000);
|
toast.error(`Failed to play track: ${errorMessage}`, 5000);
|
||||||
} finally {
|
} finally {
|
||||||
isPlayingTrack = null;
|
isPlayingTrack = null;
|
||||||
@@ -145,9 +148,9 @@
|
|||||||
try {
|
try {
|
||||||
// Queue store now handles everything in Rust - just pass the track
|
// Queue store now handles everything in Rust - just pass the track
|
||||||
await queue.addToQueue(track, position);
|
await queue.addToQueue(track, position);
|
||||||
console.log(`Added "${track.name}" to queue (${position})`);
|
log.debug(`Added "${track.name}" to queue (${position})`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to add to queue:", e);
|
log.error("Failed to add to queue:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
|
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("VideoDownloadButton");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
itemId: string;
|
itemId: string;
|
||||||
@@ -57,17 +60,17 @@
|
|||||||
try {
|
try {
|
||||||
const userId = $auth.user?.id;
|
const userId = $auth.user?.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.error("No user ID found");
|
log.error("No user ID found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
|
|
||||||
console.log("🎬 Starting video download for item:", itemId, "quality:", quality);
|
log.debug("🎬 Starting video download for item:", itemId, "quality:", quality);
|
||||||
|
|
||||||
// Get stream URL based on quality
|
// Get stream URL based on quality
|
||||||
const streamUrl = await repo.getVideoDownloadUrl(itemId, quality);
|
const streamUrl = await repo.getVideoDownloadUrl(itemId, quality);
|
||||||
console.log(" Stream URL obtained");
|
log.debug(" Stream URL obtained");
|
||||||
|
|
||||||
// Get target directory
|
// Get target directory
|
||||||
const targetDir = await commands.storageGetPath();
|
const targetDir = await commands.storageGetPath();
|
||||||
@@ -85,7 +88,7 @@
|
|||||||
filePath = `videos/${safeName}.mp4`;
|
filePath = `videos/${safeName}.mp4`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(" File path:", filePath);
|
log.debug(" File path:", filePath);
|
||||||
|
|
||||||
// Queue download with video metadata
|
// Queue download with video metadata
|
||||||
const downloadId = await downloads.downloadVideo(
|
const downloadId = await downloads.downloadVideo(
|
||||||
@@ -101,16 +104,16 @@
|
|||||||
episodeNumber,
|
episodeNumber,
|
||||||
seasonNumber
|
seasonNumber
|
||||||
);
|
);
|
||||||
console.log(" Download queued with ID:", downloadId);
|
log.debug(" Download queued with ID:", downloadId);
|
||||||
|
|
||||||
// Pin the item metadata
|
// Pin the item metadata
|
||||||
await downloads.pinItem(itemId);
|
await downloads.pinItem(itemId);
|
||||||
|
|
||||||
// Actually start the download
|
// Actually start the download
|
||||||
await commands.startDownload(downloadId, streamUrl, targetDir);
|
await commands.startDownload(downloadId, streamUrl, targetDir);
|
||||||
console.log(" Download started");
|
log.debug(" Download started");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start video download:", error);
|
log.error("Failed to start video download:", error);
|
||||||
} finally {
|
} finally {
|
||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { syncService } from "$lib/services/syncService";
|
import { syncService } from "$lib/services/syncService";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("WatchedToggleButton");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Episode, season or series id. */
|
/** Episode, season or series id. */
|
||||||
@@ -81,7 +84,7 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Put the button back where it was — the change did not happen.
|
// Put the button back where it was — the change did not happen.
|
||||||
optimistic = null;
|
optimistic = null;
|
||||||
console.error("Failed to change watched state:", e);
|
log.error("Failed to change watched state:", e);
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
import VolumeControl from "./VolumeControl.svelte";
|
import VolumeControl from "./VolumeControl.svelte";
|
||||||
import CachedImage from "../common/CachedImage.svelte";
|
import CachedImage from "../common/CachedImage.svelte";
|
||||||
import { currentQueueItem } from "$lib/stores/queue";
|
import { currentQueueItem } from "$lib/stores/queue";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("AudioPlayer");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
media: MediaItem | null;
|
media: MediaItem | null;
|
||||||
@@ -130,7 +133,7 @@
|
|||||||
queue.skipTo(index);
|
queue.skipTo(index);
|
||||||
await playerController.skipTo(index);
|
await playerController.skipTo(index);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to skip to queue item:", e);
|
log.error("Failed to skip to queue item:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -38,6 +38,9 @@
|
|||||||
import CastButton from "$lib/components/sessions/CastButton.svelte";
|
import CastButton from "$lib/components/sessions/CastButton.svelte";
|
||||||
import VolumeControl from "./VolumeControl.svelte";
|
import VolumeControl from "./VolumeControl.svelte";
|
||||||
import CachedImage from "../common/CachedImage.svelte";
|
import CachedImage from "../common/CachedImage.svelte";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("MiniPlayer");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
media: MediaItem | null;
|
media: MediaItem | null;
|
||||||
@@ -159,7 +162,7 @@
|
|||||||
await playerController.seek(newPosition);
|
await playerController.seek(newPosition);
|
||||||
haptics.tap();
|
haptics.tap();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to seek:", err);
|
log.error("Failed to seek:", err);
|
||||||
toast.show("Failed to seek", "error");
|
toast.show("Failed to seek", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,7 +233,7 @@
|
|||||||
// Vertical swipe
|
// Vertical swipe
|
||||||
if (Math.abs(diffY) > swipeThreshold && diffY > 0) {
|
if (Math.abs(diffY) > swipeThreshold && diffY > 0) {
|
||||||
// Swiped up - Open full player
|
// Swiped up - Open full player
|
||||||
console.log("[MiniPlayer] Swipe-up detected, expanding player");
|
log.debug("Swipe-up detected, expanding player");
|
||||||
haptics.tap();
|
haptics.tap();
|
||||||
onExpand?.();
|
onExpand?.();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import { queue } from "$lib/stores/queue";
|
import { queue } from "$lib/stores/queue";
|
||||||
import CachedImage from "../common/CachedImage.svelte";
|
import CachedImage from "../common/CachedImage.svelte";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("QueueView");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: MediaItem[];
|
items: MediaItem[];
|
||||||
@@ -82,7 +85,7 @@
|
|||||||
// Sync with backend
|
// Sync with backend
|
||||||
await playerController.moveInQueue(fromIndex, toIndex);
|
await playerController.moveInQueue(fromIndex, toIndex);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to move queue item:", e);
|
log.error("Failed to move queue item:", e);
|
||||||
// The store already updated optimistically, refresh if needed
|
// The store already updated optimistically, refresh if needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +112,7 @@
|
|||||||
queue.removeFromQueue(index);
|
queue.removeFromQueue(index);
|
||||||
await playerController.removeFromQueue(index);
|
await playerController.removeFromQueue(index);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to remove from queue:", err);
|
log.error("Failed to remove from queue:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -75,6 +75,9 @@
|
|||||||
planHandoffReturn,
|
planHandoffReturn,
|
||||||
type BackgroundAudioState,
|
type BackgroundAudioState,
|
||||||
} from "./backgroundAudioHandoff";
|
} from "./backgroundAudioHandoff";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("VideoPlayer");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
media: MediaItem | null;
|
media: MediaItem | null;
|
||||||
@@ -287,11 +290,11 @@
|
|||||||
// TRACES: UR-021 | IR-016, JA-009 | DR-024
|
// TRACES: UR-021 | IR-016, JA-009 | DR-024
|
||||||
const audioTracks = $derived(() => {
|
const audioTracks = $derived(() => {
|
||||||
if (!media || !media.mediaStreams) {
|
if (!media || !media.mediaStreams) {
|
||||||
console.log("[VideoPlayer] No media or mediaStreams available");
|
log.debug("No media or mediaStreams available");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const tracks = media.mediaStreams.filter(stream => stream.kind === "audio");
|
const tracks = media.mediaStreams.filter(stream => stream.kind === "audio");
|
||||||
console.log("[VideoPlayer] Found audio tracks:", tracks.length, tracks);
|
log.debug("Found audio tracks:", tracks.length, tracks);
|
||||||
return tracks;
|
return tracks;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -304,7 +307,7 @@
|
|||||||
if (preference.audioTrackDisplayTitle) {
|
if (preference.audioTrackDisplayTitle) {
|
||||||
const match = tracks.find(t => t.displayTitle === preference.audioTrackDisplayTitle);
|
const match = tracks.find(t => t.displayTitle === preference.audioTrackDisplayTitle);
|
||||||
if (match) {
|
if (match) {
|
||||||
console.log("[VideoPlayer] Matched audio track by display title:", match.displayTitle);
|
log.debug("Matched audio track by display title:", match.displayTitle);
|
||||||
return match.index;
|
return match.index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,14 +316,14 @@
|
|||||||
if (preference.audioTrackLanguage) {
|
if (preference.audioTrackLanguage) {
|
||||||
const match = tracks.find(t => t.language === preference.audioTrackLanguage);
|
const match = tracks.find(t => t.language === preference.audioTrackLanguage);
|
||||||
if (match) {
|
if (match) {
|
||||||
console.log("[VideoPlayer] Matched audio track by language:", match.language);
|
log.debug("Matched audio track by language:", match.language);
|
||||||
return match.index;
|
return match.index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to default track
|
// Fall back to default track
|
||||||
const defaultTrack = tracks.find(t => t.isDefault) || tracks[0];
|
const defaultTrack = tracks.find(t => t.isDefault) || tracks[0];
|
||||||
console.log("[VideoPlayer] Using default/first audio track:", defaultTrack.displayTitle || defaultTrack.language);
|
log.debug("Using default/first audio track:", defaultTrack.displayTitle || defaultTrack.language);
|
||||||
return defaultTrack.index;
|
return defaultTrack.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,15 +338,15 @@
|
|||||||
const preference = await commands.storageGetSeriesAudioPreference(userId, media.seriesId);
|
const preference = await commands.storageGetSeriesAudioPreference(userId, media.seriesId);
|
||||||
|
|
||||||
if (preference) {
|
if (preference) {
|
||||||
console.log("[VideoPlayer] Loaded series audio preference:", preference);
|
log.debug("Loaded series audio preference:", preference);
|
||||||
const matchedIndex = findBestAudioTrack(preference);
|
const matchedIndex = findBestAudioTrack(preference);
|
||||||
if (matchedIndex !== null) {
|
if (matchedIndex !== null) {
|
||||||
selectedAudioTrackIndex = matchedIndex;
|
selectedAudioTrackIndex = matchedIndex;
|
||||||
console.log("[VideoPlayer] Applied series audio preference, track index:", matchedIndex);
|
log.debug("Applied series audio preference, track index:", matchedIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[VideoPlayer] Failed to load series audio preference:", err);
|
log.warn("Failed to load series audio preference:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,11 +358,11 @@
|
|||||||
// TRACES: UR-020 | DR-176 | UT-168
|
// TRACES: UR-020 | DR-176 | UT-168
|
||||||
const subtitleTracks = $derived(() => {
|
const subtitleTracks = $derived(() => {
|
||||||
if (!media || !media.mediaStreams) {
|
if (!media || !media.mediaStreams) {
|
||||||
console.log("[VideoPlayer] No media or mediaStreams available for subtitles");
|
log.debug("No media or mediaStreams available for subtitles");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const tracks = subtitleStreamsOf(media.mediaStreams);
|
const tracks = subtitleStreamsOf(media.mediaStreams);
|
||||||
console.log("[VideoPlayer] Found subtitle tracks:", tracks.length, tracks);
|
log.debug("Found subtitle tracks:", tracks.length, tracks);
|
||||||
return tracks;
|
return tracks;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -547,7 +550,7 @@
|
|||||||
if (isHlsStream && Hls.isSupported()) {
|
if (isHlsStream && Hls.isSupported()) {
|
||||||
// Clean up existing HLS instance if any - CRITICAL for preventing dual audio
|
// Clean up existing HLS instance if any - CRITICAL for preventing dual audio
|
||||||
if (hls) {
|
if (hls) {
|
||||||
console.log('[VideoPlayer] Cleaning up existing HLS instance');
|
log.debug('Cleaning up existing HLS instance');
|
||||||
// Detach from media element first to stop all audio/video
|
// Detach from media element first to stop all audio/video
|
||||||
hls.detachMedia();
|
hls.detachMedia();
|
||||||
// Stop loading and flush buffers
|
// Stop loading and flush buffers
|
||||||
@@ -571,7 +574,7 @@
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!videoElement) return;
|
if (!videoElement) return;
|
||||||
|
|
||||||
console.log('[VideoPlayer] Creating new HLS instance for:', currentStreamUrl);
|
log.debug('Creating new HLS instance for:', currentStreamUrl);
|
||||||
|
|
||||||
// Create new HLS instance
|
// Create new HLS instance
|
||||||
hls = new Hls({
|
hls = new Hls({
|
||||||
@@ -599,14 +602,14 @@
|
|||||||
|
|
||||||
// Listen for media attached event
|
// Listen for media attached event
|
||||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||||
console.log('[VideoPlayer] HLS.js attached to video element');
|
log.debug('HLS.js attached to video element');
|
||||||
// Load the HLS stream
|
// Load the HLS stream
|
||||||
hls!.loadSource(currentStreamUrl);
|
hls!.loadSource(currentStreamUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for manifest parsed event
|
// Listen for manifest parsed event
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
console.log('[VideoPlayer] HLS manifest parsed, ready to play');
|
log.debug('HLS manifest parsed, ready to play');
|
||||||
});
|
});
|
||||||
|
|
||||||
// On the Android WebView the element's own `canplay` may not fire for
|
// On the Android WebView the element's own `canplay` may not fire for
|
||||||
@@ -623,7 +626,7 @@
|
|||||||
if (canplayFallbackTimeout) clearTimeout(canplayFallbackTimeout);
|
if (canplayFallbackTimeout) clearTimeout(canplayFallbackTimeout);
|
||||||
canplayFallbackTimeout = setTimeout(() => {
|
canplayFallbackTimeout = setTimeout(() => {
|
||||||
if (!isMediaReady && videoElement && videoElement.readyState >= 2) {
|
if (!isMediaReady && videoElement && videoElement.readyState >= 2) {
|
||||||
console.warn('[VideoPlayer] HLS canplay fallback - revealing video (readyState:', videoElement.readyState, ')');
|
log.warn('HLS canplay fallback - revealing video (readyState:', videoElement.readyState, ')');
|
||||||
markMediaReady();
|
markMediaReady();
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
@@ -633,7 +636,7 @@
|
|||||||
|
|
||||||
// Handle errors
|
// Handle errors
|
||||||
hls.on(Hls.Events.ERROR, (event, data) => {
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||||
console.error('[VideoPlayer] HLS error:', data);
|
log.error('HLS error:', data);
|
||||||
if (data.fatal) {
|
if (data.fatal) {
|
||||||
// Is this the stream ending or the stream breaking? Jellyfin's
|
// Is this the stream ending or the stream breaking? Jellyfin's
|
||||||
// transcoded HLS doesn't always emit #EXT-X-ENDLIST, so both arrive
|
// transcoded HLS doesn't always emit #EXT-X-ENDLIST, so both arrive
|
||||||
@@ -650,25 +653,25 @@
|
|||||||
attempts: hlsFatalRecoveryAttempts,
|
attempts: hlsFatalRecoveryAttempts,
|
||||||
})) {
|
})) {
|
||||||
case 'ended':
|
case 'ended':
|
||||||
console.log('[VideoPlayer] Fatal network error near end of stream - treating as ended');
|
log.debug('Fatal network error near end of stream - treating as ended');
|
||||||
notifyEnded();
|
notifyEnded();
|
||||||
break;
|
break;
|
||||||
case 'retry':
|
case 'retry':
|
||||||
console.error('[VideoPlayer] Fatal network error, trying to recover (attempt', hlsFatalRecoveryAttempts, ')');
|
log.error('Fatal network error, trying to recover (attempt', hlsFatalRecoveryAttempts, ')');
|
||||||
hls!.startLoad();
|
hls!.startLoad();
|
||||||
break;
|
break;
|
||||||
case 'giveUp':
|
case 'giveUp':
|
||||||
console.error('[VideoPlayer] Fatal network error, max recovery attempts reached');
|
log.error('Fatal network error, max recovery attempts reached');
|
||||||
hls!.destroy();
|
hls!.destroy();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
console.error('[VideoPlayer] Fatal media error, trying to recover');
|
log.error('Fatal media error, trying to recover');
|
||||||
hls!.recoverMediaError();
|
hls!.recoverMediaError();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.error('[VideoPlayer] Unrecoverable HLS error');
|
log.error('Unrecoverable HLS error');
|
||||||
hls!.destroy();
|
hls!.destroy();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -678,7 +681,7 @@
|
|||||||
|
|
||||||
// Cleanup on effect re-run
|
// Cleanup on effect re-run
|
||||||
return () => {
|
return () => {
|
||||||
console.log('[VideoPlayer] Effect cleanup: destroying HLS instance');
|
log.debug('Effect cleanup: destroying HLS instance');
|
||||||
if (hls) {
|
if (hls) {
|
||||||
hls.detachMedia();
|
hls.detachMedia();
|
||||||
hls.stopLoad();
|
hls.stopLoad();
|
||||||
@@ -691,11 +694,11 @@
|
|||||||
};
|
};
|
||||||
} else if (isHlsStream && videoElement.canPlayType('application/vnd.apple.mpegurl')) {
|
} else if (isHlsStream && videoElement.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
// Native HLS support (Safari)
|
// Native HLS support (Safari)
|
||||||
console.log('[VideoPlayer] Using native HLS support');
|
log.debug('Using native HLS support');
|
||||||
videoElement.src = currentStreamUrl;
|
videoElement.src = currentStreamUrl;
|
||||||
} else {
|
} else {
|
||||||
// Not an HLS stream, use regular video element
|
// Not an HLS stream, use regular video element
|
||||||
console.log('[VideoPlayer] Using regular video element for non-HLS stream');
|
log.debug('Using regular video element for non-HLS stream');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -704,24 +707,24 @@
|
|||||||
if (videoElement) {
|
if (videoElement) {
|
||||||
videoElement.muted = false;
|
videoElement.muted = false;
|
||||||
videoElement.volume = 1.0;
|
videoElement.volume = 1.0;
|
||||||
console.log("[VideoPlayer] Video element configured: muted=", videoElement.muted, "volume=", videoElement.volume);
|
log.debug("Video element configured: muted=", videoElement.muted, "volume=", videoElement.volume);
|
||||||
|
|
||||||
// DIAGNOSTIC: Check if video has audio tracks
|
// DIAGNOSTIC: Check if video has audio tracks
|
||||||
if ((videoElement as any).audioTracks) {
|
if ((videoElement as any).audioTracks) {
|
||||||
console.log("[VideoPlayer] Audio tracks count:", (videoElement as any).audioTracks.length);
|
log.debug("Audio tracks count:", (videoElement as any).audioTracks.length);
|
||||||
|
|
||||||
// Set initial audio track (prefer default track)
|
// Set initial audio track (prefer default track)
|
||||||
if (selectedAudioTrackIndex === null && audioTracks().length > 0) {
|
if (selectedAudioTrackIndex === null && audioTracks().length > 0) {
|
||||||
const defaultTrack = audioTracks().find(t => t.isDefault);
|
const defaultTrack = audioTracks().find(t => t.isDefault);
|
||||||
selectedAudioTrackIndex = defaultTrack ? defaultTrack.index : audioTracks()[0].index;
|
selectedAudioTrackIndex = defaultTrack ? defaultTrack.index : audioTracks()[0].index;
|
||||||
console.log("[VideoPlayer] Selected default audio track:", selectedAudioTrackIndex);
|
log.debug("Selected default audio track:", selectedAudioTrackIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((videoElement as any).mozHasAudio !== undefined) {
|
if ((videoElement as any).mozHasAudio !== undefined) {
|
||||||
console.log("[VideoPlayer] mozHasAudio:", (videoElement as any).mozHasAudio);
|
log.debug("mozHasAudio:", (videoElement as any).mozHasAudio);
|
||||||
}
|
}
|
||||||
if ((videoElement as any).webkitAudioDecodedByteCount !== undefined) {
|
if ((videoElement as any).webkitAudioDecodedByteCount !== undefined) {
|
||||||
console.log("[VideoPlayer] webkitAudioDecodedByteCount:", (videoElement as any).webkitAudioDecodedByteCount);
|
log.debug("webkitAudioDecodedByteCount:", (videoElement as any).webkitAudioDecodedByteCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -749,7 +752,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
console.log("[VideoPlayer] Initial position changed, seeking to:", pos);
|
log.debug("Initial position changed, seeking to:", pos);
|
||||||
lastAppliedInitialPosition = pos;
|
lastAppliedInitialPosition = pos;
|
||||||
if (videoElement) {
|
if (videoElement) {
|
||||||
videoElement.currentTime = pos;
|
videoElement.currentTime = pos;
|
||||||
@@ -775,7 +778,7 @@
|
|||||||
selectedQuality = settings.streamingQuality ?? "original";
|
selectedQuality = settings.streamingQuality ?? "original";
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.warn("[VideoPlayer] Failed to load streaming qualities:", err);
|
log.warn("Failed to load streaming qualities:", err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -808,8 +811,8 @@
|
|||||||
// Initialize player via Rust - Rust will decide which backend to use based on platform
|
// Initialize player via Rust - Rust will decide which backend to use based on platform
|
||||||
if (media && currentStreamUrl) {
|
if (media && currentStreamUrl) {
|
||||||
try {
|
try {
|
||||||
console.log("[VideoPlayer] Initializing player for:", media.name);
|
log.debug("Initializing player for:", media.name);
|
||||||
console.log("[VideoPlayer] Stream URL:", currentStreamUrl);
|
log.debug("Stream URL:", currentStreamUrl);
|
||||||
|
|
||||||
// Resolve subtitle URLs for the native (ExoPlayer) path. These must be
|
// Resolve subtitle URLs for the native (ExoPlayer) path. These must be
|
||||||
// in hand *before* the play request: ExoPlayer sideloads subtitles as
|
// in hand *before* the play request: ExoPlayer sideloads subtitles as
|
||||||
@@ -827,7 +830,7 @@
|
|||||||
sentSubtitleTracks = mediaSourceId
|
sentSubtitleTracks = mediaSourceId
|
||||||
? await resolveSubtitleTracks(media.mediaStreams, (index) => getSubtitleUrl(index))
|
? await resolveSubtitleTracks(media.mediaStreams, (index) => getSubtitleUrl(index))
|
||||||
: [];
|
: [];
|
||||||
console.log(`[VideoPlayer] Sending ${sentSubtitleTracks.length} subtitle tracks to the backend`);
|
log.debug(`Sending ${sentSubtitleTracks.length} subtitle tracks to the backend`);
|
||||||
|
|
||||||
// Call Rust backend to start playback
|
// Call Rust backend to start playback
|
||||||
// Rust will choose ExoPlayer (Android), libmpv (Linux), or tell us to use HTML5
|
// Rust will choose ExoPlayer (Android), libmpv (Linux), or tell us to use HTML5
|
||||||
@@ -847,7 +850,7 @@
|
|||||||
// Rust tells us which backend it's using
|
// Rust tells us which backend it's using
|
||||||
useHtml5Element = response.useHtml5Element;
|
useHtml5Element = response.useHtml5Element;
|
||||||
backendChosen = true;
|
backendChosen = true;
|
||||||
console.log(`[VideoPlayer] Backend: ${response.backend}, useHtml5Element: ${useHtml5Element}`);
|
log.debug(`Backend: ${response.backend}, useHtml5Element: ${useHtml5Element}`);
|
||||||
|
|
||||||
// Rust reported a native backend (Android/ExoPlayer). Honour it only if
|
// Rust reported a native backend (Android/ExoPlayer). Honour it only if
|
||||||
// the user opted into the experimental native path; otherwise fall back
|
// the user opted into the experimental native path; otherwise fall back
|
||||||
@@ -858,13 +861,13 @@
|
|||||||
// just started, or ExoPlayer and the <video> element both decode the
|
// just started, or ExoPlayer and the <video> element both decode the
|
||||||
// same stream and the audio doubles.
|
// same stream and the audio doubles.
|
||||||
if (!useHtml5Element && !$experimentalNativeVideo) {
|
if (!useHtml5Element && !$experimentalNativeVideo) {
|
||||||
console.log("[VideoPlayer] Native backend available but experimentalNativeVideo is off - using HTML5");
|
log.debug("Native backend available but experimentalNativeVideo is off - using HTML5");
|
||||||
useHtml5Element = true;
|
useHtml5Element = true;
|
||||||
try {
|
try {
|
||||||
await commands.playerStop();
|
await commands.playerStop();
|
||||||
didStopBackendEarly = true;
|
didStopBackendEarly = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[VideoPlayer] Failed to stop native backend:", err);
|
log.warn("Failed to stop native backend:", err);
|
||||||
}
|
}
|
||||||
} else if (!useHtml5Element) {
|
} else if (!useHtml5Element) {
|
||||||
// Native path: clear the opaque layers between the viewport and the
|
// Native path: clear the opaque layers between the viewport and the
|
||||||
@@ -872,7 +875,7 @@
|
|||||||
// Paired with disableNativeVideoCompositing() in the teardown path —
|
// Paired with disableNativeVideoCompositing() in the teardown path —
|
||||||
// leaving this on renders the rest of the app over a transparent
|
// leaving this on renders the rest of the app over a transparent
|
||||||
// window.
|
// window.
|
||||||
console.log("[VideoPlayer] Using native ExoPlayer video surface");
|
log.debug("Using native ExoPlayer video surface");
|
||||||
enableNativeVideoCompositing();
|
enableNativeVideoCompositing();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -880,14 +883,14 @@
|
|||||||
// For transcoded content, we need to keep the backend running to handle seeking/audio track switching
|
// For transcoded content, we need to keep the backend running to handle seeking/audio track switching
|
||||||
if (useHtml5Element && !needsTranscoding && !didStopBackendEarly) {
|
if (useHtml5Element && !needsTranscoding && !didStopBackendEarly) {
|
||||||
try {
|
try {
|
||||||
console.log("[VideoPlayer] Using HTML5 for direct stream - stopping backend player to prevent dual audio");
|
log.debug("Using HTML5 for direct stream - stopping backend player to prevent dual audio");
|
||||||
await commands.playerStop();
|
await commands.playerStop();
|
||||||
didStopBackendEarly = true; // Track that we stopped the backend
|
didStopBackendEarly = true; // Track that we stopped the backend
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[VideoPlayer] Failed to stop backend player:", err);
|
log.warn("Failed to stop backend player:", err);
|
||||||
}
|
}
|
||||||
} else if (useHtml5Element && needsTranscoding) {
|
} else if (useHtml5Element && needsTranscoding) {
|
||||||
console.log("[VideoPlayer] Using HTML5 for transcoded stream - keeping backend for seeking/transcoding decisions");
|
log.debug("Using HTML5 for transcoded stream - keeping backend for seeking/transcoding decisions");
|
||||||
// Backend is kept running but should not play audio since HTML5 element handles playback
|
// Backend is kept running but should not play audio since HTML5 element handles playback
|
||||||
didStartNativePlayback = true; // Track that we need to stop backend on unmount
|
didStartNativePlayback = true; // Track that we need to stop backend on unmount
|
||||||
}
|
}
|
||||||
@@ -972,12 +975,12 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[VideoPlayer] Failed to initialize player:", err);
|
log.error("Failed to initialize player:", err);
|
||||||
if (backendChosen) {
|
if (backendChosen) {
|
||||||
// The backend already accepted the item; a later error (e.g. event
|
// The backend already accepted the item; a later error (e.g. event
|
||||||
// subscription) must not silently switch the seek/controls path to
|
// subscription) must not silently switch the seek/controls path to
|
||||||
// HTML5 while the native backend keeps playing.
|
// HTML5 while the native backend keeps playing.
|
||||||
console.warn("[VideoPlayer] Backend already initialized - keeping native mode despite error");
|
log.warn("Backend already initialized - keeping native mode despite error");
|
||||||
} else {
|
} else {
|
||||||
// Fallback to HTML5 on error
|
// Fallback to HTML5 on error
|
||||||
useHtml5Element = true;
|
useHtml5Element = true;
|
||||||
@@ -1042,8 +1045,8 @@
|
|||||||
// Flattened to a single string on purpose: the Android WebView console
|
// Flattened to a single string on purpose: the Android WebView console
|
||||||
// bridge stringifies objects as "[object Object]" in logcat, which made
|
// bridge stringifies objects as "[object Object]" in logcat, which made
|
||||||
// this whole payload useless when diagnosing over adb.
|
// this whole payload useless when diagnosing over adb.
|
||||||
console.log(
|
log.debug(
|
||||||
`[VideoPlayer Debug] t=${videoElement.currentTime.toFixed(2)}` +
|
`Debug t=${videoElement.currentTime.toFixed(2)}` +
|
||||||
` display=${currentTime.toFixed(2)}` +
|
` display=${currentTime.toFixed(2)}` +
|
||||||
` readyState=${videoElement.readyState}` +
|
` readyState=${videoElement.readyState}` +
|
||||||
` networkState=${videoElement.networkState}` +
|
` networkState=${videoElement.networkState}` +
|
||||||
@@ -1111,7 +1114,7 @@
|
|||||||
|
|
||||||
// Clean up HLS.js instance - prevent dual audio on unmount
|
// Clean up HLS.js instance - prevent dual audio on unmount
|
||||||
if (hls) {
|
if (hls) {
|
||||||
console.log("[VideoPlayer] Destroying HLS.js instance on unmount");
|
log.debug("Destroying HLS.js instance on unmount");
|
||||||
hls.detachMedia(); // Detach from video element first
|
hls.detachMedia(); // Detach from video element first
|
||||||
hls.stopLoad(); // Stop loading and flush buffers
|
hls.stopLoad(); // Stop loading and flush buffers
|
||||||
hls.destroy();
|
hls.destroy();
|
||||||
@@ -1129,10 +1132,10 @@
|
|||||||
// Skip if we already stopped the backend early (non-transcoded + HTML5)
|
// Skip if we already stopped the backend early (non-transcoded + HTML5)
|
||||||
if (didStartNativePlayback && !didStopBackendEarly) {
|
if (didStartNativePlayback && !didStopBackendEarly) {
|
||||||
try {
|
try {
|
||||||
console.log("[VideoPlayer] Stopping backend player on component unmount");
|
log.debug("Stopping backend player on component unmount");
|
||||||
await commands.playerStop();
|
await commands.playerStop();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[VideoPlayer] Failed to stop backend player:", err);
|
log.error("Failed to stop backend player:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1187,20 +1190,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleLoadedMetadata() {
|
function handleLoadedMetadata() {
|
||||||
console.log("[VideoPlayer] loadedmetadata event");
|
log.debug("loadedmetadata event");
|
||||||
// Intrinsic dimensions are known now, which is what PiP sizes its window
|
// Intrinsic dimensions are known now, which is what PiP sizes its window
|
||||||
// from — before this they are 0 and the ratio would be rejected. (DR-160)
|
// from — before this they are 0 and the ratio would be rejected. (DR-160)
|
||||||
reportPipVideoState();
|
reportPipVideoState();
|
||||||
console.log("[VideoPlayer] Video element duration:", videoElement?.duration);
|
log.debug("Video element duration:", videoElement?.duration);
|
||||||
console.log("[VideoPlayer] Media item runTimeTicks:", media?.runTimeTicks);
|
log.debug("Media item runTimeTicks:", media?.runTimeTicks);
|
||||||
console.log("[VideoPlayer] Needs transcoding:", needsTranscoding);
|
log.debug("Needs transcoding:", needsTranscoding);
|
||||||
|
|
||||||
// For direct streams without runTimeTicks, use video element's duration
|
// For direct streams without runTimeTicks, use video element's duration
|
||||||
if (videoElement && videoElement.duration && !isNaN(videoElement.duration) && videoElement.duration !== Infinity) {
|
if (videoElement && videoElement.duration && !isNaN(videoElement.duration) && videoElement.duration !== Infinity) {
|
||||||
const newDuration = videoElement.duration;
|
const newDuration = videoElement.duration;
|
||||||
console.log("[VideoPlayer] Setting videoDuration to:", newDuration);
|
log.debug("Setting videoDuration to:", newDuration);
|
||||||
videoDuration = newDuration;
|
videoDuration = newDuration;
|
||||||
console.log("[VideoPlayer] videoDuration state is now:", videoDuration);
|
log.debug("videoDuration state is now:", videoDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell the Rust controller the media is loaded and its duration (mirrors the
|
// Tell the Rust controller the media is loaded and its duration (mirrors the
|
||||||
@@ -1209,8 +1212,8 @@
|
|||||||
|
|
||||||
// Use setTimeout to log the derived value after reactive updates
|
// Use setTimeout to log the derived value after reactive updates
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log("[VideoPlayer] Derived duration value:", duration);
|
log.debug("Derived duration value:", duration);
|
||||||
console.log("[VideoPlayer] Duration source:", media?.runTimeTicks ? "runTimeTicks" : "video element");
|
log.debug("Duration source:", media?.runTimeTicks ? "runTimeTicks" : "video element");
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1251,15 +1254,15 @@
|
|||||||
el.volume = 1.0;
|
el.volume = 1.0;
|
||||||
if (shouldPlay) await el.play();
|
if (shouldPlay) await el.play();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[VideoPlayer] Failed to resume after background audio:", err);
|
log.error("Failed to resume after background audio:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (el.readyState >= 1 /* HAVE_METADATA */) {
|
if (el.readyState >= 1 /* HAVE_METADATA */) {
|
||||||
console.log("[VideoPlayer] Applying foreground seek to:", (seekOffset + seekTo).toFixed(1));
|
log.debug("Applying foreground seek to:", (seekOffset + seekTo).toFixed(1));
|
||||||
await doSeek();
|
await doSeek();
|
||||||
} else {
|
} else {
|
||||||
console.log("[VideoPlayer] Deferring foreground seek until loadedmetadata:", (seekOffset + seekTo).toFixed(1));
|
log.debug("Deferring foreground seek until loadedmetadata:", (seekOffset + seekTo).toFixed(1));
|
||||||
el.addEventListener("loadedmetadata", () => { void doSeek(); }, { once: true });
|
el.addEventListener("loadedmetadata", () => { void doSeek(); }, { once: true });
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1267,7 +1270,7 @@
|
|||||||
|
|
||||||
function markMediaReady() {
|
function markMediaReady() {
|
||||||
if (isMediaReady) return;
|
if (isMediaReady) return;
|
||||||
console.log("[VideoPlayer] Marking media ready");
|
log.debug("Marking media ready");
|
||||||
isMediaReady = true;
|
isMediaReady = true;
|
||||||
// A handoff return can be revealed here (not via canplay) — apply its seek.
|
// A handoff return can be revealed here (not via canplay) — apply its seek.
|
||||||
void applyPendingForegroundSeek();
|
void applyPendingForegroundSeek();
|
||||||
@@ -1275,14 +1278,14 @@
|
|||||||
|
|
||||||
async function handleCanPlay() {
|
async function handleCanPlay() {
|
||||||
// Media is ready to play - transition from Loading to Playing state (DR-001)
|
// Media is ready to play - transition from Loading to Playing state (DR-001)
|
||||||
console.log("[VideoPlayer] canplay event fired - media is ready");
|
log.debug("canplay event fired - media is ready");
|
||||||
isMediaReady = true;
|
isMediaReady = true;
|
||||||
|
|
||||||
// Ensure video is unmuted and at max volume (critical for Android)
|
// Ensure video is unmuted and at max volume (critical for Android)
|
||||||
if (videoElement) {
|
if (videoElement) {
|
||||||
videoElement.muted = false;
|
videoElement.muted = false;
|
||||||
videoElement.volume = 1.0;
|
videoElement.volume = 1.0;
|
||||||
console.log("[VideoPlayer] Video unmuted on canplay, volume: 1.0");
|
log.debug("Video unmuted on canplay, volume: 1.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returning from background audio: resume the <video> at the position native
|
// Returning from background audio: resume the <video> at the position native
|
||||||
@@ -1294,7 +1297,7 @@
|
|||||||
|
|
||||||
// Seek to initial position if resuming playback
|
// Seek to initial position if resuming playback
|
||||||
if (initialPosition && initialPosition > 0 && !hasPerformedInitialSeek && videoElement) {
|
if (initialPosition && initialPosition > 0 && !hasPerformedInitialSeek && videoElement) {
|
||||||
console.log("[VideoPlayer] Seeking to initial position:", initialPosition);
|
log.debug("Seeking to initial position:", initialPosition);
|
||||||
hasPerformedInitialSeek = true;
|
hasPerformedInitialSeek = true;
|
||||||
lastAppliedInitialPosition = initialPosition; // mark this value as applied so the change-effect ignores it
|
lastAppliedInitialPosition = initialPosition; // mark this value as applied so the change-effect ignores it
|
||||||
|
|
||||||
@@ -1325,7 +1328,7 @@
|
|||||||
await videoElement.play();
|
await videoElement.play();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[VideoPlayer] Failed to seek to initial position:", err);
|
log.error("Failed to seek to initial position:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1335,7 +1338,7 @@
|
|||||||
const error = video.error;
|
const error = video.error;
|
||||||
|
|
||||||
// Log comprehensive error details
|
// Log comprehensive error details
|
||||||
console.error("[VideoPlayer] Video error event:", {
|
log.error("Video error event:", {
|
||||||
code: error?.code,
|
code: error?.code,
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
networkState: video.networkState,
|
networkState: video.networkState,
|
||||||
@@ -1354,28 +1357,28 @@
|
|||||||
|
|
||||||
const errorCode = error?.code || 0;
|
const errorCode = error?.code || 0;
|
||||||
const msg = errorMessages[errorCode] || `Unknown error (code ${errorCode})`;
|
const msg = errorMessages[errorCode] || `Unknown error (code ${errorCode})`;
|
||||||
console.error("[VideoPlayer] Error interpretation:", msg);
|
log.error("Error interpretation:", msg);
|
||||||
|
|
||||||
// Log additional debugging info
|
// Log additional debugging info
|
||||||
console.error("[VideoPlayer] Stream URL:", currentStreamUrl);
|
log.error("Stream URL:", currentStreamUrl);
|
||||||
console.error("[VideoPlayer] Needs transcoding:", needsTranscoding);
|
log.error("Needs transcoding:", needsTranscoding);
|
||||||
|
|
||||||
// Network state meanings: 0=EMPTY, 1=IDLE, 2=LOADING, 3=NO_SOURCE
|
// Network state meanings: 0=EMPTY, 1=IDLE, 2=LOADING, 3=NO_SOURCE
|
||||||
const networkStates = ["NETWORK_EMPTY", "NETWORK_IDLE", "NETWORK_LOADING", "NETWORK_NO_SOURCE"];
|
const networkStates = ["NETWORK_EMPTY", "NETWORK_IDLE", "NETWORK_LOADING", "NETWORK_NO_SOURCE"];
|
||||||
console.error("[VideoPlayer] Network state:", networkStates[video.networkState] || video.networkState);
|
log.error("Network state:", networkStates[video.networkState] || video.networkState);
|
||||||
|
|
||||||
// Ready state meanings: 0=NOTHING, 1=METADATA, 2=CURRENT_DATA, 3=FUTURE_DATA, 4=ENOUGH_DATA
|
// Ready state meanings: 0=NOTHING, 1=METADATA, 2=CURRENT_DATA, 3=FUTURE_DATA, 4=ENOUGH_DATA
|
||||||
const readyStates = ["HAVE_NOTHING", "HAVE_METADATA", "HAVE_CURRENT_DATA", "HAVE_FUTURE_DATA", "HAVE_ENOUGH_DATA"];
|
const readyStates = ["HAVE_NOTHING", "HAVE_METADATA", "HAVE_CURRENT_DATA", "HAVE_FUTURE_DATA", "HAVE_ENOUGH_DATA"];
|
||||||
console.error("[VideoPlayer] Ready state:", readyStates[video.readyState] || video.readyState);
|
log.error("Ready state:", readyStates[video.readyState] || video.readyState);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWaiting() {
|
function handleWaiting() {
|
||||||
console.log("[VideoPlayer] waiting event - buffering");
|
log.debug("waiting event - buffering");
|
||||||
isBuffering = true;
|
isBuffering = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePlaying() {
|
function handlePlaying() {
|
||||||
console.log("[VideoPlayer] playing event - playback resumed");
|
log.debug("playing event - playback resumed");
|
||||||
isBuffering = false;
|
isBuffering = false;
|
||||||
// Safety net: if we reached `playing` we are definitely renderable, even if
|
// Safety net: if we reached `playing` we are definitely renderable, even if
|
||||||
// `canplay`/hls FRAG_BUFFERED were missed on this WebView.
|
// `canplay`/hls FRAG_BUFFERED were missed on this WebView.
|
||||||
@@ -1383,9 +1386,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleLoadStart() {
|
function handleLoadStart() {
|
||||||
console.log("[VideoPlayer] loadstart event - starting to load:", currentStreamUrl);
|
log.debug("loadstart event - starting to load:", currentStreamUrl);
|
||||||
console.log("[VideoPlayer] Video element readyState:", videoElement?.readyState);
|
log.debug("Video element readyState:", videoElement?.readyState);
|
||||||
console.log("[VideoPlayer] Video element networkState:", videoElement?.networkState);
|
log.debug("Video element networkState:", videoElement?.networkState);
|
||||||
|
|
||||||
// Clear any existing fallback timeout
|
// Clear any existing fallback timeout
|
||||||
if (canplayFallbackTimeout) {
|
if (canplayFallbackTimeout) {
|
||||||
@@ -1395,12 +1398,12 @@
|
|||||||
// Set up a fallback timeout in case canplay event never fires
|
// Set up a fallback timeout in case canplay event never fires
|
||||||
canplayFallbackTimeout = setTimeout(() => {
|
canplayFallbackTimeout = setTimeout(() => {
|
||||||
if (!isMediaReady && videoElement) {
|
if (!isMediaReady && videoElement) {
|
||||||
console.warn("[VideoPlayer] canplay event did not fire within 5 seconds");
|
log.warn("canplay event did not fire within 5 seconds");
|
||||||
console.log("[VideoPlayer] Fallback check - readyState:", videoElement.readyState, "networkState:", videoElement.networkState);
|
log.debug("Fallback check - readyState:", videoElement.readyState, "networkState:", videoElement.networkState);
|
||||||
|
|
||||||
// Check if video is actually ready despite event not firing
|
// Check if video is actually ready despite event not firing
|
||||||
if (videoElement.readyState >= 3) { // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA
|
if (videoElement.readyState >= 3) { // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA
|
||||||
console.log("[VideoPlayer] Video appears ready (readyState >= 3), forcing media ready state");
|
log.debug("Video appears ready (readyState >= 3), forcing media ready state");
|
||||||
markMediaReady();
|
markMediaReady();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1430,7 +1433,7 @@
|
|||||||
jrayActors = actors;
|
jrayActors = actors;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[VideoPlayer] JRay lookup failed:", err);
|
log.warn("JRay lookup failed:", err);
|
||||||
if (token === jrayRequestId) jrayActors = [];
|
if (token === jrayRequestId) jrayActors = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1508,8 +1511,8 @@
|
|||||||
// reason. Log the element state so an unexplained pause/resume loop can be
|
// reason. Log the element state so an unexplained pause/resume loop can be
|
||||||
// attributed from an adb capture instead of guessed at.
|
// attributed from an adb capture instead of guessed at.
|
||||||
const el = videoElement;
|
const el = videoElement;
|
||||||
console.log(
|
log.debug(
|
||||||
`[VideoPlayer] pause event — t=${el ? el.currentTime.toFixed(2) : "?"}` +
|
`pause event — t=${el ? el.currentTime.toFixed(2) : "?"}` +
|
||||||
` readyState=${el?.readyState}` +
|
` readyState=${el?.readyState}` +
|
||||||
` networkState=${el?.networkState}` +
|
` networkState=${el?.networkState}` +
|
||||||
` seeking=${el?.seeking}` +
|
` seeking=${el?.seeking}` +
|
||||||
@@ -1553,7 +1556,7 @@
|
|||||||
try {
|
try {
|
||||||
await playerController.toggle();
|
await playerController.toggle();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[VideoPlayer] Failed to toggle playback:", err);
|
log.error("Failed to toggle playback:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1595,7 +1598,7 @@
|
|||||||
isDraggingSeekBar = false;
|
isDraggingSeekBar = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("[VideoPlayer] Seeking to:", targetTime.toFixed(2));
|
log.debug("Seeking to:", targetTime.toFixed(2));
|
||||||
|
|
||||||
// Optimistic display; the primitive updates currentTime/seekOffset as it
|
// Optimistic display; the primitive updates currentTime/seekOffset as it
|
||||||
// completes (reloadSource drives the stream URL via the adapter bridge).
|
// completes (reloadSource drives the stream URL via the adapter bridge).
|
||||||
@@ -1617,9 +1620,9 @@
|
|||||||
startTimeUpdates();
|
startTimeUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[VideoPlayer] Seek completed at:", currentTime.toFixed(2), "offset:", seekOffset);
|
log.debug("Seek completed at:", currentTime.toFixed(2), "offset:", seekOffset);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[VideoPlayer] Seek failed:", err);
|
log.error("Seek failed:", err);
|
||||||
} finally {
|
} finally {
|
||||||
isSeeking = false;
|
isSeeking = false;
|
||||||
isDraggingSeekBar = false;
|
isDraggingSeekBar = false;
|
||||||
@@ -1654,12 +1657,12 @@
|
|||||||
|
|
||||||
function toggleBackgroundAudio() {
|
function toggleBackgroundAudio() {
|
||||||
backgroundAudioOn = !backgroundAudioOn;
|
backgroundAudioOn = !backgroundAudioOn;
|
||||||
console.log("[VideoPlayer] Background-audio toggle ->", backgroundAudioOn);
|
log.debug("Background-audio toggle ->", backgroundAudioOn);
|
||||||
// Arm/disarm native background-audio mode AND flip auto-PiP the other way,
|
// Arm/disarm native background-audio mode AND flip auto-PiP the other way,
|
||||||
// so exactly one background behavior is active.
|
// so exactly one background behavior is active.
|
||||||
const armed = setBackgroundAudioEnabled(backgroundAudioOn);
|
const armed = setBackgroundAudioEnabled(backgroundAudioOn);
|
||||||
if (!armed) {
|
if (!armed) {
|
||||||
console.warn("[VideoPlayer] Background audio NOT armed natively (no bridge)");
|
log.warn("Background audio NOT armed natively (no bridge)");
|
||||||
}
|
}
|
||||||
setAutoEnterEnabled(!backgroundAudioOn);
|
setAutoEnterEnabled(!backgroundAudioOn);
|
||||||
}
|
}
|
||||||
@@ -1675,7 +1678,7 @@
|
|||||||
// if the element is mid-teardown — which shipped audio starting from 0:00.
|
// if the element is mid-teardown — which shipped audio starting from 0:00.
|
||||||
const pos = computeHandoffPosition(currentTime, 0);
|
const pos = computeHandoffPosition(currentTime, 0);
|
||||||
const wasPlaying = isPlaying;
|
const wasPlaying = isPlaying;
|
||||||
console.log("[VideoPlayer] Background-audio handoff at position:", pos.toFixed(1));
|
log.debug("Background-audio handoff at position:", pos.toFixed(1));
|
||||||
handoffState = { active: true, wasPlaying };
|
handoffState = { active: true, wasPlaying };
|
||||||
try {
|
try {
|
||||||
if (!media) return;
|
if (!media) return;
|
||||||
@@ -1716,7 +1719,7 @@
|
|||||||
videoElement.load();
|
videoElement.load();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[VideoPlayer] Background-audio handoff failed:", err);
|
log.error("Background-audio handoff failed:", err);
|
||||||
handoffState = { ...initialHandoffState };
|
handoffState = { ...initialHandoffState };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1734,7 +1737,7 @@
|
|||||||
try {
|
try {
|
||||||
// Absolute position the native audio reached (base offset applied in Rust).
|
// Absolute position the native audio reached (base offset applied in Rust).
|
||||||
const pos = await commands.playerExitBackgroundAudio();
|
const pos = await commands.playerExitBackgroundAudio();
|
||||||
console.log("[VideoPlayer] Returning from background audio at:", pos.toFixed(1));
|
log.debug("Returning from background audio at:", pos.toFixed(1));
|
||||||
|
|
||||||
isMediaReady = false;
|
isMediaReady = false;
|
||||||
// The foreground seek below (pendingForegroundSeek/handleCanPlay) OWNS the
|
// The foreground seek below (pendingForegroundSeek/handleCanPlay) OWNS the
|
||||||
@@ -1837,7 +1840,7 @@
|
|||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
currentStreamUrl = targetUrl;
|
currentStreamUrl = targetUrl;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[VideoPlayer] Background-audio return failed:", err);
|
log.error("Background-audio return failed:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1856,7 +1859,7 @@
|
|||||||
// WebKitGTK rejects when the gesture isn't recognised as user-activated;
|
// WebKitGTK rejects when the gesture isn't recognised as user-activated;
|
||||||
// the immersive call below is what matters on Android, so don't let a
|
// the immersive call below is what matters on Android, so don't let a
|
||||||
// rejection here abort it.
|
// rejection here abort it.
|
||||||
console.warn("[VideoPlayer] requestFullscreen rejected:", err);
|
log.warn("requestFullscreen rejected:", err);
|
||||||
});
|
});
|
||||||
enterImmersive();
|
enterImmersive();
|
||||||
isFullscreen = true;
|
isFullscreen = true;
|
||||||
@@ -1906,7 +1909,7 @@
|
|||||||
});
|
});
|
||||||
pendingSeekTarget = newTime;
|
pendingSeekTarget = newTime;
|
||||||
|
|
||||||
console.log("[VideoPlayer] Relative seek:", {
|
log.debug("Relative seek:", {
|
||||||
offset: `${seconds > 0 ? "+" : ""}${seconds}s`,
|
offset: `${seconds > 0 ? "+" : ""}${seconds}s`,
|
||||||
from: currentTime.toFixed(2),
|
from: currentTime.toFixed(2),
|
||||||
to: newTime.toFixed(2),
|
to: newTime.toFixed(2),
|
||||||
@@ -2092,7 +2095,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function selectAudioTrack(streamIndex: number, arrayIndex: number) {
|
async function selectAudioTrack(streamIndex: number, arrayIndex: number) {
|
||||||
console.log("[VideoPlayer] Selecting audio track - streamIndex:", streamIndex, "arrayIndex:", arrayIndex);
|
log.debug("Selecting audio track - streamIndex:", streamIndex, "arrayIndex:", arrayIndex);
|
||||||
const previousTrackIndex = selectedAudioTrackIndex;
|
const previousTrackIndex = selectedAudioTrackIndex;
|
||||||
selectedAudioTrackIndex = streamIndex;
|
selectedAudioTrackIndex = streamIndex;
|
||||||
showAudioTrackMenu = false;
|
showAudioTrackMenu = false;
|
||||||
@@ -2113,7 +2116,7 @@
|
|||||||
startTimeUpdates();
|
startTimeUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[VideoPlayer] Successfully changed audio track");
|
log.debug("Successfully changed audio track");
|
||||||
|
|
||||||
// Save series audio preference for future episodes
|
// Save series audio preference for future episodes
|
||||||
if (media && media.seriesId) {
|
if (media && media.seriesId) {
|
||||||
@@ -2132,14 +2135,14 @@
|
|||||||
selectedTrack.language || null,
|
selectedTrack.language || null,
|
||||||
streamIndex
|
streamIndex
|
||||||
);
|
);
|
||||||
console.log("[VideoPlayer] Saved series audio preference:", selectedTrack.displayTitle || selectedTrack.language);
|
log.debug("Saved series audio preference:", selectedTrack.displayTitle || selectedTrack.language);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[VideoPlayer] Failed to save series audio preference:", err);
|
log.warn("Failed to save series audio preference:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[VideoPlayer] Failed to change audio track:", err);
|
log.error("Failed to change audio track:", err);
|
||||||
// Revert to previous track on error
|
// Revert to previous track on error
|
||||||
selectedAudioTrackIndex = previousTrackIndex;
|
selectedAudioTrackIndex = previousTrackIndex;
|
||||||
}
|
}
|
||||||
@@ -2177,9 +2180,9 @@
|
|||||||
if (videoElement && !videoElement.paused) {
|
if (videoElement && !videoElement.paused) {
|
||||||
startTimeUpdates();
|
startTimeUpdates();
|
||||||
}
|
}
|
||||||
console.log("[VideoPlayer] Streaming quality changed:", quality);
|
log.debug("Streaming quality changed:", quality);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[VideoPlayer] Failed to change streaming quality:", err);
|
log.error("Failed to change streaming quality:", err);
|
||||||
selectedQuality = previous;
|
selectedQuality = previous;
|
||||||
} finally {
|
} finally {
|
||||||
changingQuality = false;
|
changingQuality = false;
|
||||||
@@ -2210,7 +2213,7 @@
|
|||||||
const trackStreamIndex = parseInt(track.getAttribute("data-stream-index") || "-1");
|
const trackStreamIndex = parseInt(track.getAttribute("data-stream-index") || "-1");
|
||||||
if (trackStreamIndex === streamIndex && track.track) {
|
if (trackStreamIndex === streamIndex && track.track) {
|
||||||
track.track.mode = "showing";
|
track.track.mode = "showing";
|
||||||
console.log("[VideoPlayer] Enabled subtitle track:", streamIndex);
|
log.debug("Enabled subtitle track:", streamIndex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2230,7 +2233,7 @@
|
|||||||
* TRACES: UR-020 | DR-023, IR-016 | UT-147
|
* TRACES: UR-020 | DR-023, IR-016 | UT-147
|
||||||
*/
|
*/
|
||||||
async function selectSubtitle(streamIndex: number | null) {
|
async function selectSubtitle(streamIndex: number | null) {
|
||||||
console.log("[VideoPlayer] Selecting subtitle - streamIndex:", streamIndex);
|
log.debug("Selecting subtitle - streamIndex:", streamIndex);
|
||||||
selectedSubtitleIndex = streamIndex;
|
selectedSubtitleIndex = streamIndex;
|
||||||
showSubtitleMenu = false;
|
showSubtitleMenu = false;
|
||||||
|
|
||||||
@@ -2242,9 +2245,9 @@
|
|||||||
try {
|
try {
|
||||||
const indexToUse = nativeSubtitleArrayIndex(sentSubtitleTracks, streamIndex);
|
const indexToUse = nativeSubtitleArrayIndex(sentSubtitleTracks, streamIndex);
|
||||||
await commands.playerSetSubtitleTrack(indexToUse);
|
await commands.playerSetSubtitleTrack(indexToUse);
|
||||||
console.log("[VideoPlayer] Native backend subtitle track changed - streamIndex:", streamIndex, "position:", indexToUse);
|
log.debug("Native backend subtitle track changed - streamIndex:", streamIndex, "position:", indexToUse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[VideoPlayer] Failed to set subtitle track:", error);
|
log.error("Failed to set subtitle track:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
import { toast } from "$lib/stores/toast";
|
import { toast } from "$lib/stores/toast";
|
||||||
import CreatePlaylistModal from "./CreatePlaylistModal.svelte";
|
import CreatePlaylistModal from "./CreatePlaylistModal.svelte";
|
||||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("AddToPlaylist");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
@@ -39,7 +42,7 @@
|
|||||||
playlists = result.items;
|
playlists = result.items;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[AddToPlaylist] Failed to load playlists:", e);
|
log.error("Failed to load playlists:", e);
|
||||||
toast.error("Failed to load playlists");
|
toast.error("Failed to load playlists");
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
@@ -54,7 +57,7 @@
|
|||||||
toast.success(`Added to "${playlist.name}"`);
|
toast.success(`Added to "${playlist.name}"`);
|
||||||
onClose?.();
|
onClose?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[AddToPlaylist] Failed to add:", e);
|
log.error("Failed to add:", e);
|
||||||
toast.error("Failed to add to playlist");
|
toast.error("Failed to add to playlist");
|
||||||
} finally {
|
} finally {
|
||||||
adding = null;
|
adding = null;
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import { toast } from "$lib/stores/toast";
|
import { toast } from "$lib/stores/toast";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("CreatePlaylist");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
@@ -27,7 +30,7 @@
|
|||||||
onClose?.();
|
onClose?.();
|
||||||
goto(`/library/${result.id}`);
|
goto(`/library/${result.id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[CreatePlaylist] Failed:", e);
|
log.error("Failed:", e);
|
||||||
toast.error("Failed to create playlist");
|
toast.error("Failed to create playlist");
|
||||||
} finally {
|
} finally {
|
||||||
creating = false;
|
creating = false;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
import { playbackPosition } from "$lib/stores/player";
|
import { playbackPosition } from "$lib/stores/player";
|
||||||
import { lmsSync, isLmsSession, macForSession } from "$lib/stores/lmsSync";
|
import { lmsSync, isLmsSession, macForSession } from "$lib/stores/lmsSync";
|
||||||
import type { Session } from "$lib/api/types";
|
import type { Session } from "$lib/api/types";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("SessionPicker");
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
@@ -41,7 +44,7 @@
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to select session:", error);
|
log.error("Failed to select session:", error);
|
||||||
// Error is already stored in playbackMode store
|
// Error is already stored in playbackMode store
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +70,7 @@
|
|||||||
await lmsSync.fuseZone(masterMac, zoneMac);
|
await lmsSync.fuseZone(masterMac, zoneMac);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to toggle LMS zone:", error);
|
log.error("Failed to toggle LMS zone:", error);
|
||||||
// Error is surfaced via the lmsSync store
|
// Error is surfaced via the lmsSync store
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +82,7 @@
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to transfer to local:", error);
|
log.error("Failed to transfer to local:", error);
|
||||||
// Error is already stored in playbackMode store
|
// Error is already stored in playbackMode store
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +94,7 @@
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to disconnect:", error);
|
log.error("Failed to disconnect:", error);
|
||||||
// Error is already stored in playbackMode store
|
// Error is already stored in playbackMode store
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { AdapterHost, PlayerAdapter, PlayerLoadOptions } from "./types";
|
import type { AdapterHost, PlayerAdapter, PlayerLoadOptions } from "./types";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("Html5PlayerAdapter");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Narrow seam the owning component provides so the adapter can execute the
|
* Narrow seam the owning component provides so the adapter can execute the
|
||||||
@@ -110,7 +113,7 @@ export class Html5PlayerAdapter implements PlayerAdapter {
|
|||||||
// play promise while the element keeps trying. Surfacing it would report
|
// play promise while the element keeps trying. Surfacing it would report
|
||||||
// an error roughly once a second for the duration of the stall.
|
// an error roughly once a second for the duration of the stall.
|
||||||
if (isPlayInterruptedError(err)) {
|
if (isPlayInterruptedError(err)) {
|
||||||
console.debug("[Html5PlayerAdapter] play() interrupted by pause (stall recovery)");
|
log.debug("play() interrupted by pause (stall recovery)");
|
||||||
} else {
|
} else {
|
||||||
this.host.onError(`play() failed: ${err}`);
|
this.host.onError(`play() failed: ${err}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
|
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
import type { AdapterHost } from "./types";
|
import type { AdapterHost } from "./types";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("rustReportHost");
|
||||||
|
|
||||||
const POSITION_REPORT_INTERVAL_MS = 250;
|
const POSITION_REPORT_INTERVAL_MS = 250;
|
||||||
|
|
||||||
@@ -37,7 +40,7 @@ export async function reportState(
|
|||||||
try {
|
try {
|
||||||
await commands.playerReportState(state, mediaId);
|
await commands.playerReportState(state, mediaId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[rustReportHost] Failed to report state:", err);
|
log.warn("Failed to report state:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +57,7 @@ export async function reportPosition(
|
|||||||
try {
|
try {
|
||||||
await commands.playerReportPosition(position, Number.isFinite(duration) ? duration : 0);
|
await commands.playerReportPosition(position, Number.isFinite(duration) ? duration : 0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[rustReportHost] Failed to report position:", err);
|
log.warn("Failed to report position:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ export async function reportMediaLoaded(duration: number): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
await commands.playerReportMediaLoaded(Number.isFinite(duration) ? duration : 0);
|
await commands.playerReportMediaLoaded(Number.isFinite(duration) ? duration : 0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[rustReportHost] Failed to report media loaded:", err);
|
log.warn("Failed to report media loaded:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +88,7 @@ export function createRustReportHost(
|
|||||||
onPosition: (position, duration) => void reportPosition(position, duration),
|
onPosition: (position, duration) => void reportPosition(position, duration),
|
||||||
onMediaLoaded: (duration) => void reportMediaLoaded(duration),
|
onMediaLoaded: (duration) => void reportMediaLoaded(duration),
|
||||||
onEnded: view.onEnded ?? (() => {}),
|
onEnded: view.onEnded ?? (() => {}),
|
||||||
onError: view.onError ?? ((message) => console.warn("[rustReportHost] adapter error:", message)),
|
onError: view.onError ?? ((message) => log.warn("adapter error:", message)),
|
||||||
onStreamUrlChanged: view.onStreamUrlChanged ?? (() => {}),
|
onStreamUrlChanged: view.onStreamUrlChanged ?? (() => {}),
|
||||||
onBuffering: view.onBuffering ?? (() => {}),
|
onBuffering: view.onBuffering ?? (() => {}),
|
||||||
onReady: view.onReady ?? (() => {}),
|
onReady: view.onReady ?? (() => {}),
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("deviceId");
|
||||||
|
|
||||||
let cachedDeviceId: string | null = null;
|
let cachedDeviceId: string | null = null;
|
||||||
|
|
||||||
@@ -34,7 +37,7 @@ export async function getDeviceId(): Promise<string> {
|
|||||||
cachedDeviceId = deviceId;
|
cachedDeviceId = deviceId;
|
||||||
return deviceId;
|
return deviceId;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[deviceId] Failed to get device ID from backend:", e);
|
log.error("Failed to get device ID from backend:", e);
|
||||||
throw new Error("Failed to initialize device ID: " + String(e));
|
throw new Error("Failed to initialize device ID: " + String(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { commands } from "$lib/api/bindings";
|
|||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import { isConnected } from "$lib/stores/connectivity";
|
import { isConnected } from "$lib/stores/connectivity";
|
||||||
import { setFavorite } from "$lib/stores/favorites";
|
import { setFavorite } from "$lib/stores/favorites";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("Favorites");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the favorite status of an item.
|
* Toggle the favorite status of an item.
|
||||||
@@ -59,7 +62,7 @@ export async function toggleFavorite(
|
|||||||
// 3. Mark as synced
|
// 3. Mark as synced
|
||||||
await commands.storageMarkSynced(userId, itemId);
|
await commands.storageMarkSynced(userId, itemId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to sync favorite to server:", error);
|
log.error("Failed to sync favorite to server:", error);
|
||||||
// Favorite is stored locally and will be synced later
|
// Favorite is stored locally and will be synced later
|
||||||
// via sync queue (when implemented)
|
// via sync queue (when implemented)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("ImageCache");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Statistics about the thumbnail cache
|
* Statistics about the thumbnail cache
|
||||||
@@ -48,7 +51,7 @@ export async function getCachedImageUrl(
|
|||||||
return convertFileSrc(cachedPath);
|
return convertFileSrc(cachedPath);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.debug("Failed to check thumbnail cache:", e);
|
log.debug("Failed to check thumbnail cache:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build server URL
|
// Build server URL
|
||||||
@@ -63,7 +66,7 @@ export async function getCachedImageUrl(
|
|||||||
// Trigger background caching (fire and forget)
|
// Trigger background caching (fire and forget)
|
||||||
commands.thumbnailSave(itemId, imageType, tag, serverImageUrl).catch((e) => {
|
commands.thumbnailSave(itemId, imageType, tag, serverImageUrl).catch((e) => {
|
||||||
// Silently fail - caching is best-effort
|
// Silently fail - caching is best-effort
|
||||||
console.debug("Background thumbnail cache failed:", e);
|
log.debug("Background thumbnail cache failed:", e);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return server URL for immediate display
|
// Return server URL for immediate display
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
import { commands } from '$lib/api/bindings';
|
import { commands } from '$lib/api/bindings';
|
||||||
import type { NetworkType } from '$lib/api/bindings';
|
import type { NetworkType } from '$lib/api/bindings';
|
||||||
|
import { createLogger } from '$lib/utils/logger';
|
||||||
|
|
||||||
|
const log = createLogger('NetworkType');
|
||||||
|
|
||||||
/** The Android bridge, present only in the Android WebView. */
|
/** The Android bridge, present only in the Android WebView. */
|
||||||
interface AndroidNetworkTypeBridge {
|
interface AndroidNetworkTypeBridge {
|
||||||
@@ -62,7 +65,7 @@ export async function reportNetworkState(): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Never let network reporting break the UI — the gate fails closed on
|
// Never let network reporting break the UI — the gate fails closed on
|
||||||
// the Rust side, so a missed report at worst delays a queued download.
|
// the Rust side, so a missed report at worst delays a queued download.
|
||||||
console.warn('[NetworkType] Failed to report network state:', error);
|
log.warn('Failed to report network state:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +106,7 @@ export async function areDownloadsAllowed(): Promise<boolean> {
|
|||||||
try {
|
try {
|
||||||
return await commands.getDownloadsAllowed();
|
return await commands.getDownloadsAllowed();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[NetworkType] Failed to query download gate:', error);
|
log.warn('Failed to query download gate:', error);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import { goto } from "$app/navigation";
|
|||||||
import { cancelAutoplayCountdown } from "$lib/api/autoplay";
|
import { cancelAutoplayCountdown } from "$lib/api/autoplay";
|
||||||
import { nextEpisode } from "$lib/stores/nextEpisode";
|
import { nextEpisode } from "$lib/stores/nextEpisode";
|
||||||
import type { MediaItem } from "$lib/api/types";
|
import type { MediaItem } from "$lib/api/types";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("NextEpisode");
|
||||||
|
|
||||||
/** Guard against double-navigation */
|
/** Guard against double-navigation */
|
||||||
let isNavigating = false;
|
let isNavigating = false;
|
||||||
@@ -46,11 +49,11 @@ export async function cancelAutoPlay() {
|
|||||||
*/
|
*/
|
||||||
function navigateToEpisode(episode: MediaItem) {
|
function navigateToEpisode(episode: MediaItem) {
|
||||||
if (isNavigating) {
|
if (isNavigating) {
|
||||||
console.warn("[NextEpisode] Already navigating, skipping duplicate navigation to", episode.id);
|
log.warn("Already navigating, skipping duplicate navigation to", episode.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isNavigating = true;
|
isNavigating = true;
|
||||||
console.log("[NextEpisode] Navigating to next episode:", episode.id, episode.name);
|
log.debug("Navigating to next episode:", episode.id, episode.name);
|
||||||
nextEpisode.hidePopup();
|
nextEpisode.hidePopup();
|
||||||
goto(`/player/${episode.id}?restart=true`, { replaceState: true }).finally(() => {
|
goto(`/player/${episode.id}?restart=true`, { replaceState: true }).finally(() => {
|
||||||
isNavigating = false;
|
isNavigating = false;
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import { writable, type Writable } from "svelte/store";
|
|||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import { isConnected } from "$lib/stores/connectivity";
|
import { isConnected } from "$lib/stores/connectivity";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("OfflineCatalog");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When true (and offline), library grids reveal greyed-out versions of media
|
* When true (and offline), library grids reveal greyed-out versions of media
|
||||||
@@ -58,7 +61,7 @@ async function pushCatalogVisibility(connected: boolean, showCatalog: boolean):
|
|||||||
try {
|
try {
|
||||||
await commands.setShowServerCatalog(include);
|
await commands.setShowServerCatalog(include);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[OfflineCatalog] Failed to set catalog visibility:", err);
|
log.warn("Failed to set catalog visibility:", err);
|
||||||
// The backend is still on the old gate, so forget that we sent this —
|
// The backend is still on the old gate, so forget that we sent this —
|
||||||
// otherwise the next identical transition is skipped as a no-op and the
|
// otherwise the next identical transition is skipped as a no-op and the
|
||||||
// frontend and backend disagree about the filter for the rest of the
|
// frontend and backend disagree about the filter for the rest of the
|
||||||
@@ -115,12 +118,12 @@ export async function syncCatalog(): Promise<void> {
|
|||||||
syncInProgress = true;
|
syncInProgress = true;
|
||||||
try {
|
try {
|
||||||
const result = await commands.syncFullCatalog(handle);
|
const result = await commands.syncFullCatalog(handle);
|
||||||
console.info(
|
log.info(
|
||||||
`[OfflineCatalog] Synced ${result.itemsCached} items (${result.librariesFailed} libraries failed)`
|
`Synced ${result.itemsCached} items (${result.librariesFailed} libraries failed)`
|
||||||
);
|
);
|
||||||
await refreshSyncStatus();
|
await refreshSyncStatus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[OfflineCatalog] Full catalog sync failed:", err);
|
log.warn("Full catalog sync failed:", err);
|
||||||
} finally {
|
} finally {
|
||||||
syncInProgress = false;
|
syncInProgress = false;
|
||||||
}
|
}
|
||||||
@@ -136,12 +139,12 @@ export async function resumeQueued(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const result = await commands.resumeQueuedDownloads(handle);
|
const result = await commands.resumeQueuedDownloads(handle);
|
||||||
if (result.resolved > 0 || result.failed > 0) {
|
if (result.resolved > 0 || result.failed > 0) {
|
||||||
console.info(
|
log.info(
|
||||||
`[OfflineCatalog] Resumed queued downloads: ${result.resolved} resolved, ${result.failed} failed`
|
`Resumed queued downloads: ${result.resolved} resolved, ${result.failed} failed`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[OfflineCatalog] Failed to resume queued downloads:", err);
|
log.warn("Failed to resume queued downloads:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +154,7 @@ export async function refreshSyncStatus(): Promise<void> {
|
|||||||
const status = await commands.catalogSyncStatus();
|
const status = await commands.catalogSyncStatus();
|
||||||
lastCatalogSync.set(status.lastSyncedAt ?? null);
|
lastCatalogSync.set(status.lastSyncedAt ?? null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.debug("[OfflineCatalog] Failed to fetch sync status:", err);
|
log.debug("Failed to fetch sync status:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("capabilities");
|
||||||
|
|
||||||
export interface PlaybackCapabilities {
|
export interface PlaybackCapabilities {
|
||||||
/** Audio renders through a webview `<audio>` element, not a native backend. */
|
/** Audio renders through a webview `<audio>` element, not a native backend. */
|
||||||
@@ -52,7 +55,7 @@ export async function getPlaybackCapabilities(): Promise<PlaybackCapabilities> {
|
|||||||
};
|
};
|
||||||
return cached;
|
return cached;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[capabilities] player_get_capabilities failed:", err);
|
log.warn("player_get_capabilities failed:", err);
|
||||||
// Do NOT cache the fallback — a later call should get the real answer.
|
// Do NOT cache the fallback — a later call should get the real answer.
|
||||||
return FALLBACK;
|
return FALLBACK;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
|
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("PlaybackReporting");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record the start of playback **locally**, with the context it started from.
|
* Record the start of playback **locally**, with the context it started from.
|
||||||
@@ -32,8 +35,8 @@ export async function reportPlaybackStart(
|
|||||||
const positionMs = Math.floor(positionSeconds * 1000);
|
const positionMs = Math.floor(positionSeconds * 1000);
|
||||||
const userId = auth.getUserId();
|
const userId = auth.getUserId();
|
||||||
|
|
||||||
console.log(
|
log.debug(
|
||||||
"[PlaybackReporting] reportPlaybackStart - itemId:",
|
"reportPlaybackStart - itemId:",
|
||||||
itemId,
|
itemId,
|
||||||
"positionSeconds:",
|
"positionSeconds:",
|
||||||
positionSeconds,
|
positionSeconds,
|
||||||
@@ -47,7 +50,7 @@ export async function reportPlaybackStart(
|
|||||||
try {
|
try {
|
||||||
await commands.storageUpdatePlaybackContext(userId, itemId, positionMs, contextType, contextId);
|
await commands.storageUpdatePlaybackContext(userId, itemId, positionMs, contextType, contextId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[PlaybackReporting] Failed to update playback context:", e);
|
log.error("Failed to update playback context:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +79,7 @@ export async function reportPlaybackProgress(
|
|||||||
|
|
||||||
// Reduce logging for frequent progress updates
|
// Reduce logging for frequent progress updates
|
||||||
if (Math.floor(positionSeconds) % 30 === 0) {
|
if (Math.floor(positionSeconds) % 30 === 0) {
|
||||||
console.log("[PlaybackReporting] reportPlaybackProgress - itemId:", itemId, "position:", positionSeconds);
|
log.debug("reportPlaybackProgress - itemId:", itemId, "position:", positionSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update local DB only (progress updates are frequent, don't report to server)
|
// Update local DB only (progress updates are frequent, don't report to server)
|
||||||
@@ -84,7 +87,7 @@ export async function reportPlaybackProgress(
|
|||||||
try {
|
try {
|
||||||
await commands.storageUpdatePlaybackProgress(userId, itemId, positionMs);
|
await commands.storageUpdatePlaybackProgress(userId, itemId, positionMs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[PlaybackReporting] Failed to update local progress:", e);
|
log.error("Failed to update local progress:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,14 +104,14 @@ export async function reportPlaybackStopped(itemId: string, positionSeconds: num
|
|||||||
const positionMs = Math.floor(positionSeconds * 1000);
|
const positionMs = Math.floor(positionSeconds * 1000);
|
||||||
const userId = auth.getUserId();
|
const userId = auth.getUserId();
|
||||||
|
|
||||||
console.log("[PlaybackReporting] reportPlaybackStopped - itemId:", itemId, "positionSeconds:", positionSeconds);
|
log.debug("reportPlaybackStopped - itemId:", itemId, "positionSeconds:", positionSeconds);
|
||||||
|
|
||||||
// Update local DB first (always works, even offline)
|
// Update local DB first (always works, even offline)
|
||||||
if (userId) {
|
if (userId) {
|
||||||
try {
|
try {
|
||||||
await commands.storageUpdatePlaybackProgress(userId, itemId, positionMs);
|
await commands.storageUpdatePlaybackProgress(userId, itemId, positionMs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[PlaybackReporting] Failed to update local progress:", e);
|
log.error("Failed to update local progress:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +123,7 @@ export async function reportPlaybackStopped(itemId: string, positionSeconds: num
|
|||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
await repo.reportPlaybackStopped(itemId, positionMs);
|
await repo.reportPlaybackStopped(itemId, positionMs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[PlaybackReporting] Stop-report did not reach the server; queued for sync:", e);
|
log.warn("Stop-report did not reach the server; queued for sync:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,14 +136,14 @@ export async function reportPlaybackStopped(itemId: string, positionSeconds: num
|
|||||||
export async function markAsPlayed(itemId: string): Promise<void> {
|
export async function markAsPlayed(itemId: string): Promise<void> {
|
||||||
const userId = auth.getUserId();
|
const userId = auth.getUserId();
|
||||||
|
|
||||||
console.log("[PlaybackReporting] markAsPlayed - itemId:", itemId);
|
log.debug("markAsPlayed - itemId:", itemId);
|
||||||
|
|
||||||
// Update local DB first
|
// Update local DB first
|
||||||
if (userId) {
|
if (userId) {
|
||||||
try {
|
try {
|
||||||
await commands.storageMarkPlayed(userId, itemId);
|
await commands.storageMarkPlayed(userId, itemId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[PlaybackReporting] Failed to mark as played in local DB:", e);
|
log.error("Failed to mark as played in local DB:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +156,6 @@ export async function markAsPlayed(itemId: string): Promise<void> {
|
|||||||
await repo.reportPlaybackStopped(itemId, item.durationMs);
|
await repo.reportPlaybackStopped(itemId, item.durationMs);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[PlaybackReporting] Failed to report as played:", e);
|
log.error("Failed to report as played:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import { preloadUpcomingTracks } from "$lib/services/preload";
|
|||||||
import { playerController } from "$lib/player";
|
import { playerController } from "$lib/player";
|
||||||
import type { MediaItem } from "$lib/api/types";
|
import type { MediaItem } from "$lib/api/types";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("playerEvents");
|
||||||
|
|
||||||
// PlayerStatusEvent and SleepTimerMode are generated by tauri-specta and
|
// PlayerStatusEvent and SleepTimerMode are generated by tauri-specta and
|
||||||
// imported from $lib/api/bindings — they are the authoritative shapes emitted
|
// imported from $lib/api/bindings — they are the authoritative shapes emitted
|
||||||
@@ -34,7 +37,7 @@ let isInitialized = false;
|
|||||||
*/
|
*/
|
||||||
export async function initPlayerEvents(): Promise<void> {
|
export async function initPlayerEvents(): Promise<void> {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
console.warn("Player events already initialized");
|
log.warn("Player events already initialized");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,9 +46,9 @@ export async function initPlayerEvents(): Promise<void> {
|
|||||||
handlePlayerEvent(event.payload);
|
handlePlayerEvent(event.payload);
|
||||||
});
|
});
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
console.log("Player event listener initialized");
|
log.debug("Player event listener initialized");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to initialize player events:", e);
|
log.error("Failed to initialize player events:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +101,7 @@ function handlePlayerEvent(event: PlayerStatusEvent): void {
|
|||||||
|
|
||||||
case "buffering":
|
case "buffering":
|
||||||
// Could show buffering indicator in UI
|
// Could show buffering indicator in UI
|
||||||
console.debug(`Buffering: ${event.percent}%`);
|
log.debug(`Buffering: ${event.percent}%`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "error":
|
case "error":
|
||||||
@@ -196,7 +199,7 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
|
|||||||
// When local playback starts, ensure mode is set to local
|
// When local playback starts, ensure mode is set to local
|
||||||
const mode = get(playbackMode);
|
const mode = get(playbackMode);
|
||||||
if (mode.mode !== "local") {
|
if (mode.mode !== "local") {
|
||||||
console.log("Setting playback mode to local");
|
log.debug("Setting playback mode to local");
|
||||||
playbackMode.setMode("local");
|
playbackMode.setMode("local");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +218,7 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
|
|||||||
// Trigger preloading of upcoming tracks in the background
|
// Trigger preloading of upcoming tracks in the background
|
||||||
preloadUpcomingTracks().catch((e) => {
|
preloadUpcomingTracks().catch((e) => {
|
||||||
// Preload failures are non-critical, already logged in the service
|
// Preload failures are non-critical, already logged in the service
|
||||||
console.debug("[playerEvents] Preload failed (non-critical):", e);
|
log.debug("Preload failed (non-critical):", e);
|
||||||
});
|
});
|
||||||
} else if (state === "paused" && currentItem) {
|
} else if (state === "paused" && currentItem) {
|
||||||
// Keep current position and duration from store. The same track is
|
// Keep current position and duration from store. The same track is
|
||||||
@@ -240,7 +243,7 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
|
|||||||
// When local playback stops, revert to idle mode
|
// When local playback stops, revert to idle mode
|
||||||
const currentMode = get(playbackMode);
|
const currentMode = get(playbackMode);
|
||||||
if (currentMode.mode === "local") {
|
if (currentMode.mode === "local") {
|
||||||
console.log("Setting playback mode to idle");
|
log.debug("Setting playback mode to idle");
|
||||||
playbackMode.setMode("idle");
|
playbackMode.setMode("idle");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +257,7 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
|
|||||||
function handleMediaLoaded(duration: number): void {
|
function handleMediaLoaded(duration: number): void {
|
||||||
// Media is now loaded and ready
|
// Media is now loaded and ready
|
||||||
// The state_changed event will handle setting the playing state
|
// The state_changed event will handle setting the playing state
|
||||||
console.debug(`Media loaded, duration: ${duration}s`);
|
log.debug(`Media loaded, duration: ${duration}s`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -268,7 +271,7 @@ async function handlePlaybackEnded(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
await commands.playerOnPlaybackEnded(null, null);
|
await commands.playerOnPlaybackEnded(null, null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[playerEvents] Failed to handle playback ended:", e);
|
log.error("Failed to handle playback ended:", e);
|
||||||
// Fallback: set idle state on error
|
// Fallback: set idle state on error
|
||||||
player.setIdle();
|
player.setIdle();
|
||||||
}
|
}
|
||||||
@@ -287,18 +290,18 @@ async function handlePlaybackEnded(): Promise<void> {
|
|||||||
* TRACES: UR-004, UR-040 | DR-130
|
* TRACES: UR-004, UR-040 | DR-130
|
||||||
*/
|
*/
|
||||||
async function handleError(message: string, recoverable: boolean): Promise<void> {
|
async function handleError(message: string, recoverable: boolean): Promise<void> {
|
||||||
console.error(`Playback error (recoverable: ${recoverable}): ${message}`);
|
log.error(`Playback error (recoverable: ${recoverable}): ${message}`);
|
||||||
|
|
||||||
if (recoverable) {
|
if (recoverable) {
|
||||||
try {
|
try {
|
||||||
if (await commands.playerRecoverStream()) {
|
if (await commands.playerRecoverStream()) {
|
||||||
console.log("Stream re-opened after a recoverable error - not stopping");
|
log.debug("Stream re-opened after a recoverable error - not stopping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fall through to the normal stop: a failed recovery attempt is still an
|
// Fall through to the normal stop: a failed recovery attempt is still an
|
||||||
// error, and leaving the player running would strand it mid-failure.
|
// error, and leaving the player running would strand it mid-failure.
|
||||||
console.error("Stream recovery attempt failed:", e);
|
log.error("Stream recovery attempt failed:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,9 +311,9 @@ async function handleError(message: string, recoverable: boolean): Promise<void>
|
|||||||
// This also reports playback stopped to Jellyfin server
|
// This also reports playback stopped to Jellyfin server
|
||||||
try {
|
try {
|
||||||
await commands.playerStop();
|
await commands.playerStop();
|
||||||
console.log("Backend player stopped after error");
|
log.debug("Backend player stopped after error");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to stop player after error:", e);
|
log.error("Failed to stop player after error:", e);
|
||||||
// Continue with state cleanup even if stop fails
|
// Continue with state cleanup even if stop fails
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,7 +362,7 @@ function handleControlCommand(action: string, position: number | null): void {
|
|||||||
void adapter.pause();
|
void adapter.pause();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn("[playerEvents] Unknown control command:", action);
|
log.warn("Unknown control command:", action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
import { commands } from '$lib/api/bindings';
|
import { commands } from '$lib/api/bindings';
|
||||||
import type { CacheConfig } from '$lib/api/bindings';
|
import type { CacheConfig } from '$lib/api/bindings';
|
||||||
import { auth } from '$lib/stores/auth';
|
import { auth } from '$lib/stores/auth';
|
||||||
|
import { createLogger } from '$lib/utils/logger';
|
||||||
|
|
||||||
|
const log = createLogger('Preload');
|
||||||
|
|
||||||
interface PreloadOptions {
|
interface PreloadOptions {
|
||||||
/** Enable debug logging */
|
/** Enable debug logging */
|
||||||
@@ -28,17 +31,17 @@ export async function preloadUpcomingTracks(options: PreloadOptions = {}): Promi
|
|||||||
const userId = overrideUserId || auth.getUserId();
|
const userId = overrideUserId || auth.getUserId();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
if (debug) console.log('[Preload] No active user session, skipping preload');
|
if (debug) log.debug('No active user session, skipping preload');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debug) console.log('[Preload] Triggering preload for user:', userId);
|
if (debug) log.debug('Triggering preload for user:', userId);
|
||||||
|
|
||||||
// downloadBasePath is currently unused in the backend
|
// downloadBasePath is currently unused in the backend
|
||||||
const result = await commands.playerPreloadUpcoming(userId, '/downloads');
|
const result = await commands.playerPreloadUpcoming(userId, '/downloads');
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log('[Preload] Result:', {
|
log.debug('Result:', {
|
||||||
queued: result.queuedCount,
|
queued: result.queuedCount,
|
||||||
alreadyDownloaded: result.alreadyDownloaded,
|
alreadyDownloaded: result.alreadyDownloaded,
|
||||||
skipped: result.skipped
|
skipped: result.skipped
|
||||||
@@ -47,12 +50,12 @@ export async function preloadUpcomingTracks(options: PreloadOptions = {}): Promi
|
|||||||
|
|
||||||
// Log meaningful results
|
// Log meaningful results
|
||||||
if (result.queuedCount > 0) {
|
if (result.queuedCount > 0) {
|
||||||
console.log(`[Preload] Queued ${result.queuedCount} track(s) for background download`);
|
log.debug(`Queued ${result.queuedCount} track(s) for background download`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Fail silently - preloading is a background optimization
|
// Fail silently - preloading is a background optimization
|
||||||
// Don't interrupt the user's playback experience
|
// Don't interrupt the user's playback experience
|
||||||
console.warn('[Preload] Failed to preload upcoming tracks:', error);
|
log.warn('Failed to preload upcoming tracks:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import { auth } from "$lib/stores/auth";
|
|||||||
// hand-written mirror — the mirror had already drifted (it predates `itemName`),
|
// hand-written mirror — the mirror had already drifted (it predates `itemName`),
|
||||||
// and a drifted duplicate is how a field silently stops reaching the UI.
|
// and a drifted duplicate is how a field silently stops reaching the UI.
|
||||||
import type { SyncQueueItem } from "$lib/api/bindings";
|
import type { SyncQueueItem } from "$lib/api/bindings";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("SyncService");
|
||||||
export type { SyncQueueItem };
|
export type { SyncQueueItem };
|
||||||
|
|
||||||
export type SyncOperation =
|
export type SyncOperation =
|
||||||
@@ -42,14 +45,14 @@ class SyncService {
|
|||||||
* Start the sync service (lifecycle managed by Rust backend)
|
* Start the sync service (lifecycle managed by Rust backend)
|
||||||
*/
|
*/
|
||||||
start(): void {
|
start(): void {
|
||||||
console.log("[SyncService] Started");
|
log.debug("Started");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the sync service (lifecycle managed by Rust backend)
|
* Stop the sync service (lifecycle managed by Rust backend)
|
||||||
*/
|
*/
|
||||||
stop(): void {
|
stop(): void {
|
||||||
console.log("[SyncService] Stopped");
|
log.debug("Stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,7 +77,7 @@ class SyncService {
|
|||||||
payload ? JSON.stringify(payload) : null
|
payload ? JSON.stringify(payload) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[SyncService] Queued ${operation} for item ${itemId}, id: ${id}`);
|
log.debug(`Queued ${operation} for item ${itemId}, id: ${id}`);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +158,7 @@ class SyncService {
|
|||||||
*/
|
*/
|
||||||
async cleanup(daysOld: number = 7): Promise<number> {
|
async cleanup(daysOld: number = 7): Promise<number> {
|
||||||
const deleted = await commands.syncCleanupCompleted(daysOld);
|
const deleted = await commands.syncCleanupCompleted(daysOld);
|
||||||
console.log(`[SyncService] Cleaned up ${deleted} old completed operations`);
|
log.debug(`Cleaned up ${deleted} old completed operations`);
|
||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +197,7 @@ class SyncService {
|
|||||||
const userId = auth.getUserId();
|
const userId = auth.getUserId();
|
||||||
if (userId) {
|
if (userId) {
|
||||||
await commands.syncClearUser(userId);
|
await commands.syncClearUser(userId);
|
||||||
console.log("[SyncService] Cleared sync queue for user");
|
log.debug("Cleared sync queue for user");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-39
@@ -13,6 +13,9 @@ import type { User, AuthResult } from "$lib/api/types";
|
|||||||
import type { Session, AuthServerInfo as ServerInfo } from "$lib/api/bindings";
|
import type { Session, AuthServerInfo as ServerInfo } from "$lib/api/bindings";
|
||||||
import { connectivity } from "./connectivity";
|
import { connectivity } from "./connectivity";
|
||||||
import { getDeviceId, clearCache as clearDeviceIdCache } from "$lib/services/deviceId";
|
import { getDeviceId, clearCache as clearDeviceIdCache } from "$lib/services/deviceId";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("Auth");
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
@@ -70,7 +73,7 @@ function createAuthStore() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
unlistenSessionVerified = await listen<{ user: User }>("auth:session-verified", (event) => {
|
unlistenSessionVerified = await listen<{ user: User }>("auth:session-verified", (event) => {
|
||||||
console.log("[Auth] Session verified:", event.payload.user.name);
|
log.debug("Session verified:", event.payload.user.name);
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
sessionVerified: true,
|
sessionVerified: true,
|
||||||
@@ -80,12 +83,12 @@ function createAuthStore() {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Auth] Failed to listen to session-verified event:", e);
|
log.error("Failed to listen to session-verified event:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
unlistenNeedsReauth = await listen<{ reason: string }>("auth:needs-reauth", (event) => {
|
unlistenNeedsReauth = await listen<{ reason: string }>("auth:needs-reauth", (event) => {
|
||||||
console.log("[Auth] Session needs re-authentication:", event.payload.reason);
|
log.debug("Session needs re-authentication:", event.payload.reason);
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
sessionVerified: false,
|
sessionVerified: false,
|
||||||
@@ -95,17 +98,17 @@ function createAuthStore() {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Auth] Failed to listen to needs-reauth event:", e);
|
log.error("Failed to listen to needs-reauth event:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
unlistenNetworkError = await listen<{ message: string }>("auth:network-error", (event) => {
|
unlistenNetworkError = await listen<{ message: string }>("auth:network-error", (event) => {
|
||||||
console.log("[Auth] Network error during verification:", event.payload.message);
|
log.debug("Network error during verification:", event.payload.message);
|
||||||
// Network errors don't trigger re-auth - just log them
|
// Network errors don't trigger re-auth - just log them
|
||||||
update((s) => ({ ...s, isVerifying: false }));
|
update((s) => ({ ...s, isVerifying: false }));
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Auth] Failed to listen to network-error event:", e);
|
log.error("Failed to listen to network-error event:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +147,7 @@ function createAuthStore() {
|
|||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const securityStatus = await commands.storageGetSecurityStatus();
|
const securityStatus = await commands.storageGetSecurityStatus();
|
||||||
console.log("[Auth] Security status:", securityStatus);
|
log.debug("Security status:", securityStatus);
|
||||||
if (!securityStatus.usingKeyring) {
|
if (!securityStatus.usingKeyring) {
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
@@ -153,17 +156,17 @@ function createAuthStore() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[Auth] Failed to get security status:", error);
|
log.warn("Failed to get security status:", error);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Initialize auth manager and get session
|
// Initialize auth manager and get session
|
||||||
console.log("[Auth] Initializing auth manager...");
|
log.debug("Initializing auth manager...");
|
||||||
const session = await commands.authInitialize();
|
const session = await commands.authInitialize();
|
||||||
console.log("[Auth] Session retrieval result:", session ? "Session found" : "No session found");
|
log.debug("Session retrieval result:", session ? "Session found" : "No session found");
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
console.log("[Auth] Restoring session for user:", session.username, "on server:", session.serverUrl);
|
log.debug("Restoring session for user:", session.username, "on server:", session.serverUrl);
|
||||||
|
|
||||||
// Create RepositoryClient for cache-first access. This IS required before
|
// Create RepositoryClient for cache-first access. This IS required before
|
||||||
// we mark authenticated — the first screen (library overview) reads
|
// we mark authenticated — the first screen (library overview) reads
|
||||||
@@ -184,9 +187,9 @@ function createAuthStore() {
|
|||||||
session.userId,
|
session.userId,
|
||||||
deviceId
|
deviceId
|
||||||
);
|
);
|
||||||
console.log("[Auth] Rust player configured for automatic playback reporting");
|
log.debug("Rust player configured for automatic playback reporting");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Failed to configure Rust player:", error);
|
log.error("Failed to configure Rust player:", error);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -205,7 +208,7 @@ function createAuthStore() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Start connectivity monitoring early to avoid appearing offline on startup
|
// Start connectivity monitoring early to avoid appearing offline on startup
|
||||||
console.log("[Auth] Starting early connectivity monitoring...");
|
log.debug("Starting early connectivity monitoring...");
|
||||||
connectivity.startMonitoring(session.serverUrl, {
|
connectivity.startMonitoring(session.serverUrl, {
|
||||||
onServerReconnected: () => {
|
onServerReconnected: () => {
|
||||||
// Retry session verification when server becomes reachable
|
// Retry session verification when server becomes reachable
|
||||||
@@ -214,10 +217,10 @@ function createAuthStore() {
|
|||||||
// Lazy import to avoid an auth <-> offlineCatalog import cycle.
|
// Lazy import to avoid an auth <-> offlineCatalog import cycle.
|
||||||
import("$lib/services/offlineCatalog")
|
import("$lib/services/offlineCatalog")
|
||||||
.then((m) => m.onReconnected())
|
.then((m) => m.onReconnected())
|
||||||
.catch((err) => console.warn("[Auth] Catalog reconnect failed:", err));
|
.catch((err) => log.warn("Catalog reconnect failed:", err));
|
||||||
},
|
},
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error("[Auth] Failed to start connectivity monitoring:", error);
|
log.error("Failed to start connectivity monitoring:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start background session verification — fire-and-forget. This is
|
// Start background session verification — fire-and-forget. This is
|
||||||
@@ -228,14 +231,14 @@ function createAuthStore() {
|
|||||||
try {
|
try {
|
||||||
const verifyDeviceId = await getDeviceId();
|
const verifyDeviceId = await getDeviceId();
|
||||||
await commands.authStartVerification(verifyDeviceId);
|
await commands.authStartVerification(verifyDeviceId);
|
||||||
console.log("[Auth] Background verification started");
|
log.debug("Background verification started");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Failed to start verification:", error);
|
log.error("Failed to start verification:", error);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
} else {
|
} else {
|
||||||
// No stored session
|
// No stored session
|
||||||
console.log("[Auth] No active session found");
|
log.debug("No active session found");
|
||||||
set({
|
set({
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -250,7 +253,7 @@ function createAuthStore() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Failed to initialize:", error);
|
log.error("Failed to initialize:", error);
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -269,15 +272,15 @@ function createAuthStore() {
|
|||||||
update((s) => ({ ...s, isLoading: true, error: null }));
|
update((s) => ({ ...s, isLoading: true, error: null }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("[Auth] Connecting to server:", serverUrl);
|
log.debug("Connecting to server:", serverUrl);
|
||||||
const serverInfo = await commands.authConnectToServer(serverUrl);
|
const serverInfo = await commands.authConnectToServer(serverUrl);
|
||||||
console.log("[Auth] Connected to server:", serverInfo.name, serverInfo.version);
|
log.debug("Connected to server:", serverInfo.name, serverInfo.version);
|
||||||
console.log("[Auth] Normalized URL:", serverInfo.normalizedUrl);
|
log.debug("Normalized URL:", serverInfo.normalizedUrl);
|
||||||
|
|
||||||
update((s) => ({ ...s, isLoading: false }));
|
update((s) => ({ ...s, isLoading: false }));
|
||||||
return serverInfo;
|
return serverInfo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Failed to connect to server:", error);
|
log.error("Failed to connect to server:", error);
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -297,11 +300,11 @@ function createAuthStore() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const deviceId = await getDeviceId();
|
const deviceId = await getDeviceId();
|
||||||
console.log("[Auth] Logging in as:", username);
|
log.debug("Logging in as:", username);
|
||||||
|
|
||||||
const authResult = await commands.authLogin(serverUrl, username, password, deviceId);
|
const authResult = await commands.authLogin(serverUrl, username, password, deviceId);
|
||||||
|
|
||||||
console.log("[Auth] Login successful:", authResult.user);
|
log.debug("Login successful:", authResult.user);
|
||||||
|
|
||||||
// Save to storage
|
// Save to storage
|
||||||
await commands.storageSaveServer(authResult.serverId, serverName, serverUrl, null);
|
await commands.storageSaveServer(authResult.serverId, serverName, serverUrl, null);
|
||||||
@@ -340,9 +343,9 @@ function createAuthStore() {
|
|||||||
authResult.user.id,
|
authResult.user.id,
|
||||||
playerDeviceId
|
playerDeviceId
|
||||||
);
|
);
|
||||||
console.log("[Auth] Rust player configured for playback reporting");
|
log.debug("Rust player configured for playback reporting");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Failed to configure Rust player:", error);
|
log.error("Failed to configure Rust player:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
@@ -364,12 +367,12 @@ function createAuthStore() {
|
|||||||
const verifyDeviceId = await getDeviceId();
|
const verifyDeviceId = await getDeviceId();
|
||||||
await commands.authStartVerification(verifyDeviceId);
|
await commands.authStartVerification(verifyDeviceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Failed to start verification:", error);
|
log.error("Failed to start verification:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return authResult;
|
return authResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Login failed:", error);
|
log.error("Login failed:", error);
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
update((s) => ({ ...s, isLoading: false, error: errorMessage }));
|
update((s) => ({ ...s, isLoading: false, error: errorMessage }));
|
||||||
throw error;
|
throw error;
|
||||||
@@ -384,11 +387,11 @@ function createAuthStore() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const deviceId = await getDeviceId();
|
const deviceId = await getDeviceId();
|
||||||
console.log("[Auth] Re-authenticating...");
|
log.debug("Re-authenticating...");
|
||||||
|
|
||||||
const authResult = await commands.authReauthenticate(password, deviceId);
|
const authResult = await commands.authReauthenticate(password, deviceId);
|
||||||
|
|
||||||
console.log("[Auth] Re-authentication successful");
|
log.debug("Re-authentication successful");
|
||||||
|
|
||||||
// Update storage
|
// Update storage
|
||||||
await commands.storageSaveUser(
|
await commands.storageSaveUser(
|
||||||
@@ -417,7 +420,7 @@ function createAuthStore() {
|
|||||||
playerDeviceId
|
playerDeviceId
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Failed to reconfigure player:", error);
|
log.error("Failed to reconfigure player:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
@@ -432,7 +435,7 @@ function createAuthStore() {
|
|||||||
|
|
||||||
return authResult;
|
return authResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Re-authentication failed:", error);
|
log.error("Re-authentication failed:", error);
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
update((s) => ({ ...s, isLoading: false, error: errorMessage }));
|
update((s) => ({ ...s, isLoading: false, error: errorMessage }));
|
||||||
throw error;
|
throw error;
|
||||||
@@ -456,7 +459,7 @@ function createAuthStore() {
|
|||||||
try {
|
try {
|
||||||
await commands.playerDisableJellyfin();
|
await commands.playerDisableJellyfin();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Failed to disable player reporting:", error);
|
log.error("Failed to disable player reporting:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear repository
|
// Clear repository
|
||||||
@@ -481,7 +484,7 @@ function createAuthStore() {
|
|||||||
// Clear device ID cache on logout
|
// Clear device ID cache on logout
|
||||||
clearDeviceIdCache();
|
clearDeviceIdCache();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Logout error (continuing anyway):", error);
|
log.error("Logout error (continuing anyway):", error);
|
||||||
set(initialState);
|
set(initialState);
|
||||||
clearDeviceIdCache();
|
clearDeviceIdCache();
|
||||||
}
|
}
|
||||||
@@ -501,7 +504,7 @@ function createAuthStore() {
|
|||||||
try {
|
try {
|
||||||
return await commands.authGetSession();
|
return await commands.authGetSession();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Failed to get current session:", error);
|
log.error("Failed to get current session:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,10 +539,10 @@ function createAuthStore() {
|
|||||||
async function retryVerification() {
|
async function retryVerification() {
|
||||||
try {
|
try {
|
||||||
const deviceId = await getDeviceId();
|
const deviceId = await getDeviceId();
|
||||||
console.log("[Auth] Retrying session verification after reconnection");
|
log.debug("Retrying session verification after reconnection");
|
||||||
await commands.authStartVerification(deviceId);
|
await commands.authStartVerification(deviceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auth] Failed to retry verification:", error);
|
log.error("Failed to retry verification:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import { writable, derived } from "svelte/store";
|
|||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("ConnectivityStore");
|
||||||
|
|
||||||
export interface ConnectivityState {
|
export interface ConnectivityState {
|
||||||
/** Browser's navigator.onLine status */
|
/** Browser's navigator.onLine status */
|
||||||
@@ -76,7 +79,7 @@ function createConnectivityStore() {
|
|||||||
update((s) => ({ ...s, isOnline: true }));
|
update((s) => ({ ...s, isOnline: true }));
|
||||||
// Device regained network — ask the backend to re-verify the server now.
|
// Device regained network — ask the backend to re-verify the server now.
|
||||||
checkServerReachable().catch((err) => {
|
checkServerReachable().catch((err) => {
|
||||||
console.debug("[ConnectivityStore] Recheck after 'online' failed:", err);
|
log.debug("Recheck after 'online' failed:", err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,7 +88,7 @@ function createConnectivityStore() {
|
|||||||
// decide whether the server is actually reachable.
|
// decide whether the server is actually reachable.
|
||||||
update((s) => ({ ...s, isOnline: false }));
|
update((s) => ({ ...s, isOnline: false }));
|
||||||
checkServerReachable().catch((err) => {
|
checkServerReachable().catch((err) => {
|
||||||
console.debug("[ConnectivityStore] Recheck after 'offline' failed:", err);
|
log.debug("Recheck after 'offline' failed:", err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -132,7 +135,7 @@ function createConnectivityStore() {
|
|||||||
|
|
||||||
return isReachable;
|
return isReachable;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ConnectivityStore] Failed to check server:", error);
|
log.error("Failed to check server:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +148,7 @@ function createConnectivityStore() {
|
|||||||
isMonitoring = true;
|
isMonitoring = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("[ConnectivityStore] Starting monitoring for:", url);
|
log.debug("Starting monitoring for:", url);
|
||||||
|
|
||||||
// Set the server URL
|
// Set the server URL
|
||||||
await commands.connectivitySetServerUrl(url);
|
await commands.connectivitySetServerUrl(url);
|
||||||
@@ -163,10 +166,10 @@ function createConnectivityStore() {
|
|||||||
isChecking: status.isChecking,
|
isChecking: status.isChecking,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log("[ConnectivityStore] Started monitoring. Initial status:",
|
log.debug("Started monitoring. Initial status:",
|
||||||
status.isServerReachable ? "ONLINE" : "OFFLINE");
|
status.isServerReachable ? "ONLINE" : "OFFLINE");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ConnectivityStore] Failed to start monitoring:", error);
|
log.error("Failed to start monitoring:", error);
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
isServerReachable: false,
|
isServerReachable: false,
|
||||||
@@ -185,9 +188,9 @@ function createConnectivityStore() {
|
|||||||
await commands.connectivityStopMonitoring();
|
await commands.connectivityStopMonitoring();
|
||||||
isMonitoring = false;
|
isMonitoring = false;
|
||||||
eventHandlers = {};
|
eventHandlers = {};
|
||||||
console.log("[ConnectivityStore] Stopped monitoring");
|
log.debug("Stopped monitoring");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ConnectivityStore] Failed to stop monitoring:", error);
|
log.error("Failed to stop monitoring:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +201,7 @@ function createConnectivityStore() {
|
|||||||
try {
|
try {
|
||||||
await commands.connectivitySetServerUrl(url);
|
await commands.connectivitySetServerUrl(url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ConnectivityStore] Failed to set server URL:", error);
|
log.error("Failed to set server URL:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
-42
@@ -3,6 +3,9 @@
|
|||||||
import { writable, derived, get } from 'svelte/store';
|
import { writable, derived, get } from 'svelte/store';
|
||||||
import { commands } from '$lib/api/bindings';
|
import { commands } from '$lib/api/bindings';
|
||||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||||
|
import { createLogger } from '$lib/utils/logger';
|
||||||
|
|
||||||
|
const log = createLogger('Downloads');
|
||||||
|
|
||||||
// Event listener state
|
// Event listener state
|
||||||
let unlistenFn: UnlistenFn | null = null;
|
let unlistenFn: UnlistenFn | null = null;
|
||||||
@@ -103,7 +106,7 @@ function createDownloadsStore() {
|
|||||||
async function refreshDownloads(userId: string, statusFilter?: string[]): Promise<void> {
|
async function refreshDownloads(userId: string, statusFilter?: string[]): Promise<void> {
|
||||||
// If a refresh is already in progress, queue this request instead
|
// If a refresh is already in progress, queue this request instead
|
||||||
if (refreshInProgress) {
|
if (refreshInProgress) {
|
||||||
console.debug('🔄 Refresh already in progress, queuing request for user:', userId);
|
log.debug('🔄 Refresh already in progress, queuing request for user:', userId);
|
||||||
pendingRefreshRequest = { userId, statusFilter };
|
pendingRefreshRequest = { userId, statusFilter };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -111,13 +114,13 @@ function createDownloadsStore() {
|
|||||||
refreshInProgress = true;
|
refreshInProgress = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🔄 Refreshing downloads for user:', userId);
|
log.debug('🔄 Refreshing downloads for user:', userId);
|
||||||
const response = (await commands.getDownloads(
|
const response = (await commands.getDownloads(
|
||||||
userId,
|
userId,
|
||||||
statusFilter ?? null
|
statusFilter ?? null
|
||||||
)) as unknown as { downloads: DownloadInfo[]; stats: DownloadStats };
|
)) as unknown as { downloads: DownloadInfo[]; stats: DownloadStats };
|
||||||
console.log(' Got', response.downloads.length, 'downloads from backend');
|
log.debug(' Got', response.downloads.length, 'downloads from backend');
|
||||||
console.log(' Stats:', response.stats);
|
log.debug(' Stats:', response.stats);
|
||||||
|
|
||||||
update((state) => {
|
update((state) => {
|
||||||
const downloadsMap: Record<number, DownloadInfo> = {};
|
const downloadsMap: Record<number, DownloadInfo> = {};
|
||||||
@@ -133,7 +136,7 @@ function createDownloadsStore() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to refresh downloads:', error);
|
log.error('Failed to refresh downloads:', error);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
refreshInProgress = false;
|
refreshInProgress = false;
|
||||||
@@ -164,7 +167,7 @@ function createDownloadsStore() {
|
|||||||
albumName?: string
|
albumName?: string
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
console.log('📥 downloadItem called:', { itemId, userId, filePath, itemName, artistName, albumName });
|
log.debug('📥 downloadItem called:', { itemId, userId, filePath, itemName, artistName, albumName });
|
||||||
const downloadId = await commands.downloadItem({
|
const downloadId = await commands.downloadItem({
|
||||||
itemId,
|
itemId,
|
||||||
userId,
|
userId,
|
||||||
@@ -176,16 +179,16 @@ function createDownloadsStore() {
|
|||||||
albumName: albumName ?? null,
|
albumName: albumName ?? null,
|
||||||
expectedSize: null
|
expectedSize: null
|
||||||
});
|
});
|
||||||
console.log(' Got download ID from backend:', downloadId);
|
log.debug(' Got download ID from backend:', downloadId);
|
||||||
|
|
||||||
// Fetch download info and add to store
|
// Fetch download info and add to store
|
||||||
console.log(' Refreshing downloads...');
|
log.debug(' Refreshing downloads...');
|
||||||
await refreshDownloads(userId);
|
await refreshDownloads(userId);
|
||||||
console.log(' Refresh complete. Store state:', get({ subscribe }));
|
log.debug(' Refresh complete. Store state:', get({ subscribe }));
|
||||||
|
|
||||||
return downloadId;
|
return downloadId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to queue download:', error);
|
log.error('Failed to queue download:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -206,16 +209,16 @@ function createDownloadsStore() {
|
|||||||
basePath: string
|
basePath: string
|
||||||
): Promise<number[]> {
|
): Promise<number[]> {
|
||||||
try {
|
try {
|
||||||
console.log('📥 downloadAlbum called:', { albumId, userId, basePath });
|
log.debug('📥 downloadAlbum called:', { albumId, userId, basePath });
|
||||||
const downloadIds = await commands.downloadAlbum(handle, albumId, userId, basePath);
|
const downloadIds = await commands.downloadAlbum(handle, albumId, userId, basePath);
|
||||||
console.log(' Got download IDs from backend:', downloadIds);
|
log.debug(' Got download IDs from backend:', downloadIds);
|
||||||
|
|
||||||
// Refresh downloads
|
// Refresh downloads
|
||||||
await refreshDownloads(userId);
|
await refreshDownloads(userId);
|
||||||
|
|
||||||
return downloadIds;
|
return downloadIds;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to queue album download:', error);
|
log.error('Failed to queue album download:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -237,7 +240,7 @@ function createDownloadsStore() {
|
|||||||
seasonNumber?: number
|
seasonNumber?: number
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
console.log('🎬 downloadVideo called:', {
|
log.debug('🎬 downloadVideo called:', {
|
||||||
itemId,
|
itemId,
|
||||||
userId,
|
userId,
|
||||||
filePath,
|
filePath,
|
||||||
@@ -258,14 +261,14 @@ function createDownloadsStore() {
|
|||||||
episodeNumber: episodeNumber ?? null,
|
episodeNumber: episodeNumber ?? null,
|
||||||
seasonNumber: seasonNumber ?? null
|
seasonNumber: seasonNumber ?? null
|
||||||
});
|
});
|
||||||
console.log(' Got download ID from backend:', downloadId);
|
log.debug(' Got download ID from backend:', downloadId);
|
||||||
|
|
||||||
// Refresh downloads
|
// Refresh downloads
|
||||||
await refreshDownloads(userId);
|
await refreshDownloads(userId);
|
||||||
|
|
||||||
return downloadId;
|
return downloadId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to queue video download:', error);
|
log.error('Failed to queue video download:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -281,7 +284,7 @@ function createDownloadsStore() {
|
|||||||
qualityPreset?: string
|
qualityPreset?: string
|
||||||
): Promise<number[]> {
|
): Promise<number[]> {
|
||||||
try {
|
try {
|
||||||
console.log('📺 downloadSeries called:', {
|
log.debug('📺 downloadSeries called:', {
|
||||||
seriesId,
|
seriesId,
|
||||||
seriesName,
|
seriesName,
|
||||||
userId,
|
userId,
|
||||||
@@ -295,14 +298,14 @@ function createDownloadsStore() {
|
|||||||
basePath,
|
basePath,
|
||||||
qualityPreset ?? null
|
qualityPreset ?? null
|
||||||
);
|
);
|
||||||
console.log(' Queued', downloadIds.length, 'episodes for download');
|
log.debug(' Queued', downloadIds.length, 'episodes for download');
|
||||||
|
|
||||||
// Refresh downloads
|
// Refresh downloads
|
||||||
await refreshDownloads(userId);
|
await refreshDownloads(userId);
|
||||||
|
|
||||||
return downloadIds;
|
return downloadIds;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to queue series download:', error);
|
log.error('Failed to queue series download:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -320,7 +323,7 @@ function createDownloadsStore() {
|
|||||||
qualityPreset?: string
|
qualityPreset?: string
|
||||||
): Promise<number[]> {
|
): Promise<number[]> {
|
||||||
try {
|
try {
|
||||||
console.log('📺 downloadSeason called:', {
|
log.debug('📺 downloadSeason called:', {
|
||||||
seasonId,
|
seasonId,
|
||||||
seriesName,
|
seriesName,
|
||||||
seasonName,
|
seasonName,
|
||||||
@@ -336,14 +339,14 @@ function createDownloadsStore() {
|
|||||||
basePath,
|
basePath,
|
||||||
qualityPreset ?? null
|
qualityPreset ?? null
|
||||||
);
|
);
|
||||||
console.log(' Queued', downloadIds.length, 'episodes for download');
|
log.debug(' Queued', downloadIds.length, 'episodes for download');
|
||||||
|
|
||||||
// Refresh downloads
|
// Refresh downloads
|
||||||
await refreshDownloads(userId);
|
await refreshDownloads(userId);
|
||||||
|
|
||||||
return downloadIds;
|
return downloadIds;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to queue season download:', error);
|
log.error('Failed to queue season download:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -355,7 +358,7 @@ function createDownloadsStore() {
|
|||||||
try {
|
try {
|
||||||
await commands.pinItem(itemId);
|
await commands.pinItem(itemId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to pin item:', error);
|
log.error('Failed to pin item:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -367,7 +370,7 @@ function createDownloadsStore() {
|
|||||||
try {
|
try {
|
||||||
await commands.unpinItem(itemId);
|
await commands.unpinItem(itemId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to unpin item:', error);
|
log.error('Failed to unpin item:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -379,7 +382,7 @@ function createDownloadsStore() {
|
|||||||
try {
|
try {
|
||||||
return await commands.isItemPinned(itemId);
|
return await commands.isItemPinned(itemId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to check pin status:', error);
|
log.error('Failed to check pin status:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -391,7 +394,7 @@ function createDownloadsStore() {
|
|||||||
try {
|
try {
|
||||||
await commands.pauseDownload(downloadId);
|
await commands.pauseDownload(downloadId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to pause download:', error);
|
log.error('Failed to pause download:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -403,7 +406,7 @@ function createDownloadsStore() {
|
|||||||
try {
|
try {
|
||||||
await commands.resumeDownload(downloadId);
|
await commands.resumeDownload(downloadId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to resume download:', error);
|
log.error('Failed to resume download:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -415,7 +418,7 @@ function createDownloadsStore() {
|
|||||||
try {
|
try {
|
||||||
await commands.cancelDownload(downloadId);
|
await commands.cancelDownload(downloadId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to cancel download:', error);
|
log.error('Failed to cancel download:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -431,7 +434,7 @@ function createDownloadsStore() {
|
|||||||
return { ...state, downloads: remaining };
|
return { ...state, downloads: remaining };
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete download:', error);
|
log.error('Failed to delete download:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -448,14 +451,14 @@ function createDownloadsStore() {
|
|||||||
update((state) => {
|
update((state) => {
|
||||||
const download = state.downloads[downloadId];
|
const download = state.downloads[downloadId];
|
||||||
if (!download) {
|
if (!download) {
|
||||||
console.log(' Download not in store:', downloadId);
|
log.debug(' Download not in store:', downloadId);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedDownload = { ...download, ...updates };
|
const updatedDownload = { ...download, ...updates };
|
||||||
const newDownloads = { ...state.downloads, [downloadId]: updatedDownload };
|
const newDownloads = { ...state.downloads, [downloadId]: updatedDownload };
|
||||||
|
|
||||||
console.log(' Store updated for download', downloadId, ':', updates);
|
log.debug(' Store updated for download', downloadId, ':', updates);
|
||||||
// No count calculation - stats remain as-is until next refresh
|
// No count calculation - stats remain as-is until next refresh
|
||||||
return {
|
return {
|
||||||
downloads: newDownloads,
|
downloads: newDownloads,
|
||||||
@@ -515,30 +518,30 @@ export const audioDownloads = derived(downloads, ($d) =>
|
|||||||
*/
|
*/
|
||||||
export async function initDownloadEvents(): Promise<void> {
|
export async function initDownloadEvents(): Promise<void> {
|
||||||
if (isEventsInitialized) {
|
if (isEventsInitialized) {
|
||||||
console.warn('Download events already initialized');
|
log.warn('Download events already initialized');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🎧 Setting up download event listener...');
|
log.debug('🎧 Setting up download event listener...');
|
||||||
unlistenFn = await listen<DownloadEvent>('download-event', (event) => {
|
unlistenFn = await listen<DownloadEvent>('download-event', (event) => {
|
||||||
const payload = event.payload;
|
const payload = event.payload;
|
||||||
console.log('📬 Received download event:', payload.type, 'for download:', payload.downloadId);
|
log.debug('📬 Received download event:', payload.type, 'for download:', payload.downloadId);
|
||||||
console.log(' Full event payload:', JSON.stringify(payload));
|
log.debug(' Full event payload:', JSON.stringify(payload));
|
||||||
|
|
||||||
// Update the store based on event type
|
// Update the store based on event type
|
||||||
downloads.subscribe((state) => {
|
downloads.subscribe((state) => {
|
||||||
const download = state.downloads[payload.downloadId];
|
const download = state.downloads[payload.downloadId];
|
||||||
console.log(' Current download state:', download ? download.status : 'NOT IN STORE');
|
log.debug(' Current download state:', download ? download.status : 'NOT IN STORE');
|
||||||
})(); // Immediately unsubscribe after reading
|
})(); // Immediately unsubscribe after reading
|
||||||
|
|
||||||
handleDownloadEvent(payload);
|
handleDownloadEvent(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
isEventsInitialized = true;
|
isEventsInitialized = true;
|
||||||
console.log('✅ Download event listener registered successfully');
|
log.debug('✅ Download event listener registered successfully');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ Failed to register download event listener:', err);
|
log.error('❌ Failed to register download event listener:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,7 +602,7 @@ function handleDownloadEvent(payload: DownloadEvent): void {
|
|||||||
payload.downloadId,
|
payload.downloadId,
|
||||||
payload.totalBytes || download.fileSize || download.bytesDownloaded,
|
payload.totalBytes || download.fileSize || download.bytesDownloaded,
|
||||||
payload.filePath || download.filePath
|
payload.filePath || download.filePath
|
||||||
).catch((err) => console.error('Failed to persist download completion:', err));
|
).catch((err) => log.error('Failed to persist download completion:', err));
|
||||||
|
|
||||||
updateDownloadInStore(payload.downloadId, {
|
updateDownloadInStore(payload.downloadId, {
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
@@ -616,7 +619,7 @@ function handleDownloadEvent(payload: DownloadEvent): void {
|
|||||||
commands.markDownloadFailed(
|
commands.markDownloadFailed(
|
||||||
payload.downloadId,
|
payload.downloadId,
|
||||||
payload.error || 'Unknown error'
|
payload.error || 'Unknown error'
|
||||||
).catch((err) => console.error('Failed to persist download failure:', err));
|
).catch((err) => log.error('Failed to persist download failure:', err));
|
||||||
|
|
||||||
updateDownloadInStore(payload.downloadId, {
|
updateDownloadInStore(payload.downloadId, {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
@@ -654,7 +657,7 @@ function handleDownloadEvent(payload: DownloadEvent): void {
|
|||||||
* Helper to update a download in the store.
|
* Helper to update a download in the store.
|
||||||
*/
|
*/
|
||||||
function updateDownloadInStore(downloadId: number, updates: Partial<DownloadInfo>): void {
|
function updateDownloadInStore(downloadId: number, updates: Partial<DownloadInfo>): void {
|
||||||
console.log(' updateDownloadInStore:', downloadId, updates);
|
log.debug(' updateDownloadInStore:', downloadId, updates);
|
||||||
downloads.updateDownload(downloadId, updates);
|
downloads.updateDownload(downloadId, updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,6 +665,6 @@ function updateDownloadInStore(downloadId: number, updates: Partial<DownloadInfo
|
|||||||
* Helper to remove a download from the store.
|
* Helper to remove a download from the store.
|
||||||
*/
|
*/
|
||||||
function removeDownloadFromStore(downloadId: number): void {
|
function removeDownloadFromStore(downloadId: number): void {
|
||||||
console.log(' removeDownloadFromStore:', downloadId);
|
log.debug(' removeDownloadFromStore:', downloadId);
|
||||||
downloads.removeDownload(downloadId);
|
downloads.removeDownload(downloadId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
filterSupersededResumeItems,
|
filterSupersededResumeItems,
|
||||||
filterInProgressNextUpItems,
|
filterInProgressNextUpItems,
|
||||||
} from "./continueWatchingFilter";
|
} from "./continueWatchingFilter";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("HomeStore");
|
||||||
|
|
||||||
interface HomeState {
|
interface HomeState {
|
||||||
heroItems: MediaItem[];
|
heroItems: MediaItem[];
|
||||||
@@ -103,7 +106,7 @@ function createHomeStore() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to load home sections";
|
const message = error instanceof Error ? error.message : "Failed to load home sections";
|
||||||
update(s => ({ ...s, isLoading: false, error: message }));
|
update(s => ({ ...s, isLoading: false, error: message }));
|
||||||
console.error("Failed to load home sections:", error);
|
log.error("Failed to load home sections:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-10
@@ -7,6 +7,9 @@ import type { Library, MediaItem, SearchResult, Genre } from "$lib/api/types";
|
|||||||
import type { SearchOptions } from "$lib/api/bindings";
|
import type { SearchOptions } from "$lib/api/bindings";
|
||||||
import type { SearchScope } from "$lib/utils/searchScope";
|
import type { SearchScope } from "$lib/utils/searchScope";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("LibraryStore");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Payload of the backend `search-event` (mirrors Rust `SearchUpdateEvent`).
|
* Payload of the backend `search-event` (mirrors Rust `SearchUpdateEvent`).
|
||||||
@@ -84,16 +87,16 @@ function createLibraryStore() {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
|
|
||||||
console.log("📚 [LibraryStore] Loading libraries...");
|
log.debug("📚 Loading libraries...");
|
||||||
|
|
||||||
const libraries = await repo.getLibraries();
|
const libraries = await repo.getLibraries();
|
||||||
|
|
||||||
const loadTime = Math.round(performance.now() - startTime);
|
const loadTime = Math.round(performance.now() - startTime);
|
||||||
|
|
||||||
if (loadTime < 100) {
|
if (loadTime < 100) {
|
||||||
console.log(`🚀 [LibraryStore] CACHE HIT! Loaded ${libraries.length} libraries in ${loadTime}ms (instant)`);
|
log.debug(`🚀 CACHE HIT! Loaded ${libraries.length} libraries in ${loadTime}ms (instant)`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`⏳ [LibraryStore] Loaded ${libraries.length} libraries in ${loadTime}ms (from server)`);
|
log.debug(`⏳ Loaded ${libraries.length} libraries in ${loadTime}ms (from server)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
@@ -120,7 +123,7 @@ function createLibraryStore() {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
|
|
||||||
console.log(`📚 [LibraryStore] Loading items for parent: ${parentId.substring(0, 8)}...`);
|
log.debug(`📚 Loading items for parent: ${parentId.substring(0, 8)}...`);
|
||||||
|
|
||||||
const result = await repo.getItems(parentId, {
|
const result = await repo.getItems(parentId, {
|
||||||
startIndex: options.startIndex ?? 0,
|
startIndex: options.startIndex ?? 0,
|
||||||
@@ -134,9 +137,9 @@ function createLibraryStore() {
|
|||||||
const loadTime = Math.round(performance.now() - startTime);
|
const loadTime = Math.round(performance.now() - startTime);
|
||||||
|
|
||||||
if (loadTime < 100) {
|
if (loadTime < 100) {
|
||||||
console.log(`🚀 [LibraryStore] CACHE HIT! Loaded ${result.items.length} items in ${loadTime}ms (instant)`);
|
log.debug(`🚀 CACHE HIT! Loaded ${result.items.length} items in ${loadTime}ms (instant)`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`⏳ [LibraryStore] Loaded ${result.items.length} items in ${loadTime}ms (from server)`);
|
log.debug(`⏳ Loaded ${result.items.length} items in ${loadTime}ms (from server)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
@@ -202,11 +205,11 @@ function createLibraryStore() {
|
|||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
const item = await repo.getItem(itemId);
|
const item = await repo.getItem(itemId);
|
||||||
|
|
||||||
console.log(`[LibraryStore] loadItem(${itemId}): ${item.name} (${item.kind})`);
|
log.debug(`loadItem(${itemId}): ${item.name} (${item.kind})`);
|
||||||
console.log(`[LibraryStore] - Has people? ${item.people ? `YES (${item.people.length})` : 'NO'}`);
|
log.debug(`- Has people? ${item.people ? `YES (${item.people.length})` : 'NO'}`);
|
||||||
if (item.people && item.people.length > 0) {
|
if (item.people && item.people.length > 0) {
|
||||||
item.people.forEach((p, i) => {
|
item.people.forEach((p, i) => {
|
||||||
console.log(`[LibraryStore] [${i}] ${p.name} (type: "${p.type}", id: "${p.id}")`);
|
log.debug(` [${i}] ${p.name} (type: "${p.type}", id: "${p.id}")`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +322,7 @@ function createLibraryStore() {
|
|||||||
update((s) => ({ ...s, genres }));
|
update((s) => ({ ...s, genres }));
|
||||||
return genres;
|
return genres;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load genres:", error);
|
log.error("Failed to load genres:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import { writable, get } from "svelte/store";
|
|||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
import type { Session } from "$lib/api/types";
|
import type { Session } from "$lib/api/types";
|
||||||
import type { LmsSyncGroup } from "$lib/api/bindings";
|
import type { LmsSyncGroup } from "$lib/api/bindings";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("LmsSync");
|
||||||
|
|
||||||
const LMS_DEVICE_PREFIX = "lms-";
|
const LMS_DEVICE_PREFIX = "lms-";
|
||||||
|
|
||||||
@@ -51,7 +54,7 @@ function createLmsSyncStore() {
|
|||||||
update((s) => ({ ...s, groups, error: null }));
|
update((s) => ({ ...s, groups, error: null }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// The plugin may not be installed; treat as "no groups" rather than fatal.
|
// The plugin may not be installed; treat as "no groups" rather than fatal.
|
||||||
console.warn("[LmsSync] Failed to load sync groups:", error);
|
log.warn("Failed to load sync groups:", error);
|
||||||
update((s) => ({ ...s, groups: [] }));
|
update((s) => ({ ...s, groups: [] }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { writable, derived } from "svelte/store";
|
|||||||
import type { MediaItem } from "$lib/api/types";
|
import type { MediaItem } from "$lib/api/types";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { buildHeroMix } from "$lib/utils/heroMix";
|
import { buildHeroMix } from "$lib/utils/heroMix";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("MoviesStore");
|
||||||
|
|
||||||
/** A single "by genre" row: the genre name plus the movies in it. */
|
/** A single "by genre" row: the genre name plus the movies in it. */
|
||||||
export interface GenreRow {
|
export interface GenreRow {
|
||||||
@@ -91,7 +94,7 @@ function createMoviesStore() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to load movie sections";
|
const message = error instanceof Error ? error.message : "Failed to load movie sections";
|
||||||
update(s => ({ ...s, isLoading: false, error: message }));
|
update(s => ({ ...s, isLoading: false, error: message }));
|
||||||
console.error("Failed to load movie sections:", error);
|
log.error("Failed to load movie sections:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +123,7 @@ function createMoviesStore() {
|
|||||||
});
|
});
|
||||||
return { id: genre.id, name: genre.name, items: result.items };
|
return { id: genre.id, name: genre.name, items: result.items };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Failed to load genre row "${genre.name}":`, e);
|
log.warn(`Failed to load genre row "${genre.name}":`, e);
|
||||||
return { id: genre.id, name: genre.name, items: [] };
|
return { id: genre.id, name: genre.name, items: [] };
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -133,7 +136,7 @@ function createMoviesStore() {
|
|||||||
|
|
||||||
update(s => ({ ...s, genreRows }));
|
update(s => ({ ...s, genreRows }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load movie genre rows:", e);
|
log.warn("Failed to load movie genre rows:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { auth } from "./auth";
|
|||||||
import { excludePodcasts } from "$lib/utils/podcastFilter";
|
import { excludePodcasts } from "$lib/utils/podcastFilter";
|
||||||
import { selectDiverseGenres, sampleAcross } from "$lib/utils/genreDiversity";
|
import { selectDiverseGenres, sampleAcross } from "$lib/utils/genreDiversity";
|
||||||
import { buildHeroMix } from "$lib/utils/heroMix";
|
import { buildHeroMix } from "$lib/utils/heroMix";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("MusicStore");
|
||||||
|
|
||||||
/** A single "by genre" row: the genre name plus the albums in it. */
|
/** A single "by genre" row: the genre name plus the albums in it. */
|
||||||
export interface GenreRow {
|
export interface GenreRow {
|
||||||
@@ -124,7 +127,7 @@ function createMusicStore() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to load music sections";
|
const message = error instanceof Error ? error.message : "Failed to load music sections";
|
||||||
update(s => ({ ...s, isLoading: false, error: message }));
|
update(s => ({ ...s, isLoading: false, error: message }));
|
||||||
console.error("Failed to load music sections:", error);
|
log.error("Failed to load music sections:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +146,7 @@ function createMusicStore() {
|
|||||||
// HACK: drop the "Podcasts" folder that lives in the music library.
|
// HACK: drop the "Podcasts" folder that lives in the music library.
|
||||||
return { id: genre.id, name: genre.name, items: excludePodcasts(result.items) };
|
return { id: genre.id, name: genre.name, items: excludePodcasts(result.items) };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Failed to load genre row "${genre.name}":`, e);
|
log.warn(`Failed to load genre row "${genre.name}":`, e);
|
||||||
return { id: genre.id, name: genre.name, items: [] };
|
return { id: genre.id, name: genre.name, items: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,7 +208,7 @@ function createMusicStore() {
|
|||||||
|
|
||||||
update(s => ({ ...s, genreRows }));
|
update(s => ({ ...s, genreRows }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load music genre rows:", e);
|
log.warn("Failed to load music genre rows:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import { commands, events } from "$lib/api/bindings";
|
|||||||
import { sessions, selectedSession } from "./sessions";
|
import { sessions, selectedSession } from "./sessions";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { ticksToSeconds } from "$lib/utils/playbackUnits";
|
import { ticksToSeconds } from "$lib/utils/playbackUnits";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("PlaybackMode");
|
||||||
|
|
||||||
export type PlaybackMode = "local" | "remote" | "idle";
|
export type PlaybackMode = "local" | "remote" | "idle";
|
||||||
|
|
||||||
@@ -59,7 +62,7 @@ function createPlaybackModeStore() {
|
|||||||
// authoritative mode.
|
// authoritative mode.
|
||||||
sessions.selectSession(remoteSessionId);
|
sessions.selectSession(remoteSessionId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get playback mode:", error);
|
log.error("Failed to get playback mode:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +84,7 @@ function createPlaybackModeStore() {
|
|||||||
sessionId: string | null | undefined,
|
sessionId: string | null | undefined,
|
||||||
currentPosition?: number,
|
currentPosition?: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log("[PlaybackMode] Transferring to remote session:", sessionId);
|
log.debug("Transferring to remote session:", sessionId);
|
||||||
update((s) => ({ ...s, isTransferring: true, transferError: null }));
|
update((s) => ({ ...s, isTransferring: true, transferError: null }));
|
||||||
|
|
||||||
let aborted = false;
|
let aborted = false;
|
||||||
@@ -104,12 +107,12 @@ function createPlaybackModeStore() {
|
|||||||
|
|
||||||
// Rust handles everything - just wait for it to complete
|
// Rust handles everything - just wait for it to complete
|
||||||
// It includes its own 5-second timeout for track loading
|
// It includes its own 5-second timeout for track loading
|
||||||
console.log("[PlaybackMode] About to invoke playback_mode_transfer_to_remote with sessionId:", sessionId, "position:", positionOverride);
|
log.debug("About to invoke playback_mode_transfer_to_remote with sessionId:", sessionId, "position:", positionOverride);
|
||||||
await commands.playbackModeTransferToRemote(sessionId ?? "", positionOverride);
|
await commands.playbackModeTransferToRemote(sessionId ?? "", positionOverride);
|
||||||
console.log("[PlaybackMode] Invoke completed successfully");
|
log.debug("Invoke completed successfully");
|
||||||
|
|
||||||
if (aborted) {
|
if (aborted) {
|
||||||
console.log("[PlaybackMode] Transfer was cancelled");
|
log.debug("Transfer was cancelled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,10 +125,10 @@ function createPlaybackModeStore() {
|
|||||||
isTransferring: false,
|
isTransferring: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log("[PlaybackMode] Successfully transferred to remote");
|
log.debug("Successfully transferred to remote");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (aborted) {
|
if (aborted) {
|
||||||
console.log("[PlaybackMode] Transfer was cancelled");
|
log.debug("Transfer was cancelled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +138,7 @@ function createPlaybackModeStore() {
|
|||||||
isTransferring: false,
|
isTransferring: false,
|
||||||
transferError: message,
|
transferError: message,
|
||||||
}));
|
}));
|
||||||
console.error("Transfer to remote failed:", error);
|
log.error("Transfer to remote failed:", error);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
currentTransferAbort = null;
|
currentTransferAbort = null;
|
||||||
@@ -154,7 +157,7 @@ function createPlaybackModeStore() {
|
|||||||
* Will be fully migrated to Rust after Phase 3.
|
* Will be fully migrated to Rust after Phase 3.
|
||||||
*/
|
*/
|
||||||
async function transferToLocal(): Promise<void> {
|
async function transferToLocal(): Promise<void> {
|
||||||
console.log("[PlaybackMode] Transferring to local");
|
log.debug("Transferring to local");
|
||||||
update((s) => ({ ...s, isTransferring: true, transferError: null }));
|
update((s) => ({ ...s, isTransferring: true, transferError: null }));
|
||||||
|
|
||||||
let aborted = false;
|
let aborted = false;
|
||||||
@@ -195,7 +198,7 @@ function createPlaybackModeStore() {
|
|||||||
const itemId = (nowPlaying as any).id || (nowPlaying as any).Id;
|
const itemId = (nowPlaying as any).id || (nowPlaying as any).Id;
|
||||||
const itemName = (nowPlaying as any).name || (nowPlaying as any).Name;
|
const itemName = (nowPlaying as any).name || (nowPlaying as any).Name;
|
||||||
|
|
||||||
console.log("[PlaybackMode] Current remote item:", itemName, "position:", positionSeconds, "id:", itemId);
|
log.debug("Current remote item:", itemName, "position:", positionSeconds, "id:", itemId);
|
||||||
|
|
||||||
if (!itemId) {
|
if (!itemId) {
|
||||||
throw new Error("Cannot transfer: remote item has no ID");
|
throw new Error("Cannot transfer: remote item has no ID");
|
||||||
@@ -246,10 +249,10 @@ function createPlaybackModeStore() {
|
|||||||
isTransferring: false,
|
isTransferring: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log("[PlaybackMode] Successfully transferred to local");
|
log.debug("Successfully transferred to local");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (aborted) {
|
if (aborted) {
|
||||||
console.log("[PlaybackMode] Transfer was cancelled");
|
log.debug("Transfer was cancelled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +262,7 @@ function createPlaybackModeStore() {
|
|||||||
isTransferring: false,
|
isTransferring: false,
|
||||||
transferError: message,
|
transferError: message,
|
||||||
}));
|
}));
|
||||||
console.error("Transfer to local failed:", error);
|
log.error("Transfer to local failed:", error);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
// Always lower the Rust transferring flag so it can't stick on if any step
|
// Always lower the Rust transferring flag so it can't stick on if any step
|
||||||
@@ -268,7 +271,7 @@ function createPlaybackModeStore() {
|
|||||||
try {
|
try {
|
||||||
await commands.playbackModeSetTransferring(false);
|
await commands.playbackModeSetTransferring(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[PlaybackMode] Failed to clear transferring flag:", e);
|
log.warn("Failed to clear transferring flag:", e);
|
||||||
}
|
}
|
||||||
currentTransferAbort = null;
|
currentTransferAbort = null;
|
||||||
// Reconcile to the authoritative Rust mode in case a step above threw and
|
// Reconcile to the authoritative Rust mode in case a step above threw and
|
||||||
@@ -294,9 +297,9 @@ function createPlaybackModeStore() {
|
|||||||
if (event.payload.type === "remote_disconnect_requested") {
|
if (event.payload.type === "remote_disconnect_requested") {
|
||||||
const currentState = get({ subscribe });
|
const currentState = get({ subscribe });
|
||||||
if (currentState.mode === "remote") {
|
if (currentState.mode === "remote") {
|
||||||
console.log("[PlaybackMode] Lockscreen requested disconnect; transferring to local");
|
log.debug("Lockscreen requested disconnect; transferring to local");
|
||||||
transferToLocal().catch((e) =>
|
transferToLocal().catch((e) =>
|
||||||
console.error("[PlaybackMode] Lockscreen-triggered transfer failed:", e),
|
log.error("Lockscreen-triggered transfer failed:", e),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -331,7 +334,7 @@ function createPlaybackModeStore() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[PlaybackMode] Backend mode changed →", mode, remoteSessionId);
|
log.debug("Backend mode changed →", mode, remoteSessionId);
|
||||||
update((s) => ({ ...s, mode, remoteSessionId }));
|
update((s) => ({ ...s, mode, remoteSessionId }));
|
||||||
// Keep the selected session in step so the merged UI stores follow, but
|
// Keep the selected session in step so the merged UI stores follow, but
|
||||||
// only touch the selection when it actually differs — re-selecting the
|
// only touch the selection when it actually differs — re-selecting the
|
||||||
@@ -352,10 +355,10 @@ function createPlaybackModeStore() {
|
|||||||
if (currentState.mode === "remote" && currentState.remoteSessionId && !currentState.isTransferring) {
|
if (currentState.mode === "remote" && currentState.remoteSessionId && !currentState.isTransferring) {
|
||||||
if (!session || session.id !== currentState.remoteSessionId || !session.supportsMediaControl) {
|
if (!session || session.id !== currentState.remoteSessionId || !session.supportsMediaControl) {
|
||||||
consecutiveMisses++;
|
consecutiveMisses++;
|
||||||
console.warn(`[PlaybackMode] Remote session miss ${consecutiveMisses}/${DISCONNECT_THRESHOLD}`);
|
log.warn(`Remote session miss ${consecutiveMisses}/${DISCONNECT_THRESHOLD}`);
|
||||||
|
|
||||||
if (consecutiveMisses >= DISCONNECT_THRESHOLD) {
|
if (consecutiveMisses >= DISCONNECT_THRESHOLD) {
|
||||||
console.warn("[PlaybackMode] Remote session lost after sustained disconnection");
|
log.warn("Remote session lost after sustained disconnection");
|
||||||
consecutiveMisses = 0;
|
consecutiveMisses = 0;
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
@@ -367,7 +370,7 @@ function createPlaybackModeStore() {
|
|||||||
} else {
|
} else {
|
||||||
// Session is healthy, reset counter
|
// Session is healthy, reset counter
|
||||||
if (consecutiveMisses > 0) {
|
if (consecutiveMisses > 0) {
|
||||||
console.log("[PlaybackMode] Remote session recovered after", consecutiveMisses, "misses");
|
log.debug("Remote session recovered after", consecutiveMisses, "misses");
|
||||||
}
|
}
|
||||||
consecutiveMisses = 0;
|
consecutiveMisses = 0;
|
||||||
}
|
}
|
||||||
@@ -389,7 +392,7 @@ function createPlaybackModeStore() {
|
|||||||
*/
|
*/
|
||||||
function cancelTransfer(): void {
|
function cancelTransfer(): void {
|
||||||
if (currentTransferAbort) {
|
if (currentTransferAbort) {
|
||||||
console.log("[PlaybackMode] Cancelling transfer");
|
log.debug("Cancelling transfer");
|
||||||
currentTransferAbort();
|
currentTransferAbort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,11 +402,11 @@ function createPlaybackModeStore() {
|
|||||||
* This stops controlling the remote device and returns to idle/local state
|
* This stops controlling the remote device and returns to idle/local state
|
||||||
*/
|
*/
|
||||||
async function disconnect(): Promise<void> {
|
async function disconnect(): Promise<void> {
|
||||||
console.log("[PlaybackMode] Disconnecting from remote session");
|
log.debug("Disconnecting from remote session");
|
||||||
|
|
||||||
const currentState = get({ subscribe });
|
const currentState = get({ subscribe });
|
||||||
if (currentState.mode !== "remote") {
|
if (currentState.mode !== "remote") {
|
||||||
console.log("[PlaybackMode] Not in remote mode, nothing to disconnect");
|
log.debug("Not in remote mode, nothing to disconnect");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,10 +423,10 @@ function createPlaybackModeStore() {
|
|||||||
transferError: null,
|
transferError: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log("[PlaybackMode] Successfully disconnected");
|
log.debug("Successfully disconnected");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to disconnect";
|
const message = error instanceof Error ? error.message : "Failed to disconnect";
|
||||||
console.error("[PlaybackMode] Disconnect failed:", error);
|
log.error("Disconnect failed:", error);
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
transferError: message,
|
transferError: message,
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import { writable, derived, get } from "svelte/store";
|
|||||||
import { commands, events } from "$lib/api/bindings";
|
import { commands, events } from "$lib/api/bindings";
|
||||||
import type { MediaItem } from "$lib/api/types";
|
import type { MediaItem } from "$lib/api/types";
|
||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("Queue");
|
||||||
|
|
||||||
export type RepeatMode = "off" | "all" | "one";
|
export type RepeatMode = "off" | "all" | "one";
|
||||||
|
|
||||||
@@ -72,7 +75,7 @@ function createQueueStore() {
|
|||||||
async function syncFromRust(): Promise<void> {
|
async function syncFromRust(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const rustQueue = (await commands.playerGetQueue()) as unknown as QueueChangedEvent;
|
const rustQueue = (await commands.playerGetQueue()) as unknown as QueueChangedEvent;
|
||||||
console.log("[Queue] Synced from Rust - items:", rustQueue.items.length);
|
log.debug("Synced from Rust - items:", rustQueue.items.length);
|
||||||
set({
|
set({
|
||||||
items: rustQueue.items,
|
items: rustQueue.items,
|
||||||
currentIndex: rustQueue.currentIndex,
|
currentIndex: rustQueue.currentIndex,
|
||||||
@@ -82,7 +85,7 @@ function createQueueStore() {
|
|||||||
hasPrevious: rustQueue.hasPrevious,
|
hasPrevious: rustQueue.hasPrevious,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Queue] Failed to sync from Rust:", error);
|
log.error("Failed to sync from Rust:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-23
@@ -4,6 +4,9 @@
|
|||||||
import { writable, derived } from "svelte/store";
|
import { writable, derived } from "svelte/store";
|
||||||
import { commands, events } from "$lib/api/bindings";
|
import { commands, events } from "$lib/api/bindings";
|
||||||
import type { Session } from "$lib/api/types";
|
import type { Session } from "$lib/api/types";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("Sessions");
|
||||||
|
|
||||||
interface SessionsState {
|
interface SessionsState {
|
||||||
sessions: Session[];
|
sessions: Session[];
|
||||||
@@ -28,9 +31,9 @@ function createSessionsStore() {
|
|||||||
events.playerStatusEvent.listen((event) => {
|
events.playerStatusEvent.listen((event) => {
|
||||||
if (event.payload.type === "sessions_updated") {
|
if (event.payload.type === "sessions_updated") {
|
||||||
const sessions = event.payload.sessions as unknown as Session[];
|
const sessions = event.payload.sessions as unknown as Session[];
|
||||||
console.log(`[Sessions] Received ${sessions.length} sessions from backend`);
|
log.debug(`Received ${sessions.length} sessions from backend`);
|
||||||
sessions.forEach((s, i) => {
|
sessions.forEach((s, i) => {
|
||||||
console.log(`[Sessions] Session ${i}: id=${s.id}, device=${s.deviceName}, supportsRemoteControl=${s.supportsRemoteControl}`);
|
log.debug(`Session ${i}: id=${s.id}, device=${s.deviceName}, supportsRemoteControl=${s.supportsRemoteControl}`);
|
||||||
});
|
});
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
@@ -50,9 +53,9 @@ function createSessionsStore() {
|
|||||||
|
|
||||||
const sessions = await commands.sessionsPollNow();
|
const sessions = await commands.sessionsPollNow();
|
||||||
|
|
||||||
console.log(`[Sessions] Manual refresh returned ${sessions.length} sessions`);
|
log.debug(`Manual refresh returned ${sessions.length} sessions`);
|
||||||
sessions.forEach((s, i) => {
|
sessions.forEach((s, i) => {
|
||||||
console.log(`[Sessions] Session ${i}: id=${s.id}, device=${s.deviceName}, supportsRemoteControl=${s.supportsRemoteControl}`);
|
log.debug(`Session ${i}: id=${s.id}, device=${s.deviceName}, supportsRemoteControl=${s.supportsRemoteControl}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
@@ -69,7 +72,7 @@ function createSessionsStore() {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: message,
|
error: message,
|
||||||
}));
|
}));
|
||||||
console.error("Failed to fetch sessions:", error);
|
log.error("Failed to fetch sessions:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +93,7 @@ function createSessionsStore() {
|
|||||||
// Refresh after command to get updated state
|
// Refresh after command to get updated state
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send play/pause command:", error);
|
log.error("Failed to send play/pause command:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +106,7 @@ function createSessionsStore() {
|
|||||||
await commands.remoteSendCommand(sessionId ?? "", "Stop");
|
await commands.remoteSendCommand(sessionId ?? "", "Stop");
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send stop command:", error);
|
log.error("Failed to send stop command:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +119,7 @@ function createSessionsStore() {
|
|||||||
await commands.remoteSendCommand(sessionId ?? "", "NextTrack");
|
await commands.remoteSendCommand(sessionId ?? "", "NextTrack");
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send next track command:", error);
|
log.error("Failed to send next track command:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +132,7 @@ function createSessionsStore() {
|
|||||||
await commands.remoteSendCommand(sessionId ?? "", "PreviousTrack");
|
await commands.remoteSendCommand(sessionId ?? "", "PreviousTrack");
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send previous track command:", error);
|
log.error("Failed to send previous track command:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,7 +145,7 @@ function createSessionsStore() {
|
|||||||
await commands.remoteSessionSeek(sessionId ?? "", positionTicks);
|
await commands.remoteSessionSeek(sessionId ?? "", positionTicks);
|
||||||
// Don't refresh immediately for seek to avoid UI lag
|
// Don't refresh immediately for seek to avoid UI lag
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send seek command:", error);
|
log.error("Failed to send seek command:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +158,7 @@ function createSessionsStore() {
|
|||||||
await commands.remoteSessionSetVolume(sessionId ?? "", volume);
|
await commands.remoteSessionSetVolume(sessionId ?? "", volume);
|
||||||
// Don't refresh immediately for volume to avoid UI lag
|
// Don't refresh immediately for volume to avoid UI lag
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send volume command:", error);
|
log.error("Failed to send volume command:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,7 +171,7 @@ function createSessionsStore() {
|
|||||||
await commands.remoteSendCommand(sessionId ?? "", "ToggleMute");
|
await commands.remoteSendCommand(sessionId ?? "", "ToggleMute");
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to toggle mute:", error);
|
log.error("Failed to toggle mute:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,20 +184,20 @@ function createSessionsStore() {
|
|||||||
itemIds: string[],
|
itemIds: string[],
|
||||||
startIndex = 0
|
startIndex = 0
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log("[SESSIONS] ========== playOnSession called ==========");
|
log.debug("========== playOnSession called ==========");
|
||||||
console.log("[SESSIONS] sessionId:", sessionId);
|
log.debug("sessionId:", sessionId);
|
||||||
console.log("[SESSIONS] itemIds array:", itemIds);
|
log.debug("itemIds array:", itemIds);
|
||||||
console.log("[SESSIONS] itemIds.length:", itemIds.length);
|
log.debug("itemIds.length:", itemIds.length);
|
||||||
console.log("[SESSIONS] itemIds JSON:", JSON.stringify(itemIds));
|
log.debug("itemIds JSON:", JSON.stringify(itemIds));
|
||||||
console.log("[SESSIONS] startIndex:", startIndex);
|
log.debug("startIndex:", startIndex);
|
||||||
console.log("[SESSIONS] About to call commands.remotePlayOnSession");
|
log.debug("About to call commands.remotePlayOnSession");
|
||||||
try {
|
try {
|
||||||
// Use Rust player's Jellyfin client for remote playback
|
// Use Rust player's Jellyfin client for remote playback
|
||||||
const result = await commands.remotePlayOnSession(sessionId ?? "", itemIds, startIndex);
|
const result = await commands.remotePlayOnSession(sessionId ?? "", itemIds, startIndex);
|
||||||
console.log("[SESSIONS] invoke succeeded, result:", result);
|
log.debug("invoke succeeded, result:", result);
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SESSIONS] Failed to play on session:", error);
|
log.error("Failed to play on session:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,10 +245,10 @@ export const controllableSessions = derived(
|
|||||||
sessions,
|
sessions,
|
||||||
($sessions) => {
|
($sessions) => {
|
||||||
const controllable = $sessions.sessions.filter((s) => s.supportsRemoteControl);
|
const controllable = $sessions.sessions.filter((s) => s.supportsRemoteControl);
|
||||||
console.log(`[Sessions] Filtering ${$sessions.sessions.length} total sessions, ${controllable.length} are controllable`);
|
log.debug(`Filtering ${$sessions.sessions.length} total sessions, ${controllable.length} are controllable`);
|
||||||
$sessions.sessions.forEach((s, i) => {
|
$sessions.sessions.forEach((s, i) => {
|
||||||
const status = s.supportsRemoteControl ? "✓ CONTROLLABLE" : "✗ NOT CONTROLLABLE";
|
const status = s.supportsRemoteControl ? "✓ CONTROLLABLE" : "✗ NOT CONTROLLABLE";
|
||||||
console.log(`[Sessions] ${status}: ${s.deviceName} (id=${s.id}, supportsRemoteControl=${s.supportsRemoteControl})`);
|
log.debug(` ${status}: ${s.deviceName} (id=${s.id}, supportsRemoteControl=${s.supportsRemoteControl})`);
|
||||||
});
|
});
|
||||||
return controllable;
|
return controllable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
filterSupersededResumeItems,
|
filterSupersededResumeItems,
|
||||||
filterInProgressNextUpItems,
|
filterInProgressNextUpItems,
|
||||||
} from "./continueWatchingFilter";
|
} from "./continueWatchingFilter";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("TvStore");
|
||||||
|
|
||||||
/** A single "by genre" row: the genre name plus the series in it. */
|
/** A single "by genre" row: the genre name plus the series in it. */
|
||||||
export interface GenreRow {
|
export interface GenreRow {
|
||||||
@@ -114,7 +117,7 @@ function createTvStore() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to load TV sections";
|
const message = error instanceof Error ? error.message : "Failed to load TV sections";
|
||||||
update(s => ({ ...s, isLoading: false, error: message }));
|
update(s => ({ ...s, isLoading: false, error: message }));
|
||||||
console.error("Failed to load TV sections:", error);
|
log.error("Failed to load TV sections:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +146,7 @@ function createTvStore() {
|
|||||||
});
|
});
|
||||||
return { id: genre.id, name: genre.name, items: result.items };
|
return { id: genre.id, name: genre.name, items: result.items };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Failed to load genre row "${genre.name}":`, e);
|
log.warn(`Failed to load genre row "${genre.name}":`, e);
|
||||||
return { id: genre.id, name: genre.name, items: [] };
|
return { id: genre.id, name: genre.name, items: [] };
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -156,7 +159,7 @@ function createTvStore() {
|
|||||||
|
|
||||||
update(s => ({ ...s, genreRows }));
|
update(s => ({ ...s, genreRows }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load TV genre rows:", e);
|
log.warn("Failed to load TV genre rows:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
* Unsupported (no-op) on every non-Android platform.
|
* Unsupported (no-op) on every non-Android platform.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("BgAudio");
|
||||||
|
|
||||||
interface AndroidBackgroundAudioBridge {
|
interface AndroidBackgroundAudioBridge {
|
||||||
setEnabled(enabled: boolean): void;
|
setEnabled(enabled: boolean): void;
|
||||||
}
|
}
|
||||||
@@ -44,15 +48,15 @@ export function setBackgroundAudioEnabled(enabled: boolean): boolean {
|
|||||||
// before/without the bridge existing. Silently no-oping here leaves the UI
|
// before/without the bridge existing. Silently no-oping here leaves the UI
|
||||||
// showing "armed" while native never learns — and the handoff then never
|
// showing "armed" while native never learns — and the handoff then never
|
||||||
// fires on lock. Report it so callers can retry.
|
// fires on lock. Report it so callers can retry.
|
||||||
console.warn("[BgAudio] setEnabled: bridge missing, native NOT armed");
|
log.warn("setEnabled: bridge missing, native NOT armed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
b.setEnabled(enabled);
|
b.setEnabled(enabled);
|
||||||
console.log("[BgAudio] setEnabled ->", enabled);
|
log.debug("setEnabled ->", enabled);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[BgAudio] Failed to set enabled:", err);
|
log.warn("Failed to set enabled:", err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
* Provides tactile feedback for user actions
|
* Provides tactile feedback for user actions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("Haptics");
|
||||||
|
|
||||||
type HapticStyle = "light" | "medium" | "heavy" | "success" | "warning" | "error";
|
type HapticStyle = "light" | "medium" | "heavy" | "success" | "warning" | "error";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +32,7 @@ export function haptic(style: HapticStyle = "medium") {
|
|||||||
navigator.vibrate(patterns[style]);
|
navigator.vibrate(patterns[style]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Silently fail if vibration is not supported or blocked
|
// Silently fail if vibration is not supported or blocked
|
||||||
console.debug("Haptic feedback not available:", error);
|
log.debug("Haptic feedback not available:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
* already does the right thing and these calls are no-ops.
|
* already does the right thing and these calls are no-ops.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("Immersive");
|
||||||
|
|
||||||
interface AndroidImmersiveBridge {
|
interface AndroidImmersiveBridge {
|
||||||
enter(): void;
|
enter(): void;
|
||||||
exit(): void;
|
exit(): void;
|
||||||
@@ -38,7 +42,7 @@ export function isImmersiveSupported(): boolean {
|
|||||||
try {
|
try {
|
||||||
return bridge()?.isSupported() ?? false;
|
return bridge()?.isSupported() ?? false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[Immersive] isSupported check failed:", err);
|
log.warn("isSupported check failed:", err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +52,7 @@ export function enterImmersive(): void {
|
|||||||
try {
|
try {
|
||||||
bridge()?.enter();
|
bridge()?.enter();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Immersive] Failed to hide the system bars:", err);
|
log.error("Failed to hide the system bars:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +67,6 @@ export function exitImmersive(): void {
|
|||||||
try {
|
try {
|
||||||
bridge()?.exit();
|
bridge()?.exit();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Immersive] Failed to restore the system bars:", err);
|
log.error("Failed to restore the system bars:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import {
|
||||||
|
LOG_LEVEL_STORAGE_KEY,
|
||||||
|
createLogger,
|
||||||
|
defaultLogLevel,
|
||||||
|
getLogLevel,
|
||||||
|
isLevelEnabled,
|
||||||
|
parseLogLevel,
|
||||||
|
readStoredLogLevel,
|
||||||
|
resetLogLevel,
|
||||||
|
setLogLevel,
|
||||||
|
type LogLevel,
|
||||||
|
} from "./logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frontend leveled logging facade.
|
||||||
|
*
|
||||||
|
* TRACES: | DR-204 | UT-201
|
||||||
|
*
|
||||||
|
* The frontend used to ship 468 ungated `console.*` calls to end users — no
|
||||||
|
* levels, no gate, no way to turn them off. These tests pin the three
|
||||||
|
* properties that make the replacement safe to rely on:
|
||||||
|
*
|
||||||
|
* 1. **Gating is by severity, and errors/warnings are never gated away.** A
|
||||||
|
* production build suppresses chatter, but a user-visible failure must still
|
||||||
|
* reach the console or a bug report has nothing in it.
|
||||||
|
* 2. **The scope is what replaces the hand-written `"[Scope] …"` prefixes**, so
|
||||||
|
* it has to land in the message rather than beside it, and it must not
|
||||||
|
* mangle the remaining arguments.
|
||||||
|
* 3. **Reading the `localStorage` override can never throw.** `localStorage` is
|
||||||
|
* absent under SSR and *throws on access* in a webview with storage
|
||||||
|
* disabled; logging must not be what takes the app down.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Spies for all four backing console methods. */
|
||||||
|
function spyConsole() {
|
||||||
|
return {
|
||||||
|
log: vi.spyOn(console, "log").mockImplementation(() => {}),
|
||||||
|
info: vi.spyOn(console, "info").mockImplementation(() => {}),
|
||||||
|
warn: vi.spyOn(console, "warn").mockImplementation(() => {}),
|
||||||
|
error: vi.spyOn(console, "error").mockImplementation(() => {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Swap `globalThis.localStorage` for the duration of a test. */
|
||||||
|
function stubStorage(value: unknown) {
|
||||||
|
const original = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
|
||||||
|
Object.defineProperty(globalThis, "localStorage", {
|
||||||
|
value,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
if (original) Object.defineProperty(globalThis, "localStorage", original);
|
||||||
|
else delete (globalThis as { localStorage?: unknown }).localStorage;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("parseLogLevel", () => {
|
||||||
|
it("accepts every level name", () => {
|
||||||
|
for (const level of ["debug", "info", "warn", "error"] as LogLevel[]) {
|
||||||
|
expect(parseLogLevel(level)).toBe(level);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is case- and whitespace-insensitive, because a human types the override", () => {
|
||||||
|
expect(parseLogLevel(" DEBUG ")).toBe("debug");
|
||||||
|
expect(parseLogLevel("Warn")).toBe("warn");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects anything that is not a level", () => {
|
||||||
|
expect(parseLogLevel("trace")).toBeNull();
|
||||||
|
expect(parseLogLevel("")).toBeNull();
|
||||||
|
expect(parseLogLevel(null)).toBeNull();
|
||||||
|
expect(parseLogLevel(undefined)).toBeNull();
|
||||||
|
expect(parseLogLevel(3)).toBeNull();
|
||||||
|
expect(parseLogLevel({ level: "debug" })).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("level gating", () => {
|
||||||
|
let restoreLevel: LogLevel;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
restoreLevel = getLogLevel();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setLogLevel(restoreLevel);
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits everything at debug", () => {
|
||||||
|
setLogLevel("debug");
|
||||||
|
const spies = spyConsole();
|
||||||
|
const log = createLogger("Test");
|
||||||
|
|
||||||
|
log.debug("d");
|
||||||
|
log.info("i");
|
||||||
|
log.warn("w");
|
||||||
|
log.error("e");
|
||||||
|
|
||||||
|
expect(spies.log).toHaveBeenCalledTimes(1);
|
||||||
|
expect(spies.info).toHaveBeenCalledTimes(1);
|
||||||
|
expect(spies.warn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(spies.error).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suppresses debug and info at warn — the production default", () => {
|
||||||
|
setLogLevel("warn");
|
||||||
|
const spies = spyConsole();
|
||||||
|
const log = createLogger("Test");
|
||||||
|
|
||||||
|
log.debug("d");
|
||||||
|
log.info("i");
|
||||||
|
log.warn("w");
|
||||||
|
log.error("e");
|
||||||
|
|
||||||
|
expect(spies.log).not.toHaveBeenCalled();
|
||||||
|
expect(spies.info).not.toHaveBeenCalled();
|
||||||
|
expect(spies.warn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(spies.error).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still emits errors at the most restrictive level", () => {
|
||||||
|
// A silent failure is worse to support than a noisy console: there is no
|
||||||
|
// level at which `error` is dropped.
|
||||||
|
setLogLevel("error");
|
||||||
|
const spies = spyConsole();
|
||||||
|
const log = createLogger("Test");
|
||||||
|
|
||||||
|
log.debug("d");
|
||||||
|
log.info("i");
|
||||||
|
log.warn("w");
|
||||||
|
log.error("boom");
|
||||||
|
|
||||||
|
expect(spies.log).not.toHaveBeenCalled();
|
||||||
|
expect(spies.info).not.toHaveBeenCalled();
|
||||||
|
expect(spies.warn).not.toHaveBeenCalled();
|
||||||
|
expect(spies.error).toHaveBeenCalledWith("[Test] boom");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports which levels are enabled", () => {
|
||||||
|
setLogLevel("warn");
|
||||||
|
expect(isLevelEnabled("debug")).toBe(false);
|
||||||
|
expect(isLevelEnabled("info")).toBe(false);
|
||||||
|
expect(isLevelEnabled("warn")).toBe(true);
|
||||||
|
expect(isLevelEnabled("error")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps debug to console.log, not console.debug", () => {
|
||||||
|
// console.debug lands in the browser's hidden "Verbose" bucket, which would
|
||||||
|
// make dev logging invisible in exactly the builds that want it.
|
||||||
|
setLogLevel("debug");
|
||||||
|
const spies = spyConsole();
|
||||||
|
const debugSpy = vi.spyOn(console, "debug").mockImplementation(() => {});
|
||||||
|
|
||||||
|
createLogger("Test").debug("hello");
|
||||||
|
|
||||||
|
expect(debugSpy).not.toHaveBeenCalled();
|
||||||
|
expect(spies.log).toHaveBeenCalledWith("[Test] hello");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("scope prefixing", () => {
|
||||||
|
let restoreLevel: LogLevel;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
restoreLevel = getLogLevel();
|
||||||
|
setLogLevel("debug");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setLogLevel(restoreLevel);
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("folds the scope into a leading string message", () => {
|
||||||
|
const spies = spyConsole();
|
||||||
|
createLogger("VideoPlayer").warn("seek failed");
|
||||||
|
|
||||||
|
expect(spies.warn).toHaveBeenCalledWith("[VideoPlayer] seek failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes trailing arguments through untouched, by reference", () => {
|
||||||
|
const spies = spyConsole();
|
||||||
|
const payload = { itemId: "abc", nested: { position: 12 } };
|
||||||
|
const err = new Error("nope");
|
||||||
|
|
||||||
|
createLogger("Queue").error("failed to advance:", payload, err, 42);
|
||||||
|
|
||||||
|
expect(spies.error).toHaveBeenCalledWith("[Queue] failed to advance:", payload, err, 42);
|
||||||
|
// Same object, not a copy — devtools inspection depends on this.
|
||||||
|
expect(spies.error.mock.calls[0][1]).toBe(payload);
|
||||||
|
expect(spies.error.mock.calls[0][2]).toBe(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prepends the scope as its own argument when the first argument is not a string", () => {
|
||||||
|
const spies = spyConsole();
|
||||||
|
const err = new Error("boom");
|
||||||
|
|
||||||
|
createLogger("Auth").error(err);
|
||||||
|
|
||||||
|
expect(spies.error).toHaveBeenCalledWith("[Auth]", err);
|
||||||
|
expect(spies.error.mock.calls[0][1]).toBe(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles a call with no arguments at all", () => {
|
||||||
|
const spies = spyConsole();
|
||||||
|
createLogger("Auth").debug();
|
||||||
|
|
||||||
|
expect(spies.log).toHaveBeenCalledWith("[Auth]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps separate scopes independent", () => {
|
||||||
|
const spies = spyConsole();
|
||||||
|
createLogger("NextEpisode").info("advancing");
|
||||||
|
createLogger("PlayerPage").info("advancing");
|
||||||
|
|
||||||
|
expect(spies.info).toHaveBeenNthCalledWith(1, "[NextEpisode] advancing");
|
||||||
|
expect(spies.info).toHaveBeenNthCalledWith(2, "[PlayerPage] advancing");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves console methods at call time so spies and overrides are honoured", () => {
|
||||||
|
// A cached `console.warn` reference would bypass a devtools override or a
|
||||||
|
// later-installed spy — and every existing test that asserts on log output.
|
||||||
|
const log = createLogger("Late");
|
||||||
|
const late = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
|
||||||
|
log.warn("after the fact");
|
||||||
|
|
||||||
|
expect(late).toHaveBeenCalledWith("[Late] after the fact");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("localStorage override", () => {
|
||||||
|
let restoreStorage: () => void = () => {};
|
||||||
|
let restoreLevel: LogLevel;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
restoreLevel = getLogLevel();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
restoreStorage();
|
||||||
|
restoreStorage = () => {};
|
||||||
|
setLogLevel(restoreLevel);
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads the level a user set to gather logs for a bug report", () => {
|
||||||
|
restoreStorage = stubStorage({ getItem: vi.fn(() => "debug") });
|
||||||
|
|
||||||
|
expect(readStoredLogLevel()).toBe("debug");
|
||||||
|
expect(resetLogLevel()).toBe("debug");
|
||||||
|
expect(isLevelEnabled("debug")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("looks the level up under jellytau:logLevel", () => {
|
||||||
|
const getItem = vi.fn(() => "error");
|
||||||
|
restoreStorage = stubStorage({ getItem });
|
||||||
|
|
||||||
|
readStoredLogLevel();
|
||||||
|
|
||||||
|
expect(getItem).toHaveBeenCalledWith(LOG_LEVEL_STORAGE_KEY);
|
||||||
|
expect(LOG_LEVEL_STORAGE_KEY).toBe("jellytau:logLevel");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the build default for a missing or bogus value", () => {
|
||||||
|
restoreStorage = stubStorage({ getItem: vi.fn(() => null) });
|
||||||
|
expect(readStoredLogLevel()).toBeNull();
|
||||||
|
expect(resetLogLevel()).toBe(defaultLogLevel());
|
||||||
|
|
||||||
|
restoreStorage();
|
||||||
|
restoreStorage = stubStorage({ getItem: vi.fn(() => "extremely-verbose") });
|
||||||
|
expect(readStoredLogLevel()).toBeNull();
|
||||||
|
expect(resetLogLevel()).toBe(defaultLogLevel());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("survives localStorage being absent (SSR)", () => {
|
||||||
|
restoreStorage = stubStorage(undefined);
|
||||||
|
|
||||||
|
expect(() => readStoredLogLevel()).not.toThrow();
|
||||||
|
expect(readStoredLogLevel()).toBeNull();
|
||||||
|
expect(resetLogLevel()).toBe(defaultLogLevel());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("survives localStorage throwing on access (storage disabled)", () => {
|
||||||
|
restoreStorage = stubStorage({
|
||||||
|
getItem: () => {
|
||||||
|
throw new DOMException("The operation is insecure.", "SecurityError");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => readStoredLogLevel()).not.toThrow();
|
||||||
|
expect(readStoredLogLevel()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("survives localStorage being a stale object with no getItem", () => {
|
||||||
|
restoreStorage = stubStorage({});
|
||||||
|
|
||||||
|
expect(() => readStoredLogLevel()).not.toThrow();
|
||||||
|
expect(readStoredLogLevel()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not let a broken localStorage break logging itself", () => {
|
||||||
|
restoreStorage = stubStorage({
|
||||||
|
getItem: () => {
|
||||||
|
throw new Error("nope");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resetLogLevel();
|
||||||
|
const spies = spyConsole();
|
||||||
|
|
||||||
|
expect(() => createLogger("Boot").error("still reaches the console")).not.toThrow();
|
||||||
|
expect(spies.error).toHaveBeenCalledWith("[Boot] still reaches the console");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* Frontend leveled logging facade.
|
||||||
|
*
|
||||||
|
* TRACES: | DR-204
|
||||||
|
*
|
||||||
|
* ## Why this exists
|
||||||
|
*
|
||||||
|
* The Rust half of the app is disciplined about logging: the `log` crate behind
|
||||||
|
* `env_logger`, `LevelFilter::Info` by default, `RUST_LOG` to turn the volume up
|
||||||
|
* without a rebuild (see `src-tauri/src/lib.rs`). The frontend had nothing —
|
||||||
|
* every `console.log` written during development shipped to end users and ran on
|
||||||
|
* every device, forever.
|
||||||
|
*
|
||||||
|
* This module is the frontend's `log` crate: four levels, a compile-environment
|
||||||
|
* default, and a runtime override that is the moral equivalent of `RUST_LOG`.
|
||||||
|
*
|
||||||
|
* ## Levels
|
||||||
|
*
|
||||||
|
* `debug < info < warn < error`. A message is emitted when its level is at or
|
||||||
|
* above the active level.
|
||||||
|
*
|
||||||
|
* - **debug** — the default for anything chatty: per-tick state, cache hits,
|
||||||
|
* "entered this branch". Dev only.
|
||||||
|
* - **info** — lifecycle/state events worth having in a user's console when
|
||||||
|
* they are diagnosing something: sign-in, playback start, mode transfer.
|
||||||
|
* - **warn** — recovered-from problems. Always emitted.
|
||||||
|
* - **error** — failures the user may notice. Always emitted.
|
||||||
|
*
|
||||||
|
* ## Defaults
|
||||||
|
*
|
||||||
|
* Dev builds (`import.meta.env.DEV`) default to `debug`; production builds
|
||||||
|
* default to `warn`. Production deliberately keeps **warn and error** — this is
|
||||||
|
* a user-facing media client talking to a server that may or may not be there,
|
||||||
|
* and a silent failure is far worse to support than a noisy console. Only the
|
||||||
|
* chatter (`debug`/`info`) is suppressed.
|
||||||
|
*
|
||||||
|
* ## Runtime override (the `RUST_LOG` equivalent)
|
||||||
|
*
|
||||||
|
* A user filing a bug can turn verbose logging on in a shipped build without a
|
||||||
|
* rebuild, from the webview console:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* localStorage.setItem("jellytau:logLevel", "debug"); // then reload
|
||||||
|
* localStorage.removeItem("jellytau:logLevel"); // back to the default
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The key is read **once at module init** (so the level cannot change halfway
|
||||||
|
* through a session and confuse a bug report) and every read is guarded — SSR
|
||||||
|
* has no `localStorage`, and a webview with storage disabled *throws* on access
|
||||||
|
* rather than returning `null`. Either way we fall back to the build default.
|
||||||
|
*
|
||||||
|
* ## Scopes
|
||||||
|
*
|
||||||
|
* `createLogger("VideoPlayer")` replaces the hand-rolled `"[VideoPlayer] …"`
|
||||||
|
* prefixes that used to be typed into every call site.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const log = createLogger("VideoPlayer");
|
||||||
|
* log.debug("seeking to", position, { mode });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Pass-through, not a wrapper
|
||||||
|
*
|
||||||
|
* When a level is enabled the call goes straight to `console.*` with the
|
||||||
|
* arguments **untouched** — no stringification, no JSON, no cloning — so object
|
||||||
|
* references stay live and expandable in devtools. The scope is folded into the
|
||||||
|
* leading string argument when there is one (keeping `console` grouping and
|
||||||
|
* substitution behaviour intact), and passed as its own leading argument
|
||||||
|
* otherwise. `console` is looked up at call time so `vi.spyOn(console, …)` and
|
||||||
|
* devtools console overrides still see everything.
|
||||||
|
*
|
||||||
|
* `debug` maps to `console.log` rather than `console.debug` on purpose:
|
||||||
|
* `console.debug` lands in the browser's "Verbose" bucket, which is hidden by
|
||||||
|
* default in both Chrome DevTools and the WebKit inspector, so mapping there
|
||||||
|
* would make dev logging invisible in exactly the builds that want it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Severity ordering. Higher wins. */
|
||||||
|
const LEVEL_RANK = {
|
||||||
|
debug: 10,
|
||||||
|
info: 20,
|
||||||
|
warn: 30,
|
||||||
|
error: 40,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** A log level, in the same vocabulary as the Rust `log` crate. */
|
||||||
|
export type LogLevel = keyof typeof LEVEL_RANK;
|
||||||
|
|
||||||
|
/** The `localStorage` key that overrides the build-default level. */
|
||||||
|
export const LOG_LEVEL_STORAGE_KEY = "jellytau:logLevel";
|
||||||
|
|
||||||
|
/** Which `console` method backs each level. See the module header for `debug`. */
|
||||||
|
const CONSOLE_METHOD: Record<LogLevel, "log" | "info" | "warn" | "error"> = {
|
||||||
|
debug: "log",
|
||||||
|
info: "info",
|
||||||
|
warn: "warn",
|
||||||
|
error: "error",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A scoped logger. One method per level, all variadic like `console.*`. */
|
||||||
|
export interface Logger {
|
||||||
|
debug(...args: unknown[]): void;
|
||||||
|
info(...args: unknown[]): void;
|
||||||
|
warn(...args: unknown[]): void;
|
||||||
|
error(...args: unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce arbitrary input to a `LogLevel`, or `null` when it is not one.
|
||||||
|
* Case- and whitespace-insensitive, because this parses human-typed input.
|
||||||
|
*/
|
||||||
|
export function parseLogLevel(raw: unknown): LogLevel | null {
|
||||||
|
if (typeof raw !== "string") return null;
|
||||||
|
const normalised = raw.trim().toLowerCase();
|
||||||
|
return normalised in LEVEL_RANK ? (normalised as LogLevel) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The level a build defaults to with no override present. */
|
||||||
|
export function defaultLogLevel(): LogLevel {
|
||||||
|
return import.meta.env?.DEV ? "debug" : "warn";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the override from `localStorage`, or `null` when there is none.
|
||||||
|
*
|
||||||
|
* Never throws. `localStorage` is absent under SSR and *throws on access* in a
|
||||||
|
* webview with storage disabled or a blocked third-party context — logging must
|
||||||
|
* not be the thing that takes the app down.
|
||||||
|
*/
|
||||||
|
export function readStoredLogLevel(): LogLevel | null {
|
||||||
|
try {
|
||||||
|
if (typeof localStorage === "undefined" || localStorage === null) return null;
|
||||||
|
return parseLogLevel(localStorage.getItem(LOG_LEVEL_STORAGE_KEY));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeLevel: LogLevel = readStoredLogLevel() ?? defaultLogLevel();
|
||||||
|
|
||||||
|
/** The level currently in force. */
|
||||||
|
export function getLogLevel(): LogLevel {
|
||||||
|
return activeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the active level for the rest of the session.
|
||||||
|
*
|
||||||
|
* Does **not** persist — write {@link LOG_LEVEL_STORAGE_KEY} for that. Mainly
|
||||||
|
* here for tests and for a future settings toggle.
|
||||||
|
*/
|
||||||
|
export function setLogLevel(level: LogLevel): void {
|
||||||
|
activeLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-read the override and reapply the build default. Called once implicitly at
|
||||||
|
* module init; exposed so tests can exercise the override without a fresh
|
||||||
|
* module registry.
|
||||||
|
*/
|
||||||
|
export function resetLogLevel(): LogLevel {
|
||||||
|
activeLevel = readStoredLogLevel() ?? defaultLogLevel();
|
||||||
|
return activeLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Would a message at `level` be emitted right now? */
|
||||||
|
export function isLevelEnabled(level: LogLevel): boolean {
|
||||||
|
return LEVEL_RANK[level] >= LEVEL_RANK[activeLevel];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a logger tagged with `scope`.
|
||||||
|
*
|
||||||
|
* The scope replaces the `"[Scope] …"` prefixes that used to be hand-written
|
||||||
|
* into each call, so call sites pass the message alone.
|
||||||
|
*/
|
||||||
|
export function createLogger(scope: string): Logger {
|
||||||
|
const tag = `[${scope}]`;
|
||||||
|
|
||||||
|
const emit = (level: LogLevel, args: unknown[]): void => {
|
||||||
|
if (!isLevelEnabled(level)) return;
|
||||||
|
|
||||||
|
// Look `console` up at call time: test spies and devtools overrides replace
|
||||||
|
// the method on the object, and a cached reference would bypass them.
|
||||||
|
const method = CONSOLE_METHOD[level];
|
||||||
|
|
||||||
|
// Fold the tag into a leading string so format specifiers (`%s`, `%o`) and
|
||||||
|
// multi-line messages still read as one message. Non-string leading args
|
||||||
|
// (an Error, an object) are left strictly alone.
|
||||||
|
if (typeof args[0] === "string") {
|
||||||
|
console[method](`${tag} ${args[0]}`, ...args.slice(1));
|
||||||
|
} else {
|
||||||
|
console[method](tag, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
debug: (...args: unknown[]) => emit("debug", args),
|
||||||
|
info: (...args: unknown[]) => emit("info", args),
|
||||||
|
warn: (...args: unknown[]) => emit("warn", args),
|
||||||
|
error: (...args: unknown[]) => emit("error", args),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@
|
|||||||
* so there is no HTML5 fallback to reach for.
|
* so there is no HTML5 fallback to reach for.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("PiP");
|
||||||
|
|
||||||
interface AndroidPictureInPictureBridge {
|
interface AndroidPictureInPictureBridge {
|
||||||
enterPip(): void;
|
enterPip(): void;
|
||||||
isSupported(): boolean;
|
isSupported(): boolean;
|
||||||
@@ -41,7 +45,7 @@ export function isPipSupported(): boolean {
|
|||||||
try {
|
try {
|
||||||
return bridge()?.isSupported() ?? false;
|
return bridge()?.isSupported() ?? false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[PiP] isSupported check failed:", err);
|
log.warn("isSupported check failed:", err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +58,7 @@ export function canEnterPip(): boolean {
|
|||||||
try {
|
try {
|
||||||
return bridge()?.canEnterPip() ?? false;
|
return bridge()?.canEnterPip() ?? false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[PiP] canEnterPip check failed:", err);
|
log.warn("canEnterPip check failed:", err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +68,7 @@ export function enterPip(): void {
|
|||||||
try {
|
try {
|
||||||
bridge()?.enterPip();
|
bridge()?.enterPip();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[PiP] Failed to enter picture-in-picture:", err);
|
log.error("Failed to enter picture-in-picture:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +86,7 @@ export function setAutoEnterEnabled(enabled: boolean): void {
|
|||||||
try {
|
try {
|
||||||
bridge()?.setAutoEnterEnabled(enabled);
|
bridge()?.setAutoEnterEnabled(enabled);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[PiP] Failed to set auto-enter:", err);
|
log.warn("Failed to set auto-enter:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +119,6 @@ export function setHtml5VideoState(
|
|||||||
try {
|
try {
|
||||||
bridge()?.setHtml5VideoState(active, Math.round(width), Math.round(height), playing);
|
bridge()?.setHtml5VideoState(active, Math.round(width), Math.round(height), playing);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[PiP] Failed to report HTML5 video state:", err);
|
log.warn("Failed to report HTML5 video state:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,10 @@
|
|||||||
* document exists, and a page load wipes any inline style native had set.
|
* document exists, and a page load wipes any inline style native had set.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("SafeArea");
|
||||||
|
|
||||||
/** Window insets in CSS pixels, one per edge. */
|
/** Window insets in CSS pixels, one per edge. */
|
||||||
export interface SafeAreaInsets {
|
export interface SafeAreaInsets {
|
||||||
top: number;
|
top: number;
|
||||||
@@ -135,7 +139,7 @@ export function readNativeInsets(): SafeAreaInsets | null {
|
|||||||
try {
|
try {
|
||||||
return parseNativeInsets(bridge.get());
|
return parseNativeInsets(bridge.get());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[SafeArea] AndroidInsets bridge unusable:", err);
|
log.warn("AndroidInsets bridge unusable:", err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { nativeVideoActive } from "$lib/stores/nativeVideo";
|
import { nativeVideoActive } from "$lib/stores/nativeVideo";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("videoSurface");
|
||||||
|
|
||||||
interface AndroidVideoSurfaceBridge {
|
interface AndroidVideoSurfaceBridge {
|
||||||
setTransparent(transparent: boolean): void;
|
setTransparent(transparent: boolean): void;
|
||||||
@@ -50,7 +53,7 @@ export function isNativeSurfaceBridgeAvailable(): boolean {
|
|||||||
try {
|
try {
|
||||||
return bridge()?.isSupported() ?? false;
|
return bridge()?.isSupported() ?? false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[videoSurface] isSupported check failed:", err);
|
log.warn("isSupported check failed:", err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,17 +76,17 @@ export function enableNativeVideoCompositing(): void {
|
|||||||
// correctly behind a WebView that never stopped painting its own opaque
|
// correctly behind a WebView that never stopped painting its own opaque
|
||||||
// background. That ambiguity is what DR-172 was left holding. MainActivity's
|
// background. That ambiguity is what DR-172 was left holding. MainActivity's
|
||||||
// console bridge forwards this to logcat under the JellyTauWeb tag.
|
// console bridge forwards this to logcat under the JellyTauWeb tag.
|
||||||
console.error(
|
log.error(
|
||||||
"[videoSurface] AndroidVideoSurface bridge is MISSING - the webview will " +
|
"AndroidVideoSurface bridge is MISSING - the webview will " +
|
||||||
"stay opaque and native video will play as audio with no picture"
|
"stay opaque and native video will play as audio with no picture"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
androidVideoSurface.setTransparent(true);
|
androidVideoSurface.setTransparent(true);
|
||||||
console.log("[videoSurface] compositing enabled (setTransparent(true) sent)");
|
log.debug("compositing enabled (setTransparent(true) sent)");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[videoSurface] setTransparent(true) failed:", err);
|
log.warn("setTransparent(true) failed:", err);
|
||||||
nativeVideoActive.set(false);
|
nativeVideoActive.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +96,7 @@ export function disableNativeVideoCompositing(): void {
|
|||||||
try {
|
try {
|
||||||
bridge()?.setTransparent(false);
|
bridge()?.setTransparent(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[videoSurface] setTransparent(false) failed:", err);
|
log.warn("setTransparent(false) failed:", err);
|
||||||
}
|
}
|
||||||
// Always clear the page layer, even if the bridge call failed, so the app is
|
// Always clear the page layer, even if the bridge call failed, so the app is
|
||||||
// never left rendering over a transparent window.
|
// never left rendering over a transparent window.
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
import { useScrollRestore } from "$lib/utils/scrollContainer";
|
import { useScrollRestore } from "$lib/utils/scrollContainer";
|
||||||
import { startNetworkReporting } from "$lib/services/networkType";
|
import { startNetworkReporting } from "$lib/services/networkType";
|
||||||
import { initSafeArea } from "$lib/utils/safeArea";
|
import { initSafeArea } from "$lib/utils/safeArea";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("Layout");
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
@@ -105,7 +108,7 @@
|
|||||||
const platformName = platform();
|
const platformName = platform();
|
||||||
isAndroid.set(platformName === "android");
|
isAndroid.set(platformName === "android");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Platform detection failed:", err);
|
log.error("Platform detection failed:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prime the safe-area custom properties from the native WindowInsets bridge
|
// Prime the safe-area custom properties from the native WindowInsets bridge
|
||||||
@@ -154,7 +157,7 @@
|
|||||||
const userId = get(auth).user?.id;
|
const userId = get(auth).user?.id;
|
||||||
if (userId) {
|
if (userId) {
|
||||||
downloads.refresh(userId).catch((err) =>
|
downloads.refresh(userId).catch((err) =>
|
||||||
console.error("Initial downloads refresh failed:", err)
|
log.error("Initial downloads refresh failed:", err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +171,7 @@
|
|||||||
// without spending their retry budget (DR-131).
|
// without spending their retry budget (DR-131).
|
||||||
if (get(auth).user?.id) {
|
if (get(auth).user?.id) {
|
||||||
commands.syncProcessPending().catch((err) =>
|
commands.syncProcessPending().catch((err) =>
|
||||||
console.debug("[Layout] Startup sync drain skipped:", err)
|
log.debug("Startup sync drain skipped:", err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +209,7 @@
|
|||||||
if (session?.serverUrl) {
|
if (session?.serverUrl) {
|
||||||
connectivity.forceCheck().catch((error) => {
|
connectivity.forceCheck().catch((error) => {
|
||||||
// If check fails, monitoring might not be started yet, so start it
|
// If check fails, monitoring might not be started yet, so start it
|
||||||
console.debug("[Layout] Queue status check failed, starting monitoring:", error);
|
log.debug("Queue status check failed, starting monitoring:", error);
|
||||||
connectivity.startMonitoring(session.serverUrl, {
|
connectivity.startMonitoring(session.serverUrl, {
|
||||||
onServerReconnected: () => {
|
onServerReconnected: () => {
|
||||||
// Retry session verification when server becomes reachable
|
// Retry session verification when server becomes reachable
|
||||||
@@ -215,7 +218,7 @@
|
|||||||
void onCatalogReconnected();
|
void onCatalogReconnected();
|
||||||
},
|
},
|
||||||
}).catch((monitorError) => {
|
}).catch((monitorError) => {
|
||||||
console.error("[Layout] Failed to start connectivity monitoring:", monitorError);
|
log.error("Failed to start connectivity monitoring:", monitorError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -242,7 +245,7 @@
|
|||||||
.then((unlisten) => {
|
.then((unlisten) => {
|
||||||
unlistenDrain = unlisten;
|
unlistenDrain = unlisten;
|
||||||
})
|
})
|
||||||
.catch((err) => console.debug("[Layout] sync-queue-changed listen failed:", err));
|
.catch((err) => log.debug("sync-queue-changed listen failed:", err));
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
import { assumedLibraryRatio } from "$lib/components/library/libraryMosaic";
|
import { assumedLibraryRatio } from "$lib/components/library/libraryMosaic";
|
||||||
import { useScrollRestore } from "$lib/utils/scrollContainer";
|
import { useScrollRestore } from "$lib/utils/scrollContainer";
|
||||||
import type { MediaItem, Library } from "$lib/api/types";
|
import type { MediaItem, Library } from "$lib/api/types";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("HomePage");
|
||||||
|
|
||||||
// Home scrolls in its own box rather than the shell's, and is destroyed on
|
// Home scrolls in its own box rather than the shell's, and is destroyed on
|
||||||
// every navigation away — so its offsets live in the module-level memory,
|
// every navigation away — so its offsets live in the module-level memory,
|
||||||
@@ -40,7 +43,7 @@
|
|||||||
const platformName = await platform();
|
const platformName = await platform();
|
||||||
isAndroid = platformName === "android";
|
isAndroid = platformName === "android";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Platform detection failed:", err);
|
log.error("Platform detection failed:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isAuthenticated) {
|
if ($isAuthenticated) {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import DownloadItem from "$lib/components/downloads/DownloadItem.svelte";
|
import DownloadItem from "$lib/components/downloads/DownloadItem.svelte";
|
||||||
import DownloadedBrowse from "$lib/components/downloads/DownloadedBrowse.svelte";
|
import DownloadedBrowse from "$lib/components/downloads/DownloadedBrowse.svelte";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("DownloadsPage");
|
||||||
|
|
||||||
type ViewType = "downloaded" | "transfers";
|
type ViewType = "downloaded" | "transfers";
|
||||||
let view = $state<ViewType>("downloaded");
|
let view = $state<ViewType>("downloaded");
|
||||||
@@ -42,7 +45,7 @@
|
|||||||
await downloads.refresh(userId);
|
await downloads.refresh(userId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load downloads:", error);
|
log.error("Failed to load downloads:", error);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -60,7 +63,7 @@
|
|||||||
try {
|
try {
|
||||||
await downloads.pause(download.id);
|
await downloads.pause(download.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to pause download ${download.id}:`, error);
|
log.error(`Failed to pause download ${download.id}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +77,7 @@
|
|||||||
try {
|
try {
|
||||||
await downloads.resume(download.id);
|
await downloads.resume(download.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to resume download ${download.id}:`, error);
|
log.error(`Failed to resume download ${download.id}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
initialExpandedSeasons,
|
initialExpandedSeasons,
|
||||||
type SeasonData,
|
type SeasonData,
|
||||||
} from "$lib/components/library/seriesNavigation";
|
} from "$lib/components/library/seriesNavigation";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("LibraryDetail");
|
||||||
|
|
||||||
let item = $state<MediaItem | null>(null);
|
let item = $state<MediaItem | null>(null);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
@@ -136,11 +139,11 @@
|
|||||||
}
|
}
|
||||||
// Series-less episode: rendered by the Focus View below, series and all.
|
// Series-less episode: rendered by the Focus View below, series and all.
|
||||||
}
|
}
|
||||||
console.log(`[LibraryDetail] ✓ Loaded item: ${item?.name} (${item?.kind})`);
|
log.debug(`✓ Loaded item: ${item?.name} (${item?.kind})`);
|
||||||
console.log(`[LibraryDetail] - Has people? ${item?.people ? `YES (${item.people.length})` : 'NO'}`);
|
log.debug(`- Has people? ${item?.people ? `YES (${item.people.length})` : 'NO'}`);
|
||||||
if (item?.people) {
|
if (item?.people) {
|
||||||
item.people.forEach((p, i) => {
|
item.people.forEach((p, i) => {
|
||||||
console.log(`[LibraryDetail] [${i}] ${p.name} (${p.type})`);
|
log.debug(` [${i}] ${p.name} (${p.type})`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +157,7 @@
|
|||||||
const musicLibrary = $libraries.find(lib => lib.collectionType === "music");
|
const musicLibrary = $libraries.find(lib => lib.collectionType === "music");
|
||||||
if (musicLibrary) {
|
if (musicLibrary) {
|
||||||
library.setCurrentLibrary(musicLibrary);
|
library.setCurrentLibrary(musicLibrary);
|
||||||
console.log("[LibraryDetail] Set current library to music library for music item");
|
log.debug("Set current library to music library for music item");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,20 +166,20 @@
|
|||||||
// Ensure cast/crew data is loaded for Movies, Series, and Episodes
|
// Ensure cast/crew data is loaded for Movies, Series, and Episodes
|
||||||
// Some APIs/caches may not include people data on first load
|
// Some APIs/caches may not include people data on first load
|
||||||
if ((item?.kind === "movie" || item?.kind === "series" || item?.kind === "episode") && (!item.people || item.people.length === 0)) {
|
if ((item?.kind === "movie" || item?.kind === "series" || item?.kind === "episode") && (!item.people || item.people.length === 0)) {
|
||||||
console.log(`[LibraryDetail] ⚠ People data missing, reloading ${item?.kind}...`);
|
log.debug(`⚠ People data missing, reloading ${item?.kind}...`);
|
||||||
try {
|
try {
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
const fullItem = await repo.getItem(itemId);
|
const fullItem = await repo.getItem(itemId);
|
||||||
console.log(`[LibraryDetail] - Reloaded has people? ${fullItem.people ? `YES (${fullItem.people.length})` : 'NO'}`);
|
log.debug(`- Reloaded has people? ${fullItem.people ? `YES (${fullItem.people.length})` : 'NO'}`);
|
||||||
if (fullItem.people && fullItem.people.length > 0) {
|
if (fullItem.people && fullItem.people.length > 0) {
|
||||||
item = fullItem;
|
item = fullItem;
|
||||||
console.log(`[LibraryDetail] ✓ Updated item with ${fullItem.people.length} people`);
|
log.debug(`✓ Updated item with ${fullItem.people.length} people`);
|
||||||
fullItem.people.forEach((p, i) => {
|
fullItem.people.forEach((p, i) => {
|
||||||
console.log(`[LibraryDetail] [${i}] ${p.name} (${p.type})`);
|
log.debug(` [${i}] ${p.name} (${p.type})`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Could not reload ${item?.kind} with full cast data:`, e);
|
log.warn(`Could not reload ${item?.kind} with full cast data:`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +197,7 @@
|
|||||||
repo.getSeriesEpisodes(itemId),
|
repo.getSeriesEpisodes(itemId),
|
||||||
// Best-effort: a series still renders if the anchor cannot be resolved.
|
// Best-effort: a series still renders if the anchor cannot be resolved.
|
||||||
repo.getSeriesCurrentEpisode(itemId).catch((e) => {
|
repo.getSeriesCurrentEpisode(itemId).catch((e) => {
|
||||||
console.warn("Could not resolve the current episode:", e);
|
log.warn("Could not resolve the current episode:", e);
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@@ -220,7 +223,7 @@
|
|||||||
directFetchedEpisode = await repo.getItem(episodeIdParam);
|
directFetchedEpisode = await repo.getItem(episodeIdParam);
|
||||||
} catch {
|
} catch {
|
||||||
// Best-effort: the list entry still renders a usable hero.
|
// Best-effort: the list entry still renders a usable hero.
|
||||||
console.warn("Could not fetch focused episode directly:", episodeIdParam);
|
log.warn("Could not fetch focused episode directly:", episodeIdParam);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +325,7 @@
|
|||||||
shuffle: false,
|
shuffle: false,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to play album:", e);
|
log.error("Failed to play album:", e);
|
||||||
alert(`Failed to play album: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
alert(`Failed to play album: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
} else if ($libraryItems.length > 0) {
|
} else if ($libraryItems.length > 0) {
|
||||||
@@ -346,7 +349,7 @@
|
|||||||
shuffle: true,
|
shuffle: true,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to shuffle play album:", e);
|
log.error("Failed to shuffle play album:", e);
|
||||||
alert(`Failed to shuffle play: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
alert(`Failed to shuffle play: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
} else if (item?.kind === "series" && allEpisodes.length > 0) {
|
} else if (item?.kind === "series" && allEpisodes.length > 0) {
|
||||||
|
|||||||
@@ -29,6 +29,9 @@
|
|||||||
favoritesRouteUrl,
|
favoritesRouteUrl,
|
||||||
emptyStateMessage,
|
emptyStateMessage,
|
||||||
} from "$lib/utils/favoritesView";
|
} from "$lib/utils/favoritesView";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("FavoritesPage");
|
||||||
|
|
||||||
const scope = $derived(resolveFavoritesScope($page.url.searchParams.get("scope")));
|
const scope = $derived(resolveFavoritesScope($page.url.searchParams.get("scope")));
|
||||||
|
|
||||||
@@ -49,7 +52,7 @@
|
|||||||
const result = await repo.getFavorites(currentScope, { limit: 500 });
|
const result = await repo.getFavorites(currentScope, { limit: 500 });
|
||||||
items = result.items;
|
items = result.items;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load favorites:", error);
|
log.error("Failed to load favorites:", error);
|
||||||
loadError = "Could not load your favourites.";
|
loadError = "Could not load your favourites.";
|
||||||
items = [];
|
items = [];
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -24,6 +24,11 @@
|
|||||||
import { reportSkippedEpisode, shouldSuppressStopReport } from "$lib/services/skipReporting";
|
import { reportSkippedEpisode, shouldSuppressStopReport } from "$lib/services/skipReporting";
|
||||||
import { cleanup as cleanupNextEpisode } from "$lib/services/nextEpisodeService";
|
import { cleanup as cleanupNextEpisode } from "$lib/services/nextEpisodeService";
|
||||||
import * as html5Adapter from "$lib/player/html5Adapter";
|
import * as html5Adapter from "$lib/player/html5Adapter";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("PlayerPage");
|
||||||
|
const nextEpisodeLog = createLogger("NextEpisode");
|
||||||
|
const autoPlayLog = createLogger("AutoPlay");
|
||||||
|
|
||||||
const itemId = $derived($page.params.id);
|
const itemId = $derived($page.params.id);
|
||||||
const queueParam = $derived($page.url.searchParams.get("queue"));
|
const queueParam = $derived($page.url.searchParams.get("queue"));
|
||||||
@@ -95,7 +100,7 @@
|
|||||||
const id = itemId;
|
const id = itemId;
|
||||||
const restart = restartParam;
|
const restart = restartParam;
|
||||||
if (id && id !== loadedItemId) {
|
if (id && id !== loadedItemId) {
|
||||||
console.log("[AutoPlay] $effect triggered: loading new item", id, "(was:", loadedItemId, ") restart:", restart);
|
autoPlayLog.debug("$effect triggered: loading new item", id, "(was:", loadedItemId, ") restart:", restart);
|
||||||
// restart=true (advancing to next episode) forces start-from-beginning,
|
// restart=true (advancing to next episode) forces start-from-beginning,
|
||||||
// bypassing the resume-progress check.
|
// bypassing the resume-progress check.
|
||||||
loadAndPlay(id, restart ? 0 : undefined, restart);
|
loadAndPlay(id, restart ? 0 : undefined, restart);
|
||||||
@@ -128,16 +133,16 @@
|
|||||||
let retrievedProgressSeconds: number | null = null;
|
let retrievedProgressSeconds: number | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("loadAndPlay: Loading item", id);
|
log.debug("loadAndPlay: Loading item", id);
|
||||||
// Load item details
|
// Load item details
|
||||||
const item = await library.loadItem(id);
|
const item = await library.loadItem(id);
|
||||||
console.log("loadAndPlay: Loaded item", item.name, "kind:", item.kind);
|
log.debug("loadAndPlay: Loaded item", item.name, "kind:", item.kind);
|
||||||
currentMedia = item;
|
currentMedia = item;
|
||||||
|
|
||||||
// Check if this is a non-playable collection type that should be viewed in library instead
|
// Check if this is a non-playable collection type that should be viewed in library instead
|
||||||
const collectionKinds: MediaKind[] = ["album", "artist", "series", "season", "folder", "playlist", "channel"];
|
const collectionKinds: MediaKind[] = ["album", "artist", "series", "season", "folder", "playlist", "channel"];
|
||||||
if (item.kind && collectionKinds.includes(item.kind)) {
|
if (item.kind && collectionKinds.includes(item.kind)) {
|
||||||
console.log("loadAndPlay: Redirecting collection type to library:", item.kind);
|
log.debug("loadAndPlay: Redirecting collection type to library:", item.kind);
|
||||||
goto(`/library/${id}`);
|
goto(`/library/${id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -162,7 +167,7 @@
|
|||||||
forceRestart,
|
forceRestart,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
console.log("loadAndPlay: Track already playing, showing UI without restarting");
|
log.debug("loadAndPlay: Track already playing, showing UI without restarting");
|
||||||
isPlaying = true;
|
isPlaying = true;
|
||||||
loading = false;
|
loading = false;
|
||||||
// hasNext/hasPrevious come from the event-driven queue store.
|
// hasNext/hasPrevious come from the event-driven queue store.
|
||||||
@@ -175,7 +180,7 @@
|
|||||||
try {
|
try {
|
||||||
await commands.playerStop();
|
await commands.playerStop();
|
||||||
queue.clear();
|
queue.clear();
|
||||||
console.log("loadAndPlay: Stopped audio backend for video playback");
|
log.debug("loadAndPlay: Stopped audio backend for video playback");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore - player may not have been playing
|
// Ignore - player may not have been playing
|
||||||
}
|
}
|
||||||
@@ -185,43 +190,43 @@
|
|||||||
// When forceRestart is set (advancing to a next episode) we always start
|
// When forceRestart is set (advancing to a next episode) we always start
|
||||||
// from the beginning, skipping the resume check and resume dialog.
|
// from the beginning, skipping the resume check and resume dialog.
|
||||||
const userId = auth.getUserId();
|
const userId = auth.getUserId();
|
||||||
console.log("Resume check - userId:", userId, "itemId:", id, "startPosition:", startPosition, "forceRestart:", forceRestart);
|
log.debug("Resume check - userId:", userId, "itemId:", id, "startPosition:", startPosition, "forceRestart:", forceRestart);
|
||||||
|
|
||||||
// Live streams have no fixed position - never resume.
|
// Live streams have no fixed position - never resume.
|
||||||
if (!startPosition && !forceRestart && userId && !isLive) {
|
if (!startPosition && !forceRestart && userId && !isLive) {
|
||||||
try {
|
try {
|
||||||
const progress = await commands.storageGetPlaybackProgress(userId, id);
|
const progress = await commands.storageGetPlaybackProgress(userId, id);
|
||||||
console.log("Resume check - retrieved progress:", progress);
|
log.debug("Resume check - retrieved progress:", progress);
|
||||||
|
|
||||||
if (progress && progress.positionMs > 0 && item.durationMs) {
|
if (progress && progress.positionMs > 0 && item.durationMs) {
|
||||||
const positionSeconds = progress.positionMs / 1000;
|
const positionSeconds = progress.positionMs / 1000;
|
||||||
const totalSeconds = item.durationMs / 1000;
|
const totalSeconds = item.durationMs / 1000;
|
||||||
const progressPercent = (positionSeconds / totalSeconds) * 100;
|
const progressPercent = (positionSeconds / totalSeconds) * 100;
|
||||||
|
|
||||||
console.log("Resume check - positionSeconds:", positionSeconds, "totalSeconds:", totalSeconds, "progressPercent:", progressPercent);
|
log.debug("Resume check - positionSeconds:", positionSeconds, "totalSeconds:", totalSeconds, "progressPercent:", progressPercent);
|
||||||
|
|
||||||
// Store for later use regardless of whether dialog is shown
|
// Store for later use regardless of whether dialog is shown
|
||||||
retrievedProgressSeconds = positionSeconds;
|
retrievedProgressSeconds = positionSeconds;
|
||||||
|
|
||||||
// Show resume dialog if watched > 30 seconds and < 90% complete
|
// Show resume dialog if watched > 30 seconds and < 90% complete
|
||||||
if (positionSeconds > 30 && progressPercent < 90) {
|
if (positionSeconds > 30 && progressPercent < 90) {
|
||||||
console.log("Resume check - SHOWING RESUME DIALOG");
|
log.debug("Resume check - SHOWING RESUME DIALOG");
|
||||||
savedProgress = { positionSeconds, progressPercent };
|
savedProgress = { positionSeconds, progressPercent };
|
||||||
showResumeDialog = true;
|
showResumeDialog = true;
|
||||||
loading = false;
|
loading = false;
|
||||||
return; // Wait for user decision
|
return; // Wait for user decision
|
||||||
} else {
|
} else {
|
||||||
console.log("Resume check - NOT showing dialog. Position > 30?", positionSeconds > 30, "Progress < 90?", progressPercent < 90);
|
log.debug("Resume check - NOT showing dialog. Position > 30?", positionSeconds > 30, "Progress < 90?", progressPercent < 90);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Resume check - No valid progress found. Has progress?", !!progress, "Has position?", progress?.positionMs, "Has runtime?", !!item.durationMs);
|
log.debug("Resume check - No valid progress found. Has progress?", !!progress, "Has position?", progress?.positionMs, "Has runtime?", !!item.durationMs);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to check saved progress:", e);
|
log.error("Failed to check saved progress:", e);
|
||||||
// Continue with normal playback
|
// Continue with normal playback
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Resume check - Skipped. Reason:", !userId ? "No userId" : "Has startPosition");
|
log.debug("Resume check - Skipped. Reason:", !userId ? "No userId" : "Has startPosition");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this item is downloaded locally
|
// Check if this item is downloaded locally
|
||||||
@@ -232,7 +237,7 @@
|
|||||||
|
|
||||||
if (localDownload) {
|
if (localDownload) {
|
||||||
// Use local file for playback
|
// Use local file for playback
|
||||||
console.log("loadAndPlay: Found local download, using offline playback:", localDownload.filePath);
|
log.debug("loadAndPlay: Found local download, using offline playback:", localDownload.filePath);
|
||||||
isOfflinePlayback = true;
|
isOfflinePlayback = true;
|
||||||
|
|
||||||
// Get the storage path and resolve the file's location. A completed
|
// Get the storage path and resolve the file's location. A completed
|
||||||
@@ -241,7 +246,7 @@
|
|||||||
// TRACES: UR-071 | DR-133
|
// TRACES: UR-071 | DR-133
|
||||||
const storagePath = await commands.storageGetPath();
|
const storagePath = await commands.storageGetPath();
|
||||||
const fullPath = downloadedFilePath(storagePath, localDownload.filePath);
|
const fullPath = downloadedFilePath(storagePath, localDownload.filePath);
|
||||||
console.log("loadAndPlay: Full local path:", fullPath);
|
log.debug("loadAndPlay: Full local path:", fullPath);
|
||||||
|
|
||||||
// Serve the file over the loopback media server rather than the asset
|
// Serve the file over the loopback media server rather than the asset
|
||||||
// protocol: the asset protocol answers a range-less request with the
|
// protocol: the asset protocol answers a range-less request with the
|
||||||
@@ -249,7 +254,7 @@
|
|||||||
// the URL (it holds the port and the per-session token).
|
// the URL (it holds the port and the per-session token).
|
||||||
// TRACES: UR-071 | DR-137
|
// TRACES: UR-071 | DR-137
|
||||||
const localUrl = await commands.mediaLocalUrl(fullPath);
|
const localUrl = await commands.mediaLocalUrl(fullPath);
|
||||||
console.log("loadAndPlay: Local media URL resolved");
|
log.debug("loadAndPlay: Local media URL resolved");
|
||||||
|
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
// Local video files don't need transcoding and support native seeking
|
// Local video files don't need transcoding and support native seeking
|
||||||
@@ -260,7 +265,7 @@
|
|||||||
videoInitialPosition = effectivePosition;
|
videoInitialPosition = effectivePosition;
|
||||||
} else {
|
} else {
|
||||||
// Local audio playback via MPV backend
|
// Local audio playback via MPV backend
|
||||||
console.log("loadAndPlay: Using MPV backend for offline audio");
|
log.debug("loadAndPlay: Using MPV backend for offline audio");
|
||||||
// Use player_play_tracks - backend fetches all metadata from single ID
|
// Use player_play_tracks - backend fetches all metadata from single ID
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
const repositoryHandle = repo.getHandle();
|
const repositoryHandle = repo.getHandle();
|
||||||
@@ -286,9 +291,9 @@
|
|||||||
if (isLive) {
|
if (isLive) {
|
||||||
// Live TV channels must be "opened" before streaming; the server returns
|
// Live TV channels must be "opened" before streaming; the server returns
|
||||||
// a ready-to-play HLS transcoding URL. No resume, no seek, no progress.
|
// a ready-to-play HLS transcoding URL. No resume, no seek, no progress.
|
||||||
console.log("loadAndPlay: Opening live stream for channel:", id);
|
log.debug("loadAndPlay: Opening live stream for channel:", id);
|
||||||
const liveInfo = await repo.openLiveStream(id);
|
const liveInfo = await repo.openLiveStream(id);
|
||||||
console.log("loadAndPlay: Live stream URL:", liveInfo.streamUrl);
|
log.debug("loadAndPlay: Live stream URL:", liveInfo.streamUrl);
|
||||||
mediaSourceId = liveInfo.mediaSourceId;
|
mediaSourceId = liveInfo.mediaSourceId;
|
||||||
streamUrl = liveInfo.streamUrl;
|
streamUrl = liveInfo.streamUrl;
|
||||||
videoNeedsTranscoding = true;
|
videoNeedsTranscoding = true;
|
||||||
@@ -298,13 +303,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("loadAndPlay: Getting playback info");
|
log.debug("loadAndPlay: Getting playback info");
|
||||||
const playbackInfo = await repo.getPlaybackInfo(id);
|
const playbackInfo = await repo.getPlaybackInfo(id);
|
||||||
console.log("loadAndPlay: Got playback info, mediaSourceId:", playbackInfo.mediaSourceId);
|
log.debug("loadAndPlay: Got playback info, mediaSourceId:", playbackInfo.mediaSourceId);
|
||||||
|
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
// Playback API now detects HEVC/10-bit and returns transcoded URL when needed
|
// Playback API now detects HEVC/10-bit and returns transcoded URL when needed
|
||||||
console.log("loadAndPlay: Using video stream, directPlay:", playbackInfo.directPlay, "needsTranscoding:", playbackInfo.needsTranscoding);
|
log.debug("loadAndPlay: Using video stream, directPlay:", playbackInfo.directPlay, "needsTranscoding:", playbackInfo.needsTranscoding);
|
||||||
mediaSourceId = playbackInfo.mediaSourceId;
|
mediaSourceId = playbackInfo.mediaSourceId;
|
||||||
|
|
||||||
// Prefer a completed download over streaming. Audio has done this
|
// Prefer a completed download over streaming. Audio has done this
|
||||||
@@ -328,7 +333,7 @@
|
|||||||
|
|
||||||
streamUrl = source.url;
|
streamUrl = source.url;
|
||||||
videoNeedsTranscoding = source.needsTranscoding;
|
videoNeedsTranscoding = source.needsTranscoding;
|
||||||
console.log(
|
log.debug(
|
||||||
source.isLocal
|
source.isLocal
|
||||||
? "loadAndPlay: Playing downloaded file from disk"
|
? "loadAndPlay: Playing downloaded file from disk"
|
||||||
: `loadAndPlay: Using stream URL: ${streamUrl}`
|
: `loadAndPlay: Using stream URL: ${streamUrl}`
|
||||||
@@ -347,17 +352,17 @@
|
|||||||
const effectivePosition = startPosition ?? retrievedProgressSeconds ?? 0;
|
const effectivePosition = startPosition ?? retrievedProgressSeconds ?? 0;
|
||||||
videoInitialPosition = effectivePosition > 0 ? effectivePosition : 0;
|
videoInitialPosition = effectivePosition > 0 ? effectivePosition : 0;
|
||||||
if (videoInitialPosition > 0) {
|
if (videoInitialPosition > 0) {
|
||||||
console.log("loadAndPlay: Will seek to position after load:", videoInitialPosition);
|
log.debug("loadAndPlay: Will seek to position after load:", videoInitialPosition);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For audio, use MPV backend
|
// For audio, use MPV backend
|
||||||
console.log("loadAndPlay: Using MPV backend for audio");
|
log.debug("loadAndPlay: Using MPV backend for audio");
|
||||||
|
|
||||||
// Check if we have a queue parameter (e.g., queue=parent:albumId)
|
// Check if we have a queue parameter (e.g., queue=parent:albumId)
|
||||||
const queueParamValue = queueParam;
|
const queueParamValue = queueParam;
|
||||||
if (queueParamValue?.startsWith("parent:")) {
|
if (queueParamValue?.startsWith("parent:")) {
|
||||||
const parentId = queueParamValue.substring(7); // Remove "parent:" prefix
|
const parentId = queueParamValue.substring(7); // Remove "parent:" prefix
|
||||||
console.log("loadAndPlay: Loading queue from parent:", parentId);
|
log.debug("loadAndPlay: Loading queue from parent:", parentId);
|
||||||
|
|
||||||
// Fetch all tracks from the parent (album/playlist)
|
// Fetch all tracks from the parent (album/playlist)
|
||||||
const result = await repo.getItems(parentId, {
|
const result = await repo.getItems(parentId, {
|
||||||
@@ -372,16 +377,16 @@
|
|||||||
const startIndex = audioTracks.findIndex(t => t.id === id);
|
const startIndex = audioTracks.findIndex(t => t.id === id);
|
||||||
const actualStartIndex = startIndex >= 0 ? startIndex : 0;
|
const actualStartIndex = startIndex >= 0 ? startIndex : 0;
|
||||||
|
|
||||||
console.log("loadAndPlay: Building queue with", audioTracks.length, "tracks, startIndex:", actualStartIndex);
|
log.debug("loadAndPlay: Building queue with", audioTracks.length, "tracks, startIndex:", actualStartIndex);
|
||||||
|
|
||||||
// Build queue items with stream URLs
|
// Build queue items with stream URLs
|
||||||
// Add error handling and logging for each track
|
// Add error handling and logging for each track
|
||||||
const queueItems = await Promise.all(audioTracks.map(async (t, idx) => {
|
const queueItems = await Promise.all(audioTracks.map(async (t, idx) => {
|
||||||
try {
|
try {
|
||||||
console.log(`loadAndPlay: Fetching stream URL for track ${idx + 1}/${audioTracks.length}: ${t.name}`);
|
log.debug(`loadAndPlay: Fetching stream URL for track ${idx + 1}/${audioTracks.length}: ${t.name}`);
|
||||||
const trackStreamUrl = await repo.getAudioStreamUrl(t.id);
|
const trackStreamUrl = await repo.getAudioStreamUrl(t.id);
|
||||||
if (!trackStreamUrl) {
|
if (!trackStreamUrl) {
|
||||||
console.error(`loadAndPlay: Empty stream URL for track: ${t.name}`);
|
log.error(`loadAndPlay: Empty stream URL for track: ${t.name}`);
|
||||||
throw new Error(`Failed to get stream URL for ${t.name}`);
|
throw new Error(`Failed to get stream URL for ${t.name}`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -398,7 +403,7 @@
|
|||||||
jellyfinItemId: t.id,
|
jellyfinItemId: t.id,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`loadAndPlay: Failed to build queue item for track ${t.name}:`, e);
|
log.error(`loadAndPlay: Failed to build queue item for track ${t.name}:`, e);
|
||||||
throw e; // Re-throw to fail fast and show error to user
|
throw e; // Re-throw to fail fast and show error to user
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -411,10 +416,10 @@
|
|||||||
} as unknown as PlayQueueRequest);
|
} as unknown as PlayQueueRequest);
|
||||||
|
|
||||||
// Queue will auto-update from Rust backend event
|
// Queue will auto-update from Rust backend event
|
||||||
console.log("loadAndPlay: Successfully set up queue with", audioTracks.length, "tracks");
|
log.debug("loadAndPlay: Successfully set up queue with", audioTracks.length, "tracks");
|
||||||
} else {
|
} else {
|
||||||
// Fallback to single item playback
|
// Fallback to single item playback
|
||||||
console.log("loadAndPlay: No audio tracks found in parent, falling back to single item");
|
log.debug("loadAndPlay: No audio tracks found in parent, falling back to single item");
|
||||||
// Use player_play_tracks - backend fetches all metadata from single ID
|
// Use player_play_tracks - backend fetches all metadata from single ID
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
const repositoryHandle = repo.getHandle();
|
const repositoryHandle = repo.getHandle();
|
||||||
@@ -430,7 +435,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Queue will auto-update from Rust backend event
|
// Queue will auto-update from Rust backend event
|
||||||
console.log("loadAndPlay: Set queue with single item:", item.name);
|
log.debug("loadAndPlay: Set queue with single item:", item.name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No queue parameter - single item playback
|
// No queue parameter - single item playback
|
||||||
@@ -449,7 +454,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Queue will auto-update from Rust backend event
|
// Queue will auto-update from Rust backend event
|
||||||
console.log("loadAndPlay: Set queue with single item:", item.name);
|
log.debug("loadAndPlay: Set queue with single item:", item.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek to start position if provided
|
// Seek to start position if provided
|
||||||
@@ -463,14 +468,14 @@
|
|||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
// Fetch next episode for video episodes (for skip button)
|
// Fetch next episode for video episodes (for skip button)
|
||||||
console.log("[NextEpisode] Post-load check: isVideo=", isVideo, "currentMedia=", currentMedia?.kind, currentMedia?.name);
|
nextEpisodeLog.debug("Post-load check: isVideo=", isVideo, "currentMedia=", currentMedia?.kind, currentMedia?.name);
|
||||||
if (isVideo && currentMedia) {
|
if (isVideo && currentMedia) {
|
||||||
fetchNextEpisode(currentMedia);
|
fetchNextEpisode(currentMedia);
|
||||||
} else {
|
} else {
|
||||||
console.log("[NextEpisode] Skipped fetchNextEpisode - isVideo:", isVideo, "currentMedia:", !!currentMedia);
|
nextEpisodeLog.debug("Skipped fetchNextEpisode - isVideo:", isVideo, "currentMedia:", !!currentMedia);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("loadAndPlay error:", e);
|
log.error("loadAndPlay error:", e);
|
||||||
// Show detailed error including the full error object
|
// Show detailed error including the full error object
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
error = `${e.name}: ${e.message}`;
|
error = `${e.name}: ${e.message}`;
|
||||||
@@ -599,21 +604,21 @@
|
|||||||
// and check for next episodes. HTML5 video plays independently of the Rust
|
// and check for next episodes. HTML5 video plays independently of the Rust
|
||||||
// backend queue, so the backend needs these to know what just finished.
|
// backend queue, so the backend needs these to know what just finished.
|
||||||
const mediaId = currentMedia?.id ?? null;
|
const mediaId = currentMedia?.id ?? null;
|
||||||
console.log("[AutoPlay] Video ended. currentMedia:", mediaId, currentMedia?.name, "itemId (URL):", itemId);
|
autoPlayLog.debug("Video ended. currentMedia:", mediaId, currentMedia?.name, "itemId (URL):", itemId);
|
||||||
try {
|
try {
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
const repoHandle = repo.getHandle();
|
const repoHandle = repo.getHandle();
|
||||||
await commands.playerOnPlaybackEnded(mediaId, repoHandle);
|
await commands.playerOnPlaybackEnded(mediaId, repoHandle);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[AutoPlay] Failed to handle playback ended:", e);
|
autoPlayLog.error("Failed to handle playback ended:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchNextEpisode(media: MediaItem) {
|
async function fetchNextEpisode(media: MediaItem) {
|
||||||
nextEpisode = null;
|
nextEpisode = null;
|
||||||
console.log("[NextEpisode] fetchNextEpisode called:", { kind: media.kind, seriesId: media.seriesId, seasonId: media.seasonId, indexNumber: media.indexNumber, id: media.id, name: media.name });
|
nextEpisodeLog.debug("fetchNextEpisode called:", { kind: media.kind, seriesId: media.seriesId, seasonId: media.seasonId, indexNumber: media.indexNumber, id: media.id, name: media.name });
|
||||||
if (media.kind !== "episode" || !media.seasonId || media.indexNumber == null) {
|
if (media.kind !== "episode" || !media.seasonId || media.indexNumber == null) {
|
||||||
console.log("[NextEpisode] Skipping - not an episode or missing seasonId/indexNumber");
|
nextEpisodeLog.debug("Skipping - not an episode or missing seasonId/indexNumber");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -621,18 +626,18 @@
|
|||||||
// Fetch all episodes in the season sorted by episode number
|
// Fetch all episodes in the season sorted by episode number
|
||||||
const result = await repo.getItems(media.seasonId, { sortBy: "IndexNumber", sortOrder: "Ascending", limit: 500 });
|
const result = await repo.getItems(media.seasonId, { sortBy: "IndexNumber", sortOrder: "Ascending", limit: 500 });
|
||||||
const episodes = result.items.filter(e => e.kind === "episode");
|
const episodes = result.items.filter(e => e.kind === "episode");
|
||||||
console.log("[NextEpisode] Season has", episodes.length, "episodes, current index:", media.indexNumber);
|
nextEpisodeLog.debug("Season has", episodes.length, "episodes, current index:", media.indexNumber);
|
||||||
|
|
||||||
// Find the episode after the current one by index number
|
// Find the episode after the current one by index number
|
||||||
const currentIdx = episodes.findIndex(e => e.id === media.id);
|
const currentIdx = episodes.findIndex(e => e.id === media.id);
|
||||||
if (currentIdx >= 0 && currentIdx < episodes.length - 1) {
|
if (currentIdx >= 0 && currentIdx < episodes.length - 1) {
|
||||||
nextEpisode = episodes[currentIdx + 1];
|
nextEpisode = episodes[currentIdx + 1];
|
||||||
console.log("[NextEpisode] Set nextEpisode:", nextEpisode.name, "index:", nextEpisode.indexNumber);
|
nextEpisodeLog.debug("Set nextEpisode:", nextEpisode.name, "index:", nextEpisode.indexNumber);
|
||||||
} else {
|
} else {
|
||||||
console.log("[NextEpisode] No next episode in season (current position:", currentIdx, "of", episodes.length, ")");
|
nextEpisodeLog.debug("No next episode in season (current position:", currentIdx, "of", episodes.length, ")");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[NextEpisode] Failed to fetch next episode:", e);
|
nextEpisodeLog.error("Failed to fetch next episode:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@
|
|||||||
} from "$lib/services/networkType";
|
} from "$lib/services/networkType";
|
||||||
import { experimentalNativeVideo } from "$lib/stores/nativeVideo";
|
import { experimentalNativeVideo } from "$lib/stores/nativeVideo";
|
||||||
import { getPlaybackCapabilities } from "$lib/services/playbackCapabilities";
|
import { getPlaybackCapabilities } from "$lib/services/playbackCapabilities";
|
||||||
|
import { createLogger } from "$lib/utils/logger";
|
||||||
|
|
||||||
|
const log = createLogger("SettingsPage");
|
||||||
|
|
||||||
const episodeLimitOptions = [
|
const episodeLimitOptions = [
|
||||||
{ value: 0, label: "Unlimited" },
|
{ value: 0, label: "Unlimited" },
|
||||||
@@ -153,7 +156,7 @@
|
|||||||
// Load cache stats in parallel but don't block on it
|
// Load cache stats in parallel but don't block on it
|
||||||
loadCacheStats();
|
loadCacheStats();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load settings:", e);
|
log.error("Failed to load settings:", e);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -164,7 +167,7 @@
|
|||||||
cacheLoading = true;
|
cacheLoading = true;
|
||||||
cacheStats = await getCacheStats();
|
cacheStats = await getCacheStats();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load cache stats:", e);
|
log.error("Failed to load cache stats:", e);
|
||||||
} finally {
|
} finally {
|
||||||
cacheLoading = false;
|
cacheLoading = false;
|
||||||
}
|
}
|
||||||
@@ -176,7 +179,7 @@
|
|||||||
// Reload stats to reflect new limit
|
// Reload stats to reflect new limit
|
||||||
await loadCacheStats();
|
await loadCacheStats();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to set cache limit:", e);
|
log.error("Failed to set cache limit:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +189,7 @@
|
|||||||
await clearCache();
|
await clearCache();
|
||||||
await loadCacheStats();
|
await loadCacheStats();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to clear cache:", e);
|
log.error("Failed to clear cache:", e);
|
||||||
} finally {
|
} finally {
|
||||||
clearingCache = false;
|
clearingCache = false;
|
||||||
}
|
}
|
||||||
@@ -214,7 +217,7 @@
|
|||||||
try {
|
try {
|
||||||
await commands.playerSetAudioSettings(settings);
|
await commands.playerSetAudioSettings(settings);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to save audio settings:", e);
|
log.error("Failed to save audio settings:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +225,7 @@
|
|||||||
try {
|
try {
|
||||||
await commands.playerSetVideoSettings(videoSettings);
|
await commands.playerSetVideoSettings(videoSettings);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to save video settings:", e);
|
log.error("Failed to save video settings:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +237,7 @@
|
|||||||
// rather than at the next network change.
|
// rather than at the next network change.
|
||||||
await reportNetworkState();
|
await reportNetworkState();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to save download settings:", e);
|
log.error("Failed to save download settings:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user