Greasy Fork

Greasy Fork is available in English.

Emby收藏按钮

在Emby视频页面添加收藏按钮

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Emby收藏按钮
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  在Emby视频页面添加收藏按钮
// @author       Damon
// @match        http://192.168.123.202:8096/web/index.html*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';
    // const url = `http://192.168.123.202:8096/emby/Users/e1bca16aaf23497e8b8f33cdee760e48/FavoriteItems/${itemId}?X-Emby-Client=Emby+Web&X-Emby-Device-Name=Chrome+Windows&X-Emby-Device-Id=eb7e48ba-387b-4c1a-8b02-e47cda4a351d&X-Emby-Client-Version=4.9.1.4&X-Emby-Token=49305c8ae7304a168e30f70aac9c88bc&X-Emby-Language=zh-cn`;
    // f12获取收藏请求,将对应的配置参数替换
    // ========== 全局配置参数 ==========
    // Emby服务器基础URL
    const BASE_URL = 'http://192.168.123.202:8096';

    // 用户ID和认证信息
    const USER_ID = 'e1bca16aaf23497e8b8f33cdee760e48';
    const AUTH_TOKEN = '49305c8ae7304a168e30f70aac9c88bc';

    // 设备信息参数
    const DEVICE_PARAMS = {
        'X-Emby-Client': 'Emby Web',
        'X-Emby-Device-Name': 'Chrome Windows',
        'X-Emby-Device-Id': 'eb7e48ba-387b-4c1a-8b02-e47cda4a351d',
        'X-Emby-Client-Version': '4.9.1.4',
        'X-Emby-Token': AUTH_TOKEN,
        'X-Emby-Language': 'zh-cn'
    };

    // ========== 工具函数 ==========

    /**
     * 构建完整的API URL
     * @param {string} endpoint - API端点路径
     * @returns {string} 完整的URL
     */
    function buildApiUrl(endpoint) {
        const params = new URLSearchParams(DEVICE_PARAMS).toString();
        return `${BASE_URL}${endpoint}?${params}`;
    }

    /**
     * 检查当前页面是否为视频OSD页面
     * @returns {boolean}
     */
    function isVideoOSDPage() {
        return window.location.hash.includes('videoosd');
    }

    /**
     * 从当前页面获取ITEM_ID
     * @returns {string|null} 视频ID
     */
    function getItemId() {
        /** TODO:
          1. 获取item id,收藏状态
          2. 收藏状态,取消收藏状态
          3. 取消收藏请求
        */
        const videoElement = document.querySelector('.htmlvideoplayer');
        if (!videoElement || !videoElement.src) return null;

        const match = videoElement.src.match(/\/(\d+)\/original\.mp4/);
        return match ? match[1] : null;
    }

    /**
     * 发送收藏/取消收藏请求
     * @param {string} itemId - 视频ID
     */
    function toggleFavorite(itemId) {
        if (!itemId) {
            showNotification('无法获取视频ID', 'error');
            return;
        }

        // 构建收藏API URL
        const endpoint = `/emby/Users/${USER_ID}/FavoriteItems/${itemId}`;
        const url = buildApiUrl(endpoint);

        // TODO: 需要先检查当前收藏状态,再决定是收藏还是取消收藏
        // 目前只实现收藏功能
        fetch(url, {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            }
        })
        .then(response => response.json())
        .then(data => {
            if (data.IsFavorite === true) {
                showNotification('收藏成功!', 'success');
            }
        })
    }

    /**
     * 显示通知
     * @param {string} message - 通知消息
     * @param {string} type - 通知类型: 'success'或'error'
     */
    function showNotification(message, type) {
        // 移除已有的通知
        const existingNotification = document.getElementById('custom-notification');
        if (existingNotification) existingNotification.remove();

        // 创建通知元素
        const notification = document.createElement('div');
        notification.id = 'custom-notification';
        notification.textContent = message;

        // 设置通知样式
        Object.assign(notification.style, {
            position: 'fixed',
            top: '20px',
            right: '20px',
            padding: '12px 20px',
            background: type === 'success' ? 'rgba(76, 175, 80, 0.9)' : 'rgba(244, 67, 54, 0.9)',
            color: 'white',
            borderRadius: '4px',
            boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
            zIndex: '10000',
            fontSize: '14px',
            fontWeight: '500',
            animation: 'slideIn 0.3s ease-out',
            minWidth: '200px',
            textAlign: 'center',
            backdropFilter: 'blur(10px)'
        });

        // 添加到页面
        document.body.appendChild(notification);

        // 3秒后自动移除
        setTimeout(() => {
            notification.style.animation = 'slideOut 0.3s ease-out';
            setTimeout(() => {
                if (notification.parentNode) notification.remove();
            }, 300);
        }, 3000);
    }

    /**
     * 创建收藏按钮
     */
    function createFavoriteButton() {
        // 检查是否已存在按钮
        if (document.getElementById('custom-favorite-button')) return;

        const button = document.createElement('button');
        button.id = 'custom-favorite-button';
        button.innerHTML = '❤️';
        button.title = '收藏';

        // 设置按钮样式
        Object.assign(button.style, {
            position: 'fixed',
            right: '20px',
            top: '50%',
            transform: 'translateY(-50%)',
            zIndex: '9999',
            width: '50px',
            height: '50px',
            fontSize: '24px',
            background: 'rgba(0, 0, 0, 0.7)',
            border: '2px solid #fff',
            borderRadius: '50%',
            color: '#fff',
            cursor: 'pointer',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            transition: 'all 0.3s ease'
        });

        // 添加悬停效果
        button.addEventListener('mouseenter', function() {
            this.style.background = 'rgba(255, 0, 0, 0.7)';
            this.style.transform = 'translateY(-50%) scale(1.1)';
        });

        button.addEventListener('mouseleave', function() {
            this.style.background = 'rgba(0, 0, 0, 0.7)';
            this.style.transform = 'translateY(-50%) scale(1)';
        });

        // 点击事件
        button.addEventListener('click', function() {
            const itemId = getItemId();
            if (itemId) toggleFavorite(itemId);
        });

        // 添加到页面
        document.body.appendChild(button);

        // 存储按钮引用
        window.customFavoriteButton = button;
    }

    /**
     * 移除收藏按钮
     */
    function removeFavoriteButton() {
        const button = document.getElementById('custom-favorite-button');
        if (button) button.remove();
    }

    /**
     * 主函数:检查页面并管理按钮
     */
    function manageFavoriteButton() {
        if (isVideoOSDPage()) {
            // 等待页面内容加载
            setTimeout(() => {
                const itemId = getItemId();
                if (itemId) {
                    createFavoriteButton();
                } else {
                    // 如果没获取到ID,延迟重试
                    setTimeout(() => {
                        if (getItemId()) createFavoriteButton();
                    }, 1000);
                }
            }, 500);
        } else {
            removeFavoriteButton();
        }
    }

    // ========== 初始化 ==========

    // 页面加载时执行
    if (isVideoOSDPage()) manageFavoriteButton();

    // 监听hash变化(Emby是单页面应用)
    let lastHash = window.location.hash;
    const observer = new MutationObserver(() => {
        if (window.location.hash !== lastHash) {
            lastHash = window.location.hash;
            setTimeout(manageFavoriteButton, 300);
        }
    });

    // 开始观察
    observer.observe(document, {
        subtree: true,
        childList: true
    });

    // 添加全局样式
    const style = document.createElement('style');
    style.textContent = `
        #custom-favorite-button.favorited {
            background: rgba(255, 0, 0, 0.9) !important;
            border-color: #ff4444 !important;
        }

        #custom-favorite-button.favorited:after {
            content: '✓';
            position: absolute;
            font-size: 16px;
            color: white;
            bottom: -5px;
            right: -5px;
        }

        @keyframes slideIn {
            from {
                transform: translateX(100%);
                opacity: 0;
            }
            to {
                transform: translateX(0);
                opacity: 1;
            }
        }

        @keyframes slideOut {
            from {
                transform: translateX(0);
                opacity: 1;
            }
            to {
                transform: translateX(100%);
                opacity: 0;
            }
        }
    `;
    document.head.appendChild(style);
})();