Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new options to the download command #141

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ or by ID
twitch-dl download 1418494769
```

or all videos from a channel

```
twitch-dl download bananasaurus_rex --all
```

Download a clip by URL

```
Expand Down
6 changes: 6 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ the `TMP` environment variable, e.g.
```
TMP=/my/tmp/path/ twitch-dl download 221837124
```

You can also specify the `--tempdir` argument to the `download` command without having to modify your environment variables. For example:

```
twitch-dl download 221837124 --tempdir /my/tmp/path/
```
5 changes: 0 additions & 5 deletions docs/commands/clips.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@ twitch-dl clips <channel_name> [FLAGS] [OPTIONS]
<td class="code">-j, --json</td>
<td>Show results as JSON. Ignores <code>--pager</code>.</td>
</tr>

<tr>
<td class="code">-d, --download</td>
<td>Download all videos in given period (in source quality)</td>
</tr>
</tbody>
</table>

Expand Down
37 changes: 36 additions & 1 deletion docs/commands/download.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,25 @@ twitch-dl download <videos> [FLAGS] [OPTIONS]
<td>Don&#x27;t run ffmpeg to join the downloaded vods, implies --keep.</td>
</tr>

<tr>
<td class="code">--skipall</td>
<td>Skip the current file if it already exists without prompting.</td>
</tr>

<tr>
<td class="code">--overwrite</td>
<td>Overwrite the target file if it already exists without prompting.</td>
</tr>

<tr>
<td class="code">-x, --all</td>
<td>Download all videos on the channel. Overrides all other arguments. Pass in the channel name as the &#x27;videos&#x27; argument.</td>
</tr>

<tr>
<td class="code">-y, --skip-latest</td>
<td>Skip downloading the latest video. Only makes sense with the --all flag.</td>
</tr>
</tbody>
</table>

Expand Down Expand Up @@ -67,7 +82,7 @@ twitch-dl download <videos> [FLAGS] [OPTIONS]

<tr>
<td class="code">-q, --quality</td>
<td>Video quality, e.g. 720p. Set to &#x27;source&#x27; to get best quality.</td>
<td>Video quality, e.g. 720p30 or 720p60. Set to &#x27;source&#x27; to get best quality.</td>
</tr>

<tr>
Expand All @@ -89,6 +104,26 @@ twitch-dl download <videos> [FLAGS] [OPTIONS]
<td class="code">-c, --chapter</td>
<td>Download a single chapter of the video. Specify the chapter number or use the flag without a number to display a chapter select prompt.</td>
</tr>

<tr>
<td class="code">-t, --tempdir</td>
<td>Override the temp dir path.</td>
</tr>

<tr>
<td class="code">-d, --output-dir</td>
<td>Customize location of the output directory. Defaults to the current directory.</td>
</tr>

<tr>
<td class="code">-u, --execute-after</td>
<td>Run a CLI command after each file is downloaded and processed. In your command, use ^p for the absolute path to the file that was downloaded, and ^f for just the file name.</td>
</tr>

<tr>
<td class="code">-z, --execute-before</td>
<td>Run a CLI command before each file is downloaded. Return an exit code of 0 to indicate you want to download the file, or nonzero to indicate you want to skip the file. In your command, use ^p for the absolute path to the file that was downloaded, and ^f for just the file name.</td>
</tr>
</tbody>
</table>

Expand Down
26 changes: 26 additions & 0 deletions examples/s3_search.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/bash

# Check if AWS CLI is installed
if ! command -v aws &> /dev/null
then
echo "AWS CLI not installed. Please install and configure it."
exit 2
fi

# Check for correct number of arguments
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <s3-bucket-name> <file-name>"
exit 2
fi

BUCKET=$1
FILE=$2

# Check if file exists in the S3 bucket
if aws s3 ls "s3://$BUCKET/$FILE" > /dev/null; then
echo "File $FILE exists in bucket $BUCKET."
exit 1
else
echo "File $FILE does not exist in bucket $BUCKET."
exit 0
fi
24 changes: 24 additions & 0 deletions examples/s3_upload_nuke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash

# Check if AWS CLI is installed
if ! command -v aws &> /dev/null
then
echo "AWS CLI not installed. Please install and configure it."
exit 2
fi

# Check for correct number of arguments
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <s3-bucket-name> <file-name>"
exit 2
fi

BUCKET=$1
FILE=$2

# Upload the file to S3, then delete the local copy
set -e
aws s3 cp "${FILE}" "s3://$BUCKET/" --storage-class GLACIER
rm -f "${FILE}"

exit 0
112 changes: 94 additions & 18 deletions twitchdl/commands/download.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import asyncio
import sys
import httpx
import m3u8
import os
import re
import shutil
import subprocess
import tempfile
import shlex

from os import path
from pathlib import Path
Expand All @@ -16,7 +18,18 @@
from twitchdl.download import download_file
from twitchdl.exceptions import ConsoleError
from twitchdl.http import download_all
from twitchdl.output import print_out
from twitchdl.output import print_err, print_out

def _execute_download_command(file_path: str, command_template: str) -> bool:
print_out(f"file_path: {file_path}")
file_name = os.path.basename(file_path)
qfp = shlex.quote(str(file_path))
qfn = shlex.quote(file_name)
command = command_template.replace("^p", qfp).replace("^f", qfn)

compl = subprocess.run(command, shell=True, check=False)
print_out(f"Executed command: {command}")
return compl.returncode == 0


def _parse_playlists(playlists_m3u8):
Expand Down Expand Up @@ -101,7 +114,8 @@ def _video_target_filename(video, args):
}

try:
return args.output.format(**subs)
target = args.output.format(**subs)
return Path(args.output_dir, target) if args.output_dir else target
except KeyError as e:
supported = ", ".join(subs.keys())
raise ConsoleError("Invalid key {} used in --output. Supported keys are: {}".format(e, supported))
Expand Down Expand Up @@ -131,7 +145,8 @@ def _clip_target_filename(clip, args):
}

try:
return args.output.format(**subs)
target = args.output.format(**subs)
return Path(args.output_dir, target) if args.output_dir else target
except KeyError as e:
supported = ", ".join(subs.keys())
raise ConsoleError("Invalid key {} used in --output. Supported keys are: {}".format(e, supported))
Expand All @@ -157,17 +172,36 @@ def _get_vod_paths(playlist, start: Optional[int], end: Optional[int]) -> List[s
return files


def _crete_temp_dir(base_uri: str) -> str:
def _crete_temp_dir(base_uri: str, args) -> str:
"""Create a temp dir to store downloads if it doesn't exist."""
path = urlparse(base_uri).path.lstrip("/")
temp_dir = Path(tempfile.gettempdir(), "twitch-dl", path)
temp_dir = Path(args.tempdir if args.tempdir else tempfile.gettempdir(), "twitch-dl", path)
temp_dir.mkdir(parents=True, exist_ok=True)
return str(temp_dir)

def _download_all_videos(channel_name, args):
print_out(f"<dim>Fetching all videos for channel: {channel_name}...</dim>")
total_count, video_generator = twitch.channel_videos_generator(channel_name, sys.maxsize, 'time', 'archive')
print_out(f"<dim>Found {total_count} videos to download...</dim>")

if args.skip_latest:
next(video_generator) # Skip the latest video

for video in video_generator:
#Skip execution if the pre-execute command returns non-zero status
target_filename = _video_target_filename(video, args)
if not args.execute_before or (args.execute_before and _execute_download_command(target_filename, args.execute_before)):
_download_video(video['id'], args)
else:
print_out(f"<dim>Skipping video due to pre-execute returning nonzero: {video['id']}...</dim>")

def download(args):
for video_id in args.videos:
download_one(video_id, args)
if args.all:
# Assuming the channel name is passed in args.videos[0]
_download_all_videos(args.videos[0], args)
else:
for video_id in args.videos:
download_one(video_id, args)


def download_one(video: str, args):
Expand Down Expand Up @@ -245,11 +279,30 @@ def _download_clip(slug: str, args) -> None:
target = _clip_target_filename(clip, args)
print_out("Target: <blue>{}</blue>".format(target))

if not args.overwrite and path.exists(target):
response = input("File exists. Overwrite? [Y/n]: ")
if response.lower().strip() not in ["", "y"]:
raise ConsoleError("Aborted")
args.overwrite = True
if path.exists(target):
if args.skipall:
print("Target file exists. Skipping.")
return
if not args.overwrite:
while True:
response = input("File exists. Overwrite? [ \033[4mY\033[0mes, \033[4ma\033[0mlways yes, \033[4ms\033[0mkip, always s\033[4mk\033[0mip, a\033[4mb\033[0mort ]: ")
match response.lower().strip():
case "y":
break # Just continue
case "a":
args.overwrite = True
break
case "s":
print("Skipping.")
return
case "k":
print("Skipping.")
args.skipall = True
return
case "b":
raise ConsoleError("Aborted")
case _:
print("Invalid input.")

url = get_clip_authenticated_url(slug, args.quality)
print_out("<dim>Selected URL: {}</dim>".format(url))
Expand All @@ -259,6 +312,9 @@ def _download_clip(slug: str, args) -> None:

print_out("Downloaded: <blue>{}</blue>".format(target))

if args.execute_after:
_execute_download_command(target, args.execute_after)


def _download_video(video_id, args) -> None:
if args.start and args.end and args.end <= args.start:
Expand All @@ -276,11 +332,28 @@ def _download_video(video_id, args) -> None:
target = _video_target_filename(video, args)
print_out("Output: <blue>{}</blue>".format(target))

if not args.overwrite and path.exists(target):
response = input("File exists. Overwrite? [Y/n]: ")
if response.lower().strip() not in ["", "y"]:
raise ConsoleError("Aborted")
args.overwrite = True
if path.exists(target):
if args.skipall:
print("Target file exists. Skipping.")
return
if not args.overwrite:
while True:
response = input("File exists. Overwrite? [ \033[4mY\033[0mes, \033[4ma\033[0mlways yes, \033[4ms\033[0mkip, always s\033[4mk\033[0mip, a\033[4mb\033[0mort ]: ")
match response.lower().strip():
case "y":
break # Just continue
case "a":
args.overwrite = True
break
case "s":
return
case "k":
args.skipall = True
return
case "b":
raise ConsoleError("Aborted")
case _:
print("Invalid input.")

# Chapter select or manual offset
start, end = _determine_time_range(video_id, args)
Expand All @@ -300,7 +373,7 @@ def _download_video(video_id, args) -> None:
playlist = m3u8.loads(response.text)

base_uri = re.sub("/[^/]+$", "/", playlist_uri)
target_dir = _crete_temp_dir(base_uri)
target_dir = _crete_temp_dir(base_uri, args)
vod_paths = _get_vod_paths(playlist, start, end)

# Save playlists for debugging purposes
Expand Down Expand Up @@ -345,6 +418,9 @@ def _download_video(video_id, args) -> None:

print_out("\nDownloaded: <green>{}</green>".format(target))

if args.execute_after:
_execute_download_command(target, args.execute_after)


def _determine_time_range(video_id, args):
if args.start or args.end:
Expand Down
Loading