🐛 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 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")
+1 -1
View File
@@ -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"
+2 -7
View File
@@ -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")
+15 -13
View File
@@ -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
+14 -15
View File
@@ -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",
+12 -4
View File
@@ -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",
+13 -4
View File
@@ -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
+5 -4
View File
@@ -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
+2 -3
View File
@@ -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",