### Example `curl` Command to Get Intro Timestamps Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/api.md Demonstrates how to make a `GET` request to the IntroTimestamps API endpoint using the `curl` command-line tool. This example includes the necessary `Authorization` header and a sample episode ID to retrieve intro timestamps. ```shell curl http://127.0.0.1:8096/Episode/12345678901234567890123456789012/IntroTimestamps/v1 -H 'Authorization: MediaBrowser Token="98765432109876543210987654321098"' ``` -------------------------------- ### Build ffmpeg with Chromaprint Support on macOS Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/README.md Instructions to build and link `ffmpeg` with `chromaprint` support using Homebrew on macOS. This is a prerequisite for the Intro Skipper plugin's audio analysis capabilities, ensuring `ffmpeg` has the necessary features. The process involves uninstalling any existing `ffmpeg`, installing required dependencies, tapping a specific Homebrew tap, installing `ffmpeg` with the `--with-chromaprint` flag, and finally linking the newly built `ffmpeg`. ```Shell brew uninstall --force --ignore-dependencies ffmpeg brew install chromaprint amiaopensource/amiaos/decklinksdk brew tap homebrew-ffmpeg/ffmpeg brew install homebrew-ffmpeg/ffmpeg/ffmpeg --with-chromaprint brew link --overwrite ffmpeg ``` -------------------------------- ### API Reference: Get Support Bundle Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Retrieves the diagnostic support bundle for the Intro Skipper plugin. This bundle contains information useful for troubleshooting. ```APIDOC Endpoint: GET /IntroSkipper/SupportBundle Description: Fetches the diagnostic support bundle. Response: Type: Plain Text Content: A string containing diagnostic information. ``` -------------------------------- ### Serve Local Files for Plugin Testing Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/release.md Starts a simple HTTP server using Python to serve the generated release ZIP and manifest files locally, enabling testing of the plugin update mechanism from a local source. ```bash python3 -m http.server ``` -------------------------------- ### API Reference: Get All Shows Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Retrieves a list of all available shows, typically used to populate a show selection dropdown. ```APIDOC Endpoint: GET /Intros/Shows Description: Fetches a list of all available shows. Response: Type: JSON Object Structure: { "[Show Name 1]": ["Season 1", "Season 2", ...], "[Show Name 2]": ["Season 1", "Season 2", ...], ... } Example: { "The Office": ["Season 1", "Season 2", "Season 3"], "Friends": ["Season 1", "Season 2"] } ``` -------------------------------- ### Go Template for First Analysis Report Details Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/verifier/report.html Defines a reusable Go template block named 'ReportInfo' that structures and displays detailed information for the first analysis run. It includes the file path, Jellyfin server version and operating system, analysis settings, introduction requirements, and the start, end, and total duration of the analysis. ```go-template {{ block "ReportInfo" .OldReport }} Path `{{ .Path }}` Jellyfin {{ .ServerInfo.Version }} on {{ .ServerInfo.OperatingSystem }} Analysis Settings {{ printAnalysisSettings .PluginConfig }} Introduction Requirements {{ printIntroductionReqs .PluginConfig }} Start time {{ printTime .StartedAt }} End time {{ printTime .FinishedAt }} Duration {{ printDuration .Runtime }} {{ end }} ``` -------------------------------- ### API Reference: Get Episodes by Show and Season Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Retrieves a list of episodes for a specific show and season, including their names and IDs. ```APIDOC Endpoint: GET /Intros/Show/{showName}/{season} Description: Fetches a list of episodes for a given show and season. Parameters: showName: string (URI encoded) - The name of the show. season: string - The season number or identifier. Response: Type: JSON Array Structure: [ { "Name": "Episode Name 1", "Id": "EpisodeId1" }, { "Name": "Episode Name 2", "Id": "EpisodeId2" }, ... ] Example: [ { "Name": "Pilot", "Id": "a1b2c3d4e5f6" }, { "Name": "Diversity Day", "Id": "f6e5d4c3b2a1" } ] ``` -------------------------------- ### Example JSON Response for Intro Timestamps Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/api.md Illustrates a typical JSON response object returned by the IntroTimestamps API endpoint when an introduction is detected. It provides concrete values for the properties defined in the JSON schema. ```json { "EpisodeId": "12345678901234567890123456789012", "Valid": true, "IntroStart": 304, "IntroEnd": 397.48, "ShowSkipPromptAt": 299, "HideSkipPromptAt": 314 } ``` -------------------------------- ### Fetch JSON Data with Authentication Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Performs an authenticated GET request to a specified URL using `fetchWithAuth` and parses the response body as JSON. This function simplifies fetching JSON data from the API. ```javascript async function getJson(url) { return await fetchWithAuth(url, "GET").then(r => { return r.json(); }); } ``` -------------------------------- ### Update Episode Introduction Timestamps (JavaScript) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html This event listener handles the click event for updating episode introduction timestamps. It retrieves the new start and end times for two selected episodes (left-hand side and right-hand side) from input fields. It then constructs new introduction objects and sends POST requests to the API via `fetchWithAuth` to update the timestamps for both episodes. A success alert is displayed upon completion. ```javascript btnUpdateTimestamps.addEventListener("click", () => { const lhsId = selectEpisode1.options[selectEpisode1.selectedIndex].value; const newLhsIntro = { IntroStart: document.querySelector("#editLeftEpisodeStart").value, IntroEnd: document.querySelector("#editLeftEpisodeEnd").value }; const rhsId = selectEpisode2.options[selectEpisode2.selectedIndex].value; const newRhsIntro = { IntroStart: document.querySelector("#editRightEpisodeStart").value, IntroEnd: document.querySelector("#editRightEpisodeEnd").value }; fetchWithAuth("Intros/Episode/" + lhsId + "/UpdateIntroTimestamps", "POST", JSON.stringify(newLhsIntro)); fetchWithAuth("Intros/Episode/" + rhsId + "/UpdateIntroTimestamps", "POST", JSON.stringify(newRhsIntro)); Dashboard.alert("New introduction timestamps saved"); }); ``` -------------------------------- ### API Endpoint: Get Episode Intro Timestamps Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/api.md Documents the primary API endpoint for retrieving introduction timestamps for a given episode. It specifies the URL structure, supported HTTP methods, authorization requirements, and possible HTTP status codes, including versioning details. ```APIDOC Endpoint: /Episode/{ItemId}/IntroTimestamps Description: Retrieves introduction timestamps for a television episode. Method: GET Path Parameters: ItemId (string): Unique GUID for the episode as provided by Jellyfin. Optional Path Parameters: v{Version} (integer): API version (e.g., /v1). If not specified, version 1 is selected. Headers: Authorization (string): Required. Format: 'MediaBrowser Token="YOUR_TOKEN"'. Status Codes: 200 OK: An introduction was detected for this item. Response is a JSON object following the specified schema. 404 Not Found: Either no introduction was detected for this item or it is not a television episode. ``` -------------------------------- ### JavaScript Functions for Calculating Episode Statistics Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/verifier/report.html Core JavaScript functions for retrieving user settings and calculating statistics for episodes, seasons, and shows. This includes getting the minimum percentage threshold, computing average durations, and setting group-level statistics based on intro detection results. ```JavaScript // Gets the minimum percentage of episodes in a group (a series or season) // that must have a detected introduction. function getMinimumPercentage() { const value = document.querySelector("#minimumPercentage").value; return Number(value); } // Gets the average duration for all episodes in a parent group. // durationClass must be either "old" or "new". function getAverageDuration(parent, durationClass) { // Get all durations in the parent const elems = parent.querySelectorAll(".duration." + durationClass); // Calculate the average duration, ignoring any episode without an intro let totalDuration = 0; let totalEpisodes = 0; for (const e of elems) { const dur = Number(e.textContent); if (dur === 0) { continue; } totalDuration += dur; totalEpisodes++; } if (totalEpisodes === 0) { return 0; } return Math.round(totalDuration / totalEpisodes); } // Calculate statistics for all episodes in a parent element (a series or a season). function setGroupStatistics(parent) { // Count the total number of episodes. const total = parent.querySelectorAll("div.episode").length; // Count how many episodes have no warnings. const okayCount = count(parent, "okay") + count(parent, "improvement"); const okayPercent = Math.round((okayCount * 100) / total); const isOkay = okayPercent >= getMinimumPercentage(); // Calculate the previous and current average durations const oldDuration = getAverageDuration(parent, "old"); const newDuration = getAverageDuration(parent, "new"); // Display the statistics const stats = parent.querySelector("#stats"); stats.textContent = `${okayCount} / ${total} (${okayPercent}%) okay. r1 ${oldDuration} r2 ${newDuration}`; if (!isOkay) { stats.classList.add("warning"); } else { stats.classList.remove("warning"); } } ``` -------------------------------- ### Configure Debug Logging for ConfusedPolarBear Plugin (JSONC) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/debug_logs.md This code example demonstrates how to modify the `logging.default.json` file to enable `Debug` level logging specifically for the `ConfusedPolarBear` plugin. It shows the addition of a new entry, `"ConfusedPolarBear": "Debug"`, within the `Serilog.MinimumLevel.Override` section, ensuring proper comma placement. ```jsonc { "Serilog": { "MinimumLevel": { "Default": "Information", "Override": { "Microsoft": "Warning", "System": "Warning", // be sure to add the trailing comma after "Warning", "ConfusedPolarBear": "Debug" // newly added line } }, // rest of file ommited for brevity } } ``` -------------------------------- ### Clone and Checkout jellyfin-web for Intro Skipper Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docker/README.md Clones the `jellyfin-web` repository from GitHub. It then checks out the `intros` branch, which contains specific modifications required for the Jellyfin Intro Skipper functionality. ```Shell git clone https://github.com/ConfusedPolarBear/jellyfin-web git checkout intros ``` -------------------------------- ### Handle Support Panel Toggle and Bundle Fetch (JavaScript) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Asynchronously fetches the support bundle when the support panel is opened. It retrieves the bundle via the `IntroSkipper/SupportBundle` API, displays it in a textarea, selects all text, and attempts to copy it to the clipboard, providing user feedback. ```JavaScript // fetch the support bundle whenever the detail section is opened. async function supportToggled() { if (!support.open) { return; } // Fetch the support bundle const bundle = await fetchWithAuth("IntroSkipper/SupportBundle", "GET", null); const bundleText = await bundle.text(); // Display it to the user and select all const ta = document.querySelector("textarea#supportBundle"); ta.value = bundleText; ta.focus(); ta.setSelectionRange(0, ta.value.length); // Attempt to copy it to the clipboard automatically, falling back // to prompting the user to press Ctrl + C. try { navigator.clipboard.writeText(bundleText); Dashboard.alert("Support bundle copied to clipboard"); } catch { Dashboard.alert("Press Ctrl+C to copy support bundle"); } } ``` -------------------------------- ### Build Jellyfin Intro Skipper Docker Image Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docker/README.md Initiates the Docker build process using the current directory as the build context. This command reads the Dockerfile (expected in the current directory) and constructs the `ghcr.io/confusedpolarbear/jellyfin-intro-skipper` container image. ```Shell docker build . ``` -------------------------------- ### Perform Authenticated API Fetch Request Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Constructs and executes an authenticated `fetch` request to the server. It prepends the server address to the URL, adds an `Authorization` header with a MediaBrowser token, and sets `Content-Type` to `application/json` for POST requests. It returns the raw `Response` object. ```javascript async function fetchWithAuth(url, method, body) { url = ApiClient.serverAddress() + "/" + url; const reqInit = { method: method, headers: { "Authorization": "MediaBrowser Token=" + ApiClient.accessToken() }, body: body, }; if (method === "POST") { reqInit.headers["Content-Type"] = "application/json"; } return await fetch(url, reqInit); } ``` -------------------------------- ### Intro Skipper Analysis Configuration Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Settings controlling how the Intro Skipper analyzes media files to detect introductions and generate Edit Decision Lists (EDL). These parameters influence the accuracy and performance of the analysis process. ```APIDOC Analysis: analyze_show_extras: description: Analyze show extras (specials). type: boolean max_degree_of_parallelism: description: Maximum degree of parallelism to use when analyzing episodes. type: integer limit_analysis_to_libraries: description: Enter the names of libraries to analyze, separated by commas. If this field is left blank, all libraries on the server containing television episodes will be analyzed. type: string (comma-separated list) edl_file_generation: description: Specifies which action to write to MPlayer compatible EDL files alongside your episode files. If this value is changed after EDL files are generated, you must check the "Regenerate EDL files" checkbox below. type: enum options: - None: Do not create or modify EDL files. - Commercial Break: Skips past the intro once (recommended). - Cut: Player will remove the intro from the video. - Intro: Show a skip button (*experimental*). - Mute: Audio will be muted. - Scene Marker: Create a chapter marker. regenerate_edl_files_during_next_scan: description: If checked, the plugin will overwrite all EDL files associated with your episodes with the currently discovered introduction timestamps and EDL action. type: boolean introduction_requirements: description: Settings to modify the criteria for identifying introductions. properties: percent_of_audio_to_analyze: description: Analysis will be limited to this percentage of each episode's audio. For example, a value of 25 (the default) will limit analysis to the first quarter of each episode. type: integer (percentage) max_runtime_of_audio_to_analyze_minutes: description: Analysis will be limited to this amount of each episode's audio in minutes. For example, a value of 10 (the default) will limit analysis to the first 10 minutes of each episode. type: integer (minutes) min_introduction_duration_seconds: description: Similar sounding audio which is shorter than this duration will not be considered an introduction. type: integer (seconds) max_introduction_duration_seconds: description: Similar sounding audio which is longer than this duration will not be considered an introduction. type: integer (seconds) note: The amount of each episode's audio that will be analyzed is determined using both the percentage of audio and maximum runtime of audio to analyze. The minimum of (episode duration * percent, maximum runtime) is the amount of audio that will be analyzed. If the audio percentage or maximum runtime settings are modified, the cached fingerprints and introduction timestamps for each season you want to analyze with the modified settings will have to be deleted. Increasing either of these settings will cause episode analysis to take much longer. silence_detection_options: description: Options for configuring silence detection during analysis. properties: noise_tolerance: description: Noise tolerance in negative decibels. type: integer (decibels) minimum_silence_duration: description: Minimum silence duration in seconds before adjusting introduction end time. type: integer (seconds) ``` -------------------------------- ### Build Production Assets with npm Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docker/README.md Executes the `build:production` script using npm within the cloned `jellyfin-web` repository. This command compiles and optimizes the web assets, preparing them for deployment within the container. ```Shell npm run build:production ``` -------------------------------- ### Run Unit Tests Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/release.md Executes the unit tests for the .NET project to ensure core functionalities are working correctly before release. ```bash dotnet test ``` -------------------------------- ### Generate Intro Timestamp Report from Local Server Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/README.md This command uses the `verifier` tool to generate an introduction timestamp report from a Jellyfin server running locally. It requires the server's address and an API key for authentication. ```Shell ./verifier -address http://127.0.0.1:8096 -key api_key ``` -------------------------------- ### Handle Visualizer Panel Toggle (JavaScript) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Asynchronously populates the show selection dropdown when the visualizer panel is opened. It clears existing options, displays a loading message, fetches show data from the `Intros/Shows` API, sorts the shows, and then adds them to the dropdown before hiding the loading message. ```JavaScript // when the fingerprint visualizer opens, populate show names async function visualizerToggled() { if (!visualizer.open) { return; } // ensure the series select is empty while (selectShow.options.length > 0) { selectShow.remove(0); } Dashboard.showLoadingMsg(); shows = await getJson("Intros/Shows"); var sorted = []; for (var series in shows) { sorted.push(series); } sorted.sort(); for (var show of sorted) { addItem(selectShow, show, show); } selectShow.value = ""; Dashboard.hideLoadingMsg(); } ``` -------------------------------- ### Handle Season Selection Change (JavaScript) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Asynchronously fetches and populates episode dropdowns (for both episode 1 and episode 2) based on the selected show and season. It constructs a URL, retrieves episode data from the `Intros/Show/{show}/{season}` API, clears existing options, and then adds formatted episode names and IDs to the dropdowns. It also sets initial selections and triggers `episodeChanged`. ```JavaScript // season changed, reload all episodes async function seasonChanged() { const url = "Intros/Show/" + encodeURI(selectShow.value) + "/" + selectSeason.value; const episodes = await getJson(url); clearSelect(selectEpisode1); clearSelect(selectEpisode2); let i = 1; for (let episode of episodes) { const strI = i.toLocaleString("en", { minimumIntegerDigits: 2, maximumFractionDigits: 0 }); addItem(selectEpisode1, strI + ": " + episode.Name, episode.Id); addItem(selectEpisode2, strI + ": " + episode.Name, episode.Id); i++; } setTimeout(() => { selectEpisode1.selectedIndex = 0; selectEpisode2.selectedIndex = 1; episodeChanged(); }, 100); } ``` -------------------------------- ### Introduction Timestamp Editor Interface Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Describes the user interface elements for manually managing and erasing introduction and end credits timestamps. All times are specified in seconds. ```APIDOC IntroductionTimestampEditor: all_times_in_seconds: true fields: start_time: description: Start time of the introduction in seconds. type: integer end_time: description: End time of the introduction in seconds. type: integer actions: update_timestamps: description: Button to update the introduction timestamps for the current episode. type: button erase_all_timestamps_for_this_season: description: Button to erase all introduction timestamps for the current season. type: button erase_all_introduction_timestamps_globally: description: Button to erase all introduction timestamps across all episodes. type: button erase_all_end_credits_timestamps_globally: description: Button to erase all end credits timestamps across all episodes. type: button ``` -------------------------------- ### Load Plugin Configuration into UI Fields (JavaScript) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html This snippet fetches the plugin configuration using `ApiClient.getPluginConfiguration` and populates various form fields (text inputs and checkboxes) with the retrieved values. It iterates through predefined lists of configuration fields to correctly assign values and then hides a loading message, indicating the UI is ready for user interaction. ```javascript ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) { for (const field of configurationFields) { document.querySelector("#" + field).value = config[field]; } for (const field of booleanConfigurationFields) { document.querySelector("#" + field).checked = config[field]; } Dashboard.hideLoadingMsg(); }); ``` -------------------------------- ### Run End-to-End Tests Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/release.md Initiates the end-to-end tests for the project, requiring a Jellyfin API token for execution. This validates the full application flow and integration. ```bash JELLYFIN_TOKEN=api_key_here python3 main.py ``` -------------------------------- ### Initialize Global Variables and UI Element References (JavaScript) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Declares global variables for episode data (`lhs`, `rhs`), fingerprint comparison (`fprDiffs`, `fprDiffMinimum`), show data (`shows`), and references to various HTML UI elements (`visualizer`, `support`, `canvas`, `selectShow`, etc.). It also defines arrays for plugin configuration fields. ```JavaScript // first and second episodes to fingerprint & compare var lhs = []; var rhs = []; // fingerprint point comparison & miminum similarity threshold (at most 6 bits out of 32 can be different) var fprDiffs = []; var fprDiffMinimum = (1 - 6 / 32) * 100; // seasons grouped by show var shows = {}; // settings elements var visualizer = document.querySelector("details#visualizer"); var support = document.querySelector("details#support"); var btnEraseIntroTimestamps = document.querySelector("button#btnEraseIntroTimestamps"); var btnEraseCreditTimestamps = document.querySelector("button#btnEraseCreditTimestamps"); // all plugin configuration fields that can be get or set with .value (i.e. strings or numbers). var configurationFields = [ // analysis "MaxParallelism", "SelectedLibraries", "AnalysisPercent", "AnalysisLengthLimit", "MinimumIntroDuration", "MaximumIntroDuration", "EdlAction", // playback "ShowPromptAdjustment", "HidePromptAdjustment", "SecondsOfIntroToPlay", // internals "SilenceDetectionMaximumNoise", "SilenceDetectionMinimumDuration", // UI customization "SkipButtonIntroText", "SkipButtonEndCreditsText", "AutoSkipNotificationText" ]; var booleanConfigurationFields = [ "AnalyzeSeasonZero", "RegenerateEdlFiles", "AutoSkip", "SkipFirstEpisode", "SkipButtonVisible", ]; // visualizer elements var canvas = document.querySelector("canvas#troubleshooter"); var selectShow = document.querySelector("select#troubleshooterShow"); var selectSeason = document.querySelector("select#troubleshooterSeason"); var selectEpisode1 = document.querySelector("select#troubleshooterEpisode1"); var selectEpisode2 = document.querySelector("select#troubleshooterEpisode2"); var txtOffset = document.querySelector("input#offset"); var txtSuggested = document.querySelector("span#suggestedShifts"); var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps"); var btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps"); var timeContainer = document.querySelector("span#timestampContainer"); var windowHashInterval = 0; ``` -------------------------------- ### Tag Docker Image for Release Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/release.md Tags the Docker image with both the specific commit hash and the 'latest' tag, preparing it for pushing to the container registry and marking the most recent stable build. ```bash docker tag ghcr.io/confusedpolarbear/jellyfin-intro-skipper:{COMMIT,latest} ``` -------------------------------- ### API Endpoint: /IntroTimestamps Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/README.md This section documents the `/IntroTimestamps` API endpoint, which is responsible for providing introduction timestamp data. The `verifier` tool is designed to validate the schema of the `Intro` objects returned by this endpoint. ```APIDOC GET /IntroTimestamps Description: Retrieves introduction timestamp data for episodes. Returns: Array of Intro objects Intro Object Schema: id: string (Unique identifier for the episode or intro) timestamp: integer (Start time of the intro in milliseconds) duration: integer (Duration of the intro in milliseconds) type: string (e.g., "intro", "outro", "credits") ``` -------------------------------- ### Push Latest Docker Image Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/release.md Pushes the newly tagged 'latest' Docker image to the GitHub Container Registry, making the updated container available for deployment and consumption. ```bash docker push ghcr.io/confusedpolarbear/jellyfin-intro-skipper:latest ``` -------------------------------- ### Load Episode Chromaprint Data and Refresh UI Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html This snippet initiates loading indicators, fetches chromaprint data for two selected episodes asynchronously, hides loading messages, resets an offset value, and triggers several UI refresh and matching functions, including updating the timestamp editor. This block appears to be part of a larger event handler or initialization sequence. ```javascript board.showLoadingMsg(); lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint"); rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint"); Dashboard.hideLoadingMsg(); txtOffset.value = "0"; refreshBounds(); renderTroubleshooter(); findExactMatches(); updateTimestampEditor(); } ``` -------------------------------- ### Intro Skipper User Interface Customization Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Settings to customize the text displayed on the Intro Skipper's user interface elements. This allows for personalization of the skip buttons and notification messages. ```APIDOC UserInterfaceCustomization: skip_intro_button_text: description: Text to display in the skip intro button. type: string skip_end_credits_button_text: description: Text to display in the skip end credits button. type: string automatic_skip_notification_message: description: Message sent to a user after automatically skipping an introduction. Leave blank to skip sending a notification. type: string ``` -------------------------------- ### Intro Skipper Playback Configuration Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Settings controlling the behavior of the Intro Skipper during media playback, including automatic skipping and the display of skip prompts. These options affect the user's viewing experience. ```APIDOC Playback: show_skip_intro_button: description: If checked, a skip button will be displayed at the start of an episode's introduction. This setting only applies to the web interface. type: boolean automatically_skip_intros: description: If checked, intros will be automatically skipped. If you access Jellyfin through a reverse proxy, it must be configured to proxy web sockets. type: boolean automatically_skip_intros_in_first_episode_of_season: description: If checked, auto skip will skip introductions in the first episode of a season. type: boolean show_skip_prompt_at_seconds: description: Seconds before the introduction starts to display the skip prompt at. type: integer (seconds) hide_skip_prompt_after_seconds: description: Seconds after the introduction starts to hide the skip prompt at. type: integer (seconds) seconds_of_intro_to_play: description: Seconds of introduction that should be played. Defaults to 2. type: integer (seconds) ``` -------------------------------- ### Prepare Docker Build Context: Copy dist Folder Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docker/README.md Copies the newly generated `dist` folder, which contains the compiled web application assets, into the current directory. This step is crucial to ensure these assets are available within the Docker build context. ```Shell cp -r dist . ``` -------------------------------- ### Generate Intro Timestamp Report from Remote Server with Polling Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/README.md This command generates an introduction timestamp report from a remote Jellyfin server. It includes options to poll for task completion at a specified interval (e.g., every 20 seconds) and save the output to a designated JSON file. ```Shell ./verifier -address https://example.com -key api_key -poll 20s -o example.json ``` -------------------------------- ### Compare Two Previously Generated Intro Reports Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/README.md This command utilizes the `verifier` tool to compare two previously generated introduction timestamp reports. It helps identify differences, newly discovered intros, or missing intros between different versions or scans. ```Shell ./verifier -r1 v0.1.5.json -r2 v0.1.6.json ``` -------------------------------- ### Global Keyboard Event and Window Hash Polling (JavaScript) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html This snippet sets up a global keyboard event listener on the `document` to trigger the `keyDown` function on any key press, allowing for application-wide keyboard shortcuts or interactions. Additionally, it establishes an interval using `setInterval` to periodically check the window's hash every 2.5 seconds, likely for navigation, state changes, or deep linking functionalities. ```javascript document.addEventListener("keydown", keyDown); windowHashInterval = setInterval(checkWindowHash, 2500); ``` -------------------------------- ### Go Template for Report Data Processing and Display Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/verifier/report.html This Go template iterates through show and season data, comparing old and new episode intro detection results. It sorts shows and seasons, displays episode titles, and highlights differences and warnings related to intro start/end times and durations. It's designed for rendering dynamic HTML reports. ```Go Template {{/* store a reference to the data before the range query */}} {{ $p := . }} {{/* sort the show names and iterate over them */}} {{ range $name := sortShows .OldReport.Shows }} {{/* get the unsorted seasons for this show */}} {{ $seasons := index $p.OldReport.Shows $name }} {{/* log the show name and number of seasons */}} **{{ $name }}** {{/* sort the seasons to ensure they display in numerical order */}} {{ range $seasonNumber := (sortSeasons $seasons) }} **Season {{ $seasonNumber }}** {{/* compare each episode in the old report to the same episode in the new report */}} {{ range $episode := index $seasons $seasonNumber }} {{/* lookup and compare both episodes */}} {{ $comparison := compareEpisodes $episode.EpisodeId $p }} {{ $old := $comparison.Old }} {{ $new := $comparison.New }} {{/* set attributes indicating if an intro was found in the old and new reports */}} {{ $episode.Title }} Old: {{ $old.FormattedStart }} - {{ $old.FormattedEnd }} ({{ $old.Duration }}) (valid: {{ $old.Valid }}) New: {{ $new.FormattedStart }} - {{ $new.FormattedEnd }} ({{ $new.Duration }}) (valid: {{ $new.Valid }}) {{ if ne $comparison.WarningShort "okay" }} Warning: {{ $comparison.Warning }} {{ end }} {{ end }} {{ end }} {{ end }} ``` -------------------------------- ### Update Episode Intro Timestamp Editor Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Asynchronously fetches intro timestamps for selected left and right episodes, defaulting to zero if no intro was found. It then updates various UI elements in the timestamp editor, including episode titles and start/end time input fields, making the editor visible. ```javascript async function updateTimestampEditor() { // Get the title and ID of the left and right episodes const leftEpisode = selectEpisode1.options[selectEpisode1.selectedIndex]; const rightEpisode = selectEpisode2.options[selectEpisode2.selectedIndex]; // Try to get the timestamps of each intro, falling back a default value of zero if no intro was found let leftEpisodeIntro = await getJson("Episode/" + leftEpisode.value + "/IntroTimestamps/v1"); if (!leftEpisodeIntro.hasOwnProperty("IntroStart")) { leftEpisodeIntro = { IntroStart: 0, IntroEnd: 0 }; } let rightEpisodeIntro = await getJson("Episode/" + rightEpisode.value + "/IntroTimestamps/v1"); if (!rightEpisodeIntro.hasOwnProperty("IntroStart")) { rightEpisodeIntro = { IntroStart: 0, IntroEnd: 0 }; } // Update the editor for the first and second episodes document.querySelector("#timestampEditor").style.display = "unset"; document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text; document.querySelector("#editLeftEpisodeStart").value = Math.round(leftEpisodeIntro.IntroStart); document.querySelector("#editLeftEpisodeEnd").value = Math.round(leftEpisodeIntro.IntroEnd); document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text; document.querySelector("#editRightEpisodeStart").value = Math.round(rightEpisodeIntro.IntroStart); document.querySelector("#editRightEpisodeEnd").value = Math.round(rightEpisodeIntro.IntroEnd); } ``` -------------------------------- ### Go Template for Analysis Statistics Summary Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/verifier/report.html This Go template section outlines the key statistics presented in the report. It lists categories such as total episodes processed, those never found, changed, gains (improvements in detection), and losses (regressions in detection) related to intro timestamp analysis. ```go-template Total episodes Never found Changed Gains Losses ``` -------------------------------- ### Go Template for Including Second Analysis Report Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/verifier/report.html This snippet demonstrates how to include the 'ReportInfo' Go template block, previously defined, to display the details of the second analysis report. It reuses the structured output for consistency, applying it to the '.NewReport' data context. ```go-template {{ template "ReportInfo" .NewReport }} ``` -------------------------------- ### Handle Episode Selection Change (JavaScript) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Initiates the process of fetching fingerprints and calculating their differences when either episode 1 or episode 2 selection changes. It checks if both episodes are selected before proceeding. (Note: The provided code snippet is incomplete). ```JavaScript // episode changed, get fingerprints & calculate diff async function episodeChanged() { if (!selectEpisode1.value || !selectEpisode2.value) { return; } Dash ``` -------------------------------- ### Validate Intro API Schema for Specific Episodes Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/README.md This command instructs the `verifier` tool to validate the schema of `Intro` objects returned by the `/IntroTimestamps` API endpoint for a specified list of episode IDs. It ensures that the API responses conform to the expected data structure. ```Shell ./verifier -address http://127.0.0.1:8096 -key api_key -validate id1,id2,id3 ``` -------------------------------- ### Handle Show Selection Change (JavaScript) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Clears the season selection dropdown and populates it with seasons corresponding to the newly selected show. It iterates through the `shows` object (which contains seasons grouped by show) to add each season as an option. ```JavaScript // show changed, populate seasons async function showChanged() { clearSelect(selectSeason); // add all seasons from this show to the season select for (var season of shows[selectShow.value]) { addItem(selectSeason, season, season); } selectSeason.value = ""; } ``` -------------------------------- ### Mount Modified Jellyfin Web Interface via Docker Compose Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/web_interface.md This Docker Compose snippet demonstrates how to configure a Jellyfin service to mount a custom web interface. It shows volume mapping for both the official `jellyfin/jellyfin` container and the `linuxserver/jellyfin` container, allowing the modified `dist` folder to be used as the web interface. It also includes standard port mappings and configuration/media volume mounts. ```yaml services: jellyfin: ports: - '8096:8096' volumes: # change `:ro` to `:rw` if you are using a plugin that modifies Jellyfin's web interface from inside the container (such as Jellyscrub) - '/full/path/to/extracted/dist:/jellyfin/jellyfin-web:ro' # <== add this line if using the official container - '/full/path/to/extracted/dist:/usr/share/jellyfin/web:ro' # <== add this line if using the linuxserver container - '/config:/config' - '/media:/media:ro' image: 'jellyfin/jellyfin:10.8.0' ``` -------------------------------- ### Handle Keyboard Navigation and Offset Adjustments Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html Processes keyboard events to adjust an offset value or navigate through episodes. Arrow keys modify `txtOffset` or change the selected index of episode dropdowns (`selectEpisode1`, `selectEpisode2`), triggering `episodeChanged` and `renderTroubleshooter` functions. Ctrl key modifies offset by a larger increment. ```javascript function keyDown(e) { let episodeDelta = 0; let offsetDelta = 0; switch (e.key) { case "ArrowDown": // if the control key is pressed, shift LHS by 10s. Otherwise, shift by 1. offsetDelta = e.ctrlKey ? 10 / 0.128 : 1; break; case "ArrowUp": offsetDelta = e.ctrlKey ? -10 / 0.128 : -1; break; case "ArrowRight": episodeDelta = 2; break; case "ArrowLeft": episodeDelta = -2; break; default: return; } if (offsetDelta != 0) { txtOffset.value = Number(txtOffset.value) + Math.floor(offsetDelta); } if (episodeDelta != 0) { // calculate the number of episodes remaining in the LHS and RHS episode pickers const lhsRemaining = selectEpisode1.selectedIndex; const rhsRemaining = selectEpisode2.length - selectEpisode2.selectedIndex - 1; // if we're moving forward and ``` -------------------------------- ### Attach Event Listeners for UI Element Changes (JavaScript) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html This section defines various event listeners for different UI elements such as toggles, text inputs, and select dropdowns. Each listener triggers a specific function (e.g., `visualizerToggled`, `renderTroubleshooter`, `showChanged`, `episodeChanged`) when its associated event (e.g., 'toggle', 'change') occurs, facilitating dynamic UI updates and data rendering based on user selections. ```javascript visualizer.addEventListener("toggle", visualizerToggled); support.addEventListener("toggle", supportToggled); txtOffset.addEventListener("change", renderTroubleshooter); selectShow.addEventListener("change", showChanged); selectSeason.addEventListener("change", seasonChanged); selectEpisode1.addEventListener("change", episodeChanged); selectEpisode2.addEventListener("change", episodeChanged); ``` -------------------------------- ### JSON Schema for Intro Timestamps Response Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/api.md Defines the structure and data types of the JSON object returned by the IntroTimestamps API endpoint. It includes properties such as `EpisodeId`, `IntroStart`, `IntroEnd`, and recommended times for displaying and hiding an intro skip prompt. ```jsonc { "EpisodeId": "{item id}", // Unique GUID for this item as provided by Jellyfin. "Valid": true, // Used internally to mark items that have intros. Should be ignored as it will always be true. "IntroStart": 100.5, // Start time (in seconds) of the introduction. "IntroEnd": 130.42, // End time (in seconds) of the introduction. "ShowSkipPromptAt": 95.5, // Recommended time to display an on-screen intro skip prompt to the user. "HideSkipPromptAt": 110.5 // Recommended time to hide the on-screen intro skip prompt. } ``` -------------------------------- ### Default Jellyfin Logging Configuration (JSONC) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/docs/debug_logs.md This snippet displays the standard structure of the `logging.default.json` file in Jellyfin. It highlights the `Serilog` configuration, including `MinimumLevel` and `Override` settings for various components like Microsoft and System, before any custom modifications are applied. ```jsonc { "Serilog": { "MinimumLevel": { "Default": "Information", "Override": { "Microsoft": "Warning", "System": "Warning" } }, // rest of file ommited for brevity } } ``` -------------------------------- ### CSS Styling for Intro Skipper Report UI Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/verifier/report.html Provides comprehensive CSS rules for the intro skipper report interface. This includes dark mode support, table formatting, margin adjustments for various sections, and visual indicators (background colors) for episodes based on the status of their intro detection (e.g., improvement, missing, different timestamps). ```css /* dark mode */ body { background-color: #1e1e1e; color: white; } /* enable borders on the table row */ table { border-collapse: collapse; } table td { padding-right: 5px; } /* remove top & bottom margins */ .report-info *, .episode * { margin-bottom: 0; margin-top: 0; } /* visually separate the report header from the contents */ .report-info .report { background-color: #0c3c55; border-radius: 7px; display: inline-block; } .report h3 { display: inline; } .report.stats { margin-top: 4px; } details { margin-bottom: 10px; } summary { cursor: pointer; } /* prevent the details from taking up the entire width of the screen */ .show>details { max-width: 50%; } /* indent season headers some */ .season { margin-left: 1em; } /* indent individual episode timestamps some more */ .episode { margin-left: 1em; } /* if an intro was not found previously but is now, that's good */ .episode[data-warning="improvement"] { background-color: #044b04; } /* if an intro was found previously but isn't now, that's bad */ .episode[data-warning="only_previous"], .episode[data-warning="missing"] { background-color: firebrick; } /* if an intro was found on both runs but the timestamps are pretty different, that's interesting */ .episode[data-warning="different"] { background-color: #b77600; } #stats.warning { border: 2px solid firebrick; font-weight: bolder; } ``` -------------------------------- ### Handle Fingerprint Configuration Form Submission (JavaScript) Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html This event listener captures the submission of the 'FingerprintConfigForm'. Upon submission, it displays a loading message, reads updated values from the form fields (both text inputs and checkboxes), and then sends them to the API via `ApiClient.updatePluginConfiguration` to persist the changes. It processes the API's response using `Dashboard.processPluginConfigurationUpdateResult` and prevents the default form submission behavior. ```javascript document.querySelector('#FingerprintConfigForm') .addEventListener('submit', function (e) { Dashboard.showLoadingMsg(); ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) { for (const field of configurationFields) { config[field] = document.querySelector("#" + field).value; } for (const field of booleanConfigurationFields) { config[field] = document.querySelector("#" + field).checked; } ApiClient.updatePluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b", config) .then(function (result) { Dashboard.processPluginConfigurationUpdateResult(result); }); }); e.preventDefault(); return false; }); ``` -------------------------------- ### JavaScript Utility Functions for UI Manipulation Source: https://github.com/confusedpolarbear/intro-skipper/blob/master/ConfusedPolarBear.Plugin.IntroSkipper.Tests/e2e_tests/verifier/report.html A collection of helper JavaScript functions used for client-side UI manipulation and data formatting. These include counting visible elements based on a data attribute, formatting numbers as percentages, and setting the text content of a DOM element. ```JavaScript function count(parent, warning) { const sel = `div.episode[data-warning='${warning}']` // Don't include hidden elements in the count let count = 0; for (const elem of parent.querySelectorAll(sel)) { // offsetParent is defined when the element is not hidden if (elem.offsetParent) { count++; } } return count; } function getPercent(part, whole) { const percent = Math.round((part * 10_000) / whole) / 100; return `${part} (${percent}%)`; } function setText(selector, text) { document.querySelector(selector).textContent = text; } ```