Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f47c9dde88 | ||
|
|
9d42b5af8d | ||
|
|
8c5f66716f | ||
|
|
41e6c1032d | ||
|
|
fe07fe9f5e | ||
|
|
22a7eb8dcb | ||
|
|
07658f4fbc | ||
|
|
25ee5b73b4 | ||
|
|
8f8e251054 | ||
|
|
05529e5627 |
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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.7</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>
|
||||||
|
|
||||||
|
|||||||
@@ -814,7 +814,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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1112,12 +1116,59 @@ 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';
|
||||||
|
|
||||||
const response = await fetch(
|
let parentalFilter = '';
|
||||||
`${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.maxParentalRating) {
|
||||||
{
|
parentalFilter = `&MaxOfficialRating=${CONFIG.maxParentalRating}`;
|
||||||
headers: this.getAuthHeaders(),
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
let dateFilter = '';
|
||||||
|
if (CONFIG.maxDaysRecent) {
|
||||||
|
const pastDate = new Date();
|
||||||
|
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) {
|
||||||
@@ -3399,6 +3470,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";
|
||||||
|
|
||||||
|
|||||||
@@ -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.7",
|
||||||
"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.7/Jellyfin.Plugin.MediaBarEnhanced.zip",
|
||||||
"checksum": "f9a18cb5f38a7a8e54a45e13e5e39bd8",
|
"checksum": "2ed5fe25cdce41fa44c159649c8a7898",
|
||||||
"timestamp": "2026-03-08T16:17:51Z"
|
"timestamp": "2026-03-08T19:15:10Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"version": "1.7.0.14",
|
"version": "1.7.0.14",
|
||||||
|
|||||||
73
test_scripts/test_direct_date_created.js
Normal file
73
test_scripts/test_direct_date_created.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
56
test_scripts/test_filters_dates.js
Normal file
56
test_scripts/test_filters_dates.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
72
test_scripts/test_filters_dates_final.js
Normal file
72
test_scripts/test_filters_dates_final.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
62
test_scripts/test_filters_url.js
Normal file
62
test_scripts/test_filters_url.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user