Add option to define target filename

This commit is contained in:
Ivan Habunek 2022-01-23 09:14:40 +01:00
parent c5b5c49058
commit b24bc0eb29
No known key found for this signature in database
GPG Key ID: CDBD63C43A30BB95
6 changed files with 118 additions and 36 deletions

View File

@ -1,6 +1,12 @@
Twitch Downloader change log
============================
1.18.0 (TBA)
-------------------
* Added `--output` option to `download` command which allows setting output file
template
1.17.1 (2022-01-19)
-------------------

View File

@ -147,6 +147,42 @@ Setting quality to `source` will download the best available quality:
twitch-dl download -q source 221837124
```
### Overriding file name
The target filename can be defined by passing the `--output` option followed by
the desired file name, e.g. `--output strim.mkv`.
The filename uses
[Python format string syntax](https://docs.python.org/3/library/string.html#format-string-syntax)
and may contain placeholders in curly braces which will be replaced with
relevant information tied to the downloaded video.
The supported placeholders are:
| Placeholder | Description | Example |
| ----------------- | ------------------------------ | ------------------------------ |
| `{id}` | Video ID | 1255522958 |
| `{title}` | Video title | Dark Souls 3 First playthrough |
| `{title_slug}` | Slugified video title | dark_souls_3_first_playthrough |
| `{datetime}` | Video date and time | 2022-01-07T04:00:27Z |
| `{date}` | Video date | 2022-01-07 |
| `{time}` | Video time | 04:00:27Z |
| `{channel}` | Channel name | KatLink |
| `{channel_login}` | Channel login | katlink |
| `{format}` | File extension, see `--format` | mkv |
| `{game}` | Game name | Dark Souls III |
| `{game_slug}` | Slugified game name | dark_souls_iii |
A couple of examples:
Pattern: `"{date}_{id}_{channel_login}_{title_slug}.{format}"`<br />
Expands to: `2022-01-07_1255522958_katlink_dark_souls_3_first_playthrough.mkv`<br />
*This is the default.*
Pattern: `"{channel} - {game} - {title}.{format}"`<br />
Expands to: `KatLink - Dark Souls III - Dark Souls 3 First playthrough.mkv`
### Listing clips
List clips for the given period:

13
tests/test_utils.py Normal file
View File

@ -0,0 +1,13 @@
from twitchdl.utils import titlify, slugify
def test_titlify():
assert titlify("Foo Bar Baz.") == "Foo Bar Baz."
assert titlify("Foo (Bar) [Baz]") == "Foo (Bar) [Baz]"
assert titlify("Foo@{} Bar Baz!\"#$%&/=?*+'🔪") == "Foo Bar Baz"
def test_slugify():
assert slugify("Foo Bar Baz") == "foo_bar_baz"
assert slugify(" Foo Bar Baz ") == "foo_bar_baz"
assert slugify("Foo@{}[] Bar Baz!\"#$%&/()=?*+'🔪") == "foo_bar_baz"

View File

@ -70,36 +70,56 @@ def _join_vods(playlist_path, target, overwrite, video):
raise ConsoleError("Joining files failed")
def _video_target_filename(video, format):
match = re.search(r"^(\d{4})-(\d{2})-(\d{2})T", video['publishedAt'])
date = "".join(match.groups())
def _video_target_filename(video, args):
date, time = video['publishedAt'].split("T")
name = "_".join([
date,
video['id'],
video['creator']['login'],
utils.slugify(video['title']),
])
subs = {
"channel": video["creator"]["displayName"],
"channel_login": video["creator"]["login"],
"date": date,
"datetime": video["publishedAt"],
"format": args.format,
"game": video["game"]["name"],
"game_slug": utils.slugify(video["game"]["name"]),
"id": video["id"],
"time": time,
"title": utils.titlify(video["title"]),
"title_slug": utils.slugify(video["title"]),
}
return name + "." + format
try:
return args.output.format(**subs)
except KeyError as e:
supported = ", ".join(subs.keys())
raise ConsoleError("Invalid key {} used in --output. Supported keys are: {}".format(e, supported))
def _clip_target_filename(clip):
def _clip_target_filename(clip, args):
date, time = clip["createdAt"].split("T")
url = clip["videoQualities"][0]["sourceURL"]
_, ext = path.splitext(url)
ext = ext.lstrip(".")
match = re.search(r"^(\d{4})-(\d{2})-(\d{2})T", clip["createdAt"])
date = "".join(match.groups())
subs = {
"channel": clip["broadcaster"]["displayName"],
"channel_login": clip["broadcaster"]["login"],
"date": date,
"datetime": clip["createdAt"],
"format": ext,
"game": clip["game"]["name"],
"game_slug": utils.slugify(clip["game"]["name"]),
"id": clip["id"],
"time": time,
"title": utils.titlify(clip["title"]),
"title_slug": utils.slugify(clip["title"]),
}
name = "_".join([
date,
clip["id"],
clip["broadcaster"]["login"],
utils.slugify(clip["title"]),
])
return "{}.{}".format(name, ext)
try:
return args.output.format(**subs)
except KeyError as e:
supported = ", ".join(subs.keys())
raise ConsoleError("Invalid key {} used in --output. Supported keys are: {}".format(e, supported))
def _get_vod_paths(playlist, start, end):
@ -194,12 +214,6 @@ def _download_clip(slug, args):
if not clip:
raise ConsoleError("Clip '{}' not found".format(slug))
print_out("<dim>Fetching access token...</dim>")
access_token = twitch.get_clip_access_token(slug)
if not access_token:
raise ConsoleError("Access token not found for slug '{}'".format(slug))
print_out("Found: <green>{}</green> by <yellow>{}</yellow>, playing <blue>{}</blue> ({})".format(
clip["title"],
clip["broadcaster"]["displayName"],
@ -207,11 +221,12 @@ def _download_clip(slug, args):
utils.format_duration(clip["durationSeconds"])
))
target = _clip_target_filename(clip, args)
print_out("Target: <blue>{}</blue>".format(target))
url = get_clip_authenticated_url(slug, args.quality)
print_out("<dim>Selected URL: {}</dim>".format(url))
target = _clip_target_filename(clip)
print_out("Downloading clip...")
download_file(url, target)
@ -231,6 +246,9 @@ def _download_video(video_id, args):
print_out("Found: <blue>{}</blue> by <yellow>{}</yellow>".format(
video['title'], video['creator']['displayName']))
target = _video_target_filename(video, args)
print_out("Output: <blue>{}</blue>".format(target))
print_out("<dim>Fetching access token...</dim>")
access_token = twitch.get_access_token(video_id)
@ -277,7 +295,6 @@ def _download_video(video_id, args):
return
print_out("\n\nJoining files...")
target = _video_target_filename(video, args.format)
_join_vods(playlist_path, target, args.overwrite, video)
if args.keep:

View File

@ -168,6 +168,11 @@ COMMANDS = [
"help": "Overwrite the target file if it already exists without prompting.",
"action": "store_true",
"default": False,
}),
(["-o", "--output"], {
"help": "Output file name template. See docs for details.",
"type": str,
"default": "{date}_{id}_{channel_login}_{title_slug}.{format}"
})
],
),

View File

@ -55,12 +55,17 @@ def read_int(msg, min, max, default):
def slugify(value):
re_pattern = re.compile(r'[^\w\s-]', flags=re.U)
re_spaces = re.compile(r'[-\s]+', flags=re.U)
value = str(value)
value = unicodedata.normalize('NFKC', value)
value = re_pattern.sub('', value).strip().lower()
return re_spaces.sub('_', value)
value = unicodedata.normalize('NFKC', str(value))
value = re.sub(r'[^\w\s_-]', '', value)
value = re.sub(r'[\s_-]+', '_', value)
return value.strip("_").lower()
def titlify(value):
value = unicodedata.normalize('NFKC', str(value))
value = re.sub(r'[^\w\s\[\]().-]', '', value)
value = re.sub(r'\s+', ' ', value)
return value.strip()
VIDEO_PATTERNS = [