Refresh env, add aiohttp dep, improve resource selection and GStreamer flags
This commit is contained in:
@@ -8,7 +8,7 @@ from xml.etree import ElementTree
|
||||
|
||||
import aiohttp
|
||||
|
||||
from r36s_dlna_browser.dlna.models import MediaItem, ItemType, classify_upnp_class
|
||||
from r36s_dlna_browser.dlna.models import MediaItem, ItemType, classify_upnp_class, select_best_resource
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -103,21 +103,27 @@ def _parse_didl(didl_xml: str, base_url: str) -> List[MediaItem]:
|
||||
parent_id = item_el.get("parentID", "0")
|
||||
upnp_class = _text(item_el.find("upnp:class", _NS))
|
||||
album_art = _text(item_el.find("upnp:albumArtURI", _NS))
|
||||
item_type = classify_upnp_class(upnp_class)
|
||||
|
||||
resource_url = ""
|
||||
mime_type = ""
|
||||
duration = ""
|
||||
size = 0
|
||||
res_el = item_el.find("didl:res", _NS)
|
||||
if res_el is not None:
|
||||
resource_url = res_el.text.strip() if res_el.text else ""
|
||||
protocol_info = res_el.get("protocolInfo", "")
|
||||
# Extract mime from protocolInfo: "http-get:*:audio/mpeg:*"
|
||||
parts = protocol_info.split(":")
|
||||
if len(parts) >= 3:
|
||||
mime_type = parts[2]
|
||||
duration = res_el.get("duration", "")
|
||||
size = int(res_el.get("size", "0") or "0")
|
||||
resources = []
|
||||
for res_el in item_el.findall("didl:res", _NS):
|
||||
resources.append({
|
||||
"#text": res_el.text.strip() if res_el.text else "",
|
||||
"@protocolInfo": res_el.get("protocolInfo", ""),
|
||||
"@duration": res_el.get("duration", ""),
|
||||
"@size": res_el.get("size", "0"),
|
||||
})
|
||||
|
||||
selected_resource = select_best_resource(resources, item_type)
|
||||
if selected_resource is not None:
|
||||
resource_url = str(selected_resource["url"])
|
||||
mime_type = str(selected_resource["mime_type"])
|
||||
duration = str(selected_resource["duration"])
|
||||
size = int(selected_resource["size"])
|
||||
|
||||
# Resolve relative URLs
|
||||
if resource_url and not resource_url.startswith(("http://", "https://")):
|
||||
@@ -128,7 +134,7 @@ def _parse_didl(didl_xml: str, base_url: str) -> List[MediaItem]:
|
||||
items.append(MediaItem(
|
||||
object_id=obj_id,
|
||||
title=title,
|
||||
item_type=classify_upnp_class(upnp_class),
|
||||
item_type=item_type,
|
||||
parent_id=parent_id,
|
||||
resource_url=resource_url,
|
||||
mime_type=mime_type,
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
class ItemType(Enum):
|
||||
@@ -49,6 +51,116 @@ class MediaItem:
|
||||
return self.title or self.object_id
|
||||
|
||||
|
||||
_SUBTITLE_EXTENSIONS = {".ass", ".idx", ".srt", ".ssa", ".sub", ".sup", ".ttml", ".vtt"}
|
||||
_VIDEO_EXTENSIONS = {".avi", ".m2ts", ".m4v", ".mkv", ".mov", ".mp4", ".mpeg", ".mpg", ".ts", ".webm"}
|
||||
_AUDIO_EXTENSIONS = {".aac", ".flac", ".m4a", ".mp3", ".ogg", ".opus", ".wav"}
|
||||
_IMAGE_EXTENSIONS = {".bmp", ".gif", ".jpeg", ".jpg", ".png", ".webp"}
|
||||
_SUBTITLE_MIME_MARKERS = (
|
||||
"application/ass",
|
||||
"application/pgs",
|
||||
"application/sdp",
|
||||
"application/smil",
|
||||
"application/srt",
|
||||
"application/ssa",
|
||||
"application/ttml+xml",
|
||||
"application/vtt",
|
||||
"application/x-subrip",
|
||||
"subpicture/",
|
||||
"subtitle/",
|
||||
"text/",
|
||||
)
|
||||
|
||||
|
||||
def _extract_protocol_mime(protocol_info: str) -> str:
|
||||
parts = protocol_info.split(":")
|
||||
if len(parts) >= 3:
|
||||
return parts[2]
|
||||
return protocol_info
|
||||
|
||||
|
||||
def _resource_kind_from_mime(mime_type: str) -> ItemType | str | None:
|
||||
mime = mime_type.lower()
|
||||
if not mime:
|
||||
return None
|
||||
if any(marker in mime for marker in _SUBTITLE_MIME_MARKERS):
|
||||
return "text"
|
||||
if mime.startswith("video/"):
|
||||
return ItemType.VIDEO
|
||||
if mime.startswith("audio/"):
|
||||
return ItemType.AUDIO
|
||||
if mime.startswith("image/"):
|
||||
return ItemType.IMAGE
|
||||
return None
|
||||
|
||||
|
||||
def _resource_kind_from_url(url: str) -> ItemType | str | None:
|
||||
path = urlparse(url).path.lower()
|
||||
dot = path.rfind(".")
|
||||
ext = path[dot:] if dot >= 0 else ""
|
||||
if ext in _SUBTITLE_EXTENSIONS:
|
||||
return "text"
|
||||
if ext in _VIDEO_EXTENSIONS:
|
||||
return ItemType.VIDEO
|
||||
if ext in _AUDIO_EXTENSIONS:
|
||||
return ItemType.AUDIO
|
||||
if ext in _IMAGE_EXTENSIONS:
|
||||
return ItemType.IMAGE
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_resource(resource: dict[str, Any] | str) -> dict[str, Any]:
|
||||
if isinstance(resource, str):
|
||||
return {
|
||||
"url": resource,
|
||||
"protocol_info": "",
|
||||
"mime_type": "",
|
||||
"size": 0,
|
||||
"duration": "",
|
||||
}
|
||||
|
||||
protocol_info = str(resource.get("protocol_info", resource.get("@protocolInfo", "")))
|
||||
mime_type = str(resource.get("mime_type", "")) or _extract_protocol_mime(protocol_info)
|
||||
return {
|
||||
"url": str(resource.get("url", resource.get("uri", resource.get("#text", "")))),
|
||||
"protocol_info": protocol_info,
|
||||
"mime_type": mime_type,
|
||||
"size": int(resource.get("size", resource.get("@size", 0)) or 0),
|
||||
"duration": str(resource.get("duration", resource.get("@duration", ""))),
|
||||
}
|
||||
|
||||
|
||||
def select_best_resource(resources: Sequence[dict[str, Any] | str], item_type: ItemType) -> dict[str, Any] | None:
|
||||
best: dict[str, Any] | None = None
|
||||
best_key: tuple[int, int, int] | None = None
|
||||
|
||||
for resource in resources:
|
||||
normalized = _normalize_resource(resource)
|
||||
url = normalized["url"]
|
||||
if not url:
|
||||
continue
|
||||
|
||||
resource_kind = _resource_kind_from_mime(normalized["mime_type"]) or _resource_kind_from_url(url)
|
||||
score = 0
|
||||
if item_type != ItemType.UNKNOWN and resource_kind == item_type:
|
||||
score += 10
|
||||
elif resource_kind == "text":
|
||||
score -= 20
|
||||
elif item_type != ItemType.UNKNOWN and resource_kind is not None:
|
||||
score -= 4
|
||||
elif resource_kind in {ItemType.AUDIO, ItemType.VIDEO, ItemType.IMAGE}:
|
||||
score += 3
|
||||
|
||||
if normalized["protocol_info"].startswith("http-get:"):
|
||||
score += 1
|
||||
|
||||
key = (score, int(normalized["size"]), len(url))
|
||||
if best_key is None or key > best_key:
|
||||
best = normalized
|
||||
best_key = key
|
||||
|
||||
return best
|
||||
|
||||
|
||||
def classify_upnp_class(upnp_class: str) -> ItemType:
|
||||
"""Map a UPnP class string to our ItemType enum."""
|
||||
c = upnp_class.lower()
|
||||
@@ -79,15 +191,12 @@ def parse_didl_item(didl_item: dict) -> MediaItem:
|
||||
resources = didl_item.get("resources", didl_item.get("res", []))
|
||||
if isinstance(resources, dict):
|
||||
resources = [resources]
|
||||
if resources:
|
||||
res = resources[0]
|
||||
if isinstance(res, dict):
|
||||
resource_url = res.get("url", res.get("uri", res.get("#text", "")))
|
||||
mime_type = res.get("protocol_info", res.get("@protocolInfo", ""))
|
||||
size = int(res.get("size", res.get("@size", 0)) or 0)
|
||||
duration = res.get("duration", res.get("@duration", ""))
|
||||
elif isinstance(res, str):
|
||||
resource_url = res
|
||||
selected_resource = select_best_resource(resources, item_type)
|
||||
if selected_resource is not None:
|
||||
resource_url = str(selected_resource["url"])
|
||||
mime_type = str(selected_resource["protocol_info"] or selected_resource["mime_type"])
|
||||
size = int(selected_resource["size"])
|
||||
duration = str(selected_resource["duration"])
|
||||
|
||||
album_art = didl_item.get("album_art_uri", didl_item.get("albumArtURI", ""))
|
||||
|
||||
|
||||
@@ -260,6 +260,7 @@ class GStreamerBackend(PlayerBackend):
|
||||
return self._pipeline
|
||||
|
||||
self._pipeline = self._playbin_factory()
|
||||
self._configure_playbin_flags(self._pipeline)
|
||||
self._video_sink = self._create_appsink()
|
||||
self._pipeline.set_property("video-sink", self._video_sink)
|
||||
self._pipeline.set_property("volume", self._volume / 100.0)
|
||||
@@ -267,6 +268,24 @@ class GStreamerBackend(PlayerBackend):
|
||||
self._start_bus_thread()
|
||||
return self._pipeline
|
||||
|
||||
def _configure_playbin_flags(self, pipeline) -> None:
|
||||
play_flags = getattr(self._gst, "PlayFlags", None)
|
||||
if play_flags is None:
|
||||
return
|
||||
|
||||
flags = int(pipeline.get_property("flags"))
|
||||
for required in ("AUDIO", "VIDEO"):
|
||||
value = getattr(play_flags, required, None)
|
||||
if value is not None:
|
||||
flags |= int(value)
|
||||
|
||||
for disabled in ("TEXT", "VIS"):
|
||||
value = getattr(play_flags, disabled, None)
|
||||
if value is not None:
|
||||
flags &= ~int(value)
|
||||
|
||||
pipeline.set_property("flags", flags)
|
||||
|
||||
def _create_appsink(self):
|
||||
sink = self._appsink_factory()
|
||||
sink.set_property("emit-signals", True)
|
||||
|
||||
Reference in New Issue
Block a user