Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/mopidy_tunein/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re
from typing import TYPE_CHECKING
from urllib import request
from urllib.parse import urlsplit, urlunsplit

from mopidy.models import Album, Artist, Image, Ref, Track
from mopidy.types import Uri
Expand All @@ -18,6 +19,22 @@
logger = logging.getLogger(__name__)


def secure_image_uri(uri: str) -> Uri:
"""Give back a TuneIn image URI with the https scheme.

The API answers with http, which a browser blocks as mixed content when
it shows a Mopidy web client over https.
"""
parts = urlsplit(uri)
if (
parts.scheme == "http"
and parts.hostname
and (parts.hostname == "tunein.com" or parts.hostname.endswith(".tunein.com"))
):
return Uri(urlunsplit(parts._replace(scheme="https")))
return Uri(uri)


def unparse_uri(variant: str, identifier: str) -> Uri:
return Uri(f"tunein:{variant}:{identifier}")

Expand Down Expand Up @@ -53,7 +70,7 @@ def station_to_track(station: TuneInItem) -> Track:

def station_to_image(station: TuneInItem | None) -> Image | None:
if station is not None and "image" in station:
return Image(uri=station["image"])
return Image(uri=secure_image_uri(station["image"]))
return None


Expand Down
53 changes: 53 additions & 0 deletions tests/test_translator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

import pytest

from mopidy_tunein import translator


@pytest.mark.parametrize(
("uri", "expected"),
[
(
"http://cdn-profiles.tunein.com/s1/images/logoq.jpg",
"https://cdn-profiles.tunein.com/s1/images/logoq.jpg",
),
(
"http://cdn-radiotime-logos.tunein.com/s1q.png",
"https://cdn-radiotime-logos.tunein.com/s1q.png",
),
("http://tunein.com/logo.png", "https://tunein.com/logo.png"),
# Keep the query and the port.
(
"http://cdn-profiles.tunein.com:80/s1.png?t=2",
"https://cdn-profiles.tunein.com:80/s1.png?t=2",
),
# Leave what is already secure, and what is not TuneIn's, alone.
(
"https://cdn-profiles.tunein.com/s1.png",
"https://cdn-profiles.tunein.com/s1.png",
),
("http://example.com/s1.png", "http://example.com/s1.png"),
("http://nottunein.com/s1.png", "http://nottunein.com/s1.png"),
(
"http://tunein.com.evil.example/s1.png",
"http://tunein.com.evil.example/s1.png",
),
],
)
def test_secure_image_uri(uri: str, expected: str) -> None:
assert translator.secure_image_uri(uri) == expected


def test_station_to_image_uses_https() -> None:
station = {"image": "http://cdn-profiles.tunein.com/s1/images/logoq.jpg"}

image = translator.station_to_image(station)

assert image is not None
assert image.uri == "https://cdn-profiles.tunein.com/s1/images/logoq.jpg"


def test_station_to_image_without_an_image() -> None:
assert translator.station_to_image({"guide_id": "s1"}) is None
assert translator.station_to_image(None) is None