diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json
index 14e3c1944..24ffbc8aa 100644
--- a/src/assets/locales/en.json
+++ b/src/assets/locales/en.json
@@ -377,7 +377,9 @@
"titleAsc": "Title A-Z",
"titleDesc": "Title Z-A",
"yearAsc": "Release Date Oldest-Newest",
- "yearDesc": "Release Date Newest-Oldest"
+ "yearDesc": "Release Date Newest-Oldest",
+ "lengthAsc": "Length Shortest-Longest",
+ "lengthDesc": "Length Longest-Shortest"
}
}
},
diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts
index 3593f22b1..cc4de6b3d 100644
--- a/src/components/player/display/base.ts
+++ b/src/components/player/display/base.ts
@@ -167,8 +167,17 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
}
}
} else {
- hls.currentLevel = -1;
- hls.loadLevel = -1;
+ // Good job fucking up auto qualities on non standarts so i have to make this fix
+ const sortedLevels = sortLevelsByQuality(hls.levels);
+ const topLevel = sortedLevels[0];
+ const topIndex = topLevel ? hls.levels.indexOf(topLevel) : -1;
+ if (topIndex !== -1) {
+ hls.startLevel = topIndex;
+ hls.nextLevel = topIndex;
+ } else {
+ hls.currentLevel = -1;
+ hls.loadLevel = -1;
+ }
}
// For manual quality selection, wait for LEVEL_SWITCHED to emit quality
// to avoid showing intermediate states when HLS switches away from unplayable levels
diff --git a/src/pages/migration/Migration.tsx b/src/pages/migration/Migration.tsx
index a8e4c3155..bd66622d8 100644
--- a/src/pages/migration/Migration.tsx
+++ b/src/pages/migration/Migration.tsx
@@ -1,7 +1,7 @@
import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
-import { Icon, Icons } from "@/components/Icon";
+import { Icons } from "@/components/Icon";
import { Stepper } from "@/components/layout/Stepper";
import { BiggerCenterContainer } from "@/components/layout/ThinContainer";
import { VerticalLine } from "@/components/layout/VerticalLine";
@@ -26,25 +26,6 @@ export function MigrationPage() {
{t("migration.start.explainer")}
-
-
navigate("/migration/direct")}
diff --git a/src/pages/parts/home/BookmarksPart.tsx b/src/pages/parts/home/BookmarksPart.tsx
index 412b44714..a4ed8ddbc 100644
--- a/src/pages/parts/home/BookmarksPart.tsx
+++ b/src/pages/parts/home/BookmarksPart.tsx
@@ -10,6 +10,8 @@ import { Listbox } from "@headlessui/react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
+import { getMediaDetails } from "@/backend/metadata/tmdb";
+import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
import { EditButton } from "@/components/buttons/EditButton";
import { Dropdown, OptionItem } from "@/components/form/Dropdown";
import { Icon, Icons } from "@/components/Icon";
@@ -58,6 +60,7 @@ export function BookmarksPart({
const saved = localStorage.getItem("__MW::bookmarksSort");
return (saved as SortOption) || "date";
});
+ const [runtimeData, setRuntimeData] = useState>({});
const [activeFolderModal, setActiveFolderModal] = useState(
null,
);
@@ -74,6 +77,34 @@ export function BookmarksPart({
localStorage.setItem("__MW::bookmarksSort", sortBy);
}, [sortBy]);
+ useEffect(() => {
+ if (sortBy !== "length-asc" && sortBy !== "length-desc") return;
+ const ids = Object.keys(bookmarks);
+ const missing = ids.filter((id) => !(id in runtimeData));
+ if (missing.length === 0) return;
+
+ Promise.all(
+ missing.map(async (id) => {
+ const type = bookmarks[id].type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV;
+ try {
+ const data = await getMediaDetails(id, type, false);
+ const value = type === TMDBContentTypes.MOVIE
+ ? (data as any).runtime ?? 0
+ : (data as any).number_of_episodes ?? 0;
+ return [id, value] as [string, number];
+ } catch {
+ return [id, 0] as [string, number];
+ }
+ }),
+ ).then((results) => {
+ setRuntimeData((prev: Record) => {
+ const next = { ...prev };
+ results.forEach(([id, val]) => { next[id] = val; });
+ return next;
+ });
+ });
+ }, [sortBy, bookmarks, runtimeData]);
+
const { allGroups, rootMediaItems } = useMemo(() => {
const list = getList(bookmarks);
@@ -103,9 +134,9 @@ export function BookmarksPart({
return {
allGroups: sortedGroups,
- rootMediaItems: sortMedia(rootItems, sortBy, bookmarks, progressItems),
+ rootMediaItems: sortMedia(rootItems, sortBy, bookmarks, progressItems, runtimeData),
};
- }, [bookmarks, groupOrder, sortBy, progressItems]);
+ }, [bookmarks, groupOrder, sortBy, progressItems, runtimeData]);
useEffect(() => {
onItemsChange(Object.keys(bookmarks).length > 0);
@@ -163,6 +194,8 @@ export function BookmarksPart({
{ id: "title-desc", name: t("home.bookmarks.sorting.options.titleDesc") },
{ id: "year-asc", name: t("home.bookmarks.sorting.options.yearAsc") },
{ id: "year-desc", name: t("home.bookmarks.sorting.options.yearDesc") },
+ { id: "length-asc", name: t("home.bookmarks.sorting.options.lengthAsc") },
+ { id: "length-desc", name: t("home.bookmarks.sorting.options.lengthDesc") },
];
const selectedSortOption =
diff --git a/src/pages/parts/home/utils.ts b/src/pages/parts/home/utils.ts
index 1ce49d067..6969b55ce 100644
--- a/src/pages/parts/home/utils.ts
+++ b/src/pages/parts/home/utils.ts
@@ -18,6 +18,7 @@ export function sortMedia(
sortBy: SortOption,
bookmarks?: Record,
progressItems?: Record,
+ runtimeData?: Record,
): MediaItem[] {
- return sortMediaItems(items, sortBy, bookmarks, progressItems);
+ return sortMediaItems(items, sortBy, bookmarks, progressItems, runtimeData);
}
diff --git a/src/utils/mediaSorting.ts b/src/utils/mediaSorting.ts
index 441da491c..e3de79f96 100644
--- a/src/utils/mediaSorting.ts
+++ b/src/utils/mediaSorting.ts
@@ -7,13 +7,16 @@ export type SortOption =
| "title-asc"
| "title-desc"
| "year-asc"
- | "year-desc";
+ | "year-desc"
+ | "length-asc"
+ | "length-desc";
export function sortMediaItems(
items: MediaItem[],
sortBy: SortOption,
bookmarks?: Record,
progressItems?: Record,
+ runtimeData?: Record,
): MediaItem[] {
const sorted = [...items];
@@ -87,8 +90,37 @@ export function sortMediaItems(
break;
}
+ case "length-asc": {
+ const movies = sorted.filter((i) => i.type === "movie");
+ const shows = sorted.filter((i) => i.type === "show");
+ const byLen = (a: MediaItem, b: MediaItem) => {
+ const lenA = runtimeData?.[a.id] ?? Number.MAX_SAFE_INTEGER;
+ const lenB = runtimeData?.[b.id] ?? Number.MAX_SAFE_INTEGER;
+ if (lenA === lenB) return (a.title ?? "").localeCompare(b.title ?? "");
+ return lenA - lenB;
+ };
+ movies.sort(byLen);
+ shows.sort(byLen);
+ sorted.splice(0, sorted.length, ...movies, ...shows);
+ break;
+ }
+
+ case "length-desc": {
+ const movies = sorted.filter((i) => i.type === "movie");
+ const shows = sorted.filter((i) => i.type === "show");
+ const byLen = (a: MediaItem, b: MediaItem) => {
+ const lenA = runtimeData?.[a.id] ?? -1;
+ const lenB = runtimeData?.[b.id] ?? -1;
+ if (lenA === lenB) return (a.title ?? "").localeCompare(b.title ?? "");
+ return lenB - lenA;
+ };
+ movies.sort(byLen);
+ shows.sort(byLen);
+ sorted.splice(0, sorted.length, ...movies, ...shows);
+ break;
+ }
+
default: {
- // Fallback to date sorting for unknown sort options
sorted.sort((a, b) => {
const bookmarkA = bookmarks?.[a.id];
const bookmarkB = bookmarks?.[b.id];
@@ -104,7 +136,7 @@ export function sortMediaItems(
progressB?.updatedAt ?? 0,
);
- return dateB - dateA; // Newest first
+ return dateB - dateA;
});
break;
}