class Config{
constructor() {
//媒体库id,用逗号分隔。进入媒体库后url里的parentId
//this.parentId = "5,21463";
this.parentId = "";
//滚屏间隔 ms
this.interval = 8000;
//是否开启随机
this.random = true;
this.itemQuery={
//返回横幅的最大数量,注意每10个为一轮,一轮不足10个的会被舍弃。例如27则只显示两轮20个
Limit : 20,//(integer) Optional. The maximum number of records to return
//一般只需要关注上面几个配置,下面不了解的不需要修改
/*-------------------------------------------------------------------------------------------------------------------------------*/
Recursive : true,//(boolean) When searching within folders, this determines whether or not the search will be recursive. true/false
SortOrder : "Descending",//(string) Sort Order - Ascending,Descending
Fields : "ProductionYear",//(string) Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines
EnableUserData : false,//(boolean) Optional, include user data
ImageTypeLimit : 1,//(integer) Optional, the max number of images to return, per image type
Fields : "ProductionYear",//(string) Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines
IncludeItemTypes : "Movie,Series",//(string) Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.
ImageTypes : "Backdrop",//(string) Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.
EnableImageTypes : "Logo,Backdrop",//(string) Optional. The image types to include in the output.
SortBy : "ProductionYear, PremiereDate, SortName",//(string) Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime
ArtistType : "",//(string) Artist or AlbumArtist
MaxOfficialRating : "",//(string) Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).
HasThemeSong : "",//(boolean) Optional filter by items with theme songs.
HasThemeVideo : "",//(boolean) Optional filter by items with theme videos.
HasSubtitles : "",//(boolean) Optional filter by items with subtitles.
HasSpecialFeature : "",//(boolean) Optional filter by items with special features.
HasTrailer : "",//(boolean) Optional filter by items with trailers.
AdjacentTo : "",//(string) Optional. Return items that are siblings of a supplied item.
MinIndexNumber : "",//(integer) Optional filter by minimum index number.
MaxPlayers : "",//(integer) Optional filter by minimum number of game players.
ParentIndexNumber : "",//(integer) Optional filter by parent index number.
HasParentalRating : "",//(integer) Optional filter by items that have or do not have a parental rating
IsHD : "",//(boolean) Optional filter by items that are HD or not.
LocationTypes : "",//(string) Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.
ExcludeLocationTypes : "",//(string) Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.
IsMissing : "",//(boolean) Optional filter by items that are missing episodes or not.
IsUnaired : "",//(boolean) Optional filter by items that are unaired episodes or not.
MinCommunityRating : "",//(number) Optional filter by minimum community rating.
MinCriticRating : "",//(number) Optional filter by minimum critic rating.
AiredDuringSeason : "",//(integer) Gets all episodes that aired during a season, including specials.
MinPremiereDate : "",//(string) Optional. The minimum premiere date. Format = ISO
MinDateLastSaved : "",//(string) Optional. The minimum premiere date. Format = ISO
MinDateLastSavedForUser : "",//(string) Optional. The minimum premiere date. Format = ISO
MaxPremiereDate : "",//(string) Optional. The maximum premiere date. Format = ISO
HasOverview : "",//(boolean) Optional filter by items that have an overview or not.
HasImdbId : "",//(boolean) Optional filter by items that have an imdb id or not.
HasTmdbId : "",//(boolean) Optional filter by items that have a tmdb id or not.
HasTvdbId : "",//(boolean) Optional filter by items that have a tvdb id or not.
ExcludeItemIds : "",//(string) Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.
StartIndex : "",//(integer) Optional. The record index to start at. All items with a lower index will be dropped from the results.
ExcludeItemTypes : "",//(string) Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.
AnyProviderIdEquals : "",//(string) Optional. If specified, result will be filtered to contain only items which match at least one of the specified IDs. Each provider ID must be in the form 'prov.id', e.g. 'imdb.tt123456'. This allows multiple, comma delimeted value pairs.
Filters : "",//(string) Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes
IsFavorite : "",//(boolean) Optional filter by items that are marked as favorite, or not.
IsMovie : "",//(boolean) Optional filter for movies.
IsSeries : "",//(boolean) Optional filter for movies.
IsNews : "",//(boolean) Optional filter for news.
IsKids : "",//(boolean) Optional filter for kids.
IsSports : "",//(boolean) Optional filter for sports.
MediaTypes : "",//(string) Optional filter by MediaType. Allows multiple, comma delimited.
IsPlayed : "",//(string) Optional filter by items that are played, or not.
Genres : "",//(string) Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.
OfficialRatings : "",//(string) Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.
Tags : "",//(string) Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.
Years : "",//(string) Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.
EnableImages : "",//(boolean) Optional, include image information in output
Person : "",//(string) Optional. If specified, results will be filtered to include only those containing the specified person.
PersonIds : "",//(string) Optional. If specified, results will be filtered to include only those containing the specified person.
PersonTypes : "",//(string) Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited
Studios : "",//(string) Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.
StudioIds : "",//(string) Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.
Artists : "",//(string) Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.
ArtistIds : "",//(string) Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.
Albums : "",//(string) Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.
Ids : "",//(string) Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.
VideoTypes : "",//(string) Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.
Containers : "",//(string) Optional filter by Container. Allows multiple, comma delimeted.
AudioCodecs : "",//(string) Optional filter by AudioCodec. Allows multiple, comma delimeted.
VideoCodecs : "",//(string) Optional filter by VideoCodec. Allows multiple, comma delimeted.
SubtitleCodecs : "",//(string) Optional filter by SubtitleCodec. Allows multiple, comma delimeted.
Path : "",//(string) Optional filter by Path.
UserId : "",//(string) User Id
MinOfficialRating : "",//(string) Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).
IsLocked : "",//(string) Optional filter by items that are locked.
IsPlaceHolder : "",//(boolean) Optional filter by items that are placeholders
HasOfficialRating : "",//(boolean) Optional filter by items that have official ratings
GroupItemsIntoCollections : "",//(boolean) Whether or not to hide items behind their boxsets.
Is3D : "",//(boolean) Whether or not to hide items behind their boxsets.
SeriesStatus : "",//(string) Optional filter by Series Status. Allows multiple, comma delimeted.
NameStartsWithOrGreater : "",//(string) Optional filter by items whose name is sorted equally or greater than a given input string.
NameStartsWith : "",//(string) Optional filter by items whose name is sorted equally than a given input string.
NameLessThan : ""//(string) Optional filter by items whose name is equally or lesser than a given input string.
}
Object.getOwnPropertyNames(this.itemQuery).forEach(key => {
if(this.itemQuery[key]==""){
delete this.itemQuery[key];
}
})
this.parentIds = this.parentId.split(',');
}
}
class Home {
static start() {
this.cache = {
items: undefined,
item: new Map(),
};
this.config = new Config();
console.log(this.config.itemQuery);
this.itemQuery = this.config.itemQuery;
this.coverOptions = { type: "Backdrop", maxWidth: 3000 };
this.logoOptions = { type: "Logo", maxWidth: 3000 };
setInterval(() => {
if (window.location.href.indexOf("!/home") != -1) {
if ($(".view:not(.hide) .misty-banner").length == 0 && $(".misty-loading").length == 0) {
this.initLoading();
}
if ($(".hide .misty-banner").length != 0) {
$(".hide .misty-banner").remove();
}
if ($(".section0 .card").length != 0 && $(".view:not(.hide) .misty-banner").length == 0) {
this.init();
}
}
}, 100);
}
static async init() {
// Beta
$(".view:not(.hide)").attr("data-type", "home");
// Loading
const serverName = await this.injectCall("serverName", "");
$(".misty-loading h1").text(serverName).addClass("active");
// Banner
await this.initBanner();
this.initEvent();
}
/* 插入Loading */
static initLoading() {
const load = `
<div class="misty-loading">
<h1></h1>
<div class="mdl-spinner"><div class="mdl-spinner__layer mdl-spinner__layer-1"><div class="mdl-spinner__circle-clipper mdl-spinner__left"><div class="mdl-spinner__circle mdl-spinner__circleLeft"></div></div><div class="mdl-spinner__circle-clipper mdl-spinner__right"><div class="mdl-spinner__circle mdl-spinner__circleRight"></div></div></div></div>
</div>
`;
$("body").append(load);
}
static injectCode(code) {
let hash = md5(code + Math.random().toString());
return new Promise((resolve, reject) => {
const channel = new BroadcastChannel(hash);
channel.addEventListener("message", (event) => resolve(event.data));
const script = `
<script class="I${hash}">
setTimeout(async ()=> {
async function R${hash}(){${code}};
const channel = new BroadcastChannel("${hash}");
channel.postMessage(await R${hash}());
document.querySelector("script.I${hash}").remove()
}, 16)
</script>
`;
$(document.head || document.documentElement).append(script);
});
}
static injectCall(func, arg) {
const script = `
// const client = (await window.require(["ApiClient"]))[0];
const client = await new Promise((resolve, reject) => {
setInterval(() => {
if (window.ApiClient != undefined) resolve(window.ApiClient);
}, 16);
});
return await client.${func}(${arg})
`;
return this.injectCode(script);
}
static getItems(query) {
//由于要合并多个媒体库所以放弃做缓存
return this.injectCall("getItems", "client.getCurrentUserId(), " + JSON.stringify(query));
}
static itemsRandom(array){
let res = [], random;
while(array.length>0){
random = Math.floor(Math.random()*array.length);
res.push(array[random]);
array.splice(random, 1);
}
return res;
}
static async getItem(itemId) {
// 双缓存 优先使用 WebStorage
if (typeof Storage !== "undefined" && !localStorage.getItem("CACHE|" + itemId) && !this.cache.item.has(itemId)) {
const data = JSON.stringify(await this.injectCall("getItem", `client.getCurrentUserId(), "${itemId}"`));
if (typeof Storage !== "undefined") localStorage.setItem("CACHE|" + itemId, data);
else this.cache.item.set(itemId, data);
}
return JSON.parse(typeof Storage !== "undefined" ? localStorage.getItem("CACHE|" + itemId) : this.cache.item.get(itemId));
}
static getImageUrl(itemId, options) {
return this.injectCall("getImageUrl", itemId + ", " + JSON.stringify(options));
}
static async appendItem(i){
const detail = await this.getItem(this.data.Items[i].Id),
itemHtml = `
<div class="misty-banner-item" id="${detail.Id}">
<img draggable="false" loading="eager" decoding="async" class="misty-banner-cover" src="${await this.getImageUrl(detail.Id, this.coverOptions)}" alt="Backdrop" style="">
<div class="misty-banner-info padded-left padded-right">
<h1>${detail.Name}</h1>
<div><p>${detail.Overview}</p></div>
<div><button onclick="appRouter.showItem('${detail.Id}')">MORE</button></div>
</div>
</div>
`,
logoHtml = `
<img id="${detail.Id}" draggable="false" loading="auto" decoding="lazy" class="misty-banner-logo" data-banner="img-title" alt="Logo" src="${await this.getImageUrl(detail.Id, this.logoOptions)}">
`;
if (detail.ImageTags && detail.ImageTags.Logo) {
$(".misty-banner-logos").append(logoHtml);
}
$(".misty-banner-body").append(itemHtml);
}
//总数小于10
static bannerRollOnce(){
this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
$(".misty-banner-body").css("left", -(this.index * 100).toString() + "%");
// 信息切换
$(".misty-banner-item.active").removeClass("active");
let id = $(".misty-banner-item").eq(this.index).addClass("active").attr("id");
// LOGO切换
$(".misty-banner-logo.active").removeClass("active");
$(`.misty-banner-logo[id=${id}]`).addClass("active");
}
//总数大于10
static async bannerRoll(){
// 背景切换
this.index += this.index + 1 == $(".misty-banner-item").length ? -this.index : 1;
//正常切换
if(this.index != 0){
$(".misty-banner-body").css("left", -(this.index * 100).toString() + "%");
// 信息切换
$(".misty-banner-item.active").removeClass("active");
let id = $(".misty-banner-item").eq(this.index).addClass("active").attr("id");
// LOGO切换
$(".misty-banner-logo.active").removeClass("active");
$(`.misty-banner-logo[id=${id}]`).addClass("active");
}
//已经切换到最后
if(this.index == 0){
clearInterval(this.bannerInterval);
let l = $(".misty-banner-item").length;
//剩余小于10张直接舍弃,从头开始
if(this.count+1==this.data.Items.length||this.data.Items.length-this.count-1<10)
this.count=-1;
//向后添加,剩余10张以上添加10张
for(let i=this.count+1;i<this.count+1+(this.data.Items.length-this.count-1<10?this.data.Items.length-this.count-1:10);i++){
await this.appendItem(i)
}
//切换到第一张
$(".misty-banner-body").css("left", "0%");
$(".misty-banner-item.active").removeClass("active");
let id = $(".misty-banner-item").eq(l).addClass("active").attr("id");
$(".misty-banner-logo.active").removeClass("active");
$(`.misty-banner-logo[id=${id}]`).addClass("active");
//从dom中移除上一轮次的横幅
for(let i=0;i<l;i++){
$(".misty-banner-item").eq(0).remove();
}
this.bannerInterval = setInterval(this.bannerRoll.bind(this), this.config.interval);
}
this.count++;
}
/* 插入Banner */
static async initBanner() {
const banner = `
<div class="misty-banner">
<div class="misty-banner-body">
</div>
<div class="misty-banner-library">
<div class="misty-banner-logos"></div>
</div>
</div>
`;
$(".view:not(.hide) .homeSectionsContainer").prepend(banner);
$(".view:not(.hide) .section0").detach().appendTo(".view:not(.hide) .misty-banner-library");
// 插入数据
this.data = {Items:[]}
//配置的媒体库不为空
if(this.config.parentIds[0] != ""){
//合并所有配置的媒体库的结果
for(let parentId of this.config.parentIds){
this.itemQuery.ParentId = parentId;
let res = await this.getItems(this.itemQuery);
this.data.Items = this.data.Items.concat(res.Items);
}
} else {
//查询所有媒体库
this.data = await this.getItems(this.itemQuery);
}
if(this.config.random==true){
this.data.Items = this.itemsRandom(this.data.Items);
}
//大于10时添加10张
for(let i=0;i<(this.data.Items.length<10?this.data.Items.length:10);i++){
await this.appendItem(i)
//console.log(item.Id, detail);
}
let complete = 0;
let loading = setInterval(async () => {
// 判断图片加载完毕
$(".misty-banner-cover:not(.complete)").each((i, dom) => {
if (dom.complete) {
dom.classList.add("complete");
complete++;
}
});
if (complete == $(".misty-banner-item").length && $(".misty-banner-item").length != 0) {
clearInterval(loading);
$(".misty-loading").fadeOut(500, () => $(".misty-loading").remove());
await CommonUtils.sleep(150);
// 置入场动画
let delay = 80; // 动媒体库画间隔
let id = $(".misty-banner-item").eq(0).addClass("active").attr("id"); // 初次信息动画
$(`.misty-banner-logo[id=${id}]`).addClass("active");
await CommonUtils.sleep(200); // 间隔动画
$(".section0 > div").addClass("misty-banner-library-overflow"); // 关闭overflow 防止媒体库动画溢出
$(".misty-banner .card").each((i, dom) => setTimeout(() => $(dom).addClass("misty-banner-library-show"), i * delay)); // 媒体库动画
await CommonUtils.sleep(delay * 8 + 1000); // 等待媒体库动画完毕
$(".section0 > div").removeClass("misty-banner-library-overflow"); // 开启overflow 防止无法滚动
// 滚屏逻辑
clearInterval(this.bannerInterval);
this.index = 0;this.count = 0;
this.bannerInterval = this.data.Items.length<10?setInterval(this.bannerRollOnce.bind(this), this.config.interval):setInterval(this.bannerRoll.bind(this), this.config.interval);
}
}, 16);
}
/* 初始事件 */
static initEvent() {
// 通过注入方式, 方可调用appRouter函数, 以解决Content-Script window对象不同步问题
const script = `
// 挂载appRouter
if (!window.appRouter) window.appRouter = (await window.require(["appRouter"]))[0];
// 修复library事件参数
const serverId = ApiClient._serverInfo.Id,
librarys = document.querySelectorAll(".view:not(.hide) .section0 .card");
librarys.forEach(library => {
library.setAttribute("data-serverid", serverId);
library.setAttribute("data-type", "CollectionFolder");
});
`;
this.injectCode(script);
}
}
// 运行
if ($("meta[name=application-name]").attr("content") == "Emby" || $(".accent-emby") != undefined) {
Home.start();
}