refactor(logging): route frontend console calls through the logger

TRACES: | DR-204

484 ungated `console.*` calls across 63 non-test frontend files shipped to
end users with no way to turn them off. Mechanical substitution, no control
flow, error handling or message semantics changed:

  console.log / console.debug -> log.debug
  console.info                -> log.info
  console.warn                -> log.warn
  console.error               -> log.error

Hand-written `"[Scope] …"` prefixes are dropped where the logger's scope
now carries them; scope names that already existed are preserved verbatim
(`[Auth]`, `[VideoPlayer]`, `[PiP]`, …) and inferred from the filename
where a file had none. `src/routes/player/[id]/+page.svelte` keeps its
`NextEpisode` and `AutoPlay` sub-scopes as separate loggers rather than
flattening them into the page scope.

`grep -rn 'console\.' src/` now matches nothing outside the tests and the
facade itself.
This commit is contained in:
2026-08-20 19:29:59 +02:00
parent 4c82a0a025
commit d54d8cc7c4
63 changed files with 686 additions and 490 deletions
+4 -1
View File
@@ -4,6 +4,9 @@
import { haptics } from "$lib/utils/haptics";
import { toast } from "$lib/stores/toast";
import { favoriteOverrides } from "$lib/stores/favorites";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("FavoriteButton");
interface Props {
itemId: string;
@@ -78,7 +81,7 @@
isAnimating = false;
}, 600);
} catch (error) {
console.error("Failed to toggle favorite:", error);
log.error("Failed to toggle favorite:", error);
toast.show("Failed to update favorites", "error");
isAnimating = false;
} finally {
@@ -1,5 +1,8 @@
<script lang="ts">
import { downloads, type DownloadInfo } from "$lib/stores/downloads";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("DownloadItem");
interface Props {
download: DownloadInfo;
@@ -69,7 +72,7 @@
// Refresh to update UI
await downloads.refresh(download.userId);
} catch (error) {
console.error("Failed to pause download:", error);
log.error("Failed to pause download:", error);
}
}
@@ -79,7 +82,7 @@
// Refresh to update UI
await downloads.refresh(download.userId);
} catch (error) {
console.error("Failed to resume download:", error);
log.error("Failed to resume download:", error);
}
}
@@ -89,7 +92,7 @@
// Refresh to update UI
await downloads.refresh(download.userId);
} catch (error) {
console.error("Failed to cancel download:", error);
log.error("Failed to cancel download:", error);
}
}
@@ -99,7 +102,7 @@
// Refresh to update UI
await downloads.refresh(download.userId);
} catch (error) {
console.error("Failed to delete download:", error);
log.error("Failed to delete download:", error);
}
}
</script>
@@ -2,6 +2,9 @@
import { downloads } from "$lib/stores/downloads";
import { auth } from "$lib/stores/auth";
import type { MediaItem } from "$lib/api/types";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("AlbumDownloadButton");
interface Props {
albumId: string;
@@ -60,7 +63,7 @@
try {
const userId = $auth.user?.id;
if (!userId) {
console.error("No user ID found");
log.error("No user ID found");
return;
}
@@ -95,7 +98,7 @@
await downloads.refresh(userId);
}
} catch (error) {
console.error("Album download operation failed:", error);
log.error("Album download operation failed:", error);
} finally {
isProcessing = false;
}
@@ -8,6 +8,9 @@
import CachedImage from "$lib/components/common/CachedImage.svelte";
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
import { favoriteOverrides, resolveIsFavorite } from "$lib/stores/favorites";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("ArtistDetailView");
interface Props {
artist: MediaItem;
@@ -47,7 +50,7 @@
});
albums = albumsResult.items.filter(item => item.kind === "album");
} catch (e) {
console.warn("Failed to load albums:", e);
log.warn("Failed to load albums:", e);
} finally {
albumsLoading = false;
}
@@ -62,7 +65,7 @@
});
topTracks = tracksResult.items.filter(item => item.kind === "track");
} catch (e) {
console.warn("Failed to load tracks:", e);
log.warn("Failed to load tracks:", e);
} finally {
tracksLoading = false;
}
@@ -82,14 +85,14 @@
.slice(0, 6);
}
} catch (e) {
console.warn("Failed to load related artists:", e);
log.warn("Failed to load related artists:", e);
} finally {
artistsLoading = false;
}
singlesLoading = false;
} catch (e) {
console.error("Error loading artist content:", e);
log.error("Error loading artist content:", e);
}
}
</script>
@@ -11,6 +11,9 @@
<script lang="ts">
import { auth } from "$lib/stores/auth";
import { isServerReachable } from "$lib/stores/connectivity";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("ClearHistoryButton");
interface Props {
/** Series or season id to clear. */
@@ -51,7 +54,7 @@
await auth.getRepository().clearWatchHistory(itemId);
onCleared?.();
} catch (e) {
console.error("Failed to clear watch history:", e);
log.error("Failed to clear watch history:", e);
alert(
`Could not clear watch history: ${e instanceof Error ? e.message : String(e)}`
);
@@ -4,6 +4,9 @@
import { commands } from "$lib/api/bindings";
import DownloadButtonCore from "./DownloadButtonCore.svelte";
import type { DownloadState } from "./DownloadButtonCore.svelte";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("DownloadButton");
/**
* Single audio track download button
@@ -39,7 +42,7 @@
});
async function handleClick() {
console.log("🖱️ Download button clicked! Current status:", status);
log.debug("🖱️ Download button clicked! Current status:", status);
if (isProcessing) return;
isProcessing = true;
@@ -63,25 +66,25 @@
// Start download
const userId = $auth.user?.id;
if (!userId) {
console.error("No user ID found");
log.error("No user ID found");
return;
}
try {
const repo = auth.getRepository();
console.log("🎯 Starting download for item:", itemId);
log.debug("🎯 Starting download for item:", itemId);
// Get stream URL
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) {
throw new Error("Failed to get stream URL");
}
// Get target directory
const targetDir = await commands.storageGetPath();
console.log(" Target directory:", targetDir);
log.debug(" Target directory:", targetDir);
// Queue and start download in single atomic operation
const downloadId = await commands.downloadItemAndStart({
@@ -93,16 +96,16 @@
artistName: artistName || 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
await downloads.refresh(userId);
} catch (e) {
console.error("❌ Failed to start download:", e);
log.error("❌ Failed to start download:", e);
}
}
} catch (error) {
console.error("Download operation failed:", error);
log.error("Download operation failed:", error);
} finally {
isProcessing = false;
}
@@ -13,6 +13,9 @@
import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload";
import { useOfflineFilterReload } from "$lib/composables/useOfflineFilterReload";
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
@@ -92,7 +95,7 @@
genres = result.sort((a, b) => a.name.localeCompare(b.name));
applyFilter();
} catch (e) {
console.error("Failed to load genres:", e);
log.error("Failed to load genres:", e);
} finally {
loading = false;
}
@@ -115,7 +118,7 @@
});
genreItems = result.items;
} catch (e) {
console.error("Failed to load genre items:", e);
log.error("Failed to load genre items:", e);
} finally {
loadingItems = false;
}
@@ -20,6 +20,9 @@
import TrackList from "./TrackList.svelte";
import AlphabetScrollBar from "./AlphabetScrollBar.svelte";
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
@@ -154,7 +157,7 @@
items = excludePodcasts(result.items);
}
} catch (e) {
console.error(`Failed to load ${config.itemType}:`, e);
log.error(`Failed to load ${config.itemType}:`, e);
} finally {
loading = false;
}
+4 -1
View File
@@ -9,6 +9,9 @@
import CachedImage from "$lib/components/common/CachedImage.svelte";
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
import { favoriteOverrides, resolveIsFavorite } from "$lib/stores/favorites";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("MediaCard");
interface Props {
item: MediaItem | Library;
@@ -171,7 +174,7 @@
media.albumName ?? undefined
);
} catch (err) {
console.error("[MediaCard] Failed to queue download:", err);
log.error("Failed to queue download:", err);
queueError = "Failed to queue";
}
}
@@ -6,6 +6,9 @@
import LibraryGrid from "./LibraryGrid.svelte";
import CachedImage from "$lib/components/common/CachedImage.svelte";
import { goto } from "$app/navigation";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("PersonDetailView");
interface Props {
person: MediaItem;
@@ -34,7 +37,7 @@
movies = result.items.filter(item => item.kind === "movie");
series = result.items.filter(item => item.kind === "series");
} catch (e) {
console.error("Failed to load filmography:", e);
log.error("Failed to load filmography:", e);
} finally {
loading = false;
}
@@ -10,6 +10,9 @@
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
import { favoriteOverrides } from "$lib/stores/favorites";
import { formatDuration } from "$lib/utils/duration";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("PlaylistDetail");
interface Props {
playlist: MediaItem;
@@ -40,7 +43,7 @@
const repo = auth.getRepository();
entries = await repo.getPlaylistItems(playlist.id);
} catch (e) {
console.error("[PlaylistDetail] Failed to load items:", e);
log.error("Failed to load items:", e);
toast.error("Failed to load playlist items");
} finally {
loading = false;
@@ -62,7 +65,7 @@
},
});
} catch (e) {
console.error("[PlaylistDetail] Failed to play all:", e);
log.error("Failed to play all:", e);
toast.error("Failed to play playlist");
}
}
@@ -82,7 +85,7 @@
},
});
} catch (e) {
console.error("[PlaylistDetail] Failed to shuffle play:", e);
log.error("Failed to shuffle play:", e);
toast.error("Failed to shuffle playlist");
}
}
@@ -100,7 +103,7 @@
playlist.name = trimmed;
toast.success("Playlist renamed");
} catch (e) {
console.error("[PlaylistDetail] Failed to rename:", e);
log.error("Failed to rename:", e);
toast.error("Failed to rename playlist");
editName = playlist.name;
} finally {
@@ -115,7 +118,7 @@
toast.success("Playlist deleted");
goto("/library");
} catch (e) {
console.error("[PlaylistDetail] Failed to delete:", e);
log.error("Failed to delete:", e);
toast.error("Failed to delete playlist");
} finally {
showDeleteConfirm = false;
@@ -129,7 +132,7 @@
entries = entries.filter(e => e.playlistItemId !== entry.playlistItemId);
toast.success("Track removed");
} catch (e) {
console.error("[PlaylistDetail] Failed to remove track:", e);
log.error("Failed to remove track:", e);
toast.error("Failed to remove track");
}
}
@@ -4,6 +4,9 @@
import { auth } from "$lib/stores/auth";
import type { MediaItem, MediaKind, Person } from "$lib/api/types";
import MediaCard from "./MediaCard.svelte";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("RelatedItemsSection");
interface Props {
currentItemId: string;
@@ -57,7 +60,7 @@
return; // Success - return early
}
} 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
}
}
@@ -78,7 +81,7 @@
items = result.items.filter(item => item.id !== currentItemId);
} 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);
items = [...items, ...artistAlbums];
} 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;
} catch (e) {
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 {
loading = false;
}
@@ -3,6 +3,9 @@
import { auth } from "$lib/stores/auth";
import { commands } from "$lib/api/bindings";
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("SeasonDownloadButton");
interface Props {
seasonId: string;
@@ -46,11 +49,11 @@
try {
const userId = $auth.user?.id;
if (!userId) {
console.error("No user ID found");
log.error("No user ID found");
return;
}
console.log("📺 Starting season download for:", seasonName, "quality:", quality);
log.debug("📺 Starting season download for:", seasonName, "quality:", quality);
// Get target directory
const targetDir = await commands.storageGetPath();
@@ -67,7 +70,7 @@
quality
);
console.log(`✅ Queued ${downloadIds.length} episodes for download`);
log.debug(`✅ Queued ${downloadIds.length} episodes for download`);
// Pin the season item
await downloads.pinItem(seasonId);
@@ -77,9 +80,9 @@
// rest as slots free up.
const handle = auth.getRepository().getHandle();
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) {
console.error("Failed to start season download:", error);
log.error("Failed to start season download:", error);
} finally {
isProcessing = false;
}
@@ -3,6 +3,9 @@
import { auth } from "$lib/stores/auth";
import { commands } from "$lib/api/bindings";
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("SeriesDownloadButton");
interface Props {
seriesId: string;
@@ -40,11 +43,11 @@
try {
const userId = $auth.user?.id;
if (!userId) {
console.error("No user ID found");
log.error("No user ID found");
return;
}
console.log("📺 Starting series download for:", seriesName, "quality:", quality);
log.debug("📺 Starting series download for:", seriesName, "quality:", quality);
// Get target directory
const targetDir = await commands.storageGetPath();
@@ -59,7 +62,7 @@
quality
);
console.log(` Queued ${downloadIds.length} episodes for download`);
log.debug(` Queued ${downloadIds.length} episodes for download`);
// Pin the series item
await downloads.pinItem(seriesId);
@@ -69,9 +72,9 @@
// rest as slots free up.
const handle = auth.getRepository().getHandle();
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) {
console.error("Failed to start series download:", error);
log.error("Failed to start series download:", error);
} finally {
isProcessing = false;
}
+7 -4
View File
@@ -12,6 +12,9 @@
import AddToPlaylistModal from "$lib/components/playlist/AddToPlaylistModal.svelte";
import { calculateMenuPosition, type MenuPosition } from "$lib/utils/menuPosition";
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? */
export type QueueContext =
@@ -55,7 +58,7 @@
// If this is an album, use the backend album command (more efficient)
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({
albumId: context.albumId,
albumName: context.albumName,
@@ -91,7 +94,7 @@
// Queue will auto-update from Rust backend event
} catch (e) {
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);
} finally {
isPlayingTrack = null;
@@ -145,9 +148,9 @@
try {
// Queue store now handles everything in Rust - just pass the track
await queue.addToQueue(track, position);
console.log(`Added "${track.name}" to queue (${position})`);
log.debug(`Added "${track.name}" to queue (${position})`);
} catch (e) {
console.error("Failed to add to queue:", e);
log.error("Failed to add to queue:", e);
}
}
</script>
@@ -3,6 +3,9 @@
import { auth } from "$lib/stores/auth";
import { commands } from "$lib/api/bindings";
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("VideoDownloadButton");
interface Props {
itemId: string;
@@ -57,17 +60,17 @@
try {
const userId = $auth.user?.id;
if (!userId) {
console.error("No user ID found");
log.error("No user ID found");
return;
}
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
const streamUrl = await repo.getVideoDownloadUrl(itemId, quality);
console.log(" Stream URL obtained");
log.debug(" Stream URL obtained");
// Get target directory
const targetDir = await commands.storageGetPath();
@@ -85,7 +88,7 @@
filePath = `videos/${safeName}.mp4`;
}
console.log(" File path:", filePath);
log.debug(" File path:", filePath);
// Queue download with video metadata
const downloadId = await downloads.downloadVideo(
@@ -101,16 +104,16 @@
episodeNumber,
seasonNumber
);
console.log(" Download queued with ID:", downloadId);
log.debug(" Download queued with ID:", downloadId);
// Pin the item metadata
await downloads.pinItem(itemId);
// Actually start the download
await commands.startDownload(downloadId, streamUrl, targetDir);
console.log(" Download started");
log.debug(" Download started");
} catch (error) {
console.error("Failed to start video download:", error);
log.error("Failed to start video download:", error);
} finally {
isProcessing = false;
}
@@ -14,6 +14,9 @@
-->
<script lang="ts">
import { syncService } from "$lib/services/syncService";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("WatchedToggleButton");
interface Props {
/** Episode, season or series id. */
@@ -81,7 +84,7 @@
} catch (e) {
// Put the button back where it was — the change did not happen.
optimistic = null;
console.error("Failed to change watched state:", e);
log.error("Failed to change watched state:", e);
} finally {
busy = false;
}
+4 -1
View File
@@ -22,6 +22,9 @@
import VolumeControl from "./VolumeControl.svelte";
import CachedImage from "../common/CachedImage.svelte";
import { currentQueueItem } from "$lib/stores/queue";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("AudioPlayer");
interface Props {
media: MediaItem | null;
@@ -130,7 +133,7 @@
queue.skipTo(index);
await playerController.skipTo(index);
} catch (e) {
console.error("Failed to skip to queue item:", e);
log.error("Failed to skip to queue item:", e);
}
}
</script>
+5 -2
View File
@@ -38,6 +38,9 @@
import CastButton from "$lib/components/sessions/CastButton.svelte";
import VolumeControl from "./VolumeControl.svelte";
import CachedImage from "../common/CachedImage.svelte";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("MiniPlayer");
interface Props {
media: MediaItem | null;
@@ -159,7 +162,7 @@
await playerController.seek(newPosition);
haptics.tap();
} catch (err) {
console.error("Failed to seek:", err);
log.error("Failed to seek:", err);
toast.show("Failed to seek", "error");
}
}
@@ -230,7 +233,7 @@
// Vertical swipe
if (Math.abs(diffY) > swipeThreshold && diffY > 0) {
// Swiped up - Open full player
console.log("[MiniPlayer] Swipe-up detected, expanding player");
log.debug("Swipe-up detected, expanding player");
haptics.tap();
onExpand?.();
}
+5 -2
View File
@@ -6,6 +6,9 @@
import { auth } from "$lib/stores/auth";
import { queue } from "$lib/stores/queue";
import CachedImage from "../common/CachedImage.svelte";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("QueueView");
interface Props {
items: MediaItem[];
@@ -82,7 +85,7 @@
// Sync with backend
await playerController.moveInQueue(fromIndex, toIndex);
} 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
}
}
@@ -109,7 +112,7 @@
queue.removeFromQueue(index);
await playerController.removeFromQueue(index);
} catch (err) {
console.error("Failed to remove from queue:", err);
log.error("Failed to remove from queue:", err);
}
}
</script>
+107 -104
View File
@@ -75,6 +75,9 @@
planHandoffReturn,
type BackgroundAudioState,
} from "./backgroundAudioHandoff";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("VideoPlayer");
interface Props {
media: MediaItem | null;
@@ -287,11 +290,11 @@
// TRACES: UR-021 | IR-016, JA-009 | DR-024
const audioTracks = $derived(() => {
if (!media || !media.mediaStreams) {
console.log("[VideoPlayer] No media or mediaStreams available");
log.debug("No media or mediaStreams available");
return [];
}
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;
});
@@ -304,7 +307,7 @@
if (preference.audioTrackDisplayTitle) {
const match = tracks.find(t => t.displayTitle === preference.audioTrackDisplayTitle);
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;
}
}
@@ -313,14 +316,14 @@
if (preference.audioTrackLanguage) {
const match = tracks.find(t => t.language === preference.audioTrackLanguage);
if (match) {
console.log("[VideoPlayer] Matched audio track by language:", match.language);
log.debug("Matched audio track by language:", match.language);
return match.index;
}
}
// Fall back to default track
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;
}
@@ -335,15 +338,15 @@
const preference = await commands.storageGetSeriesAudioPreference(userId, media.seriesId);
if (preference) {
console.log("[VideoPlayer] Loaded series audio preference:", preference);
log.debug("Loaded series audio preference:", preference);
const matchedIndex = findBestAudioTrack(preference);
if (matchedIndex !== null) {
selectedAudioTrackIndex = matchedIndex;
console.log("[VideoPlayer] Applied series audio preference, track index:", matchedIndex);
log.debug("Applied series audio preference, track index:", matchedIndex);
}
}
} 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
const subtitleTracks = $derived(() => {
if (!media || !media.mediaStreams) {
console.log("[VideoPlayer] No media or mediaStreams available for subtitles");
log.debug("No media or mediaStreams available for subtitles");
return [];
}
const tracks = subtitleStreamsOf(media.mediaStreams);
console.log("[VideoPlayer] Found subtitle tracks:", tracks.length, tracks);
log.debug("Found subtitle tracks:", tracks.length, tracks);
return tracks;
});
@@ -547,7 +550,7 @@
if (isHlsStream && Hls.isSupported()) {
// Clean up existing HLS instance if any - CRITICAL for preventing dual audio
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
hls.detachMedia();
// Stop loading and flush buffers
@@ -571,7 +574,7 @@
setTimeout(() => {
if (!videoElement) return;
console.log('[VideoPlayer] Creating new HLS instance for:', currentStreamUrl);
log.debug('Creating new HLS instance for:', currentStreamUrl);
// Create new HLS instance
hls = new Hls({
@@ -599,14 +602,14 @@
// Listen for media attached event
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
hls!.loadSource(currentStreamUrl);
});
// Listen for manifest parsed event
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
@@ -623,7 +626,7 @@
if (canplayFallbackTimeout) clearTimeout(canplayFallbackTimeout);
canplayFallbackTimeout = setTimeout(() => {
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();
}
}, 5000);
@@ -633,7 +636,7 @@
// Handle errors
hls.on(Hls.Events.ERROR, (event, data) => {
console.error('[VideoPlayer] HLS error:', data);
log.error('HLS error:', data);
if (data.fatal) {
// Is this the stream ending or the stream breaking? Jellyfin's
// transcoded HLS doesn't always emit #EXT-X-ENDLIST, so both arrive
@@ -650,25 +653,25 @@
attempts: hlsFatalRecoveryAttempts,
})) {
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();
break;
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();
break;
case 'giveUp':
console.error('[VideoPlayer] Fatal network error, max recovery attempts reached');
log.error('Fatal network error, max recovery attempts reached');
hls!.destroy();
break;
}
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.error('[VideoPlayer] Fatal media error, trying to recover');
log.error('Fatal media error, trying to recover');
hls!.recoverMediaError();
break;
default:
console.error('[VideoPlayer] Unrecoverable HLS error');
log.error('Unrecoverable HLS error');
hls!.destroy();
break;
}
@@ -678,7 +681,7 @@
// Cleanup on effect re-run
return () => {
console.log('[VideoPlayer] Effect cleanup: destroying HLS instance');
log.debug('Effect cleanup: destroying HLS instance');
if (hls) {
hls.detachMedia();
hls.stopLoad();
@@ -691,11 +694,11 @@
};
} else if (isHlsStream && videoElement.canPlayType('application/vnd.apple.mpegurl')) {
// Native HLS support (Safari)
console.log('[VideoPlayer] Using native HLS support');
log.debug('Using native HLS support');
videoElement.src = currentStreamUrl;
} else {
// 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) {
videoElement.muted = false;
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
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)
if (selectedAudioTrackIndex === null && audioTracks().length > 0) {
const defaultTrack = audioTracks().find(t => t.isDefault);
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) {
console.log("[VideoPlayer] mozHasAudio:", (videoElement as any).mozHasAudio);
log.debug("mozHasAudio:", (videoElement as any).mozHasAudio);
}
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;
}
untrack(() => {
console.log("[VideoPlayer] Initial position changed, seeking to:", pos);
log.debug("Initial position changed, seeking to:", pos);
lastAppliedInitialPosition = pos;
if (videoElement) {
videoElement.currentTime = pos;
@@ -775,7 +778,7 @@
selectedQuality = settings.streamingQuality ?? "original";
})
.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
if (media && currentStreamUrl) {
try {
console.log("[VideoPlayer] Initializing player for:", media.name);
console.log("[VideoPlayer] Stream URL:", currentStreamUrl);
log.debug("Initializing player for:", media.name);
log.debug("Stream URL:", currentStreamUrl);
// Resolve subtitle URLs for the native (ExoPlayer) path. These must be
// in hand *before* the play request: ExoPlayer sideloads subtitles as
@@ -827,7 +830,7 @@
sentSubtitleTracks = mediaSourceId
? 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
// 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
useHtml5Element = response.useHtml5Element;
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
// 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
// same stream and the audio doubles.
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;
try {
await commands.playerStop();
didStopBackendEarly = true;
} catch (err) {
console.warn("[VideoPlayer] Failed to stop native backend:", err);
log.warn("Failed to stop native backend:", err);
}
} else if (!useHtml5Element) {
// Native path: clear the opaque layers between the viewport and the
@@ -872,7 +875,7 @@
// Paired with disableNativeVideoCompositing() in the teardown path —
// leaving this on renders the rest of the app over a transparent
// window.
console.log("[VideoPlayer] Using native ExoPlayer video surface");
log.debug("Using native ExoPlayer video surface");
enableNativeVideoCompositing();
}
@@ -880,14 +883,14 @@
// For transcoded content, we need to keep the backend running to handle seeking/audio track switching
if (useHtml5Element && !needsTranscoding && !didStopBackendEarly) {
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();
didStopBackendEarly = true; // Track that we stopped the backend
} catch (err) {
console.warn("[VideoPlayer] Failed to stop backend player:", err);
log.warn("Failed to stop backend player:", err);
}
} 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
didStartNativePlayback = true; // Track that we need to stop backend on unmount
}
@@ -972,12 +975,12 @@
);
}
} catch (err) {
console.error("[VideoPlayer] Failed to initialize player:", err);
log.error("Failed to initialize player:", err);
if (backendChosen) {
// The backend already accepted the item; a later error (e.g. event
// subscription) must not silently switch the seek/controls path to
// 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 {
// Fallback to HTML5 on error
useHtml5Element = true;
@@ -1042,8 +1045,8 @@
// Flattened to a single string on purpose: the Android WebView console
// bridge stringifies objects as "[object Object]" in logcat, which made
// this whole payload useless when diagnosing over adb.
console.log(
`[VideoPlayer Debug] t=${videoElement.currentTime.toFixed(2)}` +
log.debug(
`Debug t=${videoElement.currentTime.toFixed(2)}` +
` display=${currentTime.toFixed(2)}` +
` readyState=${videoElement.readyState}` +
` networkState=${videoElement.networkState}` +
@@ -1111,7 +1114,7 @@
// Clean up HLS.js instance - prevent dual audio on unmount
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.stopLoad(); // Stop loading and flush buffers
hls.destroy();
@@ -1129,10 +1132,10 @@
// Skip if we already stopped the backend early (non-transcoded + HTML5)
if (didStartNativePlayback && !didStopBackendEarly) {
try {
console.log("[VideoPlayer] Stopping backend player on component unmount");
log.debug("Stopping backend player on component unmount");
await commands.playerStop();
} 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() {
console.log("[VideoPlayer] loadedmetadata event");
log.debug("loadedmetadata event");
// 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)
reportPipVideoState();
console.log("[VideoPlayer] Video element duration:", videoElement?.duration);
console.log("[VideoPlayer] Media item runTimeTicks:", media?.runTimeTicks);
console.log("[VideoPlayer] Needs transcoding:", needsTranscoding);
log.debug("Video element duration:", videoElement?.duration);
log.debug("Media item runTimeTicks:", media?.runTimeTicks);
log.debug("Needs transcoding:", needsTranscoding);
// For direct streams without runTimeTicks, use video element's duration
if (videoElement && videoElement.duration && !isNaN(videoElement.duration) && videoElement.duration !== Infinity) {
const newDuration = videoElement.duration;
console.log("[VideoPlayer] Setting videoDuration to:", newDuration);
log.debug("Setting videoDuration to:", 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
@@ -1209,8 +1212,8 @@
// Use setTimeout to log the derived value after reactive updates
setTimeout(() => {
console.log("[VideoPlayer] Derived duration value:", duration);
console.log("[VideoPlayer] Duration source:", media?.runTimeTicks ? "runTimeTicks" : "video element");
log.debug("Derived duration value:", duration);
log.debug("Duration source:", media?.runTimeTicks ? "runTimeTicks" : "video element");
}, 0);
}
@@ -1251,15 +1254,15 @@
el.volume = 1.0;
if (shouldPlay) await el.play();
} 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 */) {
console.log("[VideoPlayer] Applying foreground seek to:", (seekOffset + seekTo).toFixed(1));
log.debug("Applying foreground seek to:", (seekOffset + seekTo).toFixed(1));
await doSeek();
} 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 });
}
return true;
@@ -1267,7 +1270,7 @@
function markMediaReady() {
if (isMediaReady) return;
console.log("[VideoPlayer] Marking media ready");
log.debug("Marking media ready");
isMediaReady = true;
// A handoff return can be revealed here (not via canplay) — apply its seek.
void applyPendingForegroundSeek();
@@ -1275,14 +1278,14 @@
async function handleCanPlay() {
// 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;
// Ensure video is unmuted and at max volume (critical for Android)
if (videoElement) {
videoElement.muted = false;
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
@@ -1294,7 +1297,7 @@
// Seek to initial position if resuming playback
if (initialPosition && initialPosition > 0 && !hasPerformedInitialSeek && videoElement) {
console.log("[VideoPlayer] Seeking to initial position:", initialPosition);
log.debug("Seeking to initial position:", initialPosition);
hasPerformedInitialSeek = true;
lastAppliedInitialPosition = initialPosition; // mark this value as applied so the change-effect ignores it
@@ -1325,7 +1328,7 @@
await videoElement.play();
}
} 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;
// Log comprehensive error details
console.error("[VideoPlayer] Video error event:", {
log.error("Video error event:", {
code: error?.code,
message: error?.message,
networkState: video.networkState,
@@ -1354,28 +1357,28 @@
const errorCode = error?.code || 0;
const msg = errorMessages[errorCode] || `Unknown error (code ${errorCode})`;
console.error("[VideoPlayer] Error interpretation:", msg);
log.error("Error interpretation:", msg);
// Log additional debugging info
console.error("[VideoPlayer] Stream URL:", currentStreamUrl);
console.error("[VideoPlayer] Needs transcoding:", needsTranscoding);
log.error("Stream URL:", currentStreamUrl);
log.error("Needs transcoding:", needsTranscoding);
// Network state meanings: 0=EMPTY, 1=IDLE, 2=LOADING, 3=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
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() {
console.log("[VideoPlayer] waiting event - buffering");
log.debug("waiting event - buffering");
isBuffering = true;
}
function handlePlaying() {
console.log("[VideoPlayer] playing event - playback resumed");
log.debug("playing event - playback resumed");
isBuffering = false;
// Safety net: if we reached `playing` we are definitely renderable, even if
// `canplay`/hls FRAG_BUFFERED were missed on this WebView.
@@ -1383,9 +1386,9 @@
}
function handleLoadStart() {
console.log("[VideoPlayer] loadstart event - starting to load:", currentStreamUrl);
console.log("[VideoPlayer] Video element readyState:", videoElement?.readyState);
console.log("[VideoPlayer] Video element networkState:", videoElement?.networkState);
log.debug("loadstart event - starting to load:", currentStreamUrl);
log.debug("Video element readyState:", videoElement?.readyState);
log.debug("Video element networkState:", videoElement?.networkState);
// Clear any existing fallback timeout
if (canplayFallbackTimeout) {
@@ -1395,12 +1398,12 @@
// Set up a fallback timeout in case canplay event never fires
canplayFallbackTimeout = setTimeout(() => {
if (!isMediaReady && videoElement) {
console.warn("[VideoPlayer] canplay event did not fire within 5 seconds");
console.log("[VideoPlayer] Fallback check - readyState:", videoElement.readyState, "networkState:", videoElement.networkState);
log.warn("canplay event did not fire within 5 seconds");
log.debug("Fallback check - readyState:", videoElement.readyState, "networkState:", videoElement.networkState);
// Check if video is actually ready despite event not firing
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();
}
}
@@ -1430,7 +1433,7 @@
jrayActors = actors;
}
} catch (err) {
console.warn("[VideoPlayer] JRay lookup failed:", err);
log.warn("JRay lookup failed:", err);
if (token === jrayRequestId) jrayActors = [];
}
}
@@ -1508,8 +1511,8 @@
// reason. Log the element state so an unexplained pause/resume loop can be
// attributed from an adb capture instead of guessed at.
const el = videoElement;
console.log(
`[VideoPlayer] pause event — t=${el ? el.currentTime.toFixed(2) : "?"}` +
log.debug(
`pause event — t=${el ? el.currentTime.toFixed(2) : "?"}` +
` readyState=${el?.readyState}` +
` networkState=${el?.networkState}` +
` seeking=${el?.seeking}` +
@@ -1553,7 +1556,7 @@
try {
await playerController.toggle();
} catch (err) {
console.error("[VideoPlayer] Failed to toggle playback:", err);
log.error("Failed to toggle playback:", err);
}
}
@@ -1595,7 +1598,7 @@
isDraggingSeekBar = false;
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
// completes (reloadSource drives the stream URL via the adapter bridge).
@@ -1617,9 +1620,9 @@
startTimeUpdates();
}
console.log("[VideoPlayer] Seek completed at:", currentTime.toFixed(2), "offset:", seekOffset);
log.debug("Seek completed at:", currentTime.toFixed(2), "offset:", seekOffset);
} catch (err) {
console.error("[VideoPlayer] Seek failed:", err);
log.error("Seek failed:", err);
} finally {
isSeeking = false;
isDraggingSeekBar = false;
@@ -1654,12 +1657,12 @@
function toggleBackgroundAudio() {
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,
// so exactly one background behavior is active.
const armed = setBackgroundAudioEnabled(backgroundAudioOn);
if (!armed) {
console.warn("[VideoPlayer] Background audio NOT armed natively (no bridge)");
log.warn("Background audio NOT armed natively (no bridge)");
}
setAutoEnterEnabled(!backgroundAudioOn);
}
@@ -1675,7 +1678,7 @@
// if the element is mid-teardown — which shipped audio starting from 0:00.
const pos = computeHandoffPosition(currentTime, 0);
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 };
try {
if (!media) return;
@@ -1716,7 +1719,7 @@
videoElement.load();
}
} catch (err) {
console.error("[VideoPlayer] Background-audio handoff failed:", err);
log.error("Background-audio handoff failed:", err);
handoffState = { ...initialHandoffState };
}
}
@@ -1734,7 +1737,7 @@
try {
// Absolute position the native audio reached (base offset applied in Rust).
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;
// The foreground seek below (pendingForegroundSeek/handleCanPlay) OWNS the
@@ -1837,7 +1840,7 @@
await Promise.resolve();
currentStreamUrl = targetUrl;
} 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;
// the immersive call below is what matters on Android, so don't let a
// rejection here abort it.
console.warn("[VideoPlayer] requestFullscreen rejected:", err);
log.warn("requestFullscreen rejected:", err);
});
enterImmersive();
isFullscreen = true;
@@ -1906,7 +1909,7 @@
});
pendingSeekTarget = newTime;
console.log("[VideoPlayer] Relative seek:", {
log.debug("Relative seek:", {
offset: `${seconds > 0 ? "+" : ""}${seconds}s`,
from: currentTime.toFixed(2),
to: newTime.toFixed(2),
@@ -2092,7 +2095,7 @@
}
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;
selectedAudioTrackIndex = streamIndex;
showAudioTrackMenu = false;
@@ -2113,7 +2116,7 @@
startTimeUpdates();
}
console.log("[VideoPlayer] Successfully changed audio track");
log.debug("Successfully changed audio track");
// Save series audio preference for future episodes
if (media && media.seriesId) {
@@ -2132,14 +2135,14 @@
selectedTrack.language || null,
streamIndex
);
console.log("[VideoPlayer] Saved series audio preference:", selectedTrack.displayTitle || selectedTrack.language);
log.debug("Saved series audio preference:", selectedTrack.displayTitle || selectedTrack.language);
}
} catch (err) {
console.warn("[VideoPlayer] Failed to save series audio preference:", err);
log.warn("Failed to save series audio preference:", 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
selectedAudioTrackIndex = previousTrackIndex;
}
@@ -2177,9 +2180,9 @@
if (videoElement && !videoElement.paused) {
startTimeUpdates();
}
console.log("[VideoPlayer] Streaming quality changed:", quality);
log.debug("Streaming quality changed:", quality);
} catch (err) {
console.error("[VideoPlayer] Failed to change streaming quality:", err);
log.error("Failed to change streaming quality:", err);
selectedQuality = previous;
} finally {
changingQuality = false;
@@ -2210,7 +2213,7 @@
const trackStreamIndex = parseInt(track.getAttribute("data-stream-index") || "-1");
if (trackStreamIndex === streamIndex && track.track) {
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
*/
async function selectSubtitle(streamIndex: number | null) {
console.log("[VideoPlayer] Selecting subtitle - streamIndex:", streamIndex);
log.debug("Selecting subtitle - streamIndex:", streamIndex);
selectedSubtitleIndex = streamIndex;
showSubtitleMenu = false;
@@ -2242,9 +2245,9 @@
try {
const indexToUse = nativeSubtitleArrayIndex(sentSubtitleTracks, streamIndex);
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) {
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 CreatePlaylistModal from "./CreatePlaylistModal.svelte";
import CachedImage from "$lib/components/common/CachedImage.svelte";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("AddToPlaylist");
interface Props {
isOpen?: boolean;
@@ -39,7 +42,7 @@
playlists = result.items;
}
} catch (e) {
console.error("[AddToPlaylist] Failed to load playlists:", e);
log.error("Failed to load playlists:", e);
toast.error("Failed to load playlists");
} finally {
loading = false;
@@ -54,7 +57,7 @@
toast.success(`Added to "${playlist.name}"`);
onClose?.();
} catch (e) {
console.error("[AddToPlaylist] Failed to add:", e);
log.error("Failed to add:", e);
toast.error("Failed to add to playlist");
} finally {
adding = null;
@@ -2,6 +2,9 @@
import { goto } from "$app/navigation";
import { auth } from "$lib/stores/auth";
import { toast } from "$lib/stores/toast";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("CreatePlaylist");
interface Props {
isOpen?: boolean;
@@ -27,7 +30,7 @@
onClose?.();
goto(`/library/${result.id}`);
} catch (e) {
console.error("[CreatePlaylist] Failed:", e);
log.error("Failed:", e);
toast.error("Failed to create playlist");
} finally {
creating = false;
@@ -6,6 +6,9 @@
import { playbackPosition } from "$lib/stores/player";
import { lmsSync, isLmsSession, macForSession } from "$lib/stores/lmsSync";
import type { Session } from "$lib/api/types";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("SessionPicker");
interface Props {
isOpen?: boolean;
@@ -41,7 +44,7 @@
onClose();
}
} catch (error) {
console.error("Failed to select session:", error);
log.error("Failed to select session:", error);
// Error is already stored in playbackMode store
}
}
@@ -67,7 +70,7 @@
await lmsSync.fuseZone(masterMac, zoneMac);
}
} 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
}
}
@@ -79,7 +82,7 @@
onClose();
}
} 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
}
}
@@ -91,7 +94,7 @@
onClose();
}
} catch (error) {
console.error("Failed to disconnect:", error);
log.error("Failed to disconnect:", error);
// Error is already stored in playbackMode store
}
}