mirror of
https://github.com/ihabunek/twitch-dl
synced 2024-08-30 18:32:25 +00:00
Add option to define target filename
This commit is contained in:
parent
c5b5c49058
commit
b24bc0eb29
@ -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)
|
||||
-------------------
|
||||
|
||||
|
36
README.md
36
README.md
@ -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
13
tests/test_utils.py
Normal 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"
|
@ -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:
|
||||
|
@ -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}"
|
||||
})
|
||||
],
|
||||
),
|
||||
|
@ -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 = [
|
||||
|
Loading…
Reference in New Issue
Block a user