// ==UserScript==
// @name         Jellyfin Enhanced with Potplayer
// @namespace    http://tampermonkey.net/
// @version      mickey-only-fixed
// @description  实现在jellyfin中调用potplyer，并优化自动刷新逻辑
// @author       mickey
// @match        *://jellyfin.mingqian.space/*
// @match        *://jellyfin-speedtest.mingqian.space/*
// @match        *://jellyfin.mingqian.online/*
// @match        *://jellyfin-speedtest.mingqian.online/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    /**
     * =========================================================================
     * 脚本一：Jellyfin Auto Refresh once (已修复“更多”按钮误触问题)
     * 拦截内部导航，仅在进入新的 /details 页面 ID 时强制刷新一次。
     * =========================================================================
     */
    function runAutoRefresh() {
        // *** 配置参数 ***
        // 建议使用 10ms 以保证数据存储的稳定性，10ms 可能太快导致 race condition
        const DELAY_MS = 10;
        // 存储键名：用于记录上一次已经刷新过的 Item ID
        const LAST_REFRESHED_ID_KEY = 'JellyfinLastRefreshedItemId';

        // 辅助函数：从 URL 中获取 id 参数
        function getItemIdFromUrl() {
            const match = location.href.match(/[?&]id=([^&#]+)/);
            return match ? match[1] : null;
        }

        // --- 核心逻辑函数：触发延迟刷新 ---
        function triggerDelayedRefresh() {
            // 1. 检查是否在详情页
            if (!location.href.includes('/details')) {
                return;
            }

            // 2. 获取当前页面的 Item ID
            const currentItemId = getItemIdFromUrl();
            if (!currentItemId) return; // 如果没找到 ID，不做处理

            // 3. 获取上次刷新的 Item ID
            const lastRefreshedId = sessionStorage.getItem(LAST_REFRESHED_ID_KEY);

            // 4. 核心判断：
            // 只有当 "当前 ID" 不等于 "上次刷新的 ID" 时，才执行刷新。
            // 这意味着如果在同一个页面内（ID没变）点击按钮触发了 pushState，这里会因为 ID 相同而跳过刷新。
            if (lastRefreshedId !== currentItemId) {
                console.log(`Script (Refresh): New Item ID detected (${currentItemId}). Preparing to reload...`);

                // 立即更新存储，标记这个 ID 已经处理过
                sessionStorage.setItem(LAST_REFRESHED_ID_KEY, currentItemId);

                setTimeout(() => {
                    console.log('Script (Refresh): Delay complete. FORCING LOCATION RELOAD now.');
                    location.reload();
                }, DELAY_MS);
            } else {
                // ID 相同，说明是在当前页面内的操作，忽略
                // console.log('Script (Refresh): Same Item ID, skipping refresh.');
            }
        }

        // --- History API 拦截设置 ---
        const originalPushState = history.pushState;
        history.pushState = function() {
            originalPushState.apply(this, arguments);
            triggerDelayedRefresh();
        };

        const originalReplaceState = history.replaceState;
        history.replaceState = function() {
            originalReplaceState.apply(this, arguments);
            triggerDelayedRefresh();
        };

        // --- 主执行体 ---
        // 监听 hash 变化
        window.addEventListener('hashchange', triggerDelayedRefresh);

        // 页面首次加载时也运行一次检查，以防直接进入详情页的情况
        triggerDelayedRefresh();
    }


    /**
     * =========================================================================
     * 脚本二：MyJellyfinLaunchPotplayer
     * 添加外部播放器按钮（如 Potplayer）。
     * =========================================================================
     */
    function runPotplayerButtons() {
        // (所有来自 MyJellyfinLaunchPotplayer-mickey-only.user.js 的代码)
        const iconConfig = {
            baseUrl: "https://emby-external-url.7o7o.cc/embyWebAddExternalUrl/icons",
            removeCustomBtns: false,
        };
        const useRealFileName = false;
        let isEmby = "";
        const mark = "embyLaunchPotplayer";
        const playBtnsWrapperId = "ExternalPlayersBtns";

        const lsKeys = {
            iconOnly: `${mark}-iconOnly`,
        };

        const OS = {
            isAndroid: () => /android/i.test(navigator.userAgent),
            isIOS: () => /iPad|iPhone|iPod/i.test(navigator.userAgent),
            isMacOS: () => /Macintosh|MacIntel/i.test(navigator.userAgent),
            isApple: () => OS.isMacOS() || OS.isIOS(),
            isWindows: () => /compatible|Windows/i.test(navigator.userAgent),
            isMobile: () => OS.isAndroid() || OS.isIOS(),
            isUbuntu: () => /Ubuntu/i.test(navigator.userAgent),
            isOthers: () => Object.entries(OS).filter(([key, val]) => key !== 'isOthers').every(([key, val]) => !val()),
        };

        const playBtns = [
            { id: "embyPot", title: "Potplayer", iconId: "icon-PotPlayer"
                , onClick: embyPot, osCheck: [OS.isWindows], },
            { id: "embyCopyUrl", title: "Copy Stream URL", iconId: "icon-Copy", onClick: embyCopyUrl, },
        ];

        const customBtns = [
            { id: "iconOnly", title: "Display Mode", iconName: "open_in_full", onClick: iconOnlyHandler, },
        ];

        if (!iconConfig.removeCustomBtns) {
            playBtns.push(...customBtns);
        }
        const fileNameReg = /.*[\\/]|(\?.*)?$/g;
        const selectors = {
            embyMediaInfoDiv: "div[is='emby-scroller']:not(.hide) .mediaInfo:not(.hide)",
            jellfinMediaInfoDiv: ".itemMiscInfo-primary:not(.hide)",
            embyMediaInfoSecondary: "div[is='emby-scroller']:not(.hide) .mediaInfoSecondary:not(.hide)",
            jellfinMediaInfoSecondary: ".itemMiscInfo-secondary:not(.hide)",
            embyBtnManualRecording: "div[is='emby-scroller']:not(.hide) .btnManualRecording:not(.hide)",
            jellfinBtnCancelTimer: ".btnCancelTimer:not(.hide)",
            embyMainDetailButtons: "div[is='emby-scroller']:not(.hide) .mainDetailButtons",
            jellfinMainDetailButtons: "div.itemDetailPage:not(.hide) div.detailPagePrimaryContainer",
            selectSubtitles: "div[is='emby-scroller']:not(.hide) select.selectSubtitles",
            selectSource: "div[is='emby-scroller']:not(.hide) select.selectSource:not([disabled])",
        };

        function init() {
            let playBtnsWrapper = document.getElementById(playBtnsWrapperId);
            if (playBtnsWrapper) {
                playBtnsWrapper.remove();
            }

            if (isEmby === "") {
                isEmby = !!document.querySelector(selectors.embyMediaInfoSecondary) ||
                        !!document.querySelector(selectors.embyMediaInfoDiv) ||
                        !!document.querySelector(selectors.embyMainDetailButtons);
            }

            let insertionPoint = document.querySelector(selectors.embyMediaInfoSecondary);
            if (!insertionPoint) {
                insertionPoint = document.querySelector(selectors.jellfinMediaInfoSecondary);
            }

            function generateButtonHTML({ id, title, desc, iconId, iconName }) {
                return `
                    <button
                        id="${id}"
                        type="button"
                        class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary"
                        title="${desc ? desc : title}"
                    >
                        <div class="detailButton-content">
                            <i class="md-icon detailButton-icon button-icon button-icon-left material-icons" id="${iconId}">
                            ${iconName ? iconName : '　'}
                            </i>
                            <span class="button-text">${title}</span>
                        </div>
                    </button>
                `;
            }

            let buttonHtml = `<div id="${playBtnsWrapperId}" class="detailButtons flex align-items-center flex-wrap-wrap" style="margin: 0.25em 0;">`;
            playBtns.forEach(btn => {
                buttonHtml += generateButtonHTML(btn);
            });
            buttonHtml += `</div>`;

            if (insertionPoint) {
                insertionPoint.insertAdjacentHTML("afterend", buttonHtml);
            } else {
                let fallbackPoint = document.querySelector(selectors.embyMediaInfoDiv) || document.querySelector(selectors.jellfinMediaInfoDiv);
                if(fallbackPoint) {
                    console.log("MyJellyfinLaunchPotplayer: 未找到 '55m' 栏, 回退到 'V/A' 栏前插入。");
                    fallbackPoint.insertAdjacentHTML("beforebegin", buttonHtml);
                } else {
                    let mainDetailButtons = document.querySelector(selectors.embyMainDetailButtons) || document.querySelector(selectors.jellfinMainDetailButtons);
                    if (mainDetailButtons) {
                        console.log("MyJellyfinLaunchPotplayer: 未找到 'V/A' 栏, 回退到 'Play' 按钮后插入。");
                        mainDetailButtons.insertAdjacentHTML("afterend", buttonHtml);
                    } else {
                        console.log("MyJellyfinLaunchPotplayer: 未找到任何按钮插入点。");
                        return;
                    }
                }
            }

            if (!isEmby) {
                let playBtnsWrapper = document.getElementById("ExternalPlayersBtns");
                if (playBtnsWrapper) {
                    playBtnsWrapper.style.display = "flex";
                    let btns = playBtnsWrapper.getElementsByTagName("button");
                    for (let i = 0; i < btns.length; i++) {
                        btns[i].classList.add("button-flat");
                    }
                }
            }

            playBtns.forEach(btn => {
                const btnEle = document.querySelector(`#${btn.id}`);
                if (btnEle) {
                    btnEle.onclick = btn.onClick;
                }
            });

            const iconBaseUrl = iconConfig.baseUrl;

            const icons = [
                { id: "icon-PotPlayer", name: "icon-PotPlayer.webp", fontSize: "1.4em" },
                { id: "icon-Copy", fontSize: "1.4em" },
            ];

            const iconsExt = getIconsExt();
            icons.map((icon, index) => {
                const element = document.querySelector(`#${icon.id}`);
                if (element) {
                    icon.url = typeof iconsExt !== 'undefined' && iconsExt && iconsExt[index] ? iconsExt[index].url : undefined;
                    const url = icon.url || `${iconBaseUrl}/${icon.name || `${icon.id}.webp`}`;
                    element.style.cssText += `
                        background-image: url(${url});
                        background-repeat: no-repeat;
                        background-size: 100% 100%;
                        font-size: ${icon.fontSize};
                    `;
                }
            });

            if (!iconConfig.removeCustomBtns) {
                iconOnlyHandler();
            }
        }

        function getIconsExt() {
            const iconsExt = [];
            return iconsExt;
        }

        function showFlag() {
            let mediaInfoDiv = document.querySelector(selectors.embyMediaInfoDiv) || document.querySelector(selectors.jellfinMediaInfoDiv);
            let btnManualRecording = document.querySelector(selectors.embyBtnManualRecording) || document.querySelector(selectors.jellfinBtnCancelTimer);
            return !!mediaInfoDiv || !!btnManualRecording;
        }

        function isItemDetailPage() {
            const hashMatch = window.location.hash.match(/\?id=([A-Za-z0-9]+)/);
            if (!hashMatch) return false;
            return showFlag();
        }

        async function getItemInfo() {
            let userId = ApiClient._serverInfo.UserId;
            let itemId = /\?id=([A-Za-z0-9]+)/.exec(window.location.hash)[1];
            let response = await ApiClient.getItem(userId, itemId);
            if (response.Type == "Series") {
                let seriesNextUpItems = await ApiClient.getNextUpEpisodes({ SeriesId: itemId, UserId: userId });
                if (seriesNextUpItems.Items.length > 0) {
                    console.log("nextUpItemId: " + seriesNextUpItems.Items[0].Id);
                    return await ApiClient.getItem(userId, seriesNextUpItems.Items[0].Id);
                }
            }
            if (response.Type == "Season") {
                let seasonItems = await ApiClient.getItems(userId, { parentId: itemId });
                if (seasonItems.Items.length > 0) {
                    console.log("seasonItemId: " + seasonItems.Items[0].Id);
                    return await ApiClient.getItem(userId, seasonItems.Items[0].Id);
                }
            }
            if (response.MediaSources?.length > 0) {
                console.log("itemId:  " + itemId);
                return response;
            }
            let firstItems = await ApiClient.getItems(userId, { parentId: itemId, Recursive: true, IsFolder: false, Limit: 1 });
            if (firstItems.Items.length > 0) {
                console.log("firstItemId: " + firstItems.Items[0].Id);
                return await ApiClient.getItem(userId, firstItems.Items[0].Id);
            }
            return response;
        }

        function getSeek(position) {
            let ticks = position * 10000;
            let parts = []
                , hours = ticks / 36e9;
            (hours = Math.floor(hours)) && parts.push(hours);
            let minutes = (ticks -= 36e9 * hours) / 6e8;
            ticks -= 6e8 * (minutes = Math.floor(minutes)),
                minutes < 10 && hours && (minutes = "0" + minutes),
                parts.push(minutes);
            let seconds = ticks / 1e7;
            return (seconds = Math.floor(seconds)) < 10 && (seconds = "0" + seconds),
                parts.push(seconds),
                parts.join(":")
        }

        function getSubPath(mediaSource) {
            let selectSubtitles = document.querySelector(selectors.selectSubtitles);
            let subTitlePath = '';
            if (selectSubtitles && selectSubtitles.value > 0) {
                let SubIndex = mediaSource.MediaStreams.findIndex(m => m.Index == selectSubtitles.value && m.IsExternal);
                if (SubIndex > -1) {
                    let subtitleCodec = mediaSource.MediaStreams[SubIndex].Codec;
                    subTitlePath = `/${mediaSource.Id}/Subtitles/${selectSubtitles.value}/Stream.${subtitleCodec}`;
                }
            }
            else {
                let chiSubIndex = mediaSource.MediaStreams.findIndex(m => m.Language == "chi" && m.IsExternal);
                if (chiSubIndex > -1) {
                    let subtitleCodec = mediaSource.MediaStreams[chiSubIndex].Codec;
                    subTitlePath = `/${mediaSource.Id}/Subtitles/${chiSubIndex}/Stream.${subtitleCodec}`;
                } else {
                    let externalSubIndex = mediaSource.MediaStreams.findIndex(m => m.IsExternal);
                    if (externalSubIndex > -1) {
                        let subtitleCodec = mediaSource.MediaStreams[externalSubIndex].Codec;
                        subTitlePath = `/${mediaSource.Id}/Subtitles/${externalSubIndex}/Stream.${subtitleCodec}`;
                    }
                }

            }
            return subTitlePath;
        }

        async function getEmbyMediaInfo() {
            let itemInfo = await getItemInfo();
            let mediaSourceId = itemInfo.MediaSources[0].Id;
            let selectSource = document.querySelector(selectors.selectSource);
            if (selectSource && selectSource.value.length > 0) {
                mediaSourceId = selectSource.value;
            }
            const accessToken = ApiClient.accessToken();
            let mediaSource = itemInfo.MediaSources.find(m => m.Id == mediaSourceId);
            let uri = isEmby ? "/emby/videos" : "/Items";
            let baseUrl = `${ApiClient._serverAddress}${uri}/${itemInfo.Id}`;
            let subPath = getSubPath(mediaSource);
            let subUrl = subPath.length > 0 ? `${baseUrl}${subPath}?api_key=${accessToken}` : "";
            let streamUrl = `${baseUrl}/`;
            if (mediaSource.Path.startsWith("http") && localStorage.getItem(lsKeys.strmDirect) === "1") {
                streamUrl = decodeURIComponent(mediaSource.Path);
            } else {
                let fileName = mediaSource.IsInfiniteStream ? `master.m3u8` : decodeURIComponent(mediaSource.Path.replace(fileNameReg, ""));
                if (isEmby) {
                    if (mediaSource.IsInfiniteStream) {
                        streamUrl += useRealFileName && mediaSource.Name ? `${mediaSource.Name}.m3u8` : fileName;
                    } else {
                        streamUrl += useRealFileName ? `stream/${fileName}` : `stream.${mediaSource.Container}`;
                    }
                } else {
                    streamUrl += `Download`;
                    streamUrl += useRealFileName ? `/${fileName}` : "";
                }
                streamUrl += `?api_key=${accessToken}&Static=true&MediaSourceId=${mediaSourceId}&DeviceId=${ApiClient._deviceId}`;
            }
            let position = parseInt(itemInfo.UserData.PlaybackPositionTicks / 10000);
            let intent = await getIntent(mediaSource, position);
            console.log(streamUrl, subUrl, intent);
            return {
                streamUrl: streamUrl,
                subUrl: subUrl,
                intent: intent,
            }
        }

        async function getIntent(mediaSource, position) {
            let title = mediaSource.IsInfiniteStream
                ? mediaSource.Name
                : decodeURIComponent(mediaSource.Path.replace(fileNameReg, ""));
            let externalSubs = mediaSource.MediaStreams.filter(m => m.IsExternal == true);
            let subs = '';
            let subs_name = '';
            let subs_filename = '';
            let subs_enable = '';
            if (externalSubs) {
                subs_name = externalSubs.map(s => s.DisplayTitle);
                subs_filename = externalSubs.map(s => s.Path.split('/').pop());
            }
            return {
                title: title,
                position: position,
                subs: subs,
                subs_name: subs_name,
                subs_filename: subs_filename,
                subs_enable: subs_enable,
                path: mediaSource.Path,
            };
        }

        async function embyPot() {
            const mediaInfo = await getEmbyMediaInfo();
            const intent = mediaInfo.intent;
            const notCurrentPotArg = "/current";
            let potUrl = `potplayer://${encodeURI(mediaInfo.streamUrl)} /sub=${encodeURI(mediaInfo.subUrl)} ${notCurrentPotArg} /seek=${getSeek(intent.position)} /title="${intent.title}"`;
            await writeClipboard(potUrl);
            console.log("Successfully wrote real deep link to clipboard: ", potUrl);
            potUrl = `potplayer://${notCurrentPotArg}/clipboard`;
            window.open(potUrl, "_self");
        }

        function lsCheckSetBoolean(event, lsKeyName) {
            let flag = localStorage.getItem(lsKeyName) === "1";
            if (event) {
                flag = !flag;
                localStorage.setItem(lsKeyName, flag ? "1" : "0");
            }
            return flag;
        }

        function iconOnlyHandler(event) {
            const btn = document.getElementById("iconOnly");
            if (!btn) {
                return;
            }

            const flag = lsCheckSetBoolean(event, lsKeys.iconOnly);
            const playBtnsWrapper = document.getElementById(playBtnsWrapperId);
            if (!playBtnsWrapper) return;

            const spans = playBtnsWrapper.querySelectorAll("span");
            spans.forEach(span => {
                span.hidden = flag;
            });
            const iArr = playBtnsWrapper.querySelectorAll("i");
            iArr.forEach(iEle => {
                iEle.classList.toggle("button-icon-left", !flag);
            });
            btn.classList.toggle("button-submit", flag);
        }

        async function embyCopyUrl() {
            const mediaInfo = await getEmbyMediaInfo();
            const streamUrl = encodeURI(mediaInfo.streamUrl);
            if (await writeClipboard(streamUrl)) {
                console.log(`copyUrl = ${streamUrl}`);
                this.innerText = 'Copy Success';
            }
        }

        async function writeClipboard(text) {
            let flag = false;
            if (navigator.clipboard) {
                try {
                    await navigator.clipboard.writeText(text);
                    flag = true;
                    console.log("Successfully used navigator.clipboard modern clipboard implementation");
                } catch (error) {
                    console.error('Error occurred when copying to clipboard using navigator.clipboard:', error);
                }
            } else {
                flag = writeClipboardLegacy(text);
                console.log("navigator.clipboard modern clipboard implementation not available, using legacy implementation");
            }
            return flag;
        }

        function writeClipboardLegacy(text) {
            let textarea = document.createElement('textarea');
            document.body.appendChild(textarea);
            textarea.style.position = 'absolute';
            textarea.style.clip = 'rect(0 0 0 0)';
            textarea.value = text;
            textarea.select();
            if (document.execCommand('copy', true)) {
                return true;
            }
            return false;
        }

        function startPageCheck() {
            console.log("MyJellyfinLaunchPotplayer: Starting page check...");
            if (isItemDetailPage() && showFlag()) {
                console.log("MyJellyfinLaunchPotplayer: Already on detail page with content. Running init() immediately.");
                init();
                return;
            }

            if (isItemDetailPage()) {
                console.log("MyJellyfinLaunchPotplayer: On detail page but content not ready. Starting MutationObserver...");
                const initialObserver = new MutationObserver(function() {
                    if (showFlag()) {
                        console.log("MyJellyfinLaunchPotplayer: Initial observer found content. Running init() and disconnecting.");
                        init();
                        initialObserver.disconnect();
                    }
                });

                initialObserver.observe(document.body, {
                    childList: true,
                    subtree: true,
                });

                setTimeout(() => {
                    initialObserver.disconnect();
                    console.log("MyJellyfinLaunchPotplayer: Initial observer timeout, disconnecting.");
                }, 15000); // 15s
            }
        }

        document.addEventListener("viewbeforeshow", function (e) {
            console.log("MyJellyfinLaunchPotplayer: viewbeforeshow event fired");
            if (isEmby === "") {
                isEmby = !!e.detail.contextPath;
            }
            let isItemDetailPage;
            if (isEmby) {
                isItemDetailPage = e.detail.contextPath.startsWith("/item?id=");
            } else {
                isItemDetailPage = e.detail.params && e.detail.params.id;
            }

            if (isItemDetailPage) {
                console.log("MyJellyfinLaunchPotplayer: Item detail page detected. Starting new MutationObserver...");
                const mutation = new MutationObserver(function() {
                    if (showFlag()) {
                        console.log("MyJellyfinLaunchPotplayer: Observer found anchor. Running init() and disconnecting.");
                        init();
                        mutation.disconnect();
                    }
                });

                mutation.observe(document.body, {
                    childList: true,
                    subtree: true,
                });
            } else {
                 let playBtnsWrapper = document.getElementById(playBtnsWrapperId);
                 if (playBtnsWrapper) {
                      console.log("MyJellyfinLaunchPotplayer: Not an item page. Removing buttons.");
                      playBtnsWrapper.remove();
                 }
            }
        });

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', startPageCheck);
        } else {
            startPageCheck();
        }
    }


    /**
     * =========================================================================
     * 启动两个脚本
     * =========================================================================
     */
    runAutoRefresh();
    runPotplayerButtons();

})();