-
Notifications
You must be signed in to change notification settings - Fork 4
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
MPEG DASH support #53
Comments
Also other thing - do someone have list of tracks loading as DASH? We are using client id from Android Tidal with version code 1003 to avoid DASH at the moment. |
Oops, sorry. I should've looked for such an issue and referenced it in my PR/commit message.
What's the expectation here? @app.route("/file/<int:tid>.<string:ext>")
async def file(tid,ext):
if ext == "flac":
q=ti.AudioQuality.HiFi
elif ext == "aac":
q=ti.AudioQuality.High
else:
abort(404)
uri=await (await track(tid)).get_file_url(q, q)
if uri.startswith('data:'):
return urlopen(uri)
else:
return redirect(uri)
I've never received an actual URL with the client ID I got from a recent version of the APK, which was my initial motivation for #77. |
Failed tests seems to be because our secrets are not propagated to forks even in pull request context. I tried to look for solution but it seems github changed something. I'll need to play with it a bit later. Separate test suite for various client IDs seems to be good idea. About dash itself - I was thinking about adding some function always returning streamable filelike. Also I wonder if it's easy to implement this without ffmpeg. My expectation is to be able to easily save music as a file on disk or use it as file-like object in python software. |
Thanks for clarifying. Please don't remove
AFAICT, Tidal's DASH manifests are very simple, being comprised of only one SegmentTemplate. I've just tried downloading all the segments of a manifest with curl and concatenating them and I've gotten a perfectly playable file out of it. Using ffmpeg, you would never have to worry about all that and need not even make a distinction between DASH and URL, though. Also, you could bake metadata right into the track, as at least the DASH streams contain only the raw audio. |
Yup. Not gonna happen. It's useful ;D
I tried concating it and I was unable to play the output file. I will probably need to play with it a bit more.
Problem with ffmpeg is that it's pretty big dependency and eg. on windows it's hard to get and make it working with python. |
Tidal Android v2.37.1 (1025) is latest version allowing direct downloading. Later versions are responding with dash manifests. (Just checked) |
Here are some ideas that may be useful. This is an async generator that downloads and yields the raw file from async def raw_generator(file_uri, limit=io.DEFAULT_BUFFER_SIZE):
if file_uri.startswith("data:"):
mpd=dom.parse(urlopen(file_uri))
print(mpd.toprettyxml())
reps=mpd.getElementsByTagName("Representation")
rep=sorted(reps, reverse=True, key=lambda r: int(r.getAttribute("bandwidth")))[0]
tmpl=rep.getElementsByTagName("SegmentTemplate")[0]
timel=tmpl.getElementsByTagName("SegmentTimeline")[0]
segments=sum(1+int(e.getAttribute("r") or 0) for e in timel.getElementsByTagName("S"))
firstSegment=int(tmpl.getAttribute("startNumber"))
segmentTmpl=tmpl.getAttribute("media")
urls=chain([tmpl.getAttribute("initialization")],(segmentTmpl.replace("$Number$", f"{i}") for i in range(firstSegment, firstSegment+segments)))
else:
urls=[file_uri]
async with aiohttp.ClientSession() as session:
for u in urls:
print(u)
async with session.get(u) as response:
while True:
buffer=await response.content.read(limit)
if not buffer:
break
else:
yield buffer This is an async generator that uses ffmpeg to remux the file into the proper container with metadata on the fly: async def ffmpeg_generator(file_uri, container, metadata, cover, realtime=False, limit=io.DEFAULT_BUFFER_SIZE):
ffargs=([ '-re' ] if realtime else [] ) + ['-i', file_uri]
if cover is not None and container == 'flac':
ffargs.extend([
'-i', cover.get_url(size=(1280,1280)),
'-map', '0',
'-map', '1',
'-metadata:s:v', 'comment=Cover (front)',
'-disposition:v', 'attached_pic'
])
if container == 'ismv':
ffargs.extend(['-frag_duration', '1000'])
ffargs.extend(list(metadata_gen(metadata, aliases=container == 'flac'))+[
'-c', 'copy',
'-f', container,
'pipe:'
])
ff=None
try:
ff=await asyncio.create_subprocess_exec('ffmpeg', *ffargs, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, limit=limit)
while True:
buffer=await ff.stdout.read(limit)
if not buffer:
break
else:
yield buffer
except asyncio.CancelledError:
pass
finally:
if ff is not None:
try:
ff.terminate()
except ProcessLookupError:
pass
def metadata_gen(md, aliases=True):
keys=[
("title", None),
("album", None),
("artist", None),
("albumartist", None),
("track", "TRACKNUMBER"),
("copyright", None),
("date", None),
("isrc", None),
("rg_track_gain", "REPLAYGAIN_TRACK_GAIN"),
("rg_track_peak", "REPLAYGAIN_TRACK_PEAK"),
("artists", "ENSEMBLE"),
("barcode", "EAN/UPN")
]
kvgen=(
zip(
((key if not aliases else (alias if alias is not None else key.upper())) for _ in iter(int,1)),
md[key] if isinstance(md[key], list) else ([md[key]] if key in md else [])
) for (key, alias) in keys
)
for values in kvgen:
for (key,value) in values:
yield '-metadata'
yield f'{key}={value}' |
Wow! Thanks! Amazing! It's on finish line but I'm loaded with other things to do now so maybe I could push it at the weekend. Do you think FLAC inside MP4 is acceptable output? |
If the goal is to just have a stream that contains the audio, then sure, any container will do (cf. |
Bigger plan is providing more blocks to deliver very various things with exchangable parts. Like I'd like to write telegram bot for downloading music already tagged and packed to zips in the fly, same with filesystem thing and maybe some sort of UI or mopidy plugin. <streaming_service>-api is also ment to be replacable as I'm planning working on Deezer, Apple Music, Qobuz, Funkwhale and maybe something else. On the top of tidal-api there will go something for automated tagging using https://github.com/beetbox/mediafile and maybe other various post-processing parts. |
Tidal seems to migrate to DASH for their music streaming.
Instead of JSON manifest file encoded into Base64 there's .mpd XML file which seems to play just fine using ffplay.
Copying this into container-less FLAC format will be neccessary.
ffmpeg is doing this just fine with this cmdline:
The text was updated successfully, but these errors were encountered: