Compare commits

..

25 Commits

Author SHA1 Message Date
CodeDevMLH
7ffcfa68c1 Update manifest.json for release v1.7.1.12 [skip ci] 2026-03-08 22:15:28 +00:00
CodeDevMLH
aaf21d3c33 Bump version to 1.7.1.12 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 45s
2026-03-08 23:14:42 +01:00
CodeDevMLH
9758ecd417 Add background color to detail and favorite buttons for improved visibility 2026-03-08 23:14:09 +01:00
CodeDevMLH
a4547d80b1 Update manifest.json for release v1.7.1.11 [skip ci] 2026-03-08 22:06:37 +00:00
CodeDevMLH
671e38ff32 Bump version to 1.7.1.11 in project file and manifest for release
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 45s
2026-03-08 23:05:51 +01:00
CodeDevMLH
0e9d0f9d09 Remove appearance properties from button styles and add background color and text color to button for improved visibility 2026-03-08 23:05:38 +01:00
CodeDevMLH
5f296f3c88 Update manifest.json for release v1.7.1.10 [skip ci] 2026-03-08 21:36:22 +00:00
CodeDevMLH
a14b3ca8b5 Bump version to 1.7.1.10 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 45s
2026-03-08 22:35:35 +01:00
CodeDevMLH
4d12e34d01 Enhance CSS styles for buttons and overlays, improving appearance and consistency 2026-03-08 22:35:31 +01:00
CodeDevMLH
59fe6f7083 Update manifest.json for release v1.7.1.9 [skip ci] 2026-03-08 20:58:25 +00:00
CodeDevMLH
dcb2164ea1 Bump version to 1.7.1.9
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 44s
2026-03-08 21:57:41 +01:00
CodeDevMLH
2f71f7b46b Improve null checks and conditionals for better stability in localization and slideshow management
Some checks failed
Auto Release Plugin / build-and-release (push) Has been cancelled
2026-03-08 21:57:22 +01:00
CodeDevMLH
70b0a2a192 Update manifest.json for release v1.7.1.8 [skip ci] 2026-03-08 19:29:25 +00:00
CodeDevMLH
300c76890b Bump version to 1.7.1.8 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 46s
2026-03-08 20:28:38 +01:00
CodeDevMLH
64e5441aff Optimize slideshow distance calculation for circular navigation 2026-03-08 20:28:20 +01:00
CodeDevMLH
f47c9dde88 Update manifest.json for release v1.7.1.7 [skip ci] 2026-03-08 19:15:10 +00:00
CodeDevMLH
9d42b5af8d Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/jellyfin-plugin-media-bar-enhanced
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 46s
2026-03-08 20:14:23 +01:00
CodeDevMLH
8c5f66716f Update version to 1.7.1.7 and enhance changelog with new features and fixes 2026-03-08 20:09:58 +01:00
CodeDevMLH
41e6c1032d Add Hide Arrows on Mobile option to configuration and update related logic 2026-03-08 20:09:47 +01:00
CodeDevMLH
fe07fe9f5e Update manifest.json for release v1.7.1.6 [skip ci] 2026-03-08 18:33:15 +00:00
CodeDevMLH
22a7eb8dcb Update version to 1.7.1.6 and enhance changelog with new features and fixes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 46s
2026-03-08 19:32:27 +01:00
CodeDevMLH
07658f4fbc Add Exclude Seasonal Content option to configuration and update related logic 2026-03-08 19:32:15 +01:00
CodeDevMLH
25ee5b73b4 Update field descriptions for Max Parental Rating and Max Days Recent inputs to clarify no limit options 2026-03-08 19:21:04 +01:00
CodeDevMLH
8f8e251054 Add max parental rating and max days recent filters to configuration 2026-03-08 19:19:20 +01:00
CodeDevMLH
05529e5627 add new tests 2026-03-08 19:18:30 +01:00
10 changed files with 453 additions and 98 deletions

View File

@@ -15,6 +15,8 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
public int MaxMovies { get; set; } = 15; public int MaxMovies { get; set; } = 15;
public int MaxTvShows { get; set; } = 15; public int MaxTvShows { get; set; } = 15;
public int MaxItems { get; set; } = 500; public int MaxItems { get; set; } = 500;
public int MaxParentalRating { get; set; } = 0;
public int MaxDaysRecent { get; set; } = 0;
public int PreloadCount { get; set; } = 3; public int PreloadCount { get; set; } = 3;
public int FadeTransitionDuration { get; set; } = 500; public int FadeTransitionDuration { get; set; } = 500;
public int MaxPaginationDots { get; set; } = 15; public int MaxPaginationDots { get; set; } = 15;
@@ -34,10 +36,12 @@ namespace Jellyfin.Plugin.MediaBarEnhanced.Configuration
public bool EnableLoadingScreen { get; set; } = true; public bool EnableLoadingScreen { get; set; } = true;
public bool EnableKeyboardControls { get; set; } = true; public bool EnableKeyboardControls { get; set; } = true;
public bool AlwaysShowArrows { get; set; } = false; public bool AlwaysShowArrows { get; set; } = false;
public bool HideArrowsOnMobile { get; set; } = true;
public string CustomMediaIds { get; set; } = ""; public string CustomMediaIds { get; set; } = "";
public bool EnableCustomMediaIds { get; set; } = true; public bool EnableCustomMediaIds { get; set; } = true;
public string PreferredVideoQuality { get; set; } = "Auto"; public string PreferredVideoQuality { get; set; } = "Auto";
public bool EnableSeasonalContent { get; set; } = false; public bool EnableSeasonalContent { get; set; } = false;
public bool ExcludeSeasonalContent { get; set; } = true;
public string SeasonalSections { get; set; } = "[]"; public string SeasonalSections { get; set; } = "[]";
public bool IsEnabled { get; set; } = true; public bool IsEnabled { get; set; } = true;
public bool EnableClientSideSettings { get; set; } = false; public bool EnableClientSideSettings { get; set; } = false;

View File

@@ -78,7 +78,7 @@
<div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be <div class="fieldDescription">If enabled, local backdrop videos (Theme Videos) will be
preferred over remote and local trailers.</div> preferred over remote and local trailers.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription" id="WaitForTrailerToEndContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd" <input is="emby-checkbox" type="checkbox" id="WaitForTrailerToEnd"
name="WaitForTrailerToEnd" /> name="WaitForTrailerToEnd" />
@@ -86,7 +86,7 @@
</label> </label>
<div class="fieldDescription">Delay slide transition until trailer finishes.</div> <div class="fieldDescription">Delay slide transition until trailer finishes.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription" id="EnableMobileVideoContainer">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="EnableMobileVideo" <input is="emby-checkbox" type="checkbox" id="EnableMobileVideo"
name="EnableMobileVideo" /> name="EnableMobileVideo" />
@@ -100,8 +100,8 @@
name="ShowTrailerButton" /> name="ShowTrailerButton" />
<span>Show Trailer Button</span> <span>Show Trailer Button</span>
</label> </label>
<div class="fieldDescription">Display a button to open trailer in modal. Only visible if <div class="fieldDescription">Display a button to open trailer in modal. Button only
trailer is not set as backdrop or if no trailer is available.</div> visible if trailer is not set as backdrop.</div>
</div> </div>
</div> </div>
@@ -184,6 +184,14 @@
Custom Media IDs above or random selection are used as fallback.</div> Custom Media IDs above or random selection are used as fallback.</div>
</div> </div>
<div id="seasonalContentContainer" style="display: none;"> <div id="seasonalContentContainer" style="display: none;">
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="ExcludeSeasonalContent"
name="ExcludeSeasonalContent" />
<span>Exclude Seasonal Content from Random Lists</span>
</label>
<div class="fieldDescription">When enabled, any items defined in your Seasonal Sections below will be explicitly excluded from being shown when the plugin pulls random items from your library.</div>
</div>
<div <div
style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;"> style="background-color: rgba(255, 255, 255, 0.05); border-left: 4px solid #00a4dc; border-radius: 4px; padding: 1em 1.5em; margin: 1.5em 0; display: flex; align-items: center; gap: 1em;">
@@ -291,16 +299,23 @@
<span>Enable Loading Screen</span> <span>Enable Loading Screen</span>
</label> </label>
<div class="fieldDescription">Show a loading screen while the slideshow initializes. (You <div class="fieldDescription">Show a loading screen while the slideshow initializes. (You
may have to reload the page twice)</div> may have to reload the page twice (after changing this setting) to take effect)</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows" <input is="emby-checkbox" type="checkbox" id="AlwaysShowArrows"
name="AlwaysShowArrows" /> name="AlwaysShowArrows" />
<span>Always Show Arrows</span> <span>Always Show Arrow Navigation Buttons</span>
</label> </label>
<div class="fieldDescription">If enabled, navigation arrows will always be visible instead <div class="fieldDescription">Force the UI arrow navigation buttons to always be visible instead of only when hovered.</div>
of only on hover.</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="HideArrowsOnMobile"
name="HideArrowsOnMobile" />
<span>Hide Arrows on Mobile</span>
</label>
<div class="fieldDescription">Completely disable the navigation arrows on mobile devices (since swiping is available).</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
@@ -312,6 +327,7 @@
Space (pause), M (mute/unmute)) for Space (pause), M (mute/unmute)) for
the slideshow.</div> the slideshow.</div>
</div> </div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Time Settings</h2> <h2 class="sectionTitle">Time Settings</h2>
<p>Leave a setting blank to use the default value.</p> <p>Leave a setting blank to use the default value.</p>
@@ -355,8 +371,9 @@
<div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for <div class="fieldDescription">Minimum distance in pixels for a swipe to be registered (for
mobile).</div> mobile).</div>
</div> </div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Content Sorting</h2> <h2 class="sectionTitle">Content Sorting and Filtering</h2>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="SortBy">Sort By</label> <label class="selectLabel" for="SortBy">Sort By</label>
<select is="emby-select" id="SortBy" name="SortBy" <select is="emby-select" id="SortBy" name="SortBy"
@@ -387,6 +404,26 @@
<b>Note:</b> Sorting settings apply to both Server content and Custom IDs. 'Original' <b>Note:</b> Sorting settings apply to both Server content and Custom IDs. 'Original'
preserves Custom List order. preserves Custom List order.
</div> </div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxParentalRating">Max Parental Rating (Age Limit)</label>
<input is="emby-input" type="number" id="MaxParentalRating" name="MaxParentalRating" />
<div class="fieldDescription">Items exceeding this age rating will not be shown. Leave blank or set to 0 for no limit. Examples: 12, 16, 18.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxDaysRecent">Max Days Recent</label>
<input is="emby-input" type="number" id="MaxDaysRecent" name="MaxDaysRecent" />
<div class="fieldDescription">Only show items added in the last X days. Leave blank or set to 0 for no limit. Example: 30.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IncludeWatchedContent"
name="IncludeWatchedContent" />
<span>Include Watched Content</span>
</label>
<div class="fieldDescription">If enabled, watched content will be included in the random
selection results.</div>
</div>
<hr style="max-width: 800px; margin: 1em 0;">
<h2 class="sectionTitle">Content Limits</h2> <h2 class="sectionTitle">Content Limits</h2>
<p>Leave a setting blank to use the default value.</p> <p>Leave a setting blank to use the default value.</p>
@@ -417,7 +454,7 @@
<label> <label>
<input is="emby-checkbox" type="checkbox" id="ShowPaginationDots" <input is="emby-checkbox" type="checkbox" id="ShowPaginationDots"
name="ShowPaginationDots" /> name="ShowPaginationDots" />
<span>Show Pagination Dots</span> <span>Show Pagination Dots/Counter</span>
</label> </label>
<div class="fieldDescription">Show or hide the pagination dots/counter navigation at the <div class="fieldDescription">Show or hide the pagination dots/counter navigation at the
bottom of the slideshow.</div> bottom of the slideshow.</div>
@@ -436,15 +473,6 @@
<input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" /> <input is="emby-input" type="number" id="MaxPlotLength" name="MaxPlotLength" />
<div class="fieldDescription">Maximum characters for the plot summary.</div> <div class="fieldDescription">Maximum characters for the plot summary.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IncludeWatchedContent"
name="IncludeWatchedContent" />
<span>Include Watched Content</span>
</label>
<div class="fieldDescription">If enabled, watched content will be included in the random
selection results.</div>
</div>
</div> </div>
<div <div
@@ -505,7 +533,8 @@
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent', 'ShowPaginationDots' 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile'
]; ];
// Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins // Manual mapping for MediaBarIsEnabled -> IsEnabled, to avoid conflicts with other plugins
@@ -563,14 +592,20 @@
var enableVideoBackdropCheckbox = page.querySelector('#EnableVideoBackdrop'); var enableVideoBackdropCheckbox = page.querySelector('#EnableVideoBackdrop');
var preferLocalContainer = page.querySelector('#PreferLocalTrailersContainer'); var preferLocalContainer = page.querySelector('#PreferLocalTrailersContainer');
var preferLocalBackdropsContainer = page.querySelector('#PreferLocalBackdropsContainer'); var preferLocalBackdropsContainer = page.querySelector('#PreferLocalBackdropsContainer');
var waitForTrailerContainer = page.querySelector('#WaitForTrailerToEndContainer');
var enableMobileVideoContainer = page.querySelector('#EnableMobileVideoContainer');
function updatePreferLocalVisibility() { function updatePreferLocalVisibility() {
if (enableVideoBackdropCheckbox && enableVideoBackdropCheckbox.checked) { if (enableVideoBackdropCheckbox && enableVideoBackdropCheckbox.checked) {
if (preferLocalContainer) preferLocalContainer.style.display = 'block'; if (preferLocalContainer) preferLocalContainer.style.display = 'block';
if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'block'; if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'block';
if (waitForTrailerContainer) waitForTrailerContainer.style.display = 'block';
if (enableMobileVideoContainer) enableMobileVideoContainer.style.display = 'block';
} else { } else {
if (preferLocalContainer) preferLocalContainer.style.display = 'none'; if (preferLocalContainer) preferLocalContainer.style.display = 'none';
if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'none'; if (preferLocalBackdropsContainer) preferLocalBackdropsContainer.style.display = 'none';
if (waitForTrailerContainer) waitForTrailerContainer.style.display = 'none';
if (enableMobileVideoContainer) enableMobileVideoContainer.style.display = 'none';
} }
} }
@@ -609,7 +644,8 @@
'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder', 'EnableSeasonalContent', 'EnableClientSideSettings', 'SortBy', 'SortOrder',
'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections', 'PreferLocalTrailers', 'ApplyLimitsToCustomIds', 'SeasonalSections',
'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers', 'PreferLocalBackdrops', 'RandomizeThemeVideos', 'RandomizeLocalTrailers',
'IncludeWatchedContent', 'ShowPaginationDots' 'IncludeWatchedContent', 'ShowPaginationDots', 'MaxParentalRating',
'MaxDaysRecent', 'ExcludeSeasonalContent', 'HideArrowsOnMobile'
]; ];
keys.forEach(function (key) { keys.forEach(function (key) {

View File

@@ -12,7 +12,7 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> --> <!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Media Bar Enhanced Plugin</Title> <Title>Jellyfin Media Bar Enhanced Plugin</Title>
<Authors>CodeDevMLH</Authors> <Authors>CodeDevMLH</Authors>
<Version>1.7.1.5</Version> <Version>1.7.1.12</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced</RepositoryUrl> <RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-media-bar-enhanced</RepositoryUrl>
</PropertyGroup> </PropertyGroup>

View File

@@ -353,14 +353,8 @@
right: 0%; right: 0%;
width: 100%; width: 100%;
height: 100%; height: 100%;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 6%, #000000 8%);
#fff0 2%, -webkit-mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 6%, #000000 8%);
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
} }
.backdrop-container.full-width-video { .backdrop-container.full-width-video {
@@ -383,14 +377,8 @@
object-position: center 20%; object-position: center 20%;
border-radius: 5px; border-radius: 5px;
z-index: 3; z-index: 3;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 6%, #000000 8%);
#fff0 2%, -webkit-mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 6%, #000000 8%);
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
} }
.backdrop-overlay { .backdrop-overlay {
@@ -402,14 +390,8 @@
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(0, 0, 0, 0.2);
border-radius: 5px; border-radius: 5px;
z-index: 4; z-index: 4;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 4%, #000000 6%);
#fff0 2%, -webkit-mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 4%, #000000 6%);
rgb(0 0 0 / 0.5) 4%,
#000000 6%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 4%,
#000000 6%);
} }
.gradient-overlay { .gradient-overlay {
@@ -423,14 +405,8 @@
rgba(29, 29, 29, 0.35) 30%, rgba(29, 29, 29, 0.35) 30%,
rgba(29, 29, 29, 0) 100%); rgba(29, 29, 29, 0) 100%);
z-index: 4; z-index: 4;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 4%, #000000 6%);
#fff0 2%, -webkit-mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 4%, #000000 6%);
rgb(0 0 0 / 0.5) 4%,
#000000 6%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 4%,
#000000 6%);
} }
.gradient-overlay.full-width-video { .gradient-overlay.full-width-video {
@@ -525,16 +501,21 @@
font-family: "Archivo Narrow", sans-serif; font-family: "Archivo Narrow", sans-serif;
font-size: 18px; font-size: 18px;
white-space: nowrap; white-space: nowrap;
background-color: rgba(255, 255, 255, 0.9);
color: rgb(0, 0, 0);
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
font-weight: 700; font-weight: 700;
gap: 6px; gap: 6px;
-webkit-tap-highlight-color: #fff0; -webkit-tap-highlight-color: #fff0;
border-radius: 8px; border-radius: 8px;
-webkit-appearance: none;
appearance: none;
} }
.detail-button { .detail-button {
font-size: 18px; font-size: 18px;
background-color: rgba(255, 255, 255, 0.9);
color: rgb(0, 0, 0); color: rgb(0, 0, 0);
border-radius: 50%; border-radius: 50%;
height: 50px; height: 50px;
@@ -543,10 +524,13 @@
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
-webkit-tap-highlight-color: #fff0; -webkit-tap-highlight-color: #fff0;
-webkit-appearance: none;
appearance: none;
} }
.favorite-button { .favorite-button {
font-size: 18px; font-size: 18px;
background-color: rgba(255, 255, 255, 0.9);
color: red; color: red;
border-radius: 50%; border-radius: 50%;
height: 50px; height: 50px;
@@ -555,6 +539,8 @@
cursor: pointer; cursor: pointer;
transition: color 0.2s; transition: color 0.2s;
-webkit-tap-highlight-color: #fff0; -webkit-tap-highlight-color: #fff0;
-webkit-appearance: none;
appearance: none;
} }
.favorite-button.favorited { .favorite-button.favorited {
@@ -662,7 +648,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 5px; border-radius: 5px;
background: rgb(255 255 255 / 0.8); background: rgba(255, 255, 255, 0.8);
color: #000; color: #000;
border: none; border: none;
font-weight: 600; font-weight: 600;
@@ -711,14 +697,8 @@
object-fit: cover; object-fit: cover;
object-position: center 20%; object-position: center 20%;
z-index: 3; z-index: 3;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 6%, #000000 8%);
#fff0 2%, -webkit-mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 6%, #000000 8%);
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
} }
.gradient-overlay { .gradient-overlay {
@@ -727,17 +707,11 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgb(0 0 0 / 0.25); background: rgba(0, 0, 0, 0.25);
z-index: 4; z-index: 4;
pointer-events: none; pointer-events: none;
mask-image: linear-gradient(to top, mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 6%, #000000 8%);
#fff0 2%, -webkit-mask-image: linear-gradient(to top, rgba(255,255,255,0) 2%, rgba(0,0,0,0.5) 6%, #000000 8%);
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
-webkit-mask-image: linear-gradient(to top,
#fff0 2%,
rgb(0 0 0 / 0.5) 6%,
#000000 8%);
} }
.dots-container { .dots-container {
@@ -814,7 +788,6 @@
@media (max-width: 400px) { @media (max-width: 400px) {
.button-container { .button-container {
gap: 10px; gap: 10px;
/* top: calc(50% + 27vh); */
} }
.play-button, .play-button,

View File

@@ -39,6 +39,8 @@ const CONFIG = {
fadeTransitionDuration: 500, fadeTransitionDuration: 500,
maxPaginationDots: 15, maxPaginationDots: 15,
showPaginationDots: true, showPaginationDots: true,
maxParentalRating: null,
maxDaysRecent: null,
slideAnimationEnabled: true, slideAnimationEnabled: true,
enableVideoBackdrop: true, enableVideoBackdrop: true,
useSponsorBlock: true, useSponsorBlock: true,
@@ -55,6 +57,7 @@ const CONFIG = {
preferredVideoQuality: "Auto", preferredVideoQuality: "Auto",
enableKeyboardControls: true, enableKeyboardControls: true,
alwaysShowArrows: false, alwaysShowArrows: false,
hideArrowsOnMobile: true,
enableCustomMediaIds: true, enableCustomMediaIds: true,
enableSeasonalContent: false, enableSeasonalContent: false,
customMediaIds: "", customMediaIds: "",
@@ -64,6 +67,7 @@ const CONFIG = {
sortOrder: "Ascending", sortOrder: "Ascending",
applyLimitsToCustomIds: false, applyLimitsToCustomIds: false,
seasonalSections: "[]", seasonalSections: "[]",
excludeSeasonalContent: true,
isEnabled: true, isEnabled: true,
}; };
@@ -832,7 +836,7 @@ const LocalizationUtils = {
} }
} }
if (window.ApiClient && STATE.jellyfinData?.accessToken) { if (window.ApiClient && STATE.jellyfinData && STATE.jellyfinData.accessToken) {
try { try {
const userId = window.ApiClient.getCurrentUserId(); const userId = window.ApiClient.getCurrentUserId();
if (userId) { if (userId) {
@@ -842,7 +846,7 @@ const LocalizationUtils = {
}); });
if (userResponse.ok) { if (userResponse.ok) {
const userData = await userResponse.json(); const userData = await userResponse.json();
if (userData.Configuration?.AudioLanguagePreference) { if (userData.Configuration && userData.Configuration.AudioLanguagePreference) {
locale = userData.Configuration.AudioLanguagePreference.toLowerCase(); locale = userData.Configuration.AudioLanguagePreference.toLowerCase();
} }
} }
@@ -852,7 +856,7 @@ const LocalizationUtils = {
} }
} }
if (!locale && window.ApiClient && STATE.jellyfinData?.accessToken) { if (!locale && window.ApiClient && (STATE.jellyfinData && STATE.jellyfinData.accessToken)) {
try { try {
const configUrl = window.ApiClient.getUrl('System/Configuration'); const configUrl = window.ApiClient.getUrl('System/Configuration');
const configResponse = await fetch(configUrl, { const configResponse = await fetch(configUrl, {
@@ -1027,7 +1031,7 @@ const LocalizationUtils = {
*/ */
getLocalizedString(key, fallback, ...args) { getLocalizedString(key, fallback, ...args) {
const locale = this.cachedLocale || 'en-us'; const locale = this.cachedLocale || 'en-us';
let translated = this.translations[locale]?.[key] || fallback; let translated = (this.translations[locale] && this.translations[locale][key]) || fallback;
if (args.length > 0) { if (args.length > 0) {
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
@@ -1111,13 +1115,60 @@ const ApiUtils = {
// Filter by isPlayed=False unless IncludeWatchedContent is enabled // Filter by isPlayed=False unless IncludeWatchedContent is enabled
const playedFilter = CONFIG.includeWatchedContent ? '' : '&isPlayed=False'; const playedFilter = CONFIG.includeWatchedContent ? '' : '&isPlayed=False';
let parentalFilter = '';
if (CONFIG.maxParentalRating) {
parentalFilter = `&MaxOfficialRating=${CONFIG.maxParentalRating}`;
}
const response = await fetch( let dateFilter = '';
`${STATE.jellyfinData.serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&hasOverview=true&imageTypes=Logo,Backdrop&${sortParams}${playedFilter}&enableUserData=true&Limit=${CONFIG.maxItems}&fields=Id`, if (CONFIG.maxDaysRecent) {
{ const pastDate = new Date();
headers: this.getAuthHeaders(), pastDate.setDate(pastDate.getDate() - CONFIG.maxDaysRecent);
dateFilter = `&minDateLastSaved=${pastDate.toISOString()}`;
}
// Exclude seasonal content from random lists
let excludeFilter = '';
if (CONFIG.excludeSeasonalContent && CONFIG.seasonalSections) {
try {
const sections = JSON.parse(CONFIG.seasonalSections || "[]");
let allExcludedIds = [];
for (const section of sections) {
if (section.MediaIds) {
const idsInThisSection = section.MediaIds.split(/[\n,]/)
.map((line) => {
const urlMatch = line.match(/\[(.*?)\]/);
let id = line;
if (urlMatch) {
id = line.replace(/\[.*?\]/, '').trim();
const guidMatch = id.match(/([0-9a-f]{32})/i);
if (guidMatch) { id = guidMatch[1]; } else { id = id.split('|')[0].trim(); }
}
return id.trim();
})
.filter((id) => id);
allExcludedIds.push(...idsInThisSection);
}
}
if (allExcludedIds.length > 0) {
excludeFilter = `&ExcludeItemIds=${allExcludedIds.join(',')}`;
}
} catch(e) {
console.error("🎬 Media Bar:", "Error extracting seasonal IDs for exclusion:", e);
} }
); }
const fetchItems = async (currentDateFilter) => {
const url = `${STATE.jellyfinData.serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&hasOverview=true&imageTypes=Logo,Backdrop&${sortParams}${playedFilter}${parentalFilter}${currentDateFilter}${excludeFilter}&enableUserData=true&Limit=${CONFIG.maxItems}&fields=Id,DateCreated`;
const resp = await fetch(url, { headers: this.getAuthHeaders() });
return resp;
};
let response = await fetchItems(dateFilter);
if (!response.ok) { if (!response.ok) {
console.error("🎬 Media Bar:", console.error("🎬 Media Bar:",
@@ -1126,12 +1177,32 @@ const ApiUtils = {
return []; return [];
} }
const data = await response.json(); let data = await response.json();
const items = data.Items || []; let items = data.Items || [];
console.log("🎬 Media Bar:", // Local exact DateCreated filter: minDateLastSaved pulls items that were merely modified recently (e.g. metadata updates)
`Successfully fetched ${items.length} random items from server` // explicitly discard them if their actual DateCreated is older than X days
); if (CONFIG.maxDaysRecent && dateFilter !== '') {
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - CONFIG.maxDaysRecent);
items = items.filter(item => {
if (!item.DateCreated) return true;
return new Date(item.DateCreated) >= pastDate;
});
}
// Fallback: If we have a date filter but no items are returned, try again without it
if (items.length === 0 && dateFilter !== '') {
console.warn("🎬 Media Bar:", `No items found within the last ${CONFIG.maxDaysRecent} days. Falling back to random fetching.`);
response = await fetchItems('');
if (response.ok) {
data = await response.json();
items = data.Items || [];
}
}
console.log("🎬 Media Bar:", `Successfully fetched ${items.length} random items from server`);
return items.map((item) => item.Id); return items.map((item) => item.Id);
} catch (error) { } catch (error) {
@@ -1705,9 +1776,12 @@ const SlideCreator = {
} }
const isLowPower = isLowPowerDevice(); const isLowPower = isLowPowerDevice();
const isIOSApp = /iPhone|iPad|iPod/i.test(navigator.userAgent);
const limitVideos = isLowPower || isIOSApp;
const itemIndex = STATE.slideshow.itemIds ? STATE.slideshow.itemIds.indexOf(itemId) : -1; const itemIndex = STATE.slideshow.itemIds ? STATE.slideshow.itemIds.indexOf(itemId) : -1;
const isActiveSlide = itemIndex !== -1 && itemIndex === STATE.slideshow.currentSlideIndex; const isActiveSlide = itemIndex !== -1 && itemIndex === STATE.slideshow.currentSlideIndex;
const shouldCreateVideo = !isLowPower || isActiveSlide; // Limit YouTube iframe bulk creation on low power devices OR iOS (which kills the WebProcess on OOM)
const shouldCreateVideo = !limitVideos || isActiveSlide;
if (isYoutube && videoId && shouldCreateVideo) { if (isYoutube && videoId && shouldCreateVideo) {
isVideo = true; isVideo = true;
@@ -2383,6 +2457,7 @@ const SlideshowManager = {
previousVisibleSlide.classList.remove("active"); previousVisibleSlide.classList.remove("active");
} }
void currentSlide.offsetWidth;
currentSlide.classList.add("active"); currentSlide.classList.add("active");
// Manage Video Playback: Stop others, Play current // Manage Video Playback: Stop others, Play current
@@ -2660,9 +2735,9 @@ const SlideshowManager = {
const totalItems = STATE.slideshow.itemIds.length; const totalItems = STATE.slideshow.itemIds.length;
let distance = Math.abs(index - currentIndex); let distance = Math.abs(index - currentIndex);
if (totalItems > keepRange * 2) {
distance = Math.min(distance, totalItems - distance); // Always calculate circular distance for slideshow
} distance = Math.min(distance, totalItems - distance);
if (distance > keepRange) { if (distance > keepRange) {
// Destroy video player if exists // Destroy video player if exists
@@ -2732,7 +2807,7 @@ const SlideshowManager = {
if (currentItemId) { if (currentItemId) {
const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`); const currentSlide = document.querySelector(`.slide[data-item-id="${currentItemId}"]`);
const video = currentSlide?.querySelector('video'); const video = currentSlide ? currentSlide.querySelector('video') : null;
if (video) { if (video) {
video.muted = STATE.slideshow.isMuted; video.muted = STATE.slideshow.isMuted;
@@ -2892,7 +2967,7 @@ const SlideshowManager = {
if (!currentSlide) return; if (!currentSlide) return;
// YouTube player: just resume, don't reload // YouTube player: just resume, don't reload
const ytPlayer = STATE.slideshow.videoPlayers?.[currentItemId]; const ytPlayer = (STATE.slideshow.videoPlayers && STATE.slideshow.videoPlayers[currentItemId]) ? STATE.slideshow.videoPlayers[currentItemId] : undefined;
if (ytPlayer && typeof ytPlayer.playVideo === 'function') { if (ytPlayer && typeof ytPlayer.playVideo === 'function') {
if (STATE.slideshow.isMuted) { if (STATE.slideshow.isMuted) {
if (typeof ytPlayer.mute === 'function') ytPlayer.mute(); if (typeof ytPlayer.mute === 'function') ytPlayer.mute();
@@ -3399,6 +3474,10 @@ const initArrowNavigation = () => {
container.appendChild(muteButton); container.appendChild(muteButton);
const showArrows = () => { const showArrows = () => {
if (CONFIG.hideArrowsOnMobile && window.matchMedia("only screen and (max-width: 768px)").matches) {
return; // disable arrow display on mobile
}
leftArrow.style.display = "block"; leftArrow.style.display = "block";
rightArrow.style.display = "block"; rightArrow.style.display = "block";

View File

@@ -9,12 +9,12 @@
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [ "versions": [
{ {
"version": "1.7.1.5", "version": "1.7.1.12",
"changelog": "- feat: add option to disable pagination dots/counter\n- fix button issue on mobile when using ElegantFin Theme", "changelog": "- feat: add option to disable pagination dots/counter\n- feat: add exclude seasonal content from random fetching option\n- Add hide arrows on mobile option \n- fix button issue on mobile when using ElegantFin Theme",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.1.5/Jellyfin.Plugin.MediaBarEnhanced.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.7.1.12/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "f9a18cb5f38a7a8e54a45e13e5e39bd8", "checksum": "f00a5abf5a2bfd7d6ca3adf20d75b110",
"timestamp": "2026-03-08T16:17:51Z" "timestamp": "2026-03-08T22:15:28Z"
}, },
{ {
"version": "1.7.0.14", "version": "1.7.0.14",

View File

@@ -0,0 +1,73 @@
(async () => {
const apiClient = window.ApiClient;
if (!apiClient) {
console.error("ApiClient not found. Are you logged in?");
return;
}
const userId = apiClient.getCurrentUserId();
const serverAddress = apiClient.serverAddress();
const maxDaysRecent = 30; // 30 Tage Limit
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - maxDaysRecent);
const dateStr = pastDate.toISOString();
console.log(`\n%c=== TEST: DateCreated Direkt-Abfrage ===`, "color: #00a4dc; font-weight: bold; font-size: 14px;");
console.log(`Wir suchen Filme, die nach dem ${dateStr} hinzugefügt wurden.\n`);
// Wir probieren alle denkbaren Parameter-Schreibweisen aus,
// die Jellyfin historisch oder in Forks für "DateCreated" akzeptieren könnte.
const testCases = [
{ name: "MinDateCreated (PascalCase)", param: `MinDateCreated=${dateStr}` },
{ name: "minDateCreated (camelCase)", param: `minDateCreated=${dateStr}` },
{ name: "DateCreatedMin", param: `DateCreatedMin=${dateStr}` },
{ name: "dateCreatedMin", param: `dateCreatedMin=${dateStr}` },
{ name: "StartDate", param: `StartDate=${dateStr}` },
{ name: "startDate", param: `startDate=${dateStr}` }
];
try {
for (let i = 0; i < testCases.length; i++) {
const test = testCases[i];
const apiUrl = `${serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&enableUserData=true&Limit=5&fields=Id,DateCreated&userId=${userId}&${test.param}`;
console.log(`%cTest ${i+1}: ${test.name}`, "color: yellow;");
console.log(`URL: ${apiUrl}`);
const response = await fetch(apiUrl, {
headers: {
Authorization: `MediaBrowser Client="${apiClient.appName()}", Device="${apiClient.deviceName()}", DeviceId="${apiClient.deviceId()}", Version="${apiClient.appVersion()}", Token="${apiClient.accessToken()}"`
}
});
if (!response.ok) {
console.error(`-> ❌ HTTP Fehler: ${response.status}`);
continue;
}
const data = await response.json();
const items = data.Items || [];
// Zur Auswertung: Wenn die Abfrage ignoriert wird, liefert er oft ALLE (hier max 5) zurück.
// Filtert er wirklich, sollten es WENIGER als 5, und zwar im besten Fall genau die RICHTIGEN sein.
console.log(`-> Ergebnis: ${items.length} Items zurückgeliefert.`);
if (items.length > 0) {
// Wir checken, ob die zurückgelieferten Items WIRKLICH neu sind
const oldItems = items.filter(item => new Date(item.DateCreated) < pastDate);
if (oldItems.length > 0) {
console.log(` ❌ Filter FEHLGESCHLAGEN: Es wurden ${oldItems.length} "alte" Filme zurückgegeben (z.B. ${oldItems[0].Name}). Er ignoriert also den Parameter.`);
} else {
console.log(` ✅ Filter KÖNNTE funktionieren: Alle zurückgegebenen Filme sind neuer als unser Datum! (Erster: ${items[0].Name})`);
}
} else {
console.log(` ❓ Keine Items gefunden (entweder strenger Filter oder gar keine neuen Filme vorhanden).`);
}
console.log("\n");
}
} catch (error) {
console.error("Fehler beim Abruf:", error);
}
})();

View File

@@ -0,0 +1,56 @@
(async () => {
const apiClient = window.ApiClient;
if (!apiClient) {
console.error("ApiClient not found. Are you logged in?");
return;
}
const userId = apiClient.getCurrentUserId();
const serverAddress = apiClient.serverAddress();
const maxDaysRecent = 30; // Test: Added in last 30 days
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - maxDaysRecent);
const dateStr = pastDate.toISOString();
console.log(`Searching for items added after: ${dateStr}`);
const testUrls = [
// Test 1: minDateCreated (CamelCase)
`${serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&enableUserData=true&Limit=5&fields=Id,DateCreated&userId=${userId}&minDateCreated=${dateStr}`,
// Test 2: minDateLastSaved
`${serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&enableUserData=true&Limit=5&fields=Id,DateCreated&userId=${userId}&minDateLastSaved=${dateStr}`,
];
try {
for (let i = 0; i < testUrls.length; i++) {
const apiUrl = testUrls[i];
console.log(`\n%cTest ${i+1}: Testing URL:\n${apiUrl}`, "color: yellow;");
const response = await fetch(apiUrl, {
headers: {
Authorization: `MediaBrowser Client="${apiClient.appName()}", Device="${apiClient.deviceName()}", DeviceId="${apiClient.deviceId()}", Version="${apiClient.appVersion()}", Token="${apiClient.accessToken()}"`
}
});
if (!response.ok) {
console.error(`Failed to fetch items: ${response.status} ${response.statusText}`);
continue;
}
const data = await response.json();
const items = data.Items || [];
console.log(`%cErgebnis: ${items.length} Items gefunden!`, "color: #00a4dc; font-weight: bold;");
if(items.length > 0) {
console.log("Gefundene Items:");
items.forEach(item => {
console.log(`- Name: ${item.Name}, DateCreated: ${item.DateCreated}, Art: ${item.Type}`);
});
}
}
} catch (error) {
console.error("Fehler beim Abrufen der URL:", error);
}
})();

View File

@@ -0,0 +1,72 @@
(async () => {
const apiClient = window.ApiClient;
if (!apiClient) {
console.error("ApiClient not found. Are you logged in?");
return;
}
const userId = apiClient.getCurrentUserId();
const serverAddress = apiClient.serverAddress();
const maxDaysRecent = 30; // Test: Added in last 30 days
// 1. Calculate the cutoff date
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - maxDaysRecent);
const dateStr = pastDate.toISOString();
console.log(`\n%c=== TEST: 2-Stufen "Zuletzt Hinzugefügt" Filter ===`, "color: #00a4dc; font-weight: bold; font-size: 14px;");
console.log(`Suche Items neuer als: ${dateStr} (${maxDaysRecent} Tage alt)\n`);
const apiUrl = `${serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&enableUserData=true&Limit=50&fields=Id,DateCreated&userId=${userId}&minDateLastSaved=${dateStr}`;
try {
console.log(`%cSchritt 1: API Call mit minDateLastSaved...`, "color: yellow;");
console.log(`URL: ${apiUrl}`);
const response = await fetch(apiUrl, {
headers: {
Authorization: `MediaBrowser Client="${apiClient.appName()}", Device="${apiClient.deviceName()}", DeviceId="${apiClient.deviceId()}", Version="${apiClient.appVersion()}", Token="${apiClient.accessToken()}"`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch items: ${response.status} ${response.statusText}`);
}
const data = await response.json();
let items = data.Items || [];
console.log(`-> API lieferte ${items.length} potenziell neue/geänderte Items zurück.\n`);
if (items.length > 0) {
console.log("Die API hielt diese Items für neu/aktuell:");
items.forEach(item => console.log(` - ${item.Name} (DateCreated: ${item.DateCreated})`));
}
console.log(`\n%cSchritt 2: Lokaler DateCreated Filter (Wie das Plugin ihn jetzt nutzt)...`, "color: yellow;");
// Exakt dieser Filter-Block ist jetzt auch so in mediaBarEnhanced.js
const finalItems = items.filter(item => {
if (!item.DateCreated) return true; // Fallback falls Jellyfin kein Datum schickt
const dCreated = new Date(item.DateCreated);
return dCreated >= pastDate;
});
console.log(`%c-> FINALES ERGEBNIS: ${finalItems.length} echte Neuzugänge bleiben übrig!`, "color: #00ff00; font-weight: bold; font-size: 14px;");
if(finalItems.length > 0) {
console.log("Diese Items schaffen es in die Slideshow:");
finalItems.forEach(item => {
console.log(` 🎬 Name: ${item.Name}, DateCreated: ${item.DateCreated}`);
});
}
// Teste den Fallback
if(finalItems.length === 0 && items.length > 0) {
console.log("\n%c💡 HINWEIS: Da nach Filterung 0 Items übrig bleiben, greift in der Slideshow jetzt automatisch unser Fallback und zeigt zufällige Filme aller Jahre!", "color: orange;");
}
} catch (error) {
console.error("Fehler beim Abruf:", error);
}
})();

View File

@@ -0,0 +1,62 @@
(async () => {
const apiClient = window.ApiClient;
if (!apiClient) {
console.error("ApiClient not found. Are you logged in?");
return;
}
const userId = apiClient.getCurrentUserId();
const serverAddress = apiClient.serverAddress();
// Example test configuration flags
const maxItems = 50;
const includeWatchedContent = false; // set to false to ONLY show unplayed (newly watched)
const maxParentalRating = 12; // Test age limit
const maxDaysRecent = 30; // Test recency limit
// Build the query parameters just like in mediaBarEnhanced.js
const sortParams = "sortBy=Random";
const playedFilter = includeWatchedContent ? '' : '&isPlayed=False';
let parentalFilter = '';
if (maxParentalRating) {
parentalFilter = `&MaxOfficialRating=${maxParentalRating}`;
}
let dateFilter = '';
if (maxDaysRecent) {
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - maxDaysRecent);
dateFilter = `&MinDateCreated=${pastDate.toISOString()}`;
}
const apiUrl = `${serverAddress}/Items?IncludeItemTypes=Movie,Series&Recursive=true&hasOverview=true&imageTypes=Logo,Backdrop&${sortParams}${playedFilter}${parentalFilter}${dateFilter}&enableUserData=true&Limit=${maxItems}&fields=Id&userId=${userId}`;
try {
console.log(`Testing generated URL with filters:\n%c${apiUrl}`, "color: yellow;");
// Execute the fetch
const response = await fetch(apiUrl, {
headers: {
Authorization: `MediaBrowser Client="${apiClient.appName()}", Device="${apiClient.deviceName()}", DeviceId="${apiClient.deviceId()}", Version="${apiClient.appVersion()}", Token="${apiClient.accessToken()}"`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch items: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const items = data.Items || [];
console.log(`%cErgebnis: ${items.length} Items gefunden!`, "color: #00a4dc; font-weight: bold;");
if(items.length > 0) {
console.log("Erstes Item als Beispiel:");
console.dir(items[0]);
}
} catch (error) {
console.error("Fehler beim Abrufen der URL:", error);
}
})();