From 85088e737aeca59d43f891f4a78840c09687de9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Dudzi=C5=84ski?= <56404247+oskvr37@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:13:53 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20video=20downloading=20(#?= =?UTF-8?q?235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * rename `VideoQuality` to `StreamVideoQuality` * remove bad logic from predicting video quality * print info when skipping video * bump to 3.1.1 alpha --- examples/download_video.py | 4 ++-- pyproject.toml | 2 +- tiddl/cli/commands/download/__init__.py | 9 ++----- tiddl/cli/commands/download/downloader.py | 28 ++++++++++++---------- tiddl/core/api/api.py | 29 +++++++++++------------ tiddl/core/api/models/__init__.py | 16 +++++++++---- tiddl/core/api/models/base.py | 17 +++++++++---- tiddl/core/api/models/resources.py | 9 +++---- tiddl/core/utils/const.py | 5 ++-- 9 files changed, 66 insertions(+), 53 deletions(-) diff --git a/examples/download_video.py b/examples/download_video.py index 161210b..28d12cf 100644 --- a/examples/download_video.py +++ b/examples/download_video.py @@ -1,7 +1,7 @@ from pathlib import Path +from tiddl.core.api.models.base import StreamVideoQuality 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.ffmpeg import convert_to_mp4, is_ffmpeg_installed @@ -10,7 +10,7 @@ from .fetch_api import api # Old Town Road by Lil Nas X VIDEO_ID = 113483426 -QUALITY: VideoQuality = "HIGH" +QUALITY: StreamVideoQuality = "HIGH" if __name__ == "__main__": print("fetching video_stream") diff --git a/pyproject.toml b/pyproject.toml index be68f8f..8598f04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tiddl" -version = "3.1.0" +version = "3.1.1a1" description = "Download Tidal tracks with CLI downloader." readme = "README.md" requires-python = ">=3.13" diff --git a/tiddl/cli/commands/download/__init__.py b/tiddl/cli/commands/download/__init__.py index d60de0d..1cf3cdc 100644 --- a/tiddl/cli/commands/download/__init__.py +++ b/tiddl/cli/commands/download/__init__.py @@ -162,13 +162,8 @@ def download_callback( return TRACK_QUALITY elif isinstance(item, Video): - if item.quality == "LOW": - return "sd" - - if item.quality == "MEDIUM": - if VIDEO_QUALITY == "hd": - return "hd" - return "fhd" + # TODO add missing Video.quality literals so this function can work properly + return VIDEO_QUALITY raise TypeError("Unsupported item type") diff --git a/tiddl/cli/commands/download/downloader.py b/tiddl/cli/commands/download/downloader.py index 3697f7f..312e351 100644 --- a/tiddl/cli/commands/download/downloader.py +++ b/tiddl/cli/commands/download/downloader.py @@ -1,25 +1,24 @@ -import shutil import asyncio -import aiohttp -import aiofiles - +import shutil from logging import getLogger - from pathlib import Path from tempfile import NamedTemporaryFile -from tiddl.core.api.models import TrackQuality, VideoQuality, Track, Video -from tiddl.core.api import TidalAPI, ApiError +import aiofiles +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.ffmpeg import convert_to_mp4, extract_flac from tiddl.core.utils.const import ( TRACK_QUALITY_LITERAL, VIDEO_QUALITY_LITERAL, track_qualities, video_qualities, ) -from tiddl.cli.config import VIDEOS_FILTER_LITERAL -from tiddl.cli.utils.download import get_existing_track_filename +from tiddl.core.utils.ffmpeg import convert_to_mp4, extract_flac from .output import RichOutput @@ -34,7 +33,7 @@ track_qualities_color: dict[TrackQuality, str] = { "HI_RES_LOSSLESS": "[yellow]", } -video_qualities_color: dict[VideoQuality, str] = { +video_qualities_color: dict[StreamVideoQuality, str] = { "LOW": "[gray]360p", "MEDIUM": "[cyan]720p", "HIGH": "[yellow]1080p", @@ -46,7 +45,7 @@ class Downloader: rich_output: RichOutput semaphore: asyncio.Semaphore track_quality: TrackQuality - video_quality: VideoQuality + video_quality: StreamVideoQuality videos_filter: VIDEOS_FILTER_LITERAL skip_existing: bool download_path: Path @@ -121,7 +120,10 @@ class Downloader: elif (isinstance(item, Video) and self.videos_filter == "none") or ( 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 should_extract_flac = False diff --git a/tiddl/core/api/api.py b/tiddl/core/api/api.py index 165da45..b14d5d8 100644 --- a/tiddl/core/api/api.py +++ b/tiddl/core/api/api.py @@ -1,34 +1,33 @@ -from requests_cache import DO_NOT_CACHE, EXPIRE_IMMEDIATELY - from typing import Literal, TypeAlias +from requests_cache import DO_NOT_CACHE, EXPIRE_IMMEDIATELY + from .client import TidalClient -from .models.resources import ( - Album, - Artist, - Playlist, - Track, - Video, - TrackQuality, - VideoQuality, -) from .models.base import ( AlbumItems, AlbumItemsCredits, ArtistAlbumsItems, ArtistVideosItems, Favorites, - TrackLyrics, - PlaylistItems, MixItems, + PlaylistItems, Search, SessionResponse, + TrackLyrics, TrackStream, VideoStream, ) +from .models.resources import ( + Album, + Artist, + Playlist, + StreamVideoQuality, + Track, + TrackQuality, + Video, +) from .models.review import AlbumReview - ID: TypeAlias = str | int @@ -243,7 +242,7 @@ class TidalAPI: 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( VideoStream, f"videos/{video_id}/playbackinfopostpaywall", diff --git a/tiddl/core/api/models/__init__.py b/tiddl/core/api/models/__init__.py index be78c46..2483a80 100644 --- a/tiddl/core/api/models/__init__.py +++ b/tiddl/core/api/models/__init__.py @@ -1,17 +1,25 @@ -from .resources import Album, Artist, Playlist, Track, Video, TrackQuality, VideoQuality from .base import ( AlbumItems, AlbumItemsCredits, ArtistAlbumsItems, Favorites, - TrackLyrics, - PlaylistItems, MixItems, + PlaylistItems, Search, SessionResponse, + TrackLyrics, TrackStream, VideoStream, ) +from .resources import ( + Album, + Artist, + Playlist, + StreamVideoQuality, + Track, + TrackQuality, + Video, +) __all__ = [ "Album", @@ -20,7 +28,7 @@ __all__ = [ "Track", "Video", "TrackQuality", - "VideoQuality", + "StreamVideoQuality", "AlbumItems", "AlbumItemsCredits", "ArtistAlbumsItems", diff --git a/tiddl/core/api/models/base.py b/tiddl/core/api/models/base.py index e4904ce..8f1c73a 100644 --- a/tiddl/core/api/models/base.py +++ b/tiddl/core/api/models/base.py @@ -1,7 +1,16 @@ -from pydantic import BaseModel -from typing import Optional, List, Literal, Union +from typing import List, Literal, Optional, 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): @@ -133,7 +142,7 @@ class VideoStream(BaseModel): videoId: int streamType: Literal["ON_DEMAND"] assetPresentation: Literal["FULL"] - videoQuality: VideoQuality + videoQuality: StreamVideoQuality # streamingSessionId: str # only in web? manifestMimeType: Literal["application/dash+xml", "application/vnd.tidal.emu"] manifestHash: str diff --git a/tiddl/core/api/models/resources.py b/tiddl/core/api/models/resources.py index 5a45f89..b8f6609 100644 --- a/tiddl/core/api/models/resources.py +++ b/tiddl/core/api/models/resources.py @@ -1,11 +1,12 @@ -from pydantic import BaseModel 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"] # 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"] @@ -83,7 +84,7 @@ class Video(BaseModel): imageId: str vibrantColor: Optional[str] = None duration: int - quality: VideoQuality + quality: Literal["MP4_1080P"] | str streamReady: bool adSupportedStreamReady: bool djReady: bool diff --git a/tiddl/core/utils/const.py b/tiddl/core/utils/const.py index 7dc8942..bfce5f4 100644 --- a/tiddl/core/utils/const.py +++ b/tiddl/core/utils/const.py @@ -1,7 +1,6 @@ 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"] VIDEO_QUALITY_LITERAL = Literal["sd", "hd", "fhd"] @@ -13,7 +12,7 @@ track_qualities: dict[TRACK_QUALITY_LITERAL, TrackQuality] = { "max": "HI_RES_LOSSLESS", } -video_qualities: dict[VIDEO_QUALITY_LITERAL, VideoQuality] = { +video_qualities: dict[VIDEO_QUALITY_LITERAL, StreamVideoQuality] = { "sd": "LOW", "hd": "MEDIUM", "fhd": "HIGH",