mirror of
https://github.com/oskvr37/tiddl.git
synced 2026-06-13 12:15:13 +03:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34c1b1fd4e | |||
| d85fb96a19 | |||
| a4a7e66b84 | |||
| 7258df8ec8 | |||
| ed0918e7b0 | |||
| a147c94110 | |||
| 2eb25b81f9 | |||
| 1f1e89a97a | |||
| f32bab434c | |||
| 13b3c8b03b | |||
| a2b9f8d5cf | |||
| 526c8c5b0e | |||
| 8e93e4ec9a | |||
| a5a039f6a8 | |||
| 7a18b0f6b8 | |||
| a95645b3fc | |||
| c53b8ce1fa | |||
| 78a0aee1b7 | |||
| 5374d1f64f | |||
| b6607ce64d | |||
| b385722946 |
@@ -93,6 +93,16 @@ This command will:
|
||||
|
||||
More about file templating [on wiki](https://github.com/oskvr37/tiddl/wiki/Template-formatting).
|
||||
|
||||
## Custom tiddl home path
|
||||
|
||||
You can set `TIDDL_PATH` environment variable to use custom home path for tiddl.
|
||||
|
||||
Example CLI usage:
|
||||
|
||||
```sh
|
||||
TIDDL_PATH=~/custom/tiddl tiddl auth login
|
||||
```
|
||||
|
||||
# Development
|
||||
|
||||
Clone the repository
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tiddl"
|
||||
version = "2.2.2"
|
||||
version = "2.5.0"
|
||||
description = "Download Tidal tracks with CLI downloader."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
@@ -20,7 +20,7 @@ dependencies = [
|
||||
"requests-cache>=1.2.1",
|
||||
"click>=8.1.7",
|
||||
"mutagen>=1.47.0",
|
||||
"ffmpeg-python>=0.2.0",
|
||||
"python-ffmpeg>=2.0.0",
|
||||
"m3u8>=6.0.0",
|
||||
"rich>=13.9.4"
|
||||
]
|
||||
|
||||
@@ -83,6 +83,11 @@ class TestApi(unittest.TestCase):
|
||||
def test_video_stream(self):
|
||||
self.api.getVideoStream(373513584)
|
||||
|
||||
def test_lyrics(self):
|
||||
track_id = 103805726
|
||||
lyrics = self.api.getLyrics(track_id)
|
||||
self.assertEqual(lyrics.trackId, track_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -26,6 +26,7 @@ from tiddl.models.api import (
|
||||
TrackStream,
|
||||
Video,
|
||||
VideoStream,
|
||||
Lyrics
|
||||
)
|
||||
|
||||
from tiddl.models.constants import TrackQuality
|
||||
@@ -218,6 +219,11 @@ class TidalApi:
|
||||
SessionResponse, "sessions", expire_after=DO_NOT_CACHE
|
||||
)
|
||||
|
||||
def getLyrics(self, track_id: str | int):
|
||||
return self.fetch(
|
||||
Lyrics, f"tracks/{track_id}/lyrics", {"countryCode": self.country_code}
|
||||
)
|
||||
|
||||
def getTrack(self, track_id: str | int):
|
||||
return self.fetch(
|
||||
Track, f"tracks/{track_id}", {"countryCode": self.country_code}
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import logging
|
||||
|
||||
from requests import request
|
||||
|
||||
from .exceptions import AuthError
|
||||
from .models import auth
|
||||
from tiddl.exceptions import AuthError
|
||||
from tiddl.models import auth
|
||||
|
||||
AUTH_URL = "https://auth.tidal.com/v1/oauth2"
|
||||
CLIENT_ID = "zU4XHVVkc2tDPo4t"
|
||||
|
||||
@@ -3,14 +3,12 @@ import logging
|
||||
|
||||
from rich.logging import RichHandler
|
||||
|
||||
from .ctx import ContextObj, passContext, Context
|
||||
from .auth import AuthGroup
|
||||
from .download import UrlGroup, FavGroup, SearchGroup, FileGroup
|
||||
from .config import ConfigCommand
|
||||
|
||||
from tiddl.config import HOME_PATH
|
||||
|
||||
from .auth import refresh
|
||||
from tiddl.cli.ctx import ContextObj, passContext, Context
|
||||
from tiddl.cli.auth import AuthGroup
|
||||
from tiddl.cli.download import UrlGroup, FavGroup, SearchGroup, FileGroup
|
||||
from tiddl.cli.config import ConfigCommand
|
||||
from tiddl.cli.auth import refresh
|
||||
|
||||
|
||||
@click.group()
|
||||
|
||||
+3
-6
@@ -11,8 +11,7 @@ from tiddl.auth import (
|
||||
removeToken,
|
||||
AuthError,
|
||||
)
|
||||
|
||||
from .ctx import passContext, Context
|
||||
from tiddl.cli.ctx import passContext, Context
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -52,7 +51,7 @@ def login(ctx: Context):
|
||||
|
||||
if ctx.obj.config.auth.token:
|
||||
logger.info("Already logged in.")
|
||||
refresh(ctx)
|
||||
ctx.invoke(refresh)
|
||||
return
|
||||
|
||||
auth = getDeviceAuth()
|
||||
@@ -74,9 +73,7 @@ def login(ctx: Context):
|
||||
time_left = auth_end_at - time()
|
||||
minutes, seconds = time_left // 60, int(time_left % 60)
|
||||
|
||||
click.echo(
|
||||
f"\rTime left: {minutes:.0f}:{seconds:02d}", nl=False
|
||||
)
|
||||
click.echo(f"\rTime left: {minutes:.0f}:{seconds:02d}", nl=False)
|
||||
continue
|
||||
|
||||
if e.error == "expired_token":
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
import click
|
||||
|
||||
from tiddl.config import CONFIG_PATH
|
||||
|
||||
from .ctx import Context, passContext
|
||||
from tiddl.cli.ctx import Context, passContext
|
||||
|
||||
|
||||
@click.command("config")
|
||||
|
||||
@@ -17,7 +17,7 @@ from tiddl.download import parseTrackStream, parseVideoStream
|
||||
from tiddl.exceptions import ApiError, AuthError
|
||||
from tiddl.metadata import Cover, addMetadata, addVideoMetadata
|
||||
from tiddl.models.api import AlbumItemsCredits
|
||||
from tiddl.models.constants import ARG_TO_QUALITY, TrackArg
|
||||
from tiddl.models.constants import ARG_TO_QUALITY, TrackArg, SinglesFilter
|
||||
from tiddl.models.resource import Track, Video, Album
|
||||
from tiddl.utils import (
|
||||
TidalResource,
|
||||
@@ -26,16 +26,13 @@ from tiddl.utils import (
|
||||
trackExists,
|
||||
)
|
||||
|
||||
from typing import List, Literal, Union
|
||||
from tiddl.cli.ctx import Context, passContext
|
||||
from tiddl.cli.download.fav import FavGroup
|
||||
from tiddl.cli.download.file import FileGroup
|
||||
from tiddl.cli.download.search import SearchGroup
|
||||
from tiddl.cli.download.url import UrlGroup
|
||||
|
||||
from .fav import FavGroup
|
||||
from .file import FileGroup
|
||||
from .search import SearchGroup
|
||||
from .url import UrlGroup
|
||||
|
||||
from ..ctx import Context, passContext
|
||||
|
||||
SinglesFilter = Literal["none", "only", "include"]
|
||||
from typing import List, Union
|
||||
|
||||
|
||||
@click.command("download")
|
||||
@@ -81,9 +78,22 @@ SinglesFilter = Literal["none", "only", "include"]
|
||||
"-s",
|
||||
"SINGLES_FILTER",
|
||||
type=click.Choice(SinglesFilter.__args__),
|
||||
default="none",
|
||||
help="Defines how to treat artist EPs and singles, used while downloading artist.",
|
||||
)
|
||||
@click.option(
|
||||
"--lyrics",
|
||||
"-l",
|
||||
"EMBED_LYRICS",
|
||||
is_flag=True,
|
||||
help="Embed track lyrics in file metadata.",
|
||||
)
|
||||
@click.option(
|
||||
"--video",
|
||||
"-v",
|
||||
"DOWNLOAD_VIDEO",
|
||||
is_flag=True,
|
||||
help="Enable downloading videos",
|
||||
)
|
||||
@passContext
|
||||
def DownloadCommand(
|
||||
ctx: Context,
|
||||
@@ -93,17 +103,28 @@ def DownloadCommand(
|
||||
THREADS_COUNT: int,
|
||||
DO_NOT_SKIP: bool,
|
||||
SINGLES_FILTER: SinglesFilter,
|
||||
EMBED_LYRICS: bool,
|
||||
DOWNLOAD_VIDEO: bool
|
||||
):
|
||||
"""Download resources"""
|
||||
DOWNLOAD_VIDEO = DOWNLOAD_VIDEO or ctx.obj.config.download.download_video
|
||||
SINGLES_FILTER = SINGLES_FILTER or ctx.obj.config.download.singles_filter
|
||||
EMBED_LYRICS = EMBED_LYRICS or ctx.obj.config.download.embed_lyrics
|
||||
|
||||
# TODO: pretty print
|
||||
logging.debug(
|
||||
(QUALITY, TEMPLATE, PATH, THREADS_COUNT, DO_NOT_SKIP, SINGLES_FILTER)
|
||||
(
|
||||
QUALITY,
|
||||
TEMPLATE,
|
||||
PATH,
|
||||
THREADS_COUNT,
|
||||
DO_NOT_SKIP,
|
||||
SINGLES_FILTER,
|
||||
EMBED_LYRICS
|
||||
)
|
||||
)
|
||||
|
||||
DOWNLOAD_QUALITY = ARG_TO_QUALITY[
|
||||
QUALITY or ctx.obj.config.download.quality
|
||||
]
|
||||
DOWNLOAD_QUALITY = ARG_TO_QUALITY[QUALITY or ctx.obj.config.download.quality]
|
||||
|
||||
api = ctx.obj.getApi()
|
||||
|
||||
@@ -137,9 +158,7 @@ def DownloadCommand(
|
||||
urls, extension = parseTrackStream(track_stream)
|
||||
elif isinstance(item, Video):
|
||||
video_stream = api.getVideoStream(item.id)
|
||||
description = (
|
||||
f"Video '{item.title}' {video_stream.videoQuality} quality"
|
||||
)
|
||||
description = f"Video '{item.title}' {video_stream.videoQuality} quality"
|
||||
|
||||
urls = parseVideoStream(video_stream)
|
||||
extension = ".ts"
|
||||
@@ -173,11 +192,7 @@ def DownloadCommand(
|
||||
)
|
||||
|
||||
stream_data += req.content
|
||||
speed = (
|
||||
len(stream_data)
|
||||
/ (perf_counter() - time_start)
|
||||
/ (1024 * 128)
|
||||
)
|
||||
speed = len(stream_data) / (perf_counter() - time_start) / (1024 * 128)
|
||||
size = len(stream_data) / 1024**2
|
||||
progress.update(
|
||||
task_id,
|
||||
@@ -204,11 +219,14 @@ def DownloadCommand(
|
||||
|
||||
if not cover_data and item.album.cover:
|
||||
cover_data = Cover(item.album.cover).content
|
||||
|
||||
if EMBED_LYRICS:
|
||||
lyrics_subtitles = api.getLyrics(item.id).subtitles
|
||||
else:
|
||||
lyrics_subtitles = ""
|
||||
|
||||
try:
|
||||
addMetadata(
|
||||
path, item, cover_data, credits, album_artist=album_artist
|
||||
)
|
||||
addMetadata(path, item, cover_data, credits, album_artist=album_artist, lyrics=lyrics_subtitles)
|
||||
except Exception as e:
|
||||
logging.error(f"Can not add metadata to: {path}, {e}")
|
||||
|
||||
@@ -255,7 +273,7 @@ def DownloadCommand(
|
||||
logging.warning(f"Track '{item.title}' skipped")
|
||||
return
|
||||
elif isinstance(item, Video):
|
||||
if path.with_suffix(".mp4").exists():
|
||||
if path.with_suffix(".mp4").exists() or not DOWNLOAD_VIDEO:
|
||||
logging.warning(f"Video '{item.title}' skipped")
|
||||
return
|
||||
|
||||
@@ -271,7 +289,12 @@ def DownloadCommand(
|
||||
def downloadAlbum(album: Album):
|
||||
logging.info(f"Album {album.title!r}")
|
||||
|
||||
cover_data = Cover(album.cover).content if album.cover else b""
|
||||
cover = (
|
||||
Cover(uid=album.cover, size=ctx.obj.config.cover.size)
|
||||
if album.cover
|
||||
else None
|
||||
)
|
||||
is_cover_saved = False
|
||||
|
||||
offset = 0
|
||||
|
||||
@@ -285,18 +308,21 @@ def DownloadCommand(
|
||||
album_artist=album.artist.name,
|
||||
)
|
||||
|
||||
if cover and not is_cover_saved and ctx.obj.config.cover.save:
|
||||
path = Path(PATH) if PATH else ctx.obj.config.download.path
|
||||
cover_path = path / Path(filename).parent
|
||||
cover.save(cover_path, ctx.obj.config.cover.filename)
|
||||
is_cover_saved = True
|
||||
|
||||
submitItem(
|
||||
item.item,
|
||||
filename,
|
||||
cover_data,
|
||||
cover.content if cover else b"",
|
||||
item.credits,
|
||||
album.artist.name,
|
||||
)
|
||||
|
||||
if (
|
||||
album_items.limit + album_items.offset
|
||||
> album_items.totalNumberOfItems
|
||||
):
|
||||
if album_items.limit + album_items.offset > album_items.totalNumberOfItems:
|
||||
break
|
||||
|
||||
offset += album_items.limit
|
||||
@@ -363,14 +389,11 @@ def DownloadCommand(
|
||||
offset = 0
|
||||
|
||||
while True:
|
||||
playlist_items = api.getPlaylistItems(
|
||||
playlist.uuid, offset=offset
|
||||
)
|
||||
playlist_items = api.getPlaylistItems(playlist.uuid, offset=offset)
|
||||
|
||||
for item in playlist_items.items:
|
||||
filename = formatResource(
|
||||
template=TEMPLATE
|
||||
or ctx.obj.config.template.playlist,
|
||||
template=TEMPLATE or ctx.obj.config.template.playlist,
|
||||
resource=item.item,
|
||||
playlist_title=playlist.title,
|
||||
playlist_index=item.item.index // 100000,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import click
|
||||
|
||||
from tiddl.utils import TidalResource, ResourceTypeLiteral
|
||||
from ..ctx import Context, passContext
|
||||
from tiddl.cli.ctx import Context, passContext
|
||||
|
||||
ResourceTypeList: list[ResourceTypeLiteral] = ["track", "video", "album", "artist", "playlist"]
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import json
|
||||
from io import TextIOWrapper
|
||||
from os.path import splitext
|
||||
|
||||
from ..ctx import Context, passContext
|
||||
from tiddl.utils import TidalResource
|
||||
from tiddl.cli.ctx import Context, passContext
|
||||
|
||||
|
||||
@click.group("file")
|
||||
|
||||
@@ -2,8 +2,7 @@ import click
|
||||
|
||||
from tiddl.utils import TidalResource
|
||||
from tiddl.models.resource import Artist, Album, Playlist, Track, Video
|
||||
|
||||
from ..ctx import Context, passContext
|
||||
from tiddl.cli.ctx import Context, passContext
|
||||
|
||||
|
||||
@click.group("search")
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import click
|
||||
|
||||
from ..ctx import Context, passContext
|
||||
|
||||
from tiddl.utils import TidalResource
|
||||
from tiddl.cli.ctx import Context, passContext
|
||||
|
||||
|
||||
class TidalURL(click.ParamType):
|
||||
|
||||
+20
-5
@@ -1,15 +1,20 @@
|
||||
# 3.0 TODO: change config path to ~/.config/tiddl.json
|
||||
|
||||
from os import environ, makedirs
|
||||
from pydantic import BaseModel
|
||||
from pathlib import Path
|
||||
|
||||
from tiddl.models.constants import TrackArg
|
||||
from tiddl.models.constants import TrackArg, SinglesFilter
|
||||
|
||||
TIDDL_ENV_KEY = "TIDDL_PATH"
|
||||
|
||||
# 3.0 TODO: rename HOME_PATH to TIDDL_PATH
|
||||
# 3.0 TODO: add /tiddl to Path.home()
|
||||
HOME_PATH = Path(environ[TIDDL_ENV_KEY]) if environ.get(TIDDL_ENV_KEY) else Path.home()
|
||||
|
||||
makedirs(HOME_PATH, exist_ok=True)
|
||||
|
||||
HOME_PATH = Path.home()
|
||||
CONFIG_PATH = HOME_PATH / "tiddl.json"
|
||||
CONFIG_INDENT = 2
|
||||
|
||||
|
||||
class TemplateConfig(BaseModel):
|
||||
track: str = "{artist} - {title}"
|
||||
video: str = "{artist} - {title}"
|
||||
@@ -21,6 +26,9 @@ class DownloadConfig(BaseModel):
|
||||
quality: TrackArg = "high"
|
||||
path: Path = Path.home() / "Music" / "Tiddl"
|
||||
threads: int = 4
|
||||
singles_filter: SinglesFilter = "none"
|
||||
embed_lyrics: bool = False
|
||||
download_video: bool = False
|
||||
|
||||
|
||||
class AuthConfig(BaseModel):
|
||||
@@ -31,9 +39,16 @@ class AuthConfig(BaseModel):
|
||||
country_code: str = ""
|
||||
|
||||
|
||||
class CoverConfig(BaseModel):
|
||||
save: bool = False
|
||||
size: int = 1280
|
||||
filename: str = "cover.jpg"
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
template: TemplateConfig = TemplateConfig()
|
||||
download: DownloadConfig = DownloadConfig()
|
||||
cover: CoverConfig = CoverConfig()
|
||||
auth: AuthConfig = AuthConfig()
|
||||
omit_cache: bool = False
|
||||
|
||||
|
||||
+23
-10
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from os import makedirs
|
||||
from pathlib import Path
|
||||
|
||||
from mutagen.easymp4 import EasyMP4 as MutagenEasyMP4
|
||||
@@ -23,6 +24,7 @@ def addMetadata(
|
||||
cover_data=b"",
|
||||
credits: List[AlbumItemsCredits.ItemWithCredits.CreditsEntry] = [],
|
||||
album_artist="",
|
||||
lyrics="",
|
||||
):
|
||||
logger.debug((track_path, track.id))
|
||||
|
||||
@@ -38,8 +40,8 @@ def addMetadata(
|
||||
picture.mime = "image/jpeg"
|
||||
metadata.add_picture(picture)
|
||||
|
||||
metadata["TITLE"] = track.title
|
||||
metadata["WORK"] = track.title
|
||||
metadata["TITLE"] = track.title + (" ({})".format(track.version) if track.version else "")
|
||||
metadata["WORK"] = track.title + (" ({})".format(track.version) if track.version else "")
|
||||
metadata["TRACKNUMBER"] = str(track.trackNumber)
|
||||
metadata["DISCNUMBER"] = str(track.volumeNumber)
|
||||
|
||||
@@ -75,13 +77,22 @@ def addMetadata(
|
||||
contributor.name for contributor in entry.contributors
|
||||
]
|
||||
|
||||
if lyrics:
|
||||
metadata["LYRICS"] = lyrics
|
||||
|
||||
elif extension == ".m4a":
|
||||
if cover_data:
|
||||
if lyrics or cover_data:
|
||||
metadata = MutagenMP4(track_path)
|
||||
metadata["covr"] = [
|
||||
MP4Cover(cover_data, imageformat=MP4Cover.FORMAT_JPEG)
|
||||
]
|
||||
metadata.save(track_path)
|
||||
|
||||
if lyrics:
|
||||
metadata["\xa9lyr"] = [lyrics]
|
||||
|
||||
if cover_data:
|
||||
metadata["covr"] = [
|
||||
MP4Cover(cover_data, imageformat=MP4Cover.FORMAT_JPEG)
|
||||
]
|
||||
|
||||
metadata.save()
|
||||
|
||||
metadata = MutagenEasyMP4(track_path)
|
||||
metadata.update(
|
||||
@@ -170,16 +181,18 @@ class Cover:
|
||||
|
||||
return req.content
|
||||
|
||||
def save(self, directory_path: Path):
|
||||
def save(self, directory_path: Path, filename="cover.jpg"):
|
||||
if not self.content:
|
||||
logger.error("cover file content is empty")
|
||||
return
|
||||
|
||||
file = directory_path / "cover.jpg"
|
||||
|
||||
file = directory_path / filename
|
||||
|
||||
if file.exists():
|
||||
logger.debug(f"cover already exists ({file})")
|
||||
return
|
||||
|
||||
makedirs(directory_path, exist_ok=True)
|
||||
|
||||
try:
|
||||
with file.open("wb") as f:
|
||||
|
||||
+13
-4
@@ -1,7 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Literal, Union
|
||||
|
||||
from .resource import Album, Artist, Playlist, Track, TrackQuality, Video
|
||||
from tiddl.models.resource import Album, Artist, Playlist, Track, TrackQuality, Video
|
||||
|
||||
__all__ = [
|
||||
"SessionResponse",
|
||||
@@ -11,6 +11,7 @@ __all__ = [
|
||||
"Favorites",
|
||||
"TrackStream",
|
||||
"Search",
|
||||
"Lyrics"
|
||||
]
|
||||
|
||||
|
||||
@@ -114,9 +115,7 @@ class TrackStream(BaseModel):
|
||||
assetPresentation: Literal["FULL"]
|
||||
audioMode: Literal["STEREO"]
|
||||
audioQuality: TrackQuality
|
||||
manifestMimeType: Literal[
|
||||
"application/dash+xml", "application/vnd.tidal.bts"
|
||||
]
|
||||
manifestMimeType: Literal["application/dash+xml", "application/vnd.tidal.bts"]
|
||||
manifestHash: str
|
||||
manifest: str
|
||||
albumReplayGain: float
|
||||
@@ -169,3 +168,13 @@ class Search(BaseModel):
|
||||
tracks: Tracks
|
||||
videos: Videos
|
||||
topHit: Optional[TopHit] = None
|
||||
|
||||
|
||||
class Lyrics(BaseModel):
|
||||
isRightToLeft: bool
|
||||
lyrics: str
|
||||
lyricsProvider: str
|
||||
providerCommontrackId: str
|
||||
providerLyricsId: str
|
||||
subtitles: str
|
||||
trackId: int
|
||||
|
||||
@@ -16,7 +16,7 @@ class AuthUser(BaseModel):
|
||||
postalcode: Optional[str]
|
||||
usState: Optional[str]
|
||||
phoneNumber: Optional[str]
|
||||
birthday: Optional[str]
|
||||
birthday: Optional[int]
|
||||
channelId: int
|
||||
parentId: int
|
||||
acceptedEULA: bool
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import Literal
|
||||
|
||||
TrackQuality = Literal["LOW", "HIGH", "LOSSLESS", "HI_RES_LOSSLESS"]
|
||||
TrackArg = Literal["low", "normal", "high", "master"]
|
||||
SinglesFilter = Literal["none", "only", "include"]
|
||||
|
||||
ARG_TO_QUALITY: dict[TrackArg, TrackQuality] = {
|
||||
"low": "LOW",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Literal, Dict
|
||||
from .constants import TrackQuality
|
||||
|
||||
from tiddl.models.constants import TrackQuality
|
||||
|
||||
|
||||
__all__ = ["Track", "Video", "Album", "Playlist", "Artist"]
|
||||
@@ -51,7 +52,7 @@ class Track(BaseModel):
|
||||
artist: Optional[Artist] = None
|
||||
artists: List[Artist]
|
||||
album: Album
|
||||
mixes: Dict[str, str]
|
||||
mixes: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
class Video(BaseModel):
|
||||
@@ -66,7 +67,7 @@ class Video(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
cover: str
|
||||
vibrantColor: str
|
||||
vibrantColor: Optional[str] = None
|
||||
videoCover: Optional[str] = None
|
||||
|
||||
id: int
|
||||
@@ -76,7 +77,7 @@ class Video(BaseModel):
|
||||
streamStartDate: Optional[datetime] = None
|
||||
imagePath: Optional[str] = None
|
||||
imageId: str
|
||||
vibrantColor: str
|
||||
vibrantColor: Optional[str] = None
|
||||
duration: int
|
||||
quality: str
|
||||
streamReady: bool
|
||||
@@ -119,8 +120,8 @@ class Album(BaseModel):
|
||||
numberOfTracks: int
|
||||
numberOfVideos: int
|
||||
numberOfVolumes: int
|
||||
releaseDate: str
|
||||
copyright: str
|
||||
releaseDate: Optional[str] = None
|
||||
copyright: Optional[str] = None
|
||||
type: str
|
||||
version: Optional[str] = None
|
||||
url: str
|
||||
|
||||
+10
-6
@@ -1,8 +1,9 @@
|
||||
import re
|
||||
import os
|
||||
import ffmpeg
|
||||
import logging
|
||||
|
||||
from ffmpeg import FFmpeg
|
||||
|
||||
from pydantic import BaseModel
|
||||
from urllib.parse import urlparse
|
||||
from pathlib import Path
|
||||
@@ -89,7 +90,7 @@ def formatTrack(
|
||||
"playlist_number": playlist_index or 0,
|
||||
}
|
||||
|
||||
formatted_track = template.format(**track_dict)
|
||||
formatted_track = template.format(**track_dict).strip()
|
||||
|
||||
disallowed_chars = r'[\\:"*?<>|]+'
|
||||
invalid_chars = re.findall(disallowed_chars, formatted_track)
|
||||
@@ -151,7 +152,7 @@ def formatResource(
|
||||
elif isinstance(resource, Video):
|
||||
resource_dict.update({"quality": resource.quality})
|
||||
|
||||
formatted_template = template.format(**resource_dict)
|
||||
formatted_template = template.format(**resource_dict).strip()
|
||||
|
||||
disallowed_chars = r'[\\:"*?<>|]+'
|
||||
invalid_chars = re.findall(disallowed_chars, formatted_template)
|
||||
@@ -216,9 +217,12 @@ def convertFileExtension(
|
||||
if is_video:
|
||||
ffmpeg_args["c:v"] = "copy"
|
||||
|
||||
ffmpeg.input(str(source_file)).output(str(output_file), **ffmpeg_args).run(
|
||||
overwrite_output=1
|
||||
)
|
||||
(
|
||||
FFmpeg()
|
||||
.option("y")
|
||||
.input(url=str(source_file))
|
||||
.output(url=str(output_file), options=None, **ffmpeg_args)
|
||||
).execute()
|
||||
|
||||
if remove_source:
|
||||
os.remove(source_file)
|
||||
|
||||
Reference in New Issue
Block a user