Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DASH support #82

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
8 changes: 4 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
name: PyTest

strategy:
max-parallel: 2
max-parallel: 3
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
python-version: [ '3.7', '3.8', '3.9', 'pypy-3.7' ]
Expand Down Expand Up @@ -43,6 +43,6 @@ jobs:

- run: poetry run pytest
env:
TIDAL_APK_URL: ${{ secrets.TIDAL_NONDASH_APK_URL }}
TIDAL_CLIENT_ID: ${{ secrets.TIDAL_NONDASH_CLIENT_ID }}
TIDAL_REFRESH_TOKEN: ${{ secrets.TIDAL_NONDASH_REFRESH_TOKEN }}
TIDAL_APK_URL: ${{ secrets.TIDAL_APK_URL }}
TIDAL_CLIENT_ID: ${{ secrets.TIDAL_CLIENT_ID }}
TIDAL_REFRESH_TOKEN: ${{ secrets.TIDAL_REFRESH_TOKEN }}
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ aiohttp = "^3.6"
music-service-async-interface = { git = "https://github.com/FUMR/music-service-async-interface.git", rev = "2bc9cd8a1fda0486f325e127e4d7e80a5b0fedcf" }
androguard = { version = "^3.3.5", optional = true }
http-seekable-file = { git = "https://github.com/JuniorJPDJ/http-seekable-file.git", tag = "v0.3.0", extras = ["async"], optional = true }
mpegdash = "^0.2"

[tool.poetry.dev-dependencies]
pre-commit = "^2.12"
Expand Down
82 changes: 62 additions & 20 deletions tests/test_tidal_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@
import os
from typing import Optional, Sized

import mpegdash
import pytest

from tidal_async import Album, Artist, AudioQuality, Playlist, TidalSession, Track, extract_client_id
from tidal_async import (
Album,
Artist,
AudioQuality,
Playlist,
TidalSession,
Track,
dash_mpd_from_data_url,
extract_client_id,
)

# TODO [#63]: Unit tests!
# - [ ] login process (not sure how to do this - it's interactive oauth2)
Expand Down Expand Up @@ -283,24 +293,6 @@ async def test_cover_download(sess: TidalSession, object_url, cover_size, sha256
@pytest.mark.parametrize(
"id_, required_quality, preferred_quality, file_size, mimetype, etag",
(
(
152676390,
AudioQuality.Normal,
AudioQuality.Normal,
3114802,
"audio/mp4",
'"780130927f84364021b1300423d60f47"',
),
# DASH Support needed (#53)
# (
# 152676390,
# AudioQuality.High,
# AudioQuality.High,
# 10347474,
# "audio/mp4",
# '"970df936b04363528662c9c74b714d13-2"',
# ),
(152676390, AudioQuality.HiFi, AudioQuality.HiFi, 30980344, "audio/flac", '"3bb27f3e6d8f7fd987bcc0d3cdc7c452"'),
(
152676390,
AudioQuality.Master,
Expand All @@ -311,13 +303,63 @@ async def test_cover_download(sess: TidalSession, object_url, cover_size, sha256
),
),
)
async def test_track_download(sess: TidalSession, id_, required_quality, preferred_quality, file_size, mimetype, etag):
async def test_track_download_direct(
sess: TidalSession, id_, required_quality, preferred_quality, file_size, mimetype, etag
):
track = await sess.track(id_)
file = await track.get_async_file(required_quality, preferred_quality)

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


@pytest.mark.asyncio
@pytest.mark.parametrize(
"id_, required_quality, preferred_quality, codec, bandwidth, length, segments",
(
(
152676390,
AudioQuality.Normal,
AudioQuality.Normal,
"mp4a.40.5",
96984,
"PT4M17.614S",
64,
),
(
152676390,
AudioQuality.High,
AudioQuality.High,
"mp4a.40.2",
321691,
"PT4M17.545S",
64,
),
(
152676390,
AudioQuality.HiFi,
AudioQuality.HiFi,
"flac",
957766,
"PT4M17.499S",
64,
),
),
)
async def test_track_download_dash(
sess: TidalSession, id_, required_quality, preferred_quality, codec, bandwidth, length, segments
):
track = await sess.track(id_)
url = await track.get_file_url(required_quality, preferred_quality)
mpd = dash_mpd_from_data_url(url)

rep = mpd.periods[0].adaptation_sets[0].representations[0]

assert rep.codecs == codec
assert rep.bandwidth == bandwidth
assert mpd.media_presentation_duration == length
assert sum(s.r if s.r else 1 for s in rep.segment_templates[0].segment_timelines[0].Ss)


@pytest.mark.asyncio
async def test_client_id_extraction():
from io import BytesIO
Expand Down
3 changes: 2 additions & 1 deletion tidal_async/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from .api import Album, Artist, AudioMode, AudioQuality, Cover, Playlist, TidalObject, Track
from .session import TidalMultiSession, TidalSession
from .utils import cli_auth_url_getter, extract_client_id
from .utils import cli_auth_url_getter, dash_mpd_from_data_url, extract_client_id

__all__ = [
"AudioMode",
Expand All @@ -17,4 +17,5 @@
"TidalMultiSession",
"cli_auth_url_getter",
"extract_client_id",
"dash_mpd_from_data_url",
]
6 changes: 3 additions & 3 deletions tidal_async/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,10 @@ async def get_file_url(
if quality < required_quality:
raise InsufficientAudioQuality(f"Got {quality} for {self}, required audio quality is {required_quality}")

try:
manifest = json.loads(base64.b64decode(playback_info["manifest"]))
except json.decoder.JSONDecodeError:
if playback_info["manifestMimeType"] == "application/dash+xml":
return f'data:application/dash+xml;base64,{playback_info["manifest"]}'

manifest = json.loads(base64.b64decode(playback_info["manifest"]))
return manifest["urls"][0]

async def _lyrics(self) -> Optional[dict]:
Expand Down
15 changes: 15 additions & 0 deletions tidal_async/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import asyncio
import base64
import contextlib
import functools
from urllib.parse import urlparse

import mpegdash.nodes
import mpegdash.parser
from music_service_async_interface import InvalidURL


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


def dash_mpd_from_data_url(url: str) -> "mpegdash.nodes.MPEGDASH":
"""Parses MPEG-DASH MPD manifest
:param url: URL with `data` scheme containing encoded MPD manifest returned from `Track.get_file_url()`
:return: parsed MPEG DASH MPD manifest
"""
assert url.startswith("data:application/dash+xml;base64,")
mpd_str = base64.b64decode(url.rsplit(",", 1)[1]).decode("utf-8")
mpd = mpegdash.parser.MPEGDASHParser.parse(mpd_str)
return mpd


try:
from zipfile import ZipFile

Expand Down