├── README.md ├── actorPlus ├── README.md └── actorPlus.js ├── emby-swiper ├── README.md ├── emby-swiper-trailer └── emby-swiper.js ├── emby-tab ├── README.md └── emby-tab.js ├── fanart_show ├── README.md └── fanart_show.js ├── itemSortForNewDevice ├── README.md └── itemSortForNewDevice.js ├── playbackRate ├── README.md └── playbackRate.js └── trailer ├── 2024-03-28 22-53-09-110.mp4 ├── README.md └── trailer.js /README.md: -------------------------------------------------------------------------------- 1 | ## 使用方法1: 2 | - 配合Emby自定义JavaScript及Css项目:https://github.com/Shurelol/Emby.CustomCssJS 使用更佳。 3 | 4 | ## 使用方法2: 5 | 1. 在客户端或网页的 `index.html`
标签最后,插入以下内容: 6 | ``` 7 | 8 | ``` 9 | 2. 将 `XXX.js` 文件下载放在index.html同级目录中,非window记得授权可读。 10 | 11 | ## 效果说明: 12 | | 序号 | 名称 | 描述 | 13 | | :---: | :--- | :--- | 14 | | 1 | actorPlus | 隐藏没有头像的演员和制作人员 | 15 | | 2 | emby-swiper | 主页轮播图 | 16 | | 3 | emby-tab | 自定义隐藏emby页面顶部中间导航栏按钮 | 17 | | 4 | fanart_show | 显示同人图(fanart图) | 18 | | 5 | itemSortForNewDevice | 修改新设备媒体库各标签默认排序 | 19 | | 6 | playbackRate | 小秘同款播放页手势及按键交互体验 | 20 | | 7 | trailer | Emby影视卡片鼠标悬停播放(预告)(另 XingyiHua2024大手子做得很不错:[https://github.com/XingyiHua2024/Emby-Javascript-Details](https://github.com/XingyiHua2024/Emby-Javascript-Details) ) | 21 | -------------------------------------------------------------------------------- /actorPlus/README.md: -------------------------------------------------------------------------------- 1 | ## 隐藏没有头像的演员和制作人员 2 | 3 | ### 使用前 4 |  5 | 6 | ### 使用后 7 | 8 |  9 | -------------------------------------------------------------------------------- /actorPlus/actorPlus.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | /* page item.Type "Person" "Movie" "Series" "Season" "Episode" "BoxSet" "video-osd" so. */ 4 | const show_pages = ["Movie", "Series", "Episode", "Season", "video-osd"]; 5 | var item, paly_mutation; 6 | 7 | /* document.addEventListener("itemshow", function (e) { 8 | item = e.detail.item; 9 | // if (showFlag() && item.People) { 10 | // item.People = item.People.filter(p => p.PrimaryImageTag); 11 | // } 12 | }); */ 13 | document.addEventListener("viewbeforeshow", function (e) { 14 | paly_mutation?.disconnect(); 15 | if (e.detail.path === "/item" || e.detail.type === "video-osd") { 16 | if (!e.detail.isRestored) { 17 | const mutation = new MutationObserver(async function () { 18 | item = e.target.controller?.currentItem || e.target.controller?.videoOsd?.currentItem || e.target.controller?.currentPlayer?.streamInfo?.item; 19 | if (item) { 20 | mutation.disconnect(); 21 | if (showFlag()) { 22 | if (!item.People) { 23 | item = await ApiClient.getItem(ApiClient.getCurrentUserId(), item.Id); 24 | } 25 | if (item.People.length === 0) { 26 | return; 27 | } 28 | e.detail.path === "/item" && (e.target.querySelector(".peopleItemsContainer").fetchData = filterPeople.bind(item)); 29 | /* item.People = item.People.filter(p => p.PrimaryImageTag); 30 | e.detail.type === "video-osd" && setTimeout(() => { 31 | e.target.controller.videoOsd && (e.target.controller.videoOsd.currentItem.People = item.People); 32 | e.target.controller.currentItem && (e.target.controller.currentItem.People = item.People); 33 | }, 1000); */ 34 | if (e.detail.type === "video-osd") { 35 | paly_mutation = new MutationObserver(function () { 36 | let itemsContainer = e.target.querySelector('[data-index="2"].videoosd-itemstab .itemsContainer'); 37 | if (itemsContainer) { 38 | paly_mutation.disconnect(); 39 | itemsContainer.fetchData = filterPeople.bind(item); 40 | } 41 | 42 | }); 43 | paly_mutation.observe(e.target.querySelector('[data-index="2"].videoosd-itemstab'), { 44 | childList: true, 45 | characterData: true, 46 | subtree: true, 47 | }); 48 | } 49 | } 50 | } 51 | }); 52 | mutation.observe(document.body, { 53 | childList: true, 54 | characterData: true, 55 | subtree: true, 56 | }); 57 | } else { 58 | item = e.target.controller.currentItem || e.target.controller.videoOsd?.currentItem; 59 | } 60 | } 61 | }); 62 | function filterPeople(query) { 63 | var serverId = item.ServerId, 64 | people = (item.People || []).filter(function (p) { 65 | return p.PrimaryImageTag && (p.ServerId = serverId, "Person" !== p.Type && (p.PersonType = p.Type, p.Type = "Person"), !0) 66 | }), 67 | totalRecordCount = people.length; 68 | return query && (people = people.slice(query.StartIndex || 0), 69 | query.Limit && people.length > query.Limit && (people.length = query.Limit)), 70 | Promise.resolve({ 71 | Items: people, 72 | TotalRecordCount: totalRecordCount 73 | }) 74 | } 75 | function showFlag() { 76 | for (let show_page of show_pages) { 77 | if (item.Type == show_page) { 78 | return true; 79 | } 80 | } 81 | return false; 82 | } 83 | 84 | })(); 85 | -------------------------------------------------------------------------------- /emby-swiper/README.md: -------------------------------------------------------------------------------- 1 | ## 主页轮播图 2 | 3 | 参考项目地址:https://github.com/Nolovenodie/emby-crx 使用swiper插件:https://swiperjs.com 4 | 5 | 想隐藏主屏模块1的,自行添加自动义css: 6 | ``` 7 | @media (min-width: 50em) { 8 | .section0 { 9 | display: none; 10 | } 11 | } 12 | ``` 13 | 2024-11-19 添加背景播放youtube的预告片(前提有刮削到):emby-swiper-trailer.js; 14 | 注意:如果之前使用过emby-swiper;换成emby-swiper-trailer.js第二天生效; 15 | 或者在设置-主页-改变一下轮播设置,使得原缓存失效(缓存保存一天;0点起) 16 | ### 预览图 17 |  18 | -------------------------------------------------------------------------------- /emby-tab/README.md: -------------------------------------------------------------------------------- 1 | ## 自定义隐藏emby页面顶部中间导航栏按钮 2 | -------------------------------------------------------------------------------- /emby-tab/emby-tab.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | var item, customtabs = { 4 | movies: [ 5 | { name: "节目", id: "series", enabled: false, }, 6 | { name: "电影", id: "videos", enabled: true, }, 7 | { name: "推荐", id: "suggestions", enabled: true, }, 8 | { name: "预告片", id: "trailers", enabled: false, }, 9 | { name: "照片", id: "photos", enabled: false, }, 10 | { name: "专辑艺术家", id: "albumartists", enabled: false, }, 11 | { name: "艺术家", id: "artists", enabled: false, }, 12 | { name: "播放列表", id: "playlists", enabled: true, }, 13 | { name: "合集", id: "collections", enabled: true, }, 14 | { name: "风格", id: "genres", enabled: true, }, 15 | { name: "标签", id: "tags", enabled: true, }, 16 | { name: "最爱", id: "favorites", enabled: true, }, 17 | { name: "文件夹", id: "folders", enabled: true, }, 18 | ], 19 | tvshows: [ 20 | { name: "节目", id: "series", enabled: true, }, 21 | { name: "推荐", id: "suggestions", enabled: false, }, 22 | { name: "即将上映", id: "upcoming", enabled: false, }, 23 | { name: "最爱", id: "favorites", enabled: true, }, 24 | { name: "合集", id: "collections", enabled: false, }, 25 | { name: "风格", id: "genres", enabled: true, }, 26 | { name: "标签", id: "tags", enabled: true, }, 27 | { name: "网络", id: "studios", enabled: false, }, 28 | { name: "集", id: "episodes", enabled: true, }, 29 | { name: "文件夹", id: "folders", enabled: true, }, 30 | ], 31 | homevideos: [ 32 | { name: "节目", id: "series", enabled: false, }, 33 | { name: "视频", id: "videos", enabled: true, }, 34 | { name: "推荐", id: "suggestions", enabled: false, }, 35 | { name: "预告片", id: "trailers", enabled: false, }, 36 | { name: "照片", id: "photos", enabled: true, }, 37 | { name: "专辑艺术家", id: "albumartists", enabled: false, }, 38 | { name: "艺术家", id: "artists", enabled: false, }, 39 | { name: "播放列表", id: "playlists", enabled: false, }, 40 | { name: "合集", id: "collections", enabled: false, }, 41 | { name: "风格", id: "genres", enabled: false, }, 42 | { name: "标签", id: "tags", enabled: false, }, 43 | { name: "最爱", id: "favorites", enabled: false, }, 44 | { name: "文件夹", id: "folders", enabled: true, }, 45 | ], 46 | music: [ 47 | { name: "建议", id: "suggestions", enabled: false, }, 48 | { name: "专辑", id: "albums", enabled: true, }, 49 | { name: "专辑艺术家", id: "albumartists", enabled: true, }, 50 | { name: "艺术家", id: "artists", enabled: true, }, 51 | { name: "作曲家", id: "composers", enabled: false, }, 52 | { name: "播放列表", id: "playlists", enabled: true, }, 53 | { name: "风格", id: "genres", enabled: false, }, 54 | { name: "歌曲", id: "songs", enabled: true, }, 55 | { name: "标签", id: "tags", enabled: false, }, 56 | { name: "文件夹", id: "folders", enabled: true, }, 57 | ], 58 | }; 59 | document.addEventListener("viewbeforeshow", function (e) { 60 | if (e.detail.path === "/videos" || e.detail.path === "/music" || e.detail.path === "/tv") { 61 | if (!e.detail.isRestored) { 62 | const mutation = new MutationObserver(async function () { 63 | item = e.target.controller?.item 64 | if (item) { 65 | mutation.disconnect(); 66 | // !ApiClient.isMinServerVersion("4.8.0.60") && (customtabs.tvshows = customtabs.tvshows.filter(function (tab) { return tab.id !== "tags" })); 67 | let embytabs = document.querySelectorAll(".headerMiddle .main-tab-button"); 68 | if (item.CollectionType === 'tvshows') { 69 | if (embytabs.length === 8) { 70 | customtabs.tvshows = customtabs.tvshows.filter(function (tab) { return tab.id !== "collections" && tab.id !== "tags" }) 71 | } else if (embytabs.length === 9) { 72 | customtabs.tvshows = customtabs.tvshows.filter(function (tab) { return tab.id !== "tags" }) 73 | } 74 | } 75 | let optionstab = customtabs[item.CollectionType]; 76 | if (optionstab) { 77 | e.target.controller._tabs = optionstab; 78 | for (let i = 0; i < embytabs.length; i++) { 79 | if (!optionstab[i].enabled) { 80 | embytabs[i].classList.add("hide"); 81 | } else { 82 | embytabs[i].textContent = optionstab[i].name; 83 | } 84 | 85 | } 86 | }; 87 | } 88 | }); 89 | mutation.observe(document.body, { 90 | childList: true, 91 | characterData: true, 92 | subtree: true, 93 | }); 94 | } else { 95 | item = e.target.controller?.item; 96 | } 97 | } 98 | }); 99 | })() 100 | -------------------------------------------------------------------------------- /fanart_show/README.md: -------------------------------------------------------------------------------- 1 | ## 显示同人图(fanart图) 2 | 3 | 使用Swiper插件:https://swiperjs.com 4 | 及Viewer插件:https://fengyuanchen.github.io/viewerjs 5 | 6 | ### 预览图 7 |  8 | -------------------------------------------------------------------------------- /itemSortForNewDevice/README.md: -------------------------------------------------------------------------------- 1 | ## 修改新设备媒体库各标签默认排序 2 | -------------------------------------------------------------------------------- /itemSortForNewDevice/itemSortForNewDevice.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | var sort = { 4 | movies: { 5 | videos: { sortby: "ProductionYear,PremiereDate,SortName", sortorder: "Descending" }, 6 | collections: { sortby: "DateCreated,SortName", sortorder: "Descending" }, 7 | folders: { sortby: "SortName", sortorder: "Ascending" } 8 | }, 9 | tvshows: { 10 | series: { sortby: "ProductionYear,PremiereDate,SortName", sortorder: "Descending" }, 11 | videos: { sortby: "ProductionYear,PremiereDate,SortName", sortorder: "Descending" }, 12 | collections: { sortby: "DateCreated,SortName", sortorder: "Descending" }, 13 | folders: { sortby: "SortName", sortorder: "Ascending" } 14 | }, 15 | homevideos: { 16 | videos: { sortby: "DateCreated,SortName", sortorder: "Descending" }, 17 | photos: { sortby: "DateCreated,SortName", sortorder: "Descending" }, 18 | folders: { sortby: "SortName", sortorder: "Ascending" } 19 | }, 20 | music: { 21 | albums: { sortby: "ProductionYear,PremiereDate,SortName", sortorder: "Descending" }, 22 | albumartists: { sortby: "SortName", sortorder: "Ascending" }, 23 | artists: { sortby: "SortName", sortorder: "Ascending" }, 24 | composers: { sortby: "SortName", sortorder: "Ascending" }, 25 | songs: { sortby: "ProductionYear,PremiereDate,SortName", sortorder: "Descending" }, 26 | folders: { sortby: "SortName", sortorder: "Ascending" } 27 | } 28 | }; 29 | const mutation = new MutationObserver(function () { 30 | if (window.ApiClient?.connected) { 31 | setItemSort(); 32 | mutation.disconnect(); 33 | } 34 | }); 35 | mutation.observe(document.body, { 36 | childList: true, 37 | characterData: true, 38 | subtree: true, 39 | }); 40 | async function setItemSort() { 41 | // let libdata = await ApiClient.getItems(ApiClient.getCurrentUserId()); 42 | let libdata = await ApiClient.getUserViews({}, ApiClient.getCurrentUserId()); 43 | for (let item of libdata.Items) { 44 | let types = sort[item.CollectionType]; 45 | if (types) { 46 | for (let key in types) { 47 | let sortbyId = ApiClient.getCurrentUserId() + "-" + item.Id + "-1-" + key + "-sortby" 48 | , sortorderId = ApiClient.getCurrentUserId() + "-" + item.Id + "-1-" + key + "-sortorder"; 49 | if (!localStorage.getItem(sortbyId)) { 50 | localStorage.setItem(sortbyId, types[key].sortby); 51 | localStorage.setItem(sortorderId, types[key].sortorder); 52 | } 53 | } 54 | } 55 | } 56 | }; 57 | })() 58 | -------------------------------------------------------------------------------- /playbackRate/README.md: -------------------------------------------------------------------------------- 1 | ## 小秘同款播放页手势及按键交互体验 2 | 3 | 主要代码提取自小秘和(叮。叮。当。),特此鸣谢!俺只是大自然的搬运工! 4 | -------------------------------------------------------------------------------- /playbackRate/playbackRate.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | var keydownFlag = false; 4 | var intervalId, viewnode, view, 5 | videoOsdVolumeControls, 6 | brightnessSlider, 7 | brightnessSliderContainer, 8 | videoOsdVolumeSliderWrapper, 9 | nowPlayingVolumeSlider, 10 | nowPlayingSliderValue, 11 | my_touches_type, 12 | my_touches_start, 13 | my_touches_value, 14 | my_touches_nowvalue, 15 | my_touches_rate, 16 | my_touches_time, 17 | dragByGuesture; 18 | const keyMap = { 19 | "ArrowRight": { rate: 2, message: "2.0 倍数" }, 20 | "Right": { rate: 2, message: "2.0 倍数" }, 21 | "NavigationRight": { rate: 2, message: "2.0 倍数" }, 22 | "GamepadDPadRight": { rate: 2, message: "2.0 倍数" }, 23 | "GamepadLeftThumbStickRight": { rate: 2, message: "2.0 倍数" }, 24 | "GamepadLeftThumbstickRight": { rate: 2, message: "2.0 倍数" }, 25 | "ArrowLeft": { rate: 1, message: "1.0 倍数" }, 26 | "Left": { rate: 1, message: "1.0 倍数" }, 27 | "NavigationLeft": { rate: 1, message: "1.0 倍数" }, 28 | "GamepadDPadLeft": { rate: 1, message: "1.0 倍数" }, 29 | "GamepadLeftThumbStickLeft": { rate: 1, message: "1.0 倍数" }, 30 | "GamepadLeftThumbstickLeft": { rate: 1, message: "1.0 倍数" }, 31 | "`": { rate: 1, message: "1.0 倍数" }, 32 | "0": { rate: 1, message: "1.0 倍数" }, 33 | "1": { rate: 1.5, message: "1.5 倍数" }, 34 | "2": { rate: 2, message: "2.0 倍数" }, 35 | "3": { rate: 2.5, message: "2.5 倍数" }, 36 | "4": { rate: 3, message: "3.0 倍数" }, 37 | "5": { rate: 3.5, message: "3.5 倍数" }, 38 | "6": { rate: 4, message: "4.0 倍数" }, 39 | }; 40 | var isTouchDevice = 'ontouchstart' in document.documentElement; 41 | document.addEventListener("viewbeforeshow", function (e) { 42 | if (e.detail.type === "video-osd") { 43 | viewnode = e.target; 44 | view = viewnode.controller.videoOsd ?? viewnode.controller; 45 | window.addEventListener("keydown", keydownEvent); 46 | window.addEventListener("keyup", keyupEvent); 47 | if (isTouchDevice) { 48 | videoOsdVolumeControls = viewnode.querySelector(".videoOsdVolumeControls"), 49 | brightnessSlider = viewnode.querySelector(".videoOsdBrightnessSlider"), 50 | brightnessSliderContainer = viewnode.querySelector(".brightnessSliderContainer"), 51 | videoOsdVolumeSliderWrapper = viewnode.querySelector(".videoOsdVolumeSliderWrapper"), 52 | nowPlayingVolumeSlider = viewnode.querySelector(".videoOsdVolumeSlider"), 53 | nowPlayingSliderValue = view.nowPlayingPositionSlider.valueAsNumber; 54 | my_touches_type = null; 55 | my_touches_start = null; 56 | my_touches_value = 0; 57 | my_touches_nowvalue = 0; 58 | my_touches_rate = null; 59 | my_touches_time = null; 60 | dragByGuesture = false; 61 | viewnode.addEventListener("touchstart", touchstartEvent); 62 | viewnode.addEventListener("touchmove", touchmoveEvent); 63 | viewnode.addEventListener("touchcancel", touchcancelEvent); 64 | viewnode.addEventListener("touchend", touchendEvent); 65 | } 66 | } else { 67 | window.removeEventListener("keydown", keydownEvent); 68 | window.removeEventListener("keyup", keyupEvent); 69 | if (isTouchDevice) { 70 | viewnode?.removeEventListener("touchstart", touchstartEvent); 71 | viewnode?.removeEventListener("touchmove", touchmoveEvent); 72 | viewnode?.removeEventListener("touchcancel", touchcancelEvent); 73 | viewnode?.removeEventListener("touchend", touchendEvent); 74 | viewnode = null, view = null; 75 | } 76 | } 77 | }); 78 | 79 | function keydownEvent(event) { 80 | var key = event.key; 81 | if (keydownFlag || !keyMap[key]) { 82 | return; 83 | } 84 | keydownFlag = true; 85 | var handleKeyDown = function () { 86 | var rate = keyMap[key].rate; 87 | var message = keyMap[key].message; 88 | require(["toast"], function (toast) { 89 | toast(message); 90 | }); 91 | view.currentPlayer.setPlaybackRate(rate); 92 | }; 93 | intervalId = setTimeout(handleKeyDown, 500); 94 | } 95 | function keyupEvent(event) { 96 | var key = event.key; 97 | if (keyMap[key]) { 98 | clearTimeout(intervalId); 99 | keydownFlag = false; 100 | } 101 | } 102 | function touchstartEvent(e) { 103 | nowPlayingSliderValue = view.nowPlayingPositionSlider.valueAsNumber; 104 | if (my_touches_start == null && !view.currentVisibleMenu && (!view.currentLockState || view.currentLockState === 0)) { 105 | brightnessSliderContainer.style.setProperty("display", "none", "important"); 106 | videoOsdVolumeControls.style.setProperty("display", "none", "important"); 107 | my_touches_type = null; 108 | my_touches_start = e.touches[0]; 109 | my_touches_value = 0; 110 | my_touches_time = setTimeout(() => { 111 | if ( 112 | my_touches_type === null && 113 | my_touches_rate === null && 114 | !view.nowPlayingPositionSlider.dragging && 115 | Math.abs(e.touches[0].clientX - my_touches_start.clientX) < 10 && 116 | Math.abs(e.touches[0].clientY - my_touches_start.clientY) < 10 && 117 | touchRange(my_touches_start) 118 | ) { 119 | my_touches_rate = view.currentPlayer.getPlaybackRate(); 120 | window.navigator.vibrate(15); 121 | view.currentPlayer.setPlaybackRate(my_touches_rate * 2); 122 | view.boundHideOsd(); 123 | require(["toast"], function (toast) { 124 | toast(my_touches_rate * 2 + " 倍数"); 125 | }); 126 | } 127 | }, 700); 128 | } 129 | } 130 | 131 | function touchmoveEvent(e) { 132 | if (dragByGuesture || (!view.nowPlayingPositionSlider.dragging && my_touches_start != null && my_touches_rate === null)) { 133 | var x = e.touches[0].pageX - my_touches_start.pageX; 134 | var y = e.touches[0].pageY - my_touches_start.pageY; 135 | if (touchRange(my_touches_start)) { 136 | if ((my_touches_type === null && Math.abs(x) > 20) || my_touches_type === "play") { 137 | if (my_touches_type === null) { 138 | my_touches_type = "play"; 139 | my_touches_start = e.touches[0]; 140 | my_touches_nowvalue = nowPlayingSliderValue; 141 | } else { 142 | my_touches_value = my_touches_func(e.touches[0], x, my_touches_nowvalue, screen.width, true); 143 | dragByGuesture = true; 144 | view.nowPlayingPositionSlider.beginEditing(my_touches_value); 145 | } 146 | } else if (Math.abs(y) > 20 || my_touches_type === "volume" || my_touches_type === "bright") { 147 | if ((Math.abs(x) < 20 && Math.abs(y) > 20 && my_touches_type === null && my_touches_start.pageX >= screen.width / 2) || my_touches_type === "volume") { 148 | if (my_touches_type === null) { 149 | my_touches_start = e.touches[0]; 150 | my_touches_type = "volume"; 151 | my_touches_nowvalue = view.currentPlayer.getVolume(); 152 | videoOsdVolumeControls.classList.add("videoOsdVolumeControls-showhover"); 153 | videoOsdVolumeControls.classList.remove("videoOsdVolumeControls-hidetouch", "hide"); 154 | videoOsdVolumeControls.style.setProperty("display", "flex", "important"); 155 | videoOsdVolumeSliderWrapper.style.setProperty("display", "flex", "important"); 156 | } else { 157 | my_touches_value = Math.floor(my_touches_func(e.touches[0], y, my_touches_nowvalue, screen.height, false)); 158 | if (view.currentPlayer.getVolume() != my_touches_value) { 159 | nowPlayingVolumeSlider.setValue(my_touches_value); 160 | view.currentPlayer.setVolume(my_touches_value); 161 | } 162 | } 163 | view.boundShowOsdDefaultParams(); 164 | } else if ((Math.abs(x) < 20 && Math.abs(y) > 20 && my_touches_type === null && my_touches_start.pageX < screen.width / 2) || my_touches_type === "bright") { 165 | if (my_touches_type === null) { 166 | my_touches_start = e.touches[0]; 167 | my_touches_type = "bright"; 168 | my_touches_nowvalue = view.currentPlayer.getBrightness(); 169 | brightnessSliderContainer.classList.remove("hide"); 170 | brightnessSliderContainer.style.setProperty("display", "flex", "important"); 171 | } else { 172 | my_touches_value = Math.floor( 173 | my_touches_func(e.touches[0], y, my_touches_nowvalue, screen.height, false) 174 | ); 175 | if (view.currentPlayer.getBrightness() != my_touches_value) { 176 | brightnessSlider.setValue(my_touches_value); 177 | view.currentPlayer.setBrightness(my_touches_value); 178 | } 179 | } 180 | view.boundShowOsdDefaultParams(); 181 | } 182 | } 183 | } 184 | } 185 | } 186 | 187 | function touchcancelEvent(e) { 188 | if (my_touches_start && (my_touches_value !== 0 || my_touches_type != null)) { 189 | if (my_touches_type === "play") { 190 | nowPlayingSliderValue = my_touches_value; 191 | view.nowPlayingPositionSlider.setValue(my_touches_value); 192 | view.nowPlayingPositionSlider.endEditing(!0, nowPlayingSliderValue); 193 | view.boundHideOsd(); 194 | } else if (my_touches_type === "volume") { 195 | videoOsdVolumeControls.classList.add("videoOsdVolumeControls-hidetouch", "hide"); 196 | videoOsdVolumeControls.style.setProperty("display", "none", "important"); 197 | videoOsdVolumeSliderWrapper.style.setProperty("display", "none", "important"); 198 | } else if (my_touches_type === "bright") { 199 | brightnessSliderContainer.style.setProperty("display", "none", "important"); 200 | } 201 | my_touches_start = null; 202 | my_touches_type = null; 203 | } 204 | my_touches_start = null; 205 | if (my_touches_time != null) { 206 | clearTimeout(my_touches_time); 207 | my_touches_time = null; 208 | } 209 | if (my_touches_rate != null) { 210 | view.currentPlayer.setPlaybackRate(my_touches_rate); 211 | my_touches_rate = null; 212 | } 213 | dragByGuesture = false; 214 | } 215 | function touchendEvent(e) { 216 | if (my_touches_start && (my_touches_value !== 0 || my_touches_type != null)) { 217 | if (my_touches_type === "play") { 218 | window.navigator.vibrate(10); 219 | nowPlayingSliderValue = my_touches_value; 220 | view.nowPlayingPositionSlider.setValue(my_touches_value); 221 | view.nowPlayingPositionSlider.endEditing(!0, my_touches_value); 222 | view.boundHideOsd(); 223 | } else if (my_touches_type === "volume") { 224 | window.navigator.vibrate(7); 225 | videoOsdVolumeControls.classList.add("videoOsdVolumeControls-hidetouch", "hide"); 226 | videoOsdVolumeControls.style.setProperty("display", "none", "important"); 227 | videoOsdVolumeSliderWrapper.style.setProperty("display", "none", "important"); 228 | } else if (my_touches_type === "bright") { 229 | window.navigator.vibrate(7); 230 | brightnessSliderContainer.style.setProperty("display", "none", "important"); 231 | } 232 | my_touches_start = null; 233 | my_touches_type = null; 234 | } 235 | my_touches_start = null; 236 | if (my_touches_time != null) { 237 | clearTimeout(my_touches_time); 238 | my_touches_time = null; 239 | } 240 | if (my_touches_rate != null) { 241 | view.currentPlayer.setPlaybackRate(my_touches_rate); 242 | my_touches_rate = null; 243 | } 244 | dragByGuesture = false; 245 | } 246 | function my_touches_func(e, v, t, x, s) { 247 | var step = (Math.abs(v) / (s ? x * 5 : x * 0.5)) * 100; 248 | step = v > 0 ? step : -step; 249 | v = s ? t + step : t - step; 250 | step = v >= 100 ? 100 : v <= 0 ? 0 : v; 251 | if (v >= 99 || v <= 1) { 252 | my_touches_start = e; 253 | my_touches_nowvalue = step; 254 | } 255 | return step; 256 | } 257 | function touchRange(touch) { 258 | var margin = 20; 259 | if (touch.clientX > margin && 260 | touch.clientX < screen.width - margin && 261 | touch.clientY > margin && 262 | touch.clientY < screen.height - margin) { 263 | return true; 264 | } 265 | return false; 266 | } 267 | })(); 268 | -------------------------------------------------------------------------------- /trailer/2024-03-28 22-53-09-110.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newday-life/emby-web-mod/694e093ab7ad2eb347c87a8367dbce44b65c9086/trailer/2024-03-28 22-53-09-110.mp4 -------------------------------------------------------------------------------- /trailer/README.md: -------------------------------------------------------------------------------- 1 | ## Emby影视卡片鼠标悬停播放(预告片) 2 | 3 | 4 | 代码参考Bolin Chan的项目https://github.com/bolin-dev/JavPack 中的JavDB.trailer.user.js,特此致谢! 5 | 6 | https://github.com/newday-life/emby-front-end-mod/assets/25839463/d4826e18-9d8e-4360-8f72-d58189d4af5d 7 | 8 | -------------------------------------------------------------------------------- /trailer/trailer.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | class Util { 3 | static upLocal() { 4 | const storageName = []; 5 | for (var i = 0; i < localStorage.length; i++) { 6 | if (localStorage.key(i).includes("TRAILERCACHE|")) { 7 | let val = localStorage.getItem(localStorage.key(i)); 8 | if (!val) continue; 9 | try { 10 | val = JSON.parse(val); 11 | if (Date.now() - val.time > val.expired) { 12 | storageName.push(localStorage.key(i)); 13 | } 14 | } catch (error) { 15 | storageName.push(localStorage.key(i)); 16 | console.log("error:", error); 17 | } 18 | 19 | } 20 | } 21 | storageName.length > 0 && storageName.forEach((name) => { 22 | localStorage.removeItem(name); 23 | }); 24 | } 25 | } 26 | Util.upLocal(); 27 | /* 预告URL缓存2天 */ 28 | const trailer_cache_time = 2 * 8.64e7; 29 | const storage = { 30 | set(key, val, expired) { 31 | let obj = { data: val, time: Date.now(), expired }; 32 | localStorage.setItem(key, JSON.stringify(obj)); 33 | }, 34 | get(key) { 35 | let val = localStorage.getItem(key); 36 | if (!val) { return val; } 37 | val = JSON.parse(val); 38 | if (Date.now() - val.time > val.expired) { 39 | localStorage.removeItem(key); 40 | return null 41 | } 42 | return val.data; 43 | }, 44 | remove(key) { 45 | localStorage.removeItem(key); 46 | } 47 | }; 48 | 49 | const cache = { 50 | trailer: new Map() 51 | }; 52 | 53 | function useVideo() { 54 | const handleKeydown = (e) => { 55 | e.preventDefault(); 56 | e.stopPropagation(); 57 | const { code, target } = e; 58 | if (code === "KeyM") target.muted = !target.muted; 59 | if (code === "KeyW" || code === "ArrowUp") target.volume += 0.1; 60 | if (code === "KeyA" || code === "ArrowLeft") target.currentTime -= 2; 61 | if (code === "KeyS" || code === "ArrowDown") target.volume -= 0.1; 62 | if (code === "KeyD" || code === "ArrowRight") target.currentTime += 4; 63 | }; 64 | 65 | const handleVolumechange = ({ target }) => localStorage.setItem("volume", target.volume); 66 | 67 | return (src, poster) => { 68 | const video = document.createElement("video"); 69 | 70 | video.src = src; 71 | video.title = ""; 72 | video.poster = poster; 73 | video.controls = true; 74 | video.preload = "none"; 75 | video.volume = localStorage.getItem("volume") ?? 0.2; 76 | 77 | video.addEventListener("keydown", handleKeydown); 78 | video.addEventListener("volumechange", handleVolumechange); 79 | return video; 80 | }; 81 | } 82 | 83 | const createVideo = useVideo(); 84 | 85 | (function () { 86 | const TARGET_SELECTOR = ".itemsContainer .cardOverlayContainer"; 87 | 88 | ((a) => { 89 | if (typeof GM_addStyle == "function") { 90 | GM_addStyle(a); 91 | return; 92 | } 93 | const t = document.createElement("style"); 94 | (t.textContent = a), document.head.append(t); 95 | })(` 96 | ${TARGET_SELECTOR} video { 97 | position: absolute; 98 | inset: 0; 99 | z-index: 1; 100 | width: 100%; 101 | height: 100%; 102 | background: #000; 103 | opacity: 0; 104 | transition: opacity 0.2s ease-in-out; 105 | object-fit: contain; 106 | } 107 | ${TARGET_SELECTOR} video.fade-in { 108 | opacity: 1; 109 | } 110 | `); 111 | 112 | let currElem = null; 113 | 114 | function handleMouse(onHover) { 115 | const interval = 200; 116 | const sensitivity = 0; 117 | 118 | let scrollTimer = null; 119 | let isScrolling = false; 120 | let trackSpeedInterval = null; 121 | 122 | let prevX = null; 123 | let prevY = null; 124 | let prevTime = null; 125 | 126 | let lastX = null; 127 | let lastY = null; 128 | let lastTime = null; 129 | 130 | const handleMouseover = (e) => { 131 | if (currElem) return; 132 | 133 | const target = e.target.closest(TARGET_SELECTOR); 134 | if (!target) return; 135 | 136 | prevX = e.pageX; 137 | prevY = e.pageY; 138 | prevTime = Date.now(); 139 | 140 | currElem = target; 141 | currElem.addEventListener("mousemove", handleMousemove); 142 | trackSpeedInterval = setInterval(trackSpeed, interval); 143 | }; 144 | 145 | const handleMousemove = (e) => { 146 | lastX = e.pageX; 147 | lastY = e.pageY; 148 | lastTime = Date.now(); 149 | }; 150 | 151 | const trackSpeed = () => { 152 | let speed; 153 | 154 | if (!lastTime || lastTime === prevTime) { 155 | speed = 0; 156 | } else { 157 | speed = Math.sqrt(Math.pow(prevX - lastX, 2) + Math.pow(prevY - lastY, 2)) / (lastTime - prevTime); 158 | } 159 | 160 | if (speed <= sensitivity && isElementInViewport(currElem) && !isScrolling) { 161 | destroy(currElem); 162 | onHover(currElem); 163 | } else { 164 | prevX = lastX; 165 | prevY = lastY; 166 | prevTime = Date.now(); 167 | } 168 | }; 169 | 170 | const isElementInViewport = (elem) => { 171 | const rect = elem.getBoundingClientRect(); 172 | return ( 173 | rect.top >= 0 && 174 | rect.left >= 0 && 175 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && 176 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) 177 | ); 178 | }; 179 | 180 | const handleMouseout = ({ relatedTarget }) => { 181 | if (!currElem) return; 182 | 183 | let node = relatedTarget; 184 | while (node) { 185 | if (node === currElem) return; 186 | node = node.parentNode; 187 | } 188 | 189 | destroy(currElem); 190 | onLeave(); 191 | currElem = null; 192 | }; 193 | 194 | const destroy = (elem) => { 195 | elem.removeEventListener("mousemove", handleMousemove); 196 | clearInterval(trackSpeedInterval); 197 | }; 198 | 199 | const onLeave = () => { 200 | if (window.youtubeplayer) { youtubeplayer.destroy(); youtubeplayer = null; } 201 | const videos = document.querySelectorAll(`${TARGET_SELECTOR} video`); 202 | videos.forEach((video) => { 203 | video.classList.remove("fade-in"); 204 | setTimeout(() => video.remove(), 200); 205 | }); 206 | }; 207 | 208 | const onOver = () => { 209 | if (!currElem) return; 210 | 211 | destroy(currElem); 212 | onLeave(); 213 | currElem = null; 214 | }; 215 | 216 | const handleScroll = () => { 217 | isScrolling = true; 218 | clearTimeout(scrollTimer); 219 | scrollTimer = setTimeout(() => { 220 | isScrolling = false; 221 | }, 500); 222 | }; 223 | 224 | document.addEventListener("mouseover", handleMouseover); 225 | document.addEventListener("mouseout", handleMouseout); 226 | document.addEventListener("visibilitychange", onOver); 227 | window.addEventListener("scroll", handleScroll); 228 | window.addEventListener("blur", onOver); 229 | } 230 | 231 | function handleHover() { 232 | const setVideo = (elem, trailer, cover) => { 233 | const video = createVideo(trailer, cover); 234 | elem.append(video); 235 | 236 | video.muted = true; 237 | video.currentTime = 4; 238 | video.focus(); 239 | video.setAttribute('crossorigin', 'anonymous'); 240 | video.play(); 241 | 242 | const ctx = new AudioContext(); 243 | const canAutoPlay = ctx.state === "running"; 244 | ctx.close(); 245 | 246 | if (canAutoPlay) video.muted = false; 247 | setTimeout(() => video.classList.add("fade-in"), 50); 248 | }; 249 | const setYouTube = (elem, id) => { 250 | const videoContainer = document.createElement('video'); 251 | videoContainer.id = 'player'; 252 | videoContainer.style.zIndex = 1; 253 | videoContainer.style["pointer-events"] = "none"; 254 | elem.append(videoContainer); 255 | window.youtubeplayer = new YT.Player(videoContainer, { 256 | height: '100%', width: '100%', videoId: id, 257 | events: { 258 | 'onReady': event => { 259 | event.target.mute(); 260 | event.target.playVideo(); 261 | }, 262 | 'onStateChange': event => { 263 | 264 | if (event.data === YT.PlayerState.UNSTARTED) { 265 | // event.target.playVideo(); 266 | } 267 | if (event.data === YT.PlayerState.ENDED) { 268 | if (youtubeplayer) { youtubeplayer.destroy(); youtubeplayer = null; } 269 | } 270 | 271 | }, 272 | 'onError': () => { 273 | console.error(`YouTube prevented playback of '${id}'`); 274 | if (youtubeplayer) { youtubeplayer.destroy(); youtubeplayer = null; } 275 | } 276 | }, 277 | playerVars: { 278 | autoplay: 1, 279 | controls: 0, 280 | enablejsapi: 1, 281 | modestbranding: 1, 282 | rel: 0, 283 | showinfo: 0, 284 | fs: 0, 285 | playsinline: 1, 286 | }, 287 | }); 288 | } 289 | const setRemoteTrailer = (elem, url) => { 290 | if (!url || elem.querySelector("video")) return; 291 | if (url.includes('youtube.com') || url.includes('youtu.be')) { 292 | let youtubeid = new URL(url).searchParams.get('v') || new URL(url).pathname.split('/').pop(); 293 | 294 | if (window.YT) { 295 | window.YT.ready(function () { if (elem === currElem) setYouTube(elem, youtubeid) }) 296 | } 297 | else { 298 | var _reason, firstScriptTag; 299 | !document.getElementById("youtubeapi") && 300 | (((_reason = document.createElement("script")).src = "https://www.youtube.com/iframe_api"), _reason.id = "youtubeapi", 301 | (firstScriptTag = document.getElementsByTagName("script")[0]).parentNode.insertBefore(_reason, firstScriptTag), _reason.onload = function () { 302 | let isexistsyt = setInterval(() => { 303 | window.YT && (clearInterval(isexistsyt), window.YT.ready(function () { if (elem === currElem) setYouTube(elem, youtubeid) })); 304 | }, 1000); 305 | setTimeout(() => clearInterval(isexistsyt), 10000); 306 | }); 307 | } 308 | }; 309 | }; 310 | const LOAD_TRAILER = "x-loading-trailers"; 311 | 312 | return async (elem) => { 313 | const { classList, dataset } = elem; 314 | if (classList.contains(LOAD_TRAILER)) return; 315 | let { trailer, cover, mid } = dataset; 316 | if (trailer) { 317 | if (trailer.includes('youtube.com') || trailer.includes('youtu.be')) { 318 | return setRemoteTrailer(elem, trailer); 319 | } else { 320 | return setVideo(elem, trailer, cover); 321 | } 322 | } 323 | 324 | if (!cover || !mid) { 325 | const parentNode = elem.closest(".card"); 326 | if (!parentNode) return; 327 | var _itemSource = parentNode.closest(".itemsContainer")?._itemSource || parentNode.closest(".itemsContainer")?.items; 328 | if (!_itemSource) return; 329 | var _item = _itemSource[parentNode._dataItemIndex ?? parentNode.dataset.index] 330 | if (!_item) return; 331 | if (_item.Type !== 'Movie' || _item.MarkerType === 'Chapter') return; 332 | cover = parentNode.querySelector("img").src; 333 | mid = _item.Id; 334 | if (!mid) return; 335 | dataset.mid = mid; 336 | dataset.cover = cover; 337 | const setTrailer = (trailer) => { 338 | if (!trailer || elem.querySelector("video")) return; 339 | if (!dataset.trailer) { 340 | dataset.trailer = trailer; 341 | } 342 | if (elem === currElem) setVideo(elem, trailer, cover); 343 | }; 344 | 345 | const trailerMid = `TRAILERCACHE|${mid}`; 346 | trailer = storage.get(trailerMid) 347 | if (trailer) { 348 | if (trailer.includes('youtube.com') || trailer.includes('youtu.be')) { 349 | dataset.trailer = trailer; 350 | return setRemoteTrailer(elem, trailer); 351 | } else { 352 | dataset.trailer = trailer; 353 | return setVideo(elem, trailer, cover); 354 | } 355 | } 356 | 357 | 358 | classList.add(LOAD_TRAILER); 359 | const Fullitem = await ApiClient.getItem(ApiClient.getCurrentUserId(), _item.Id); 360 | if (Fullitem.LocalTrailerCount > 0) { 361 | let url = await getTrailersUrl(trailerMid, _item); 362 | dataset.trailer = url; 363 | setTrailer(url); 364 | } else if (Fullitem.RemoteTrailers?.length > 0) { 365 | let url = Fullitem.RemoteTrailers[0].Url; 366 | storage.set(trailerMid, url, trailer_cache_time); 367 | dataset.trailer = url; 368 | setRemoteTrailer(elem, url); 369 | } 370 | } 371 | classList.remove(LOAD_TRAILER); 372 | }; 373 | } 374 | handleMouse(handleHover()); 375 | })(); 376 | async function getTrailersUrl(trailerMid, item) { 377 | var videourl = ""; 378 | return ((typeof Storage !== "undefined" && !storage.get(trailerMid) && !cache.trailer.has(item.Id)) ? ( 379 | await ApiClient.getLocalTrailers(ApiClient.getCurrentUserId(), item.Id).then( 380 | async function (trailers) { 381 | for (let l = 0; l < trailers.length; ++l) { 382 | let trailerurl = (await ApiClient.getPlaybackInfo(trailers[l].Id, {}, 383 | { "MaxStaticBitrate": 140000000, "MaxStreamingBitrate": 140000000, "MusicStreamingTranscodingBitrate": 192000, "DirectPlayProfiles": [{ "Container": "mp4,m4v", "Type": "Video", "VideoCodec": "h264,h265,hevc,av1,vp8,vp9", "AudioCodec": "ac3,eac3,mp3,aac,opus,flac,vorbis" }, { "Container": "mkv", "Type": "Video", "VideoCodec": "h264,h265,hevc,av1,vp8,vp9", "AudioCodec": "ac3,eac3,mp3,aac,opus,flac,vorbis" }, { "Container": "flv", "Type": "Video", "VideoCodec": "h264", "AudioCodec": "aac,mp3" }, { "Container": "mov", "Type": "Video", "VideoCodec": "h264", "AudioCodec": "ac3,eac3,mp3,aac,opus,flac,vorbis" }, { "Container": "opus", "Type": "Audio" }, { "Container": "mp3", "Type": "Audio", "AudioCodec": "mp3" }, { "Container": "mp2,mp3", "Type": "Audio", "AudioCodec": "mp2" }, { "Container": "aac", "Type": "Audio", "AudioCodec": "aac" }, { "Container": "m4a", "AudioCodec": "aac", "Type": "Audio" }, { "Container": "mp4", "AudioCodec": "aac", "Type": "Audio" }, { "Container": "flac", "Type": "Audio" }, { "Container": "webma,webm", "Type": "Audio" }, { "Container": "wav", "Type": "Audio", "AudioCodec": "PCM_S16LE,PCM_S24LE" }, { "Container": "ogg", "Type": "Audio" }, { "Container": "webm", "Type": "Video", "AudioCodec": "vorbis,opus", "VideoCodec": "av1,VP8,VP9" }], "TranscodingProfiles": [{ "Container": "aac", "Type": "Audio", "AudioCodec": "aac", "Context": "Streaming", "Protocol": "hls", "MaxAudioChannels": "2", "MinSegments": "1", "BreakOnNonKeyFrames": true }, { "Container": "aac", "Type": "Audio", "AudioCodec": "aac", "Context": "Streaming", "Protocol": "http", "MaxAudioChannels": "2" }, { "Container": "mp3", "Type": "Audio", "AudioCodec": "mp3", "Context": "Streaming", "Protocol": "http", "MaxAudioChannels": "2" }, { "Container": "opus", "Type": "Audio", "AudioCodec": "opus", "Context": "Streaming", "Protocol": "http", "MaxAudioChannels": "2" }, { "Container": "wav", "Type": "Audio", "AudioCodec": "wav", "Context": "Streaming", "Protocol": "http", "MaxAudioChannels": "2" }, { "Container": "opus", "Type": "Audio", "AudioCodec": "opus", "Context": "Static", "Protocol": "http", "MaxAudioChannels": "2" }, { "Container": "mp3", "Type": "Audio", "AudioCodec": "mp3", "Context": "Static", "Protocol": "http", "MaxAudioChannels": "2" }, { "Container": "aac", "Type": "Audio", "AudioCodec": "aac", "Context": "Static", "Protocol": "http", "MaxAudioChannels": "2" }, { "Container": "wav", "Type": "Audio", "AudioCodec": "wav", "Context": "Static", "Protocol": "http", "MaxAudioChannels": "2" }, { "Container": "mkv", "Type": "Video", "AudioCodec": "ac3,eac3,mp3,aac,opus,flac,vorbis", "VideoCodec": "h264,h265,hevc,av1,vp8,vp9", "Context": "Static", "MaxAudioChannels": "2", "CopyTimestamps": true }, { "Container": "m4s,ts", "Type": "Video", "AudioCodec": "ac3,mp3,aac", "VideoCodec": "h264,h265,hevc", "Context": "Streaming", "Protocol": "hls", "MaxAudioChannels": "2", "MinSegments": "1", "BreakOnNonKeyFrames": true, "ManifestSubtitles": "vtt" }, { "Container": "webm", "Type": "Video", "AudioCodec": "vorbis", "VideoCodec": "vpx", "Context": "Streaming", "Protocol": "http", "MaxAudioChannels": "2" }, { "Container": "mp4", "Type": "Video", "AudioCodec": "ac3,eac3,mp3,aac,opus,flac,vorbis", "VideoCodec": "h264", "Context": "Static", "Protocol": "http" }], "ContainerProfiles": [], "CodecProfiles": [{ "Type": "VideoAudio", "Codec": "aac", "Conditions": [{ "Condition": "Equals", "Property": "IsSecondaryAudio", "Value": "false", "IsRequired": "false" }] }, { "Type": "VideoAudio", "Conditions": [{ "Condition": "Equals", "Property": "IsSecondaryAudio", "Value": "false", "IsRequired": "false" }] }, { "Type": "Video", "Codec": "h264", "Conditions": [{ "Condition": "EqualsAny", "Property": "VideoProfile", "Value": "high|main|baseline|constrained baseline|high 10", "IsRequired": false }, { "Condition": "LessThanEqual", "Property": "VideoLevel", "Value": "62", "IsRequired": false }] }, { "Type": "Video", "Codec": "hevc", "Conditions": [] }], "SubtitleProfiles": [{ "Format": "vtt", "Method": "Hls" }, { "Format": "eia_608", "Method": "VideoSideData", "Protocol": "hls" }, { "Format": "eia_708", "Method": "VideoSideData", "Protocol": "hls" }, { "Format": "vtt", "Method": "External" }, { "Format": "ass", "Method": "External" }, { "Format": "ssa", "Method": "External" }], "ResponseProfiles": [{ "Type": "Video", "Container": "m4v", "MimeType": "video/mp4" }] } 384 | )).MediaSources[0]; 385 | 386 | if (trailerurl.Protocol == "File") { 387 | videourl = `${ApiClient.serverAddress()}/emby${trailerurl.DirectStreamUrl}`; 388 | if (typeof Storage !== "undefined") storage.set(trailerMid, videourl, trailer_cache_time); 389 | cache.trailer.set(item.Id, videourl); 390 | break 391 | } else if (trailerurl.Protocol == "Http") { 392 | videourl = trailerurl.Path; 393 | if (typeof Storage !== "undefined") storage.set(trailerMid, videourl, trailer_cache_time); 394 | cache.trailer.set(item.Id, videourl); 395 | } 396 | } 397 | }) 398 | ) : videourl = (typeof Storage !== "undefined" ? storage.get(trailerMid) : cache.trailer.get(item.Id))), videourl 399 | } 400 | })(); 401 | --------------------------------------------------------------------------------