Add Internet Archive Provider (#2411)
authorOzGav <gavnosp@hotmail.com>
Sun, 28 Sep 2025 17:34:01 +0000 (03:34 +1000)
committerGitHub <noreply@github.com>
Sun, 28 Sep 2025 17:34:01 +0000 (19:34 +0200)
music_assistant/providers/internet_archive/__init__.py [new file with mode: 0644]
music_assistant/providers/internet_archive/constants.py [new file with mode: 0644]
music_assistant/providers/internet_archive/helpers.py [new file with mode: 0644]
music_assistant/providers/internet_archive/icon.svg [new file with mode: 0644]
music_assistant/providers/internet_archive/icon_monochrome.svg [new file with mode: 0644]
music_assistant/providers/internet_archive/manifest.json [new file with mode: 0644]
music_assistant/providers/internet_archive/parsers.py [new file with mode: 0644]
music_assistant/providers/internet_archive/provider.py [new file with mode: 0644]
music_assistant/providers/internet_archive/streaming.py [new file with mode: 0644]

diff --git a/music_assistant/providers/internet_archive/__init__.py b/music_assistant/providers/internet_archive/__init__.py
new file mode 100644 (file)
index 0000000..e64c9b7
--- /dev/null
@@ -0,0 +1,40 @@
+"""Internet Archive music provider for Music Assistant."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from music_assistant_models.enums import ProviderFeature
+
+from .provider import InternetArchiveProvider
+
+if TYPE_CHECKING:
+    from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig
+    from music_assistant_models.provider import ProviderManifest
+
+    from music_assistant import MusicAssistant
+    from music_assistant.models import ProviderInstanceType
+
+
+SUPPORTED_FEATURES = {
+    ProviderFeature.SEARCH,
+    ProviderFeature.ARTIST_ALBUMS,
+    ProviderFeature.ARTIST_TOPTRACKS,
+}
+
+
+async def setup(
+    mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
+) -> ProviderInstanceType:
+    """Initialize provider instance with given configuration."""
+    return InternetArchiveProvider(mass, manifest, config, SUPPORTED_FEATURES)
+
+
+async def get_config_entries(
+    mass: MusicAssistant,  # noqa: ARG001
+    instance_id: str | None = None,  # noqa: ARG001
+    action: str | None = None,  # noqa: ARG001
+    values: dict[str, ConfigValueType] | None = None,  # noqa: ARG001
+) -> tuple[ConfigEntry, ...]:
+    """Return Config entries to setup this provider."""
+    return ()
diff --git a/music_assistant/providers/internet_archive/constants.py b/music_assistant/providers/internet_archive/constants.py
new file mode 100644 (file)
index 0000000..f8c4765
--- /dev/null
@@ -0,0 +1,40 @@
+"""Constants for the Internet Archive provider."""
+
+from __future__ import annotations
+
+# Internet Archive API endpoints
+IA_SEARCH_URL = "https://archive.org/advancedsearch.php"
+IA_METADATA_URL = "https://archive.org/metadata"
+IA_DETAILS_URL = "https://archive.org/details"
+IA_DOWNLOAD_URL = "https://archive.org/download"
+IA_SERVE_URL = "https://archive.org/serve"
+
+# Audio file formats supported by IA (normalized to lowercase for consistent comparison)
+# IA API returns formats in inconsistent casing, so we normalize to lowercase internally
+SUPPORTED_AUDIO_FORMATS = {
+    "vbr mp3",
+    "mp3",
+    "128kbps mp3",
+    "64kbps mp3",
+    "flac",
+    "ogg vorbis",
+    "ogg",
+    "aac",
+    "m4a",
+    "wav",
+    "aiff",
+}
+
+# Preferred format order for audio quality (normalized to lowercase)
+# Ordered from highest to lowest quality preference
+PREFERRED_AUDIO_FORMATS = [
+    "flac",
+    "vbr mp3",
+    "ogg vorbis",
+    "mp3",
+    "128kbps mp3",
+    "64kbps mp3",
+]
+
+# Collections that should be treated as audiobooks (verified)
+AUDIOBOOK_COLLECTIONS = {"librivoxaudio"}
diff --git a/music_assistant/providers/internet_archive/helpers.py b/music_assistant/providers/internet_archive/helpers.py
new file mode 100644 (file)
index 0000000..5b1c647
--- /dev/null
@@ -0,0 +1,328 @@
+"""Helpers/utilities for the Internet Archive provider."""
+
+from __future__ import annotations
+
+import json
+import re
+from typing import TYPE_CHECKING, Any
+from urllib.parse import quote
+
+import aiohttp
+from music_assistant_models.errors import (
+    InvalidDataError,
+    MediaNotFoundError,
+    ResourceTemporarilyUnavailable,
+)
+
+from .constants import (
+    IA_DETAILS_URL,
+    IA_DOWNLOAD_URL,
+    IA_METADATA_URL,
+    IA_SEARCH_URL,
+    PREFERRED_AUDIO_FORMATS,
+    SUPPORTED_AUDIO_FORMATS,
+)
+
+if TYPE_CHECKING:
+    from music_assistant import MusicAssistant
+
+
+class InternetArchiveClient:
+    """Client for communicating with the Internet Archive API."""
+
+    def __init__(self, mass: MusicAssistant) -> None:
+        """Initialize the Internet Archive client."""
+        self.mass = mass
+
+    async def _get_json(self, url: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
+        """Make a GET request and return JSON response with proper error handling."""
+        try:
+            async with self.mass.http_session.get(
+                url, params=params, timeout=aiohttp.ClientTimeout(total=30)
+            ) as response:
+                if response.status == 429:
+                    # Rate limited - let throttler handle this
+                    backoff_time = int(response.headers.get("Retry-After", 60))
+                    raise ResourceTemporarilyUnavailable(
+                        "Internet Archive rate limit exceeded", backoff_time=backoff_time
+                    )
+
+                if response.status == 404:
+                    raise MediaNotFoundError("Item not found on Internet Archive")
+
+                if response.status >= 500:
+                    raise ResourceTemporarilyUnavailable(
+                        "Internet Archive server error", backoff_time=30
+                    )
+
+                response.raise_for_status()
+                json_data = await response.json()
+
+                if not isinstance(json_data, dict):
+                    raise InvalidDataError(f"Expected JSON object, got {type(json_data).__name__}")
+
+                return json_data
+
+        except aiohttp.ClientError as err:
+            raise ResourceTemporarilyUnavailable(f"Network error: {err}") from err
+        except TimeoutError as err:
+            raise ResourceTemporarilyUnavailable(f"Request timeout: {err}") from err
+        except json.JSONDecodeError as err:
+            raise InvalidDataError(f"Invalid JSON response: {err}") from err
+
+    async def search(
+        self,
+        query: str,
+        mediatype: str | None = None,
+        collection: str | None = None,
+        rows: int = 50,
+        page: int = 1,
+        sort: str | None = None,
+    ) -> dict[str, Any]:
+        """
+        Search the Internet Archive using the advanced search API.
+
+        Args:
+            query: Search query string
+            mediatype: Optional media type filter (e.g., 'audio')
+            collection: Optional collection filter (e.g., 'etree')
+            rows: Number of results per page (max 200)
+            page: Page number for pagination
+            sort: Sort order (e.g., 'downloads desc', 'date desc')
+
+        Returns:
+            Search response dictionary containing results and metadata
+        """
+        params: dict[str, Any] = {
+            "output": "json",
+            "rows": min(rows, 200),  # IA limits to 200 per request
+            "page": page,
+            "q": query,
+        }
+        if sort:
+            params["sort"] = sort
+
+        return await self._get_json(IA_SEARCH_URL, params)
+
+    async def get_metadata(self, identifier: str) -> dict[str, Any]:
+        """Get metadata for a specific Internet Archive item."""
+        url = f"{IA_METADATA_URL}/{identifier}"
+        return await self._get_json(url)
+
+    async def get_files(self, identifier: str) -> list[dict[str, Any]]:
+        """Get file list for an Internet Archive item."""
+        metadata = await self.get_metadata(identifier)
+        return list(metadata.get("files", []))
+
+    async def get_audio_files(self, identifier: str) -> list[dict[str, Any]]:
+        """
+        Get audio files for an item with format preference and deduplication.
+
+        Filters for supported audio formats, removes derivative low-quality files,
+        deduplicates by base filename, and selects the best quality format for
+        each unique track.
+
+        Args:
+            identifier: Internet Archive item identifier
+
+        Returns:
+            List of audio file information dictionaries, sorted by filename
+            for proper track ordering
+        """
+        files = await self.get_files(identifier)
+        files_by_basename: dict[str, list[dict[str, Any]]] = {}
+
+        for file_info in files:
+            filename = file_info.get("name", "")
+            file_format = file_info.get("format", "").lower()
+
+            if not self._is_supported_audio_format(file_format):
+                continue
+            if self._is_derivative_file(file_info, filename):
+                continue
+
+            base_name = self._get_base_filename(filename)
+            files_by_basename.setdefault(base_name, []).append(file_info)
+
+        preferred_files: list[dict[str, Any]] = []
+        for format_versions in files_by_basename.values():
+            best_file = self._select_best_audio_format(format_versions)
+            if best_file:
+                preferred_files.append(best_file)
+
+        return sorted(preferred_files, key=lambda x: x.get("name", ""))
+
+    def _is_supported_audio_format(self, file_format: str) -> bool:
+        """Check if the file format is a supported audio format."""
+        return any(fmt in file_format for fmt in SUPPORTED_AUDIO_FORMATS)
+
+    def _is_derivative_file(self, file_info: dict[str, Any], filename: str) -> bool:
+        """Check if a file is a derivative (low-quality) version."""
+        return file_info.get("source", "") == "derivative" and any(
+            skip in filename.lower() for skip in ("_64kb", "_vbr", "_sample", "_preview")
+        )
+
+    def _get_base_filename(self, filename: str) -> str:
+        """Extract base filename without extension and quality indicators for deduplication."""
+        # Remove extension first
+        base = filename.rsplit(".", 1)[0] if "." in filename else filename
+
+        # Remove common quality indicators from Internet Archive files
+        quality_patterns = [
+            r"_320kb$",
+            r"_256kb$",
+            r"_192kb$",
+            r"_128kb$",
+            r"_64kb$",
+            r"_vbr$",
+            r"_original$",
+            r"_sample$",
+            r"_preview$",
+        ]
+
+        for pattern in quality_patterns:
+            base = re.sub(pattern, "", base, flags=re.IGNORECASE)
+
+        return base
+
+    def _select_best_audio_format(
+        self, format_versions: list[dict[str, Any]]
+    ) -> dict[str, Any] | None:
+        """
+        Select the best audio format from available versions.
+
+        Prefers higher quality formats based on PREFERRED_AUDIO_FORMATS ordering.
+        Falls back to first available if no preferred format is found.
+
+        Args:
+            format_versions: List of file info dictionaries for the same track
+
+        Returns:
+            Best quality file info dictionary, or None if no valid files
+        """
+        for preferred_format in PREFERRED_AUDIO_FORMATS:
+            for file_info in format_versions:
+                if preferred_format in file_info.get("format", "").lower():
+                    return file_info
+        return format_versions[0] if format_versions else None
+
+    def get_download_url(self, identifier: str, filename: str) -> str:
+        """
+        Get download URL for a specific file.
+
+        Args:
+            identifier: Internet Archive item identifier
+            filename: Name of the file to download
+
+        Returns:
+            Full download URL for the file
+        """
+        return f"{IA_DOWNLOAD_URL}/{identifier}/{quote(filename)}"
+
+    def get_item_url(self, identifier: str) -> str:
+        """
+        Get the details page URL for an Internet Archive item.
+
+        Args:
+            identifier: Internet Archive item identifier
+
+        Returns:
+            Full URL to the item's details page
+        """
+        return f"{IA_DETAILS_URL}/{identifier}"
+
+
+def parse_duration(duration_str: str) -> int | None:
+    """
+    Parse duration string to seconds.
+
+    Handles various duration formats commonly found in Internet Archive metadata:
+    - "1:23:45" (hours:minutes:seconds)
+    - "12:34" (minutes:seconds)
+    - "123" (seconds only)
+
+    Args:
+        duration_str: Duration string to parse
+
+    Returns:
+        Duration in seconds, or None if parsing fails
+    """
+    if not duration_str:
+        return None
+    try:
+        if ":" in duration_str:
+            parts = duration_str.split(":")
+            if len(parts) == 3:  # h:m:s
+                hours, minutes, seconds = map(float, parts)
+                return int(hours * 3600 + minutes * 60 + seconds)
+            if len(parts) == 2:  # m:s
+                minutes, seconds = map(float, parts)
+                return int(minutes * 60 + seconds)
+            return None
+        return int(float(duration_str))
+    except (ValueError, TypeError):
+        return None
+
+
+def clean_text(text: str | list[str] | None) -> str:
+    """
+    Clean and normalize text fields from Internet Archive metadata.
+
+    Internet Archive metadata can contain text as strings or lists of strings.
+    This function normalizes the input to a clean string.
+
+    Args:
+        text: Text to clean (string, list of strings, or None)
+
+    Returns:
+        Cleaned text string, or empty string if no valid text found
+    """
+    if not text:
+        return ""
+    if isinstance(text, list):
+        for item in text:
+            if isinstance(item, str) and item.strip():
+                return item.strip()
+        return ""
+    return text.strip()
+
+
+def extract_year(date_str: str | list[str] | None) -> int | None:
+    """
+    Extract year from Internet Archive date string.
+
+    Internet Archive dates can be in various formats. This function attempts
+    to extract a 4-digit year from the date string.
+
+    Args:
+        date_str: Date string or list to extract year from
+
+    Returns:
+        4-digit year as integer, or None if extraction fails
+    """
+    date_text = clean_text(date_str)
+    if not date_text:
+        return None
+    try:
+        match = re.search(r"\b(19\d{2}|20\d{2})\b", date_text)
+        return int(match.group(1)) if match else None
+    except (ValueError, TypeError):
+        return None
+
+
+def get_image_url(identifier: str, filename: str | None = None) -> str | None:
+    """
+    Get image URL for an Internet Archive item.
+
+    Args:
+        identifier: Internet Archive item identifier
+        filename: Optional specific image filename
+
+    Returns:
+        Full URL to the image, or None if identifier is missing
+    """
+    if not identifier:
+        return None
+    if filename:
+        return f"{IA_DOWNLOAD_URL}/{identifier}/{quote(filename)}"
+    return f"{IA_DOWNLOAD_URL}/{identifier}/__ia_thumb.jpg"
diff --git a/music_assistant/providers/internet_archive/icon.svg b/music_assistant/providers/internet_archive/icon.svg
new file mode 100644 (file)
index 0000000..7af0c7e
--- /dev/null
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="512"
+   height="512"
+   viewBox="0 0 135.46665 135.46665"
+   version="1.1"
+   id="svg1"
+   xml:space="preserve"
+   inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
+   sodipodi:docname="icon.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     inkscape:zoom="1.4142136"
+     inkscape:cx="218.84954"
+     inkscape:cy="261.27595"
+     inkscape:window-width="1920"
+     inkscape:window-height="1129"
+     inkscape:window-x="1912"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1" /><defs
+     id="defs1" /><g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"><image
+       width="134.97258"
+       height="134.97258"
+       preserveAspectRatio="none"
+       xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAQAAABecRxxAAAAwnpUWHRSYXcgcHJvZmlsZSB0eXBl&#10;IGV4aWYAAHjabVBbEsMgCPz3FD0CDzVwHNPYmd6gxy8G7MS2m3EhrLMgqb+ej3QbIMwpl02q1gqG&#10;rFmpWSLgaCcj5JMnKKpLPX0EshJbZBekesRZn0YRsVlWLkZyD2FfBc3RXr6MvC3wmGjkRxhpGDG5&#10;gGHQ/FlQVbbrE/YOK8RPGpRlHfvnf7PtHcX6MFFnZDBmFh+AxymJmyVqjKzjon2Ny8nIcxJbyL89&#10;TaQ32/9ZGP/+cJYAAAEkaUNDUElDQyBwcm9maWxlAAB4nJ2QPUvDUBSGn1TxizpVHMQhg2sHBTM5&#10;+IXBoVDTClanNEmxmMSQpBT/Qf+J/pgOguB/cFVw9r3RwcEsXji8D4dz3vfeCw07DpJi8RCStMxd&#10;72hwNbi2l99YpUWTPXb9oMg6vbM+tefzFcvoS9t41c/9eZbCqAikc1UaZHkJ1oHYmZaZYRUbd33v&#10;RDwT22GShuIn8U6YhIbNrpfEk+DH09ymGaWXPdNXbeNyTocuNkMmjIkpaUtTdU5x2Je65Pg8UBBI&#10;YyL1ppopuRUVcnI5FvVFuk1N3laV11XKUB5jeZmEexJ5mjzM/36vfVxUm9bmPPNzv2otqBqjEbw/&#10;wvoAWs+wdlOTtfL7bTUzTjXzzzd+AYGlULEc8x7ZAAANdmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAA&#10;AAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4K&#10;PHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40&#10;LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAy&#10;LzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAg&#10;eG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpz&#10;dEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgog&#10;ICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6&#10;R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25z&#10;LmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20v&#10;eGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo5NjcxNzRlYS03&#10;YjhlLTQ0YTctYWIwZi0zNDY5MGQ5YjQzMjIiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6&#10;MjczOGRlNWEtOGYzMy00MDZkLWI5ZDItZDdlNzgyN2VlNTk5IgogICB4bXBNTTpPcmlnaW5hbERv&#10;Y3VtZW50SUQ9InhtcC5kaWQ6NzBlNGUxODItNjM4My00Nzg5LTg4NzAtZWQxMDdlOGQwY2JmIgog&#10;ICBkYzpGb3JtYXQ9ImltYWdlL3BuZyIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9y&#10;bT0iV2luZG93cyIKICAgR0lNUDpUaW1lU3RhbXA9IjE3NTgyODYzMzQ0ODIzMDkiCiAgIEdJTVA6&#10;VmVyc2lvbj0iMi4xMC4zOCIKICAgdGlmZjpPcmllbnRhdGlvbj0iMSIKICAgeG1wOkNyZWF0b3JU&#10;b29sPSJHSU1QIDIuMTAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjU6MDk6MTlUMjI6NTI6MTQr&#10;MTA6MDAiCiAgIHhtcDpNb2RpZnlEYXRlPSIyMDI1OjA5OjE5VDIyOjUyOjE0KzEwOjAwIj4KICAg&#10;PHhtcE1NOkhpc3Rvcnk+CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFj&#10;dGlvbj0ic2F2ZWQiCiAgICAgIHN0RXZ0OmNoYW5nZWQ9Ii8iCiAgICAgIHN0RXZ0Omluc3RhbmNl&#10;SUQ9InhtcC5paWQ6ZDcyMDM0ZmEtYzVlOC00NDc3LWExZTItYzljYjA1ZDM0NmU3IgogICAgICBz&#10;dEV2dDpzb2Z0d2FyZUFnZW50PSJHaW1wIDIuMTAgKFdpbmRvd3MpIgogICAgICBzdEV2dDp3aGVu&#10;PSIyMDI1LTA5LTE5VDIyOjUyOjE0Ii8+CiAgICA8L3JkZjpTZXE+CiAgIDwveG1wTU06SGlzdG9y&#10;eT4KICA8L3JkZjpEZXNjcmlwdGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;IAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAog&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg&#10;ICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVu&#10;ZD0idyI/PjANpjsAAAACYktHRAD/h4/MvwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+kJ&#10;Eww0DuerXX4AACAASURBVHja7Z13YBzF2caf2b2ik3Tq1Wq2ZVvuvRfcsbGxwcaAAdNLICFASICE&#10;QISAkEYggSTkCwmh9zgEYxv33qtkGVuyeu+9nK7sfH9IMrrTyVa527u9e3/3h629u925d2aemXln&#10;5h2AIAiCIAiCIAiCIAiCIAiCIAiCIAiCIHoi2RcBCEAkFmA+Qske7g/zjJ/hG9TyM/j0+PZhbHTw&#10;A0Px8z7YjuEA0sHQiupQU7VZFpP4IBD+WIEEO+9xfIwmNKJ+ovGspb8PSBSzNQBUiAIg4Db4g2Op&#10;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&#10;"
+       id="image1"
+       x="0.33334681"
+       y="0.33334497" /></g></svg>
diff --git a/music_assistant/providers/internet_archive/icon_monochrome.svg b/music_assistant/providers/internet_archive/icon_monochrome.svg
new file mode 100644 (file)
index 0000000..fb124df
--- /dev/null
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="512"
+   height="512"
+   viewBox="0 0 135.46665 135.46665"
+   version="1.1"
+   id="svg1"
+   xml:space="preserve"
+   inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
+   sodipodi:docname="icon_monochrome.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
+     id="namedview1"
+     pagecolor="#73ffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     inkscape:zoom="1.4142136"
+     inkscape:cx="256.3262"
+     inkscape:cy="275.41808"
+     inkscape:window-width="1920"
+     inkscape:window-height="1129"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1" /><defs
+     id="defs1" /><g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"><image
+       width="135.46666"
+       height="135.46666"
+       preserveAspectRatio="none"
+       xlink:href="data:image/png;base64,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&#10;v70ZHAAAAABJRU5ErkJggg==&#10;"
+       id="image1"
+       x="0.086311005"
+       y="-0.15396202" /></g></svg>
diff --git a/music_assistant/providers/internet_archive/manifest.json b/music_assistant/providers/internet_archive/manifest.json
new file mode 100644 (file)
index 0000000..6d7d2fc
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "domain": "internet_archive",
+  "name": "Internet Archive",
+  "description": "Browse and stream millions of free audio files â€” concerts, historical broadcasts, and audiobooks.",
+  "documentation": "https://music-assistant.io/music-providers/internet-archive/",
+  "type": "music",
+  "requirements": [],
+  "codeowners": "@ozgav",
+  "multi_instance": false,
+  "stage": "beta"
+}
diff --git a/music_assistant/providers/internet_archive/parsers.py b/music_assistant/providers/internet_archive/parsers.py
new file mode 100644 (file)
index 0000000..2f1bfed
--- /dev/null
@@ -0,0 +1,401 @@
+"""Metadata parsing utilities for the Internet Archive provider."""
+
+from __future__ import annotations
+
+import re
+from collections.abc import Callable
+from typing import Any
+
+from music_assistant_models.enums import AlbumType, ImageType
+from music_assistant_models.media_items import (
+    Album,
+    Artist,
+    Audiobook,
+    MediaItemImage,
+    Podcast,
+    ProviderMapping,
+    Track,
+)
+from music_assistant_models.unique_list import UniqueList
+
+from .constants import AUDIOBOOK_COLLECTIONS
+from .helpers import clean_text, extract_year, get_image_url
+
+
+def is_likely_album(doc: dict[str, Any]) -> bool:
+    """
+    Determine if an Internet Archive item is likely an album using metadata heuristics.
+
+    Uses collection types, media types, title analysis, and file count hints to classify items
+    without making expensive API calls to check individual file counts.
+
+    Args:
+        doc: Internet Archive document metadata
+
+    Returns:
+        True if the item is likely an album, False if likely a single track
+    """
+    mediatype = doc.get("mediatype", "")
+    collection = doc.get("collection", [])
+    title = clean_text(doc.get("title", "")).lower()
+
+    if isinstance(collection, str):
+        collection = [collection]
+
+    # etree collection items are almost always live concert albums
+    if "etree" in collection:
+        return True
+
+    # Skip obvious audiobook/speech collections - these are handled separately
+    if any(coll in AUDIOBOOK_COLLECTIONS for coll in collection):
+        return False
+
+    # Check for hints in the metadata that suggest multiple files
+    # Some IA items include file count information
+    if "files" in doc:
+        # If we have file info and it's more than 2-3 files, likely an album
+        # (accounting for derivative files like thumbnails)
+        try:
+            file_count = len(doc["files"]) if isinstance(doc["files"], list) else 0
+            if file_count > 3:  # More than just 1-2 audio files + derivatives
+                return True
+        except (TypeError, KeyError):
+            pass
+
+    # Use title keywords to identify likely albums vs singles
+    album_indicators = [
+        "album",
+        "live",
+        "concert",
+        "session",
+        "collection",
+        "compilation",
+        "complete",
+        "anthology",
+        "best of",
+        "greatest hits",
+        "discography",
+        "vol ",
+        "volume",
+        "part ",
+        "disc ",
+        "cd ",
+        "lp ",
+    ]
+
+    single_indicators = [
+        "single",
+        "track",
+        "song",
+        "remix",
+        "edit",
+        "version",
+        "demo",
+        "instrumental",
+        "acoustic version",
+    ]
+
+    # Strong album indicators in title
+    if any(indicator in title for indicator in album_indicators):
+        return True
+
+    # Strong single indicators in title
+    if any(indicator in title for indicator in single_indicators):
+        return False
+
+    # Collection-specific logic
+    if "netlabels" in collection:
+        # Netlabel releases are usually albums/EPs
+        return True
+
+    if "78rpm" in collection:
+        # 78 RPM records are usually single tracks (A-side/B-side)
+        return False
+
+    if "oldtimeradio" in collection:
+        # Radio shows are usually single episodes, treat as tracks
+        return False
+
+    if "audio_music" in collection:
+        # General music uploads - check for multi-track indicators in title
+        multi_track_indicators = ["ep", "album", "mixtape", "playlist"]
+        return any(indicator in title for indicator in multi_track_indicators)
+
+    # For unknown collections with audio mediatype, be conservative
+    # Default to single track unless we have strong evidence of multiple tracks
+    if mediatype == "audio":
+        # Look for numbering that suggests multiple parts/tracks
+        if re.search(r"\b(track|part|chapter)\s*\d+", title):
+            return True  # Likely part of a larger work
+        return bool(re.search(r"\b\d+\s*of\s*\d+\b", title))
+
+    return False
+
+
+def doc_to_audiobook(
+    doc: dict[str, Any], domain: str, instance_id: str, item_url_func: Callable[[str], str]
+) -> Audiobook | None:
+    """
+    Convert Internet Archive document to Audiobook object.
+
+    Args:
+        doc: Internet Archive document metadata
+        domain: Provider domain
+        instance_id: Provider instance identifier
+        item_url_func: Function to generate item URLs
+
+    Returns:
+        Audiobook object or None if conversion fails
+    """
+    identifier = doc.get("identifier")
+    title = clean_text(doc.get("title"))
+    creator = clean_text(doc.get("creator"))
+
+    if not identifier or not title:
+        return None
+
+    audiobook = Audiobook(
+        item_id=identifier,
+        provider=instance_id,
+        name=title,
+        provider_mappings={create_provider_mapping(identifier, domain, instance_id, item_url_func)},
+    )
+
+    # Add author/narrator
+    if creator:
+        audiobook.authors.append(creator)
+
+    # Add metadata
+    if description := clean_text(doc.get("description")):
+        audiobook.metadata.description = description
+
+    # Add thumbnail
+    add_item_image(audiobook, identifier, instance_id)
+
+    return audiobook
+
+
+def doc_to_track(
+    doc: dict[str, Any], domain: str, instance_id: str, item_url_func: Callable[[str], str]
+) -> Track | None:
+    """
+    Convert Internet Archive document to Track object.
+
+    Args:
+        doc: Internet Archive document metadata
+        domain: Provider domain
+        instance_id: Provider instance identifier
+        item_url_func: Function to generate item URLs
+
+    Returns:
+        Track object or None if conversion fails
+    """
+    identifier = doc.get("identifier")
+    title = clean_text(doc.get("title"))
+    creator = clean_text(doc.get("creator"))
+
+    if not identifier or not title:
+        return None
+
+    track = Track(
+        item_id=identifier,
+        provider=instance_id,
+        name=title,
+        provider_mappings={create_provider_mapping(identifier, domain, instance_id, item_url_func)},
+    )
+
+    # Add artist if available
+    if creator:
+        track.artists = UniqueList([create_artist(creator, domain, instance_id)])
+
+    # Add thumbnail
+    add_item_image(track, identifier, instance_id)
+
+    return track
+
+
+def doc_to_album(
+    doc: dict[str, Any], domain: str, instance_id: str, item_url_func: Callable[[str], str]
+) -> Album | None:
+    """
+    Convert Internet Archive document to Album object.
+
+    Args:
+        doc: Internet Archive document metadata
+        domain: Provider domain
+        instance_id: Provider instance identifier
+        item_url_func: Function to generate item URLs
+
+    Returns:
+        Album object or None if conversion fails
+    """
+    identifier = doc.get("identifier")
+    title = clean_text(doc.get("title"))
+    creator = clean_text(doc.get("creator"))
+
+    if not identifier or not title:
+        return None
+
+    album = Album(
+        item_id=identifier,
+        provider=instance_id,
+        name=title,
+        provider_mappings={create_provider_mapping(identifier, domain, instance_id, item_url_func)},
+    )
+
+    # Add artist if available
+    if creator:
+        album.artists = UniqueList([create_artist(creator, domain, instance_id)])
+
+    # Add metadata
+    if date := extract_year(doc.get("date")):
+        album.year = date
+
+    if description := clean_text(doc.get("description")):
+        album.metadata.description = description
+
+    # Add thumbnail
+    add_item_image(album, identifier, instance_id)
+
+    # Add album type
+    album.album_type = AlbumType.ALBUM
+
+    return album
+
+
+def doc_to_artist(creator_name: str, domain: str, instance_id: str) -> Artist:
+    """Convert creator name to Artist object."""
+    return create_artist(creator_name, domain, instance_id)
+
+
+def create_title_from_identifier(identifier: str) -> str:
+    """Create a human-readable title from an Internet Archive identifier."""
+    return identifier.replace("_", " ").replace("-", " ").title()
+
+
+def artist_exists(artist: Artist, artists: list[Artist]) -> bool:
+    """Check if an artist already exists in the list to avoid duplicates."""
+    return any(existing.name == artist.name for existing in artists)
+
+
+def create_provider_mapping(
+    identifier: str, domain: str, instance_id: str, item_url_func: Callable[[str], str]
+) -> ProviderMapping:
+    """Create a standardized provider mapping for an item."""
+    return ProviderMapping(
+        item_id=identifier,
+        provider_domain=domain,
+        provider_instance=instance_id,
+        url=item_url_func(identifier),
+        available=True,
+    )
+
+
+def create_artist(creator_name: str, domain: str, instance_id: str) -> Artist:
+    """Create an Artist object from creator name."""
+    return Artist(
+        item_id=creator_name,
+        provider=instance_id,
+        name=creator_name,
+        provider_mappings={
+            ProviderMapping(
+                item_id=creator_name,
+                provider_domain=domain,
+                provider_instance=instance_id,
+            )
+        },
+    )
+
+
+def add_item_image(
+    item: Track | Album | Audiobook | Podcast, identifier: str, instance_id: str
+) -> None:
+    """Add thumbnail image to a media item if available."""
+    if thumb_url := get_image_url(identifier):
+        item.metadata.add_image(
+            MediaItemImage(
+                type=ImageType.THUMB,
+                path=thumb_url,
+                provider=instance_id,
+                remotely_accessible=True,
+            )
+        )
+
+
+def is_audiobook_content(doc: dict[str, Any]) -> bool:
+    """
+    Determine if an Internet Archive item is audiobook content.
+
+    Checks if the item is from a known audiobook collection.
+
+    Args:
+        doc: Internet Archive document metadata
+
+    Returns:
+        True if the item is from a known audiobook collection
+    """
+    collection = doc.get("collection", [])
+    if isinstance(collection, str):
+        collection = [collection]
+
+    return any(coll in AUDIOBOOK_COLLECTIONS for coll in collection)
+
+
+def doc_to_podcast(
+    doc: dict[str, Any], domain: str, instance_id: str, item_url_func: Callable[[str], str]
+) -> Podcast | None:
+    """
+    Convert Internet Archive document to Podcast object.
+
+    Args:
+        doc: Internet Archive document metadata
+        domain: Provider domain
+        instance_id: Provider instance identifier
+        item_url_func: Function to generate item URLs
+
+    Returns:
+        Podcast object or None if conversion fails
+    """
+    identifier = doc.get("identifier")
+    title = clean_text(doc.get("title"))
+    creator = clean_text(doc.get("creator"))
+
+    if not identifier or not title:
+        return None
+
+    podcast = Podcast(
+        item_id=identifier,
+        provider=instance_id,
+        name=title,
+        provider_mappings={create_provider_mapping(identifier, domain, instance_id, item_url_func)},
+    )
+
+    # Add publisher/creator
+    if creator:
+        podcast.publisher = creator
+
+    # Add metadata
+    if description := clean_text(doc.get("description")):
+        podcast.metadata.description = description
+
+    # Add thumbnail
+    add_item_image(podcast, identifier, instance_id)
+
+    return podcast
+
+
+def is_podcast_content(doc: dict[str, Any]) -> bool:
+    """
+    Determine if an Internet Archive item is podcast content.
+
+    Args:
+        doc: Internet Archive document metadata
+
+    Returns:
+        True if the item is from a podcast collection
+    """
+    collection = doc.get("collection", [])
+    if isinstance(collection, str):
+        collection = [collection]
+
+    return "podcasts" in collection
diff --git a/music_assistant/providers/internet_archive/provider.py b/music_assistant/providers/internet_archive/provider.py
new file mode 100644 (file)
index 0000000..1e01f13
--- /dev/null
@@ -0,0 +1,960 @@
+"""Internet Archive music provider implementation."""
+
+from __future__ import annotations
+
+import contextlib
+import re
+from collections.abc import AsyncGenerator
+from typing import TYPE_CHECKING, Any
+
+import aiohttp
+from music_assistant_models.enums import MediaType, ProviderFeature
+from music_assistant_models.errors import InvalidDataError, MediaNotFoundError
+from music_assistant_models.media_items import (
+    Album,
+    Artist,
+    Audiobook,
+    MediaItemChapter,
+    Podcast,
+    PodcastEpisode,
+    ProviderMapping,
+    SearchResults,
+    Track,
+)
+from music_assistant_models.unique_list import UniqueList
+
+from music_assistant.constants import UNKNOWN_ARTIST
+from music_assistant.controllers.cache import use_cache
+from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries
+from music_assistant.models.music_provider import MusicProvider
+
+from .helpers import (
+    InternetArchiveClient,
+    clean_text,
+    extract_year,
+    parse_duration,
+)
+from .parsers import (
+    add_item_image,
+    artist_exists,
+    create_artist,
+    create_provider_mapping,
+    create_title_from_identifier,
+    doc_to_album,
+    doc_to_audiobook,
+    doc_to_podcast,
+    doc_to_track,
+    is_audiobook_content,
+    is_likely_album,
+    is_podcast_content,
+)
+from .streaming import InternetArchiveStreaming
+
+if TYPE_CHECKING:
+    from music_assistant_models.config_entries import ProviderConfig
+    from music_assistant_models.provider import ProviderManifest
+    from music_assistant_models.streamdetails import StreamDetails
+
+    from music_assistant import MusicAssistant
+
+
+class InternetArchiveProvider(MusicProvider):
+    """Implementation of Internet Archive music provider."""
+
+    def __init__(
+        self,
+        mass: MusicAssistant,
+        manifest: ProviderManifest,
+        config: ProviderConfig,
+        supported_features: set[ProviderFeature],
+    ) -> None:
+        """Initialize the provider."""
+        super().__init__(mass, manifest, config, supported_features)
+        self.throttler = ThrottlerManager(
+            rate_limit=10, period=60, retry_attempts=5, initial_backoff=5
+        )
+        self.client = InternetArchiveClient(mass)
+        self.streaming = InternetArchiveStreaming(self)
+
+    @property
+    def is_streaming_provider(self) -> bool:
+        """Return True if provider is a streaming provider."""
+        return True
+
+    @throttle_with_retries
+    async def _get_json(self, url: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
+        """Make a GET request and return JSON response with throttling."""
+        return await self.client._get_json(url, params)
+
+    @throttle_with_retries
+    async def _search(self, **kwargs: Any) -> dict[str, Any]:
+        """Throttled search wrapper."""
+        return await self.client.search(**kwargs)
+
+    @throttle_with_retries
+    async def _get_metadata(self, identifier: str) -> dict[str, Any]:
+        """Throttled metadata wrapper."""
+        return await self.client.get_metadata(identifier)
+
+    @throttle_with_retries
+    @use_cache(expiration=86400 * 30)  # 30 days - file listings are static
+    async def _get_audio_files(self, identifier: str) -> list[dict[str, Any]]:
+        """Throttled audio files wrapper."""
+        return await self.client.get_audio_files(identifier)
+
+    @use_cache(86400 * 7)  # 7 days
+    async def search(
+        self,
+        search_query: str,
+        media_types: list[MediaType],
+        limit: int = 5,
+    ) -> SearchResults:
+        """
+        Perform search on Internet Archive.
+
+        Uses multiple search strategies to maximize result coverage with
+        proper result accumulation and broader search patterns.
+
+        Args:
+            search_query: The search term to look for
+            media_types: List of media types to search for
+            limit: Maximum number of results to return per media type
+
+        Returns:
+            SearchResults object containing found items
+        """
+        if not search_query.strip():
+            return SearchResults()
+
+        # Adjust search intensity based on what's being requested
+        rows_per_strategy = min(limit * 2, 16) if len(media_types) > 1 else min(limit * 2, 100)
+
+        # Collect results in separate lists
+        tracks: list[Track] = []
+        albums: list[Album] = []
+        artists: list[Artist] = []
+        audiobooks: list[Audiobook] = []
+        podcasts: list[Podcast] = []
+
+        # Track processed identifiers to avoid duplicates across strategies
+        processed_ids: set[str] = set()
+
+        # Build search strategies based on requested media types
+        search_strategies = []
+
+        # For music searches: focus on title and creator
+        if any(mt in media_types for mt in [MediaType.TRACK, MediaType.ALBUM, MediaType.ARTIST]):
+            search_strategies.extend(
+                [
+                    (f"creator:({search_query}) AND mediatype:audio", "downloads desc"),
+                    (f"title:({search_query}) AND mediatype:audio", "downloads desc"),
+                    (f"subject:({search_query}) AND mediatype:audio", "downloads desc"),
+                ]
+            )
+
+        # For audiobooks: search within audiobook collections, still limit to audio
+        if MediaType.AUDIOBOOK in media_types:
+            audiobook_query = f"{search_query} AND collection:(librivoxaudio OR audio_bookspoetry) AND mediatype:audio"  # noqa: E501
+            search_strategies.append((audiobook_query, "downloads desc"))
+
+        # For podcasts: search within podcast collections
+        if MediaType.PODCAST in media_types:
+            podcast_query = f"{search_query} AND collection:podcasts AND mediatype:audio"
+            search_strategies.append((podcast_query, "downloads desc"))
+
+        for strategy_idx, (strategy_query, sort_order) in enumerate(search_strategies):
+            self.logger.debug("Trying search strategy %d: %s", strategy_idx + 1, strategy_query)
+
+            try:
+                search_response = await self._search(
+                    query=strategy_query,
+                    rows=rows_per_strategy,
+                    sort=sort_order,
+                )
+
+                response_data = search_response.get("response", {})
+                docs = response_data.get("docs", [])
+                self.logger.debug(
+                    "Strategy %d '%s' found %d raw results",
+                    strategy_idx + 1,
+                    strategy_query,
+                    len(docs),
+                )
+
+                # Process results and extract different media types
+                strategy_processed = 0
+                strategy_skipped = 0
+
+                for doc in docs:
+                    try:
+                        identifier = doc.get("identifier")
+                        if not identifier or identifier in processed_ids:
+                            strategy_skipped += 1
+                            continue
+
+                        # Track this identifier to avoid duplicates
+                        processed_ids.add(identifier)
+
+                        await self._process_search_result(
+                            doc, tracks, albums, artists, audiobooks, podcasts, media_types
+                        )
+                        strategy_processed += 1
+
+                        # Check if we have enough results across all types
+                        if self._has_sufficient_results(
+                            tracks, albums, artists, audiobooks, podcasts, media_types, limit
+                        ):
+                            self.logger.debug(
+                                "Sufficient results found after strategy %d, stopping search",
+                                strategy_idx + 1,
+                            )
+                            break
+
+                    except (InvalidDataError, KeyError) as err:
+                        self.logger.debug("Skipping invalid search result: %s", err)
+                        strategy_skipped += 1
+                        continue
+
+                self.logger.debug(
+                    "Strategy %d '%s': processed %d new items, skipped %d items. "
+                    "Running totals - tracks: %d, albums: %d, artists: %d, "
+                    "audiobooks: %d, podcasts: %d",
+                    strategy_idx + 1,
+                    strategy_query,
+                    strategy_processed,
+                    strategy_skipped,
+                    len(tracks),
+                    len(albums),
+                    len(artists),
+                    len(audiobooks),
+                    len(podcasts),
+                )
+
+                # If we have sufficient results, stop trying more strategies
+                if self._has_sufficient_results(
+                    tracks, albums, artists, audiobooks, podcasts, media_types, limit
+                ):
+                    break
+
+            except Exception as err:
+                self.logger.warning("Search strategy %d failed: %s", strategy_idx + 1, err)
+                continue
+
+        # Log final results for debugging
+        self.logger.debug(
+            "Search for '%s' completed. Final results - tracks: %d, albums: %d, "
+            "artists: %d, audiobooks: %d, podcasts: %d (processed %d unique items)",
+            search_query,
+            len(tracks),
+            len(albums),
+            len(artists),
+            len(audiobooks),
+            len(podcasts),
+            len(processed_ids),
+        )
+
+        return SearchResults(
+            tracks=tracks[:limit] if MediaType.TRACK in media_types else [],
+            albums=albums[:limit] if MediaType.ALBUM in media_types else [],
+            artists=artists[:limit] if MediaType.ARTIST in media_types else [],
+            audiobooks=audiobooks[:limit] if MediaType.AUDIOBOOK in media_types else [],
+            podcasts=podcasts[:limit] if MediaType.PODCAST in media_types else [],
+        )
+
+    def _has_sufficient_results(
+        self,
+        tracks: list[Track],
+        albums: list[Album],
+        artists: list[Artist],
+        audiobooks: list[Audiobook],
+        podcasts: list[Podcast],
+        media_types: list[MediaType],
+        limit: int,
+    ) -> bool:
+        """Check if we have sufficient results for all requested media types."""
+        return (
+            (MediaType.TRACK not in media_types or len(tracks) >= limit)
+            and (MediaType.ALBUM not in media_types or len(albums) >= limit)
+            and (MediaType.ARTIST not in media_types or len(artists) >= limit)
+            and (MediaType.AUDIOBOOK not in media_types or len(audiobooks) >= limit)
+            and (MediaType.PODCAST not in media_types or len(podcasts) >= limit)
+        )
+
+    async def _process_search_result(
+        self,
+        doc: dict[str, Any],
+        tracks: list[Track],
+        albums: list[Album],
+        artists: list[Artist],
+        audiobooks: list[Audiobook],
+        podcasts: list[Podcast],
+        media_types: list[MediaType],
+    ) -> None:
+        """
+        Process a single search result document from Internet Archive.
+
+        Determines the appropriate media type and creates corresponding objects.
+        Uses improved heuristics to classify items as tracks, albums, or audiobooks.
+        """
+        identifier = doc.get("identifier")
+        if not identifier:
+            raise InvalidDataError("Missing identifier in search result")
+
+        title = clean_text(doc.get("title"))
+        creator = clean_text(doc.get("creator"))
+
+        # Be lenient - allow items without title if they have identifier
+        if not title and not identifier:
+            raise InvalidDataError("Missing both title and identifier in search result")
+
+        # Use identifier as fallback title if needed
+        if not title:
+            title = create_title_from_identifier(identifier)
+
+        # Determine what type of item this is
+        mediatype = doc.get("mediatype", "")
+        collection = doc.get("collection", [])
+        if isinstance(collection, str):
+            collection = [collection]
+
+        # Check if this is audiobook content using improved detection
+        if is_audiobook_content(doc) and MediaType.AUDIOBOOK in media_types:
+            audiobook = doc_to_audiobook(
+                doc, self.domain, self.instance_id, self.client.get_item_url
+            )
+            if audiobook:
+                audiobooks.append(audiobook)
+            return  # Don't process as other media types
+
+        # Check if this is podcast content
+        if is_podcast_content(doc) and MediaType.PODCAST in media_types:
+            podcast = doc_to_podcast(doc, self.domain, self.instance_id, self.client.get_item_url)
+            if podcast:
+                podcasts.append(podcast)
+            return  # Don't process as other media types
+
+        # For etree items, usually each item is an album (concert)
+        if mediatype == "etree" or "etree" in collection:
+            if MediaType.ALBUM in media_types:
+                album = doc_to_album(doc, self.domain, self.instance_id, self.client.get_item_url)
+                if album:
+                    albums.append(album)
+
+            if MediaType.ARTIST in media_types and creator:
+                artist = create_artist(creator, self.domain, self.instance_id)
+                if artist and not artist_exists(artist, artists):
+                    artists.append(artist)
+
+        elif mediatype == "audio":
+            # Use heuristics to determine album vs track without expensive API calls
+            if is_likely_album(doc):
+                if MediaType.ALBUM in media_types:
+                    album = doc_to_album(
+                        doc, self.domain, self.instance_id, self.client.get_item_url
+                    )
+                    if album:
+                        albums.append(album)
+            elif MediaType.TRACK in media_types:
+                track = doc_to_track(doc, self.domain, self.instance_id, self.client.get_item_url)
+                if track:
+                    tracks.append(track)
+
+            if MediaType.ARTIST in media_types and creator:
+                artist = create_artist(creator, self.domain, self.instance_id)
+                if artist and not artist_exists(artist, artists):
+                    artists.append(artist)
+
+    @use_cache(expiration=86400 * 60)  # Cache for 60 days - artist "tracks" change infrequently
+    async def get_track(self, prov_track_id: str) -> Track:
+        """Get full track details by id."""
+        metadata = await self._get_metadata(prov_track_id)
+        item_metadata = metadata.get("metadata", {})
+
+        title = clean_text(item_metadata.get("title"))
+        creator = clean_text(item_metadata.get("creator"))
+
+        if not title:
+            raise MediaNotFoundError(f"Track {prov_track_id} not found or invalid")
+
+        track = Track(
+            item_id=prov_track_id,
+            provider=self.instance_id,
+            name=title,
+            provider_mappings={
+                create_provider_mapping(
+                    prov_track_id, self.domain, self.instance_id, self.client.get_item_url
+                )
+            },
+        )
+
+        # Add artist
+        if creator:
+            track.artists = UniqueList([create_artist(creator, self.domain, self.instance_id)])
+        else:
+            track.artists = UniqueList(
+                [create_artist(UNKNOWN_ARTIST, self.domain, self.instance_id)]
+            )
+
+        # Add duration from first audio file
+        try:
+            audio_files = await self._get_audio_files(prov_track_id)
+            if audio_files and audio_files[0].get("length"):
+                duration = parse_duration(audio_files[0]["length"])
+                if duration:
+                    track.duration = duration
+        except (TimeoutError, aiohttp.ClientError) as err:
+            self.logger.debug("Network error getting duration for track %s: %s", prov_track_id, err)
+        except (KeyError, ValueError, TypeError) as err:
+            self.logger.debug("Could not parse duration for track %s: %s", prov_track_id, err)
+
+        # Add metadata
+        if description := clean_text(item_metadata.get("description")):
+            track.metadata.description = description
+
+        # Add thumbnail
+        add_item_image(track, prov_track_id, self.instance_id)
+
+        return track
+
+    @use_cache(expiration=86400 * 60)  # Cache for 60 days - album catalogs change infrequently
+    async def get_album(self, prov_album_id: str) -> Album:
+        """Get full album details by id."""
+        metadata = await self._get_metadata(prov_album_id)
+        item_metadata = metadata.get("metadata", {})
+
+        title = clean_text(item_metadata.get("title"))
+        creator = clean_text(item_metadata.get("creator"))
+
+        if not title:
+            raise MediaNotFoundError(f"Album {prov_album_id} not found or invalid")
+
+        album = Album(
+            item_id=prov_album_id,
+            provider=self.instance_id,
+            name=title,
+            provider_mappings={
+                create_provider_mapping(
+                    prov_album_id, self.domain, self.instance_id, self.client.get_item_url
+                )
+            },
+        )
+
+        # Add artist
+        if creator:
+            album.artists = UniqueList([create_artist(creator, self.domain, self.instance_id)])
+        else:
+            album.artists = UniqueList(
+                [create_artist(UNKNOWN_ARTIST, self.domain, self.instance_id)]
+            )
+
+        # Add metadata
+        if date := extract_year(item_metadata.get("date")):
+            album.year = date
+
+        if description := clean_text(item_metadata.get("description")):
+            album.metadata.description = description
+
+        # Add thumbnail
+        add_item_image(album, prov_album_id, self.instance_id)
+
+        return album
+
+    @use_cache(expiration=86400 * 60)  # Cache for 60 days - artist catalogs change infrequently
+    async def get_artist(self, prov_artist_id: str) -> Artist:
+        """
+        Get full artist details by id.
+
+        Args:
+            prov_artist_id: Provider-specific artist identifier (artist name)
+
+        Returns:
+            Artist object
+        """
+        # Artist IDs are just the creator names
+        return Artist(
+            item_id=prov_artist_id,
+            provider=self.instance_id,
+            name=prov_artist_id,
+            provider_mappings={
+                ProviderMapping(
+                    item_id=prov_artist_id,
+                    provider_domain=self.domain,
+                    provider_instance=self.instance_id,
+                )
+            },
+        )
+
+    @use_cache(expiration=86400 * 30)  # Cache for 30 days - audiobook catalogs change infrequently
+    async def get_audiobook(self, prov_audiobook_id: str) -> Audiobook:
+        """Get full audiobook details by id."""
+        metadata = await self._get_metadata(prov_audiobook_id)
+        item_metadata = metadata.get("metadata", {})
+
+        title = clean_text(item_metadata.get("title"))
+        creator = clean_text(item_metadata.get("creator"))
+
+        if not title:
+            raise MediaNotFoundError(f"Audiobook {prov_audiobook_id} not found or invalid")
+
+        audiobook = Audiobook(
+            item_id=prov_audiobook_id,
+            provider=self.instance_id,
+            name=title,
+            provider_mappings={
+                create_provider_mapping(
+                    prov_audiobook_id, self.domain, self.instance_id, self.client.get_item_url
+                )
+            },
+        )
+
+        # Add author/narrator
+        if creator:
+            author_list = [creator]
+            audiobook.authors = UniqueList(author_list)
+
+        # Add metadata
+        if description := clean_text(item_metadata.get("description")):
+            audiobook.metadata.description = description
+
+        # Add thumbnail
+        add_item_image(audiobook, prov_audiobook_id, self.instance_id)
+
+        # Calculate duration and chapters
+        try:
+            total_duration, chapters = await self._calculate_audiobook_duration_and_chapters(
+                prov_audiobook_id
+            )
+            audiobook.duration = total_duration
+            if len(chapters) > 1:
+                audiobook.metadata.chapters = chapters
+
+        except Exception as err:
+            self.logger.warning(
+                f"Could not process audio files for audiobook {prov_audiobook_id}: {err}"
+            )
+            audiobook.duration = 0
+            audiobook.metadata.chapters = []
+
+        return audiobook
+
+    async def get_album_tracks(self, prov_album_id: str) -> list[Track]:
+        """Get album tracks for given album id."""
+        metadata = await self._get_metadata(prov_album_id)
+        item_metadata = metadata.get("metadata", {})
+        audio_files = await self._get_audio_files(prov_album_id)
+        tracks = []
+
+        # Pre-create album artist to avoid duplicates
+        album_artist = clean_text(item_metadata.get("creator"))
+        album_artist_normalized = album_artist.lower() if album_artist else ""
+        album_artist_obj = None
+        if album_artist:
+            album_artist_obj = create_artist(album_artist, self.domain, self.instance_id)
+        else:
+            album_artist_obj = create_artist(UNKNOWN_ARTIST, self.domain, self.instance_id)
+
+        for i, file_info in enumerate(audio_files, 1):
+            filename = file_info.get("name", "")
+
+            # Use file's title if available, otherwise clean up filename
+            track_name = file_info.get("title", filename)
+            if not track_name or track_name == filename:
+                track_name = filename.rsplit(".", 1)[0] if "." in filename else filename
+
+            # Try to extract track number from file metadata first, then filename
+            track_number = self._extract_track_number(file_info, track_name, i)
+
+            track = Track(
+                item_id=f"{prov_album_id}#{filename}",
+                provider=self.instance_id,
+                name=track_name,
+                track_number=track_number,
+                provider_mappings={
+                    ProviderMapping(
+                        item_id=f"{prov_album_id}#{filename}",
+                        provider_domain=self.domain,
+                        provider_instance=self.instance_id,
+                        url=self.client.get_download_url(prov_album_id, filename),
+                        available=True,
+                    )
+                },
+            )
+
+            # Add file-specific artist if available, otherwise use album artist
+            file_artist = file_info.get("artist") or file_info.get("creator")
+            if file_artist:
+                file_artist_cleaned = clean_text(file_artist)
+                file_artist_normalized = file_artist_cleaned.lower()
+                # Check if this is the same as album artist to avoid duplicates (case-insensitive)
+                if album_artist_normalized and file_artist_normalized == album_artist_normalized:
+                    track.artists = UniqueList([album_artist_obj])
+                else:
+                    track.artists = UniqueList(
+                        [create_artist(file_artist_cleaned, self.domain, self.instance_id)]
+                    )
+            else:
+                # Use pre-created album artist object
+                track.artists = UniqueList([album_artist_obj])
+
+            # Add duration if available
+            if duration_str := file_info.get("length"):
+                if duration := parse_duration(duration_str):
+                    track.duration = duration
+
+            # Add genre if available
+            if genre := file_info.get("genre"):
+                track.metadata.genres = {clean_text(genre)}
+
+            tracks.append(track)
+
+        return tracks
+
+    def _extract_track_number(
+        self, file_info: dict[str, Any], track_name: str, fallback: int
+    ) -> int:
+        """Extract track number from file metadata or filename."""
+        track_number = None
+
+        if "track" in file_info:
+            with contextlib.suppress(ValueError, AttributeError):
+                track_number = int(str(file_info["track"]).split("/")[0])
+
+        if track_number is None:
+            # Fallback to filename parsing
+            track_num_match = re.search(r"^(\d+)[\s\-_.]*(.+)", track_name)
+            track_number = int(track_num_match.group(1)) if track_num_match else fallback
+
+        return track_number
+
+    @use_cache(expiration=86400 * 30)  # Cache for 30 days - artist catalogs change infrequently
+    async def get_artist_albums(self, prov_artist_id: str) -> list[Album]:
+        """
+        Get albums for a specific artist.
+
+        Uses metadata heuristics to determine likely albums without expensive
+        API calls for better performance.
+
+        Args:
+            prov_artist_id: Provider-specific artist identifier (artist name)
+
+        Returns:
+            List of Album objects by the artist
+        """
+        albums: list[Album] = []
+        page = 0
+        page_size = 200  # IA's maximum
+
+        while len(albums) < 1000:  # Reasonable upper limit
+            search_response = await self._search(
+                query=f'creator:"{prov_artist_id}" AND (format:"VBR MP3" OR format:"FLAC" \
+        OR format:"Ogg Vorbis")',
+                sort="downloads desc",
+                rows=page_size,
+                page=page,
+            )
+
+            docs = search_response.get("response", {}).get("docs", [])
+            if not docs:
+                break
+
+            for doc in docs:
+                try:
+                    # Use metadata heuristics instead of expensive API calls
+                    # to determine if item is an album
+                    if is_likely_album(doc):
+                        album = doc_to_album(
+                            doc, self.domain, self.instance_id, self.client.get_item_url
+                        )
+                        if album:
+                            albums.append(album)
+                except (KeyError, ValueError, TypeError) as err:
+                    self.logger.debug(
+                        "Skipping invalid album for artist %s: %s", prov_artist_id, err
+                    )
+                    continue
+                except (TimeoutError, aiohttp.ClientError) as err:
+                    self.logger.debug(
+                        "Network error processing album for artist %s: %s", prov_artist_id, err
+                    )
+                    continue
+                except Exception as err:
+                    self.logger.exception(
+                        "Unexpected error processing album for artist %s: %s", prov_artist_id, err
+                    )
+                    continue
+            page += 1
+        return albums
+
+    @use_cache(expiration=86400 * 7)  # Cache for 1 week
+    async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]:
+        """
+        Get top tracks for a specific artist.
+
+        Uses the same search as get_artist_albums but filters for single tracks.
+
+        Args:
+            prov_artist_id: Provider-specific artist identifier (artist name)
+
+        Returns:
+            List of Track objects representing the artist's top tracks
+        """
+        tracks = []
+        search_response = await self._search(
+            query=(
+                f'creator:"{prov_artist_id}" AND '
+                f'(format:"VBR MP3" OR format:"FLAC" OR format:"Ogg Vorbis")'
+            ),
+            rows=25,  # Limit for "top" tracks
+            sort="downloads desc",
+        )
+
+        response_data = search_response.get("response", {})
+        docs = response_data.get("docs", [])
+
+        for doc in docs:
+            try:
+                # Only include items that are NOT classified as albums
+                if not is_likely_album(doc):
+                    track = doc_to_track(
+                        doc, self.domain, self.instance_id, self.client.get_item_url
+                    )
+                    if track:
+                        tracks.append(track)
+            except (KeyError, ValueError, TypeError) as err:
+                self.logger.debug("Skipping invalid track for artist %s: %s", prov_artist_id, err)
+                continue
+            except (TimeoutError, aiohttp.ClientError) as err:
+                self.logger.debug(
+                    "Network error processing track for artist %s: %s", prov_artist_id, err
+                )
+                continue
+            except Exception as err:
+                self.logger.exception(
+                    "Unexpected error processing track for artist %s: %s", prov_artist_id, err
+                )
+                continue
+
+            if len(tracks) >= 25:
+                break
+
+        return tracks
+
+    async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails:
+        """
+        Get streamdetails for a track or audiobook.
+
+        Delegates to the streaming handler for proper multi-file support.
+
+        Args:
+            item_id: Provider-specific item identifier
+            media_type: The type of media being requested
+
+        Returns:
+            StreamDetails object configured for the specific item type
+
+        Raises:
+            MediaNotFoundError: If no audio files are found for the item
+        """
+        return await self.streaming.get_stream_details(item_id, media_type)
+
+    async def _calculate_audiobook_duration_and_chapters(
+        self, item_id: str
+    ) -> tuple[int, list[MediaItemChapter]]:
+        """Calculate duration and chapters for audiobooks."""
+        audio_files = await self._get_audio_files(item_id)
+        total_duration = 0
+        chapters = []
+        current_position = 0.0
+
+        for i, file_info in enumerate(audio_files, 1):
+            chapter_duration = parse_duration(file_info.get("length", "0")) or 0
+            total_duration += chapter_duration
+
+            chapter_name = file_info.get("title") or file_info.get("name", f"Chapter {i}")
+            chapter = MediaItemChapter(
+                position=i,
+                name=clean_text(chapter_name),
+                start=current_position,
+                end=current_position + chapter_duration if chapter_duration > 0 else None,
+            )
+            chapters.append(chapter)
+            current_position += chapter_duration
+
+        return total_duration, chapters
+
+    async def get_audio_stream(
+        self, streamdetails: StreamDetails, seek_position: int = 0
+    ) -> AsyncGenerator[bytes, None]:
+        """Get audio stream from Internet Archive."""
+        # Use sock_read=None to allow long audiobook chapters to stream fully
+        timeout = aiohttp.ClientTimeout(sock_read=None, total=None)
+
+        if streamdetails.media_type == MediaType.AUDIOBOOK and isinstance(streamdetails.data, dict):
+            chapter_urls = streamdetails.data.get("chapters", [])
+            chapters_data = streamdetails.data.get("chapters_data", [])
+
+            # Calculate which chapter to start from based on seek_position
+            seek_position_ms = seek_position * 1000
+            start_chapter = 0
+
+            if seek_position > 0 and chapters_data:
+                accumulated_duration_ms = 0
+
+                for i, chapter_data in enumerate(chapters_data):
+                    chapter_duration_ms = (
+                        parse_duration(chapter_data.get("length", "0")) or 0
+                    ) * 1000
+
+                    if accumulated_duration_ms + chapter_duration_ms > seek_position_ms:
+                        start_chapter = i
+                        break
+                    accumulated_duration_ms += chapter_duration_ms
+
+            # Stream chapters starting from calculated position
+            chapters_yielded = False
+            for i in range(start_chapter, len(chapter_urls)):
+                chapter_url = chapter_urls[i]
+
+                try:
+                    async with self.mass.http_session.get(chapter_url, timeout=timeout) as response:
+                        response.raise_for_status()
+                        async for chunk in response.content.iter_chunked(8192):
+                            chapters_yielded = True
+                            yield chunk
+                except Exception as e:
+                    self.logger.error(f"Chapter {i + 1} streaming failed: {e}")
+                    continue
+
+            # If no chapters succeeded, raise an error instead of silent failure
+            if not chapters_yielded:
+                raise MediaNotFoundError(
+                    f"Failed to stream any chapters for audiobook {streamdetails.item_id}"
+                )
+
+        else:
+            # Handle single files
+            audio_files = await self._get_audio_files(streamdetails.item_id)
+            if audio_files:
+                download_url = self.client.get_download_url(
+                    streamdetails.item_id, audio_files[0]["name"]
+                )
+                async with self.mass.http_session.get(download_url, timeout=timeout) as response:
+                    response.raise_for_status()
+                    async for chunk in response.content.iter_chunked(8192):
+                        yield chunk
+
+    @use_cache(expiration=86400 * 7)  # Cache for 1 week
+    async def get_podcast(self, prov_podcast_id: str) -> Podcast:
+        """Get full podcast details by id."""
+        metadata = await self._get_metadata(prov_podcast_id)
+        item_metadata = metadata.get("metadata", {})
+
+        title = clean_text(item_metadata.get("title"))
+        creator = clean_text(item_metadata.get("creator"))
+
+        if not title:
+            raise MediaNotFoundError(f"Podcast {prov_podcast_id} not found or invalid")
+
+        podcast = Podcast(
+            item_id=prov_podcast_id,
+            provider=self.instance_id,
+            name=title,
+            provider_mappings={
+                create_provider_mapping(
+                    prov_podcast_id, self.domain, self.instance_id, self.client.get_item_url
+                )
+            },
+        )
+
+        # Add publisher/creator
+        if creator:
+            podcast.publisher = creator
+
+        # Add metadata
+        if description := clean_text(item_metadata.get("description")):
+            podcast.metadata.description = description
+
+        # Add thumbnail
+        add_item_image(podcast, prov_podcast_id, self.instance_id)
+
+        # Calculate total episodes
+        try:
+            audio_files = await self._get_audio_files(prov_podcast_id)
+            podcast.total_episodes = len(audio_files)
+        except Exception as err:
+            self.logger.warning(f"Could not get episode count for podcast {prov_podcast_id}: {err}")
+            podcast.total_episodes = None
+
+        return podcast
+
+    async def get_podcast_episodes(
+        self, prov_podcast_id: str
+    ) -> AsyncGenerator[PodcastEpisode, None]:
+        """Get podcast episodes for given podcast id."""
+        metadata = await self._get_metadata(prov_podcast_id)
+        item_metadata = metadata.get("metadata", {})
+        audio_files = await self._get_audio_files(prov_podcast_id)
+
+        # Create podcast reference for episodes
+        podcast = Podcast(
+            item_id=prov_podcast_id,
+            provider=self.instance_id,
+            name=clean_text(item_metadata.get("title", prov_podcast_id)),
+            provider_mappings={
+                create_provider_mapping(
+                    prov_podcast_id, self.domain, self.instance_id, self.client.get_item_url
+                )
+            },
+        )
+
+        for i, file_info in enumerate(audio_files, 1):
+            filename = file_info.get("name", "")
+
+            # Use file's title if available, otherwise clean up filename
+            episode_name = file_info.get("title", filename)
+            if not episode_name or episode_name == filename:
+                episode_name = filename.rsplit(".", 1)[0] if "." in filename else filename
+
+            # Try to extract episode number from file metadata first, then filename
+            episode_number = self._extract_track_number(file_info, episode_name, i)
+
+            episode = PodcastEpisode(
+                item_id=f"{prov_podcast_id}#{filename}",
+                provider=self.instance_id,
+                name=episode_name,
+                position=episode_number,
+                podcast=podcast,
+                provider_mappings={
+                    ProviderMapping(
+                        item_id=f"{prov_podcast_id}#{filename}",
+                        provider_domain=self.domain,
+                        provider_instance=self.instance_id,
+                        url=self.client.get_download_url(prov_podcast_id, filename),
+                        available=True,
+                    )
+                },
+            )
+
+            # Add duration if available
+            if duration_str := file_info.get("length"):
+                if duration := parse_duration(duration_str):
+                    episode.duration = duration
+
+            # Add episode metadata
+            if description := file_info.get("description"):
+                episode.metadata.description = clean_text(description)
+
+            yield episode
+
+    async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode:
+        """Get single podcast episode by id."""
+        if "#" not in prov_episode_id:
+            raise MediaNotFoundError(f"Invalid episode ID format: {prov_episode_id}")
+
+        podcast_id, filename = prov_episode_id.split("#", 1)
+
+        async for episode in self.get_podcast_episodes(podcast_id):
+            if episode.item_id == prov_episode_id:
+                return episode
+
+        raise MediaNotFoundError(f"Episode {prov_episode_id} not found")
diff --git a/music_assistant/providers/internet_archive/streaming.py b/music_assistant/providers/internet_archive/streaming.py
new file mode 100644 (file)
index 0000000..072487e
--- /dev/null
@@ -0,0 +1,95 @@
+"""Stream handling for the Internet Archive provider."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+from music_assistant_models.enums import ContentType, MediaType, StreamType
+from music_assistant_models.errors import MediaNotFoundError
+from music_assistant_models.media_items import AudioFormat
+from music_assistant_models.streamdetails import StreamDetails
+
+if TYPE_CHECKING:
+    from .provider import InternetArchiveProvider
+
+
+class InternetArchiveStreaming:
+    """Handles stream details and multi-file streaming for Internet Archive."""
+
+    def __init__(self, provider: InternetArchiveProvider) -> None:
+        """
+        Initialize the streaming handler.
+
+        Args:
+            provider: The Internet Archive provider instance
+        """
+        self.provider = provider
+
+    async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails:
+        """Get streamdetails for a track or audiobook."""
+        if "#" in item_id:
+            return self._get_single_file_stream(item_id, {}, media_type)
+        else:
+            audio_files = await self.provider.client.get_audio_files(item_id)
+            if not audio_files:
+                raise MediaNotFoundError(f"No audio files found for {item_id}")
+
+            if media_type == MediaType.AUDIOBOOK and len(audio_files) > 1:
+                return await self._get_multi_file_audiobook_stream(item_id, audio_files)
+            else:
+                return self._get_single_file_stream(item_id, audio_files[0], media_type)
+
+    async def _get_multi_file_audiobook_stream(
+        self, item_id: str, audio_files: list[dict[str, Any]]
+    ) -> StreamDetails:
+        """Get stream details for a multi-file audiobook."""
+        # Create list of download URLs for all chapters
+        chapter_urls = []
+
+        # Use provider's helper method for consistent duration calculation
+        total_duration, _ = await self.provider._calculate_audiobook_duration_and_chapters(item_id)
+
+        for file_info in audio_files:
+            filename = file_info["name"]
+            download_url = self.provider.client.get_download_url(item_id, filename)
+            chapter_urls.append(download_url)
+
+        duration_to_set = total_duration if total_duration > 0 else None
+
+        return StreamDetails(
+            provider=self.provider.instance_id,
+            item_id=item_id,
+            audio_format=AudioFormat(content_type=ContentType.UNKNOWN),
+            media_type=MediaType.AUDIOBOOK,
+            stream_type=StreamType.CUSTOM,
+            duration=duration_to_set,
+            data={"chapters": chapter_urls, "chapters_data": audio_files},
+            allow_seek=True,
+            can_seek=True,
+        )
+
+    def _get_single_file_stream(
+        self, item_id: str, file_info: dict[str, Any], media_type: MediaType
+    ) -> StreamDetails:
+        """Get stream details for a single file."""
+        if "#" in item_id:
+            # This is a track from an album - extract parent_id and filename
+            parent_id, filename = item_id.split("#", 1)
+            download_url = self.provider.client.get_download_url(parent_id, filename)
+        else:
+            # This is a single item
+            filename = file_info["name"]
+            download_url = self.provider.client.get_download_url(item_id, filename)
+
+        return StreamDetails(
+            provider=self.provider.instance_id,
+            item_id=item_id,
+            audio_format=AudioFormat(
+                content_type=ContentType.UNKNOWN,  # Let ffmpeg detect format
+            ),
+            media_type=media_type,
+            stream_type=StreamType.HTTP,
+            path=download_url,
+            allow_seek=True,
+            can_seek=True,
+        )