Add preferred format settings and release preference sorting

This commit is contained in:
2026-05-27 21:52:36 +00:00
parent fe71e1be8b
commit 1383fc8da3
4 changed files with 300 additions and 2 deletions
+140
View File
@@ -141,6 +141,96 @@
</button>
</div>
</section>
<section class="settings-section">
<h2 class="settings-section-title">Preferred Format</h2>
<p class="settings-section-desc">Preferred format when multiple versions are available.</p>
<div class="format-grid">
<div class="format-row format-row-stack">
<label class="format-radio-label" for="resolution-hd">
<input
id="resolution-hd"
class="format-radio"
type="radio"
name="resolution-preference"
:checked="settings.preferredResolution === 'r2'"
@change="setPreferredResolution('r2')"
/>
HD or lower
</label>
<label class="format-radio-label" for="resolution-fhd">
<input
id="resolution-fhd"
class="format-radio"
type="radio"
name="resolution-preference"
:checked="settings.preferredResolution === 'r3'"
@change="setPreferredResolution('r3')"
/>
Full HD
</label>
<label class="format-radio-label" for="resolution-4k">
<input
id="resolution-4k"
class="format-radio"
type="radio"
name="resolution-preference"
:checked="settings.preferredResolution === 'r4'"
@change="setPreferredResolution('r4')"
/>
4K
</label>
<label class="format-radio-label" for="resolution-highest">
<input
id="resolution-highest"
class="format-radio"
type="radio"
name="resolution-preference"
:checked="settings.preferredResolution === 'rmax'"
@change="setPreferredResolution('rmax')"
/>
Highest
</label>
</div>
<div class="format-row format-row-stack">
<label class="format-radio-label" for="hdr-none">
<input
id="hdr-none"
class="format-radio"
type="radio"
name="hdr-preference"
:checked="settings.preferredHdr === 'none'"
@change="setPreferredHdr('none')"
/>
No HDR
</label>
<label class="format-radio-label" for="hdr-hdr10plus">
<input
id="hdr-hdr10plus"
class="format-radio"
type="radio"
name="hdr-preference"
:checked="settings.preferredHdr === 'hdr10plus'"
@change="setPreferredHdr('hdr10plus')"
/>
HDR10+
</label>
<label class="format-radio-label" for="hdr-dovi">
<input
id="hdr-dovi"
class="format-radio"
type="radio"
name="hdr-preference"
:checked="settings.preferredHdr === 'dovi'"
@change="setPreferredHdr('dovi')"
/>
Dolby Vision
</label>
</div>
</div>
</section>
</div>
</div>
</div>
@@ -154,6 +244,13 @@ import { navAttrs } from "../composables/useKeyboardNavigation"
import logoUrl from "../assets/mediahive.webp"
import { fetchRoots, replaceRoots, pickFolderAndAddRoot } from "../api"
import HexKeyboard from "./HexKeyboard.vue"
import {
useSettings,
type ResolutionPreference,
type HdrPreference,
} from "../composables/useSettings"
const settings = useSettings()
interface RootEntry {
root_id: string
@@ -189,6 +286,14 @@ onUnmounted(() => window.removeEventListener("pywebviewready", _onPywebviewReady
const showSettings = ref(false)
const roots = ref<RootEntry[]>([])
function setPreferredResolution(value: ResolutionPreference) {
settings.preferredResolution = value
}
function setPreferredHdr(value: HdrPreference) {
settings.preferredHdr = value
}
async function refreshRoots() {
try {
const data = await fetchRoots()
@@ -555,4 +660,39 @@ onUnmounted(() => {
.roots-add-btn:hover {
background: rgba(255, 255, 255, 0.15);
}
.format-row {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 0;
}
.format-grid {
display: grid;
grid-template-columns: 9.5em 9.5em;
gap: 0.75em 1.25em;
justify-content: start;
}
.format-row-stack {
flex-direction: column;
gap: 8px;
}
.format-radio-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
color: var(--text-primary);
font-size: 0.85rem;
}
.format-radio {
width: 15px;
height: 15px;
cursor: pointer;
accent-color: var(--accent, #3b82f6);
}
</style>
+2 -1
View File
@@ -213,6 +213,7 @@ import {
import castPlaceholderFemaleUrl from "../assets/cast-placeholder-female.svg"
import castPlaceholderMaleUrl from "../assets/cast-placeholder-male.svg"
import SeriesFullView from "./SeriesFullView.vue"
import { sortTorrentsByPreference } from "../composables/useSettings"
import ReleaseVersionCard from "./ReleaseVersionCard.vue"
import ReleaseActionMenu from "./ReleaseActionMenu.vue"
import {
@@ -498,7 +499,7 @@ function getShowreelSourceAttributes(path: string): VideoSourceAttributes {
const movieVersions = computed((): Torrent[] => {
if (props.item.type !== "movies") return []
const movie = props.item.data as Movie
return Object.values(movie.torrents || {})
return sortTorrentsByPreference(Object.values(movie.torrents || {}))
})
// Page backdrop background
+2 -1
View File
@@ -150,7 +150,7 @@
</div>
<div v-if="Object.values(contextMenu.episode.torrents || {}).length > 0">
<ReleaseVersionCard
v-for="(torrent, index) in Object.values(contextMenu.episode.torrents || {})"
v-for="(torrent, index) in sortTorrentsByPreference(Object.values(contextMenu.episode.torrents || {}))"
:key="index"
class="context-menu-version"
:torrent="torrent"
@@ -189,6 +189,7 @@ import { getCoverUrl, getVideoPreviewUrl, getVideoSourceAttributes, isSafariBrow
import { navAttrs } from "../composables/useKeyboardNavigation"
import ReleaseVersionCard from "./ReleaseVersionCard.vue"
import ReleaseActionMenu from "./ReleaseActionMenu.vue"
import { sortTorrentsByPreference } from "../composables/useSettings"
const props = defineProps<{
series: Series
+156
View File
@@ -0,0 +1,156 @@
import { reactive, watch } from "vue"
import type { Torrent } from "../types"
export type ResolutionPreference = "r2" | "r3" | "r4" | "rmax"
export type HdrPreference = "none" | "hdr10plus" | "dovi"
export interface MediaHiveSettings {
preferredResolution: ResolutionPreference
preferredHdr: HdrPreference
}
const STORAGE_KEY = "MediaHive"
const RESOLUTION_PRIORITY: Record<string, number> = {
"8K": 5,
"4K": 4,
"FHD": 3,
"HD": 2,
"SD": 1,
}
// Max resolution priority allowed for each preference level
const RESOLUTION_CAP: Record<ResolutionPreference, number> = {
r2: 2,
r3: 3,
r4: 4,
rmax: 999,
}
const RESOLUTION_VALUES = ["r2", "r3", "r4", "rmax"] as const
const HDR_VALUES = ["none", "hdr10plus", "dovi"] as const
function isResolutionPreference(value: unknown): value is ResolutionPreference {
return typeof value === "string" && RESOLUTION_VALUES.includes(value as ResolutionPreference)
}
function isHdrPreference(value: unknown): value is HdrPreference {
return typeof value === "string" && HDR_VALUES.includes(value as HdrPreference)
}
function loadSettings(): MediaHiveSettings {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw) {
const parsed = JSON.parse(raw) as Partial<MediaHiveSettings>
return {
preferredResolution: isResolutionPreference(parsed.preferredResolution)
? parsed.preferredResolution
: "rmax",
preferredHdr: isHdrPreference(parsed.preferredHdr) ? parsed.preferredHdr : "none",
}
}
} catch {
// ignore parse errors
}
return { preferredResolution: "rmax", preferredHdr: "none" }
}
const settings = reactive<MediaHiveSettings>(loadSettings())
watch(
() => ({ ...settings }),
(value) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(value))
} catch {
// ignore
}
},
{ deep: true },
)
export function useSettings() {
return settings
}
/**
* Sort a list of Torrent objects according to the current format preferences.
*
* Algorithm:
* 1. Compute the "effective priority" = min(actual priority, cap).
* Releases exceeding the cap receive the capped priority.
* 2. Higher effective priority wins.
* 3. For equal effective priority, prefer HDR flavor per `preferredHdr`.
* 4. Equal in all above: fall back to raw resolution priority (larger is better)
* then size.
*/
export function sortTorrentsByPreference(torrents: Torrent[]): Torrent[] {
const cap = RESOLUTION_CAP[settings.preferredResolution]
function effectivePriority(t: Torrent): number {
const raw = RESOLUTION_PRIORITY[t.resolution ?? ""] ?? 0
return Math.min(raw, cap)
}
type HdrProfile = "sdr" | "hdr10" | "hdr10plus" | "dovi"
function detectHdrProfile(t: Torrent): HdrProfile {
const text = [t.title, t.quality, t.codec, t.audio].filter(Boolean).join(" ")
const hasDovi = t.has_dolby_vision || /dolby\s*vision|dovi|\bdv\b/i.test(text)
const hasHdr10Plus = /hdr10\+|hdr10plus/i.test(text)
const hasAnyHdr = t.is_hdr || hasHdr10Plus || hasDovi || /\bhdr\b/i.test(text)
if (hasDovi) return "dovi"
if (hasHdr10Plus) return "hdr10plus"
if (hasAnyHdr) return "hdr10"
return "sdr"
}
function scoreForPreference(profile: HdrProfile): number {
if (settings.preferredHdr === "none") {
// Prefer no HDR; HDR variants after SDR.
if (profile === "sdr") return 4
if (profile === "hdr10") return 3
if (profile === "hdr10plus") return 2
return 1
}
if (settings.preferredHdr === "hdr10plus") {
// Requested behavior: HDR10+ best, HDR10 next, SDR then, DoVi last.
if (profile === "hdr10plus") return 4
if (profile === "hdr10") return 3
if (profile === "sdr") return 2
return 1
}
if (profile === "dovi") return 4
if (profile === "hdr10plus") return 3
if (profile === "hdr10") return 2
return 1
}
function hdrPreferenceScore(t: Torrent): number {
return scoreForPreference(detectHdrProfile(t))
}
return [...torrents].sort((a, b) => {
const epA = effectivePriority(a)
const epB = effectivePriority(b)
if (epB !== epA) return epB - epA
// Same effective resolution bucket - apply HDR format preference
const hdrScoreA = hdrPreferenceScore(a)
const hdrScoreB = hdrPreferenceScore(b)
if (hdrScoreB !== hdrScoreA) {
return hdrScoreB - hdrScoreA
}
// Fall back to raw resolution priority then size
const rawA = RESOLUTION_PRIORITY[a.resolution ?? ""] ?? 0
const rawB = RESOLUTION_PRIORITY[b.resolution ?? ""] ?? 0
if (rawB !== rawA) return rawB - rawA
return (b.size ?? 0) - (a.size ?? 0)
})
}