🐛 Fixed video downloading (#235)

* rename `VideoQuality` to `StreamVideoQuality`

* remove bad logic from predicting video quality

* print info when skipping video

* bump to 3.1.1 alpha
This commit is contained in:
Oskar Dudziński
2025-11-20 16:13:53 +01:00
committed by GitHub
parent 1873d512f1
commit 85088e737a
9 changed files with 66 additions and 53 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
from pathlib import Path from pathlib import Path
from tiddl.core.api.models.base import StreamVideoQuality
from tiddl.core.metadata import add_video_metadata from tiddl.core.metadata import add_video_metadata
from tiddl.core.api.models.base import VideoQuality
from tiddl.core.utils import get_video_stream_data from tiddl.core.utils import get_video_stream_data
from tiddl.core.utils.ffmpeg import convert_to_mp4, is_ffmpeg_installed from tiddl.core.utils.ffmpeg import convert_to_mp4, is_ffmpeg_installed
@@ -10,7 +10,7 @@ from .fetch_api import api
# Old Town Road by Lil Nas X # Old Town Road by Lil Nas X
VIDEO_ID = 113483426 VIDEO_ID = 113483426
QUALITY: VideoQuality = "HIGH" QUALITY: StreamVideoQuality = "HIGH"
if __name__ == "__main__": if __name__ == "__main__":
print("fetching video_stream") print("fetching video_stream")
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tiddl" name = "tiddl"
version = "3.1.0" version = "3.1.1a1"
description = "Download Tidal tracks with CLI downloader." description = "Download Tidal tracks with CLI downloader."
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
+2 -7
View File
@@ -162,13 +162,8 @@ def download_callback(
return TRACK_QUALITY return TRACK_QUALITY
elif isinstance(item, Video): elif isinstance(item, Video):
if item.quality == "LOW": # TODO add missing Video.quality literals so this function can work properly
return "sd" return VIDEO_QUALITY
if item.quality == "MEDIUM":
if VIDEO_QUALITY == "hd":
return "hd"
return "fhd"
raise TypeError("Unsupported item type") raise TypeError("Unsupported item type")
+15 -13
View File
@@ -1,25 +1,24 @@
import shutil
import asyncio import asyncio
import aiohttp import shutil
import aiofiles
from logging import getLogger from logging import getLogger
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from tiddl.core.api.models import TrackQuality, VideoQuality, Track, Video import aiofiles
from tiddl.core.api import TidalAPI, ApiError import aiohttp
from tiddl.cli.config import VIDEOS_FILTER_LITERAL
from tiddl.cli.utils.download import get_existing_track_filename
from tiddl.core.api import ApiError, TidalAPI
from tiddl.core.api.models import StreamVideoQuality, Track, TrackQuality, Video
from tiddl.core.utils import parse_track_stream, parse_video_stream from tiddl.core.utils import parse_track_stream, parse_video_stream
from tiddl.core.utils.ffmpeg import convert_to_mp4, extract_flac
from tiddl.core.utils.const import ( from tiddl.core.utils.const import (
TRACK_QUALITY_LITERAL, TRACK_QUALITY_LITERAL,
VIDEO_QUALITY_LITERAL, VIDEO_QUALITY_LITERAL,
track_qualities, track_qualities,
video_qualities, video_qualities,
) )
from tiddl.cli.config import VIDEOS_FILTER_LITERAL from tiddl.core.utils.ffmpeg import convert_to_mp4, extract_flac
from tiddl.cli.utils.download import get_existing_track_filename
from .output import RichOutput from .output import RichOutput
@@ -34,7 +33,7 @@ track_qualities_color: dict[TrackQuality, str] = {
"HI_RES_LOSSLESS": "[yellow]", "HI_RES_LOSSLESS": "[yellow]",
} }
video_qualities_color: dict[VideoQuality, str] = { video_qualities_color: dict[StreamVideoQuality, str] = {
"LOW": "[gray]360p", "LOW": "[gray]360p",
"MEDIUM": "[cyan]720p", "MEDIUM": "[cyan]720p",
"HIGH": "[yellow]1080p", "HIGH": "[yellow]1080p",
@@ -46,7 +45,7 @@ class Downloader:
rich_output: RichOutput rich_output: RichOutput
semaphore: asyncio.Semaphore semaphore: asyncio.Semaphore
track_quality: TrackQuality track_quality: TrackQuality
video_quality: VideoQuality video_quality: StreamVideoQuality
videos_filter: VIDEOS_FILTER_LITERAL videos_filter: VIDEOS_FILTER_LITERAL
skip_existing: bool skip_existing: bool
download_path: Path download_path: Path
@@ -121,7 +120,10 @@ class Downloader:
elif (isinstance(item, Video) and self.videos_filter == "none") or ( elif (isinstance(item, Video) and self.videos_filter == "none") or (
isinstance(item, Track) and self.videos_filter == "only" isinstance(item, Track) and self.videos_filter == "only"
): ):
log.info(f"skipping {item.id} due to {self.videos_filter=}") log.debug(f"skipping {item.id} due to {self.videos_filter=}")
self.rich_output.console.print(
f"Skipping '{item.title}' due to video filter set to '{self.videos_filter}'"
)
return None, False return None, False
should_extract_flac = False should_extract_flac = False
+14 -15
View File
@@ -1,34 +1,33 @@
from requests_cache import DO_NOT_CACHE, EXPIRE_IMMEDIATELY
from typing import Literal, TypeAlias from typing import Literal, TypeAlias
from requests_cache import DO_NOT_CACHE, EXPIRE_IMMEDIATELY
from .client import TidalClient from .client import TidalClient
from .models.resources import (
Album,
Artist,
Playlist,
Track,
Video,
TrackQuality,
VideoQuality,
)
from .models.base import ( from .models.base import (
AlbumItems, AlbumItems,
AlbumItemsCredits, AlbumItemsCredits,
ArtistAlbumsItems, ArtistAlbumsItems,
ArtistVideosItems, ArtistVideosItems,
Favorites, Favorites,
TrackLyrics,
PlaylistItems,
MixItems, MixItems,
PlaylistItems,
Search, Search,
SessionResponse, SessionResponse,
TrackLyrics,
TrackStream, TrackStream,
VideoStream, VideoStream,
) )
from .models.resources import (
Album,
Artist,
Playlist,
StreamVideoQuality,
Track,
TrackQuality,
Video,
)
from .models.review import AlbumReview from .models.review import AlbumReview
ID: TypeAlias = str | int ID: TypeAlias = str | int
@@ -243,7 +242,7 @@ class TidalAPI:
expire_after=3600, expire_after=3600,
) )
def get_video_stream(self, video_id: ID, quality: VideoQuality): def get_video_stream(self, video_id: ID, quality: StreamVideoQuality):
return self.client.fetch( return self.client.fetch(
VideoStream, VideoStream,
f"videos/{video_id}/playbackinfopostpaywall", f"videos/{video_id}/playbackinfopostpaywall",
+12 -4
View File
@@ -1,17 +1,25 @@
from .resources import Album, Artist, Playlist, Track, Video, TrackQuality, VideoQuality
from .base import ( from .base import (
AlbumItems, AlbumItems,
AlbumItemsCredits, AlbumItemsCredits,
ArtistAlbumsItems, ArtistAlbumsItems,
Favorites, Favorites,
TrackLyrics,
PlaylistItems,
MixItems, MixItems,
PlaylistItems,
Search, Search,
SessionResponse, SessionResponse,
TrackLyrics,
TrackStream, TrackStream,
VideoStream, VideoStream,
) )
from .resources import (
Album,
Artist,
Playlist,
StreamVideoQuality,
Track,
TrackQuality,
Video,
)
__all__ = [ __all__ = [
"Album", "Album",
@@ -20,7 +28,7 @@ __all__ = [
"Track", "Track",
"Video", "Video",
"TrackQuality", "TrackQuality",
"VideoQuality", "StreamVideoQuality",
"AlbumItems", "AlbumItems",
"AlbumItemsCredits", "AlbumItemsCredits",
"ArtistAlbumsItems", "ArtistAlbumsItems",
+13 -4
View File
@@ -1,7 +1,16 @@
from pydantic import BaseModel from typing import List, Literal, Optional, Union
from typing import Optional, List, Literal, Union
from .resources import Album, Artist, Playlist, Track, TrackQuality, Video, VideoQuality from pydantic import BaseModel
from .resources import (
Album,
Artist,
Playlist,
StreamVideoQuality,
Track,
TrackQuality,
Video,
)
class SessionResponse(BaseModel): class SessionResponse(BaseModel):
@@ -133,7 +142,7 @@ class VideoStream(BaseModel):
videoId: int videoId: int
streamType: Literal["ON_DEMAND"] streamType: Literal["ON_DEMAND"]
assetPresentation: Literal["FULL"] assetPresentation: Literal["FULL"]
videoQuality: VideoQuality videoQuality: StreamVideoQuality
# streamingSessionId: str # only in web? # streamingSessionId: str # only in web?
manifestMimeType: Literal["application/dash+xml", "application/vnd.tidal.emu"] manifestMimeType: Literal["application/dash+xml", "application/vnd.tidal.emu"]
manifestHash: str manifestHash: str
+5 -4
View File
@@ -1,11 +1,12 @@
from pydantic import BaseModel
from datetime import datetime from datetime import datetime
from typing import Optional, List, Literal, Dict, Any from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel
TrackQuality = Literal["LOW", "HIGH", "LOSSLESS", "HI_RES_LOSSLESS"] TrackQuality = Literal["LOW", "HIGH", "LOSSLESS", "HI_RES_LOSSLESS"]
# audio_only is not stable # audio_only is not stable
VideoQuality = Literal["AUDIO_ONLY", "LOW", "MEDIUM", "HIGH"] StreamVideoQuality = Literal["AUDIO_ONLY", "LOW", "MEDIUM", "HIGH"]
MediaMetadataTags = Literal["LOSSLESS", "HIRES_LOSSLESS", "DOLBY_ATMOS"] MediaMetadataTags = Literal["LOSSLESS", "HIRES_LOSSLESS", "DOLBY_ATMOS"]
@@ -83,7 +84,7 @@ class Video(BaseModel):
imageId: str imageId: str
vibrantColor: Optional[str] = None vibrantColor: Optional[str] = None
duration: int duration: int
quality: VideoQuality quality: Literal["MP4_1080P"] | str
streamReady: bool streamReady: bool
adSupportedStreamReady: bool adSupportedStreamReady: bool
djReady: bool djReady: bool
+2 -3
View File
@@ -1,7 +1,6 @@
from typing import Literal from typing import Literal
from tiddl.core.api.models import TrackQuality, VideoQuality from tiddl.core.api.models import StreamVideoQuality, TrackQuality
TRACK_QUALITY_LITERAL = Literal["low", "normal", "high", "max"] TRACK_QUALITY_LITERAL = Literal["low", "normal", "high", "max"]
VIDEO_QUALITY_LITERAL = Literal["sd", "hd", "fhd"] VIDEO_QUALITY_LITERAL = Literal["sd", "hd", "fhd"]
@@ -13,7 +12,7 @@ track_qualities: dict[TRACK_QUALITY_LITERAL, TrackQuality] = {
"max": "HI_RES_LOSSLESS", "max": "HI_RES_LOSSLESS",
} }
video_qualities: dict[VIDEO_QUALITY_LITERAL, VideoQuality] = { video_qualities: dict[VIDEO_QUALITY_LITERAL, StreamVideoQuality] = {
"sd": "LOW", "sd": "LOW",
"hd": "MEDIUM", "hd": "MEDIUM",
"fhd": "HIGH", "fhd": "HIGH",