Show bitrate in browser, remove type labels and Play All text

- Extract bitrate alongside duration in browse_directory via get_media_info
- Display bitrate in large card view metadata (duration · bitrate · size)
- Replace Audio/Video type badge with bitrate column in list view
- Remove Play All button text, keep icon only
- Add formatBitrate helper function

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 03:37:13 +03:00
parent 5f474d6c9f
commit 4c13322936
4 changed files with 32 additions and 34 deletions

View File

@@ -121,24 +121,28 @@ class BrowserService:
return "other" return "other"
@staticmethod @staticmethod
def get_duration(file_path: Path) -> Optional[float]: def get_media_info(file_path: Path) -> dict:
"""Get duration of a media file in seconds (header-only read). """Get duration and bitrate of a media file (header-only read).
Args: Args:
file_path: Path to the media file. file_path: Path to the media file.
Returns: Returns:
Duration in seconds, or None if unavailable. Dict with 'duration' (float or None) and 'bitrate' (int or None).
""" """
result = {"duration": None, "bitrate": None}
if not HAS_MUTAGEN: if not HAS_MUTAGEN:
return None return result
try: try:
audio = MutagenFile(str(file_path)) audio = MutagenFile(str(file_path))
if audio is not None and hasattr(audio, "info") and hasattr(audio.info, "length"): if audio is not None and hasattr(audio, "info"):
return round(audio.info.length, 2) if hasattr(audio.info, "length"):
result["duration"] = round(audio.info.length, 2)
if hasattr(audio.info, "bitrate") and audio.info.bitrate:
result["bitrate"] = audio.info.bitrate
except Exception: except Exception:
pass pass
return None return result
@staticmethod @staticmethod
def browse_directory( def browse_directory(
@@ -255,9 +259,12 @@ class BrowserService:
item["modified"] = None item["modified"] = None
if item["is_media"]: if item["is_media"]:
item["duration"] = BrowserService.get_duration(item_path) info = BrowserService.get_media_info(item_path)
item["duration"] = info["duration"]
item["bitrate"] = info["bitrate"]
else: else:
item["duration"] = None item["duration"] = None
item["bitrate"] = None
return { return {
"folder_id": folder_id, "folder_id": folder_id,

View File

@@ -1542,23 +1542,12 @@
min-width: 0; min-width: 0;
} }
.browser-list-type { .browser-list-bitrate {
font-size: 0.625rem; font-size: 0.75rem;
text-transform: uppercase; color: var(--text-muted);
font-weight: 600;
color: var(--text-secondary);
padding: 0.15rem 0.5rem;
background: var(--bg-primary);
border-radius: 4px;
white-space: nowrap; white-space: nowrap;
} min-width: 55px;
text-align: right;
.browser-list-type.audio {
color: var(--accent);
}
.browser-list-type.video {
color: #3b82f6;
} }
.browser-list-duration { .browser-list-duration {

View File

@@ -193,7 +193,6 @@
</button> </button>
<button class="browser-play-all-btn" id="playAllBtn" onclick="playAllFolder()" data-i18n-title="browser.play_all" title="Play All" style="display: none;"> <button class="browser-play-all-btn" id="playAllBtn" onclick="playAllFolder()" data-i18n-title="browser.play_all" title="Play All" style="display: none;">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M8 5v14l11-7z"/></svg> <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
<span data-i18n="browser.play_all">Play All</span>
</button> </button>
</div> </div>
<div class="browser-search-wrapper" id="browserSearchWrapper" style="display: none;"> <div class="browser-search-wrapper" id="browserSearchWrapper" style="display: none;">

View File

@@ -1711,15 +1711,11 @@ function renderBrowserList(items, container) {
name.textContent = item.name; name.textContent = item.name;
row.appendChild(name); row.appendChild(name);
// Type badge // Bitrate
if (item.type !== 'folder') { const br = document.createElement('div');
const typeBadge = document.createElement('div'); br.className = 'browser-list-bitrate';
typeBadge.className = `browser-list-type ${item.type}`; br.textContent = formatBitrate(item.bitrate) || '';
typeBadge.textContent = item.type; row.appendChild(br);
row.appendChild(typeBadge);
} else {
row.appendChild(document.createElement('div'));
}
// Duration // Duration
const dur = document.createElement('div'); const dur = document.createElement('div');
@@ -1833,6 +1829,8 @@ function renderBrowserGrid(items, container) {
const parts = []; const parts = [];
const duration = formatDuration(item.duration); const duration = formatDuration(item.duration);
if (duration) parts.push(duration); if (duration) parts.push(duration);
const bitrate = formatBitrate(item.bitrate);
if (bitrate) parts.push(bitrate);
if (item.size !== null) parts.push(formatFileSize(item.size)); if (item.size !== null) parts.push(formatFileSize(item.size));
meta.textContent = parts.join(' \u00B7 '); meta.textContent = parts.join(' \u00B7 ');
if (parts.length) info.appendChild(meta); if (parts.length) info.appendChild(meta);
@@ -1902,6 +1900,11 @@ function formatDuration(seconds) {
return `${m}:${String(s).padStart(2, '0')}`; return `${m}:${String(s).padStart(2, '0')}`;
} }
function formatBitrate(bps) {
if (bps == null || bps <= 0) return null;
return Math.round(bps / 1000) + ' kbps';
}
async function loadThumbnail(imgElement, fileName) { async function loadThumbnail(imgElement, fileName) {
try { try {
const token = localStorage.getItem('media_server_token'); const token = localStorage.getItem('media_server_token');