Skip to content

Commit dd2ef36

Browse files
committed
feat(tests): MPEG DASH support
1 parent 5a731e1 commit dd2ef36

File tree

5 files changed

+83
-24
lines changed

5 files changed

+83
-24
lines changed

.github/workflows/tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ jobs:
4343

4444
- run: poetry run pytest
4545
env:
46-
TIDAL_APK_URL: ${{ secrets.TIDAL_NONDASH_APK_URL }}
47-
TIDAL_CLIENT_ID: ${{ secrets.TIDAL_NONDASH_CLIENT_ID }}
48-
TIDAL_REFRESH_TOKEN: ${{ secrets.TIDAL_NONDASH_REFRESH_TOKEN }}
46+
TIDAL_APK_URL: ${{ secrets.TIDAL_APK_URL }}
47+
TIDAL_CLIENT_ID: ${{ secrets.TIDAL_CLIENT_ID }}
48+
TIDAL_REFRESH_TOKEN: ${{ secrets.TIDAL_REFRESH_TOKEN }}

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ aiohttp = "^3.6"
1010
music-service-async-interface = { git = "https://github.com/FUMR/music-service-async-interface.git", rev = "2bc9cd8a1fda0486f325e127e4d7e80a5b0fedcf" }
1111
androguard = { version = "^3.3.5", optional = true }
1212
http-seekable-file = { git = "https://github.com/JuniorJPDJ/http-seekable-file.git", tag = "v0.3.0", extras = ["async"], optional = true }
13+
mpegdash = "^0.2"
1314

1415
[tool.poetry.dev-dependencies]
1516
pre-commit = "^2.12"

tests/test_tidal_async.py

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@
22
import os
33
from typing import Optional, Sized
44

5+
import mpegdash
56
import pytest
67

7-
from tidal_async import Album, Artist, AudioQuality, Playlist, TidalSession, Track, extract_client_id
8+
from tidal_async import (
9+
Album,
10+
Artist,
11+
AudioQuality,
12+
Playlist,
13+
TidalSession,
14+
Track,
15+
dash_mpd_from_data_url,
16+
extract_client_id,
17+
)
818

919
# TODO [#63]: Unit tests!
1020
# - [ ] login process (not sure how to do this - it's interactive oauth2)
@@ -283,24 +293,6 @@ async def test_cover_download(sess: TidalSession, object_url, cover_size, sha256
283293
@pytest.mark.parametrize(
284294
"id_, required_quality, preferred_quality, file_size, mimetype, etag",
285295
(
286-
(
287-
152676390,
288-
AudioQuality.Normal,
289-
AudioQuality.Normal,
290-
3114802,
291-
"audio/mp4",
292-
'"780130927f84364021b1300423d60f47"',
293-
),
294-
# DASH Support needed (#53)
295-
# (
296-
# 152676390,
297-
# AudioQuality.High,
298-
# AudioQuality.High,
299-
# 10347474,
300-
# "audio/mp4",
301-
# '"970df936b04363528662c9c74b714d13-2"',
302-
# ),
303-
(152676390, AudioQuality.HiFi, AudioQuality.HiFi, 30980344, "audio/flac", '"3bb27f3e6d8f7fd987bcc0d3cdc7c452"'),
304296
(
305297
152676390,
306298
AudioQuality.Master,
@@ -311,13 +303,63 @@ async def test_cover_download(sess: TidalSession, object_url, cover_size, sha256
311303
),
312304
),
313305
)
314-
async def test_track_download(sess: TidalSession, id_, required_quality, preferred_quality, file_size, mimetype, etag):
306+
async def test_track_download_direct(
307+
sess: TidalSession, id_, required_quality, preferred_quality, file_size, mimetype, etag
308+
):
315309
track = await sess.track(id_)
316310
file = await track.get_async_file(required_quality, preferred_quality)
317311

318312
assert file.mimetype == mimetype and file.resp_headers["ETag"] == etag and len(file) == file_size
319313

320314

315+
@pytest.mark.asyncio
316+
@pytest.mark.parametrize(
317+
"id_, required_quality, preferred_quality, codec, bandwidth, length, segments",
318+
(
319+
(
320+
152676390,
321+
AudioQuality.Normal,
322+
AudioQuality.Normal,
323+
"mp4a.40.5",
324+
96984,
325+
"PT4M17.614S",
326+
64,
327+
),
328+
(
329+
152676390,
330+
AudioQuality.High,
331+
AudioQuality.High,
332+
"mp4a.40.2",
333+
321691,
334+
"PT4M17.545S",
335+
64,
336+
),
337+
(
338+
152676390,
339+
AudioQuality.HiFi,
340+
AudioQuality.HiFi,
341+
"flac",
342+
957766,
343+
"PT4M17.499S",
344+
64,
345+
),
346+
),
347+
)
348+
async def test_track_download_dash(
349+
sess: TidalSession, id_, required_quality, preferred_quality, codec, bandwidth, length, segments
350+
):
351+
track = await sess.track(id_)
352+
url = await track.get_file_url(required_quality, preferred_quality)
353+
mpd = dash_mpd_from_data_url(url)
354+
355+
rep = mpd.periods[0].adaptation_sets[0].representations[0]
356+
357+
assert rep.codecs == codec
358+
assert rep.bandwidth == bandwidth
359+
assert mpd.media_presentation_duration == length
360+
assert sum(s.r if s.r else 1 for s in rep.segment_templates[0].segment_timelines[0].Ss)
361+
362+
321363
@pytest.mark.asyncio
322364
async def test_client_id_extraction():
323365
from io import BytesIO

tidal_async/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from .api import Album, Artist, AudioMode, AudioQuality, Cover, Playlist, TidalObject, Track
44
from .session import TidalMultiSession, TidalSession
5-
from .utils import cli_auth_url_getter, extract_client_id
5+
from .utils import cli_auth_url_getter, dash_mpd_from_data_url, extract_client_id
66

77
__all__ = [
88
"AudioMode",
@@ -17,4 +17,5 @@
1717
"TidalMultiSession",
1818
"cli_auth_url_getter",
1919
"extract_client_id",
20+
"dash_mpd_from_data_url",
2021
]

tidal_async/utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import asyncio
2+
import base64
23
import contextlib
34
import functools
45
from urllib.parse import urlparse
56

7+
import mpegdash.nodes
8+
import mpegdash.parser
69
from music_service_async_interface import InvalidURL
710

811

@@ -107,6 +110,18 @@ def gen_artist(obj) -> str:
107110
return main if not feat else f"{main} feat. {feat}"
108111

109112

113+
def dash_mpd_from_data_url(url: str) -> "mpegdash.nodes.MPEGDASH":
114+
"""Parses MPEG-DASH MPD manifest
115+
116+
:param url: URL with `data` scheme containing encoded MPD manifest returned from `Track.get_file_url()`
117+
:return: parsed MPEG DASH MPD manifest
118+
"""
119+
assert url.startswith("data:application/dash+xml;base64,")
120+
mpd_str = base64.b64decode(url.rsplit(",", 1)[1]).decode("utf-8")
121+
mpd = mpegdash.parser.MPEGDASHParser.parse(mpd_str)
122+
return mpd
123+
124+
110125
try:
111126
from zipfile import ZipFile
112127

0 commit comments

Comments
 (0)