615 lines
19 KiB
Python
615 lines
19 KiB
Python
import argparse
|
|
import itertools
|
|
import sys
|
|
import traceback
|
|
|
|
from voussoirkit import betterhelp
|
|
from voussoirkit import interactive
|
|
from voussoirkit import pipeable
|
|
from voussoirkit import vlogging
|
|
from voussoirkit import operatornotify
|
|
|
|
import ycdl
|
|
|
|
log = vlogging.getLogger(__name__, 'ycdl_cli')
|
|
|
|
# HELPERS ##########################################################################################
|
|
|
|
def closest_db():
|
|
return ycdl.ycdldb.YCDLDB.closest_ycdldb()
|
|
|
|
def get_channels_from_args(args):
|
|
'''
|
|
This function unifies channel IDs that are part of the command's argparser
|
|
and channels that come from --channel_list listargs into a single stream
|
|
of Channel objects.
|
|
'''
|
|
ycdldb = closest_db()
|
|
channels = []
|
|
|
|
if args.channel_list_args:
|
|
channels.extend(_channel_list_argparse(args.channel_list_args))
|
|
|
|
if args.channel_ids:
|
|
channels.extend(ycdldb.get_channels_by_id(pipeable.input_many(args.channel_ids)))
|
|
|
|
return channels
|
|
|
|
def get_videos_from_args(args):
|
|
'''
|
|
This function unifies video IDs that are part of the command's argparser
|
|
and videos that come from --video_list listargs into a single stream
|
|
of Video objects.
|
|
'''
|
|
ycdldb = closest_db()
|
|
videos = []
|
|
|
|
if args.video_list_args:
|
|
videos.extend(_video_list_argparse(args.video_list_args))
|
|
|
|
if args.video_ids:
|
|
videos.extend(ycdldb.get_videos_by_id(pipeable.input_many(args.video_ids)))
|
|
|
|
return videos
|
|
|
|
# ARGPARSE #########################################################################################
|
|
|
|
def add_channel_argparse(args):
|
|
ycdldb = closest_db()
|
|
with ycdldb.transaction:
|
|
ycdldb.add_channel(
|
|
channel_id=args.channel_id,
|
|
automark=args.automark,
|
|
download_directory=args.download_directory,
|
|
get_videos=args.get_videos,
|
|
name=args.name,
|
|
queuefile_extension=args.queuefile_extension,
|
|
)
|
|
|
|
if not (args.autoyes or interactive.getpermission('Commit?')):
|
|
ycdldb.rollback()
|
|
|
|
return 0
|
|
|
|
def _channel_list_argparse(args):
|
|
ycdldb = closest_db()
|
|
channels = sorted(ycdldb.get_channels(), key=lambda c: c.name.lower())
|
|
|
|
if args.automark:
|
|
channels = [channel for channel in channels if channel.automark == args.automark]
|
|
|
|
yield from channels
|
|
|
|
def channel_list_argparse(args):
|
|
for channel in _channel_list_argparse(args):
|
|
line = args.format.format(
|
|
automark=channel.automark,
|
|
autorefresh=channel.autorefresh,
|
|
id=channel.id,
|
|
name=channel.name,
|
|
queuefile_extension=channel.queuefile_extension,
|
|
uploads_playlist=channel.uploads_playlist,
|
|
)
|
|
pipeable.stdout(line)
|
|
|
|
return 0
|
|
|
|
def delete_channel_argparse(args):
|
|
ycdldb = closest_db()
|
|
needs_commit = False
|
|
|
|
with ycdldb.transaction:
|
|
for channel in get_channels_from_args(args):
|
|
channel.delete()
|
|
needs_commit = True
|
|
|
|
if not needs_commit:
|
|
return 0
|
|
|
|
if not (args.autoyes or interactive.getpermission('Commit?')):
|
|
ycdldb.rollback()
|
|
|
|
return 0
|
|
|
|
def download_video_argparse(args):
|
|
ycdldb = closest_db()
|
|
needs_commit = False
|
|
|
|
with ycdldb.transaction:
|
|
for video in get_videos_from_args(args):
|
|
queuefile = ycdldb.download_video(
|
|
video,
|
|
download_directory=args.download_directory,
|
|
force=args.force,
|
|
queuefile_extension=args.queuefile_extension,
|
|
)
|
|
if queuefile is not None:
|
|
needs_commit = True
|
|
|
|
if not needs_commit:
|
|
return 0
|
|
|
|
if not (args.autoyes or interactive.getpermission('Commit?')):
|
|
ycdldb.rollback()
|
|
|
|
return 0
|
|
|
|
def ignore_shorts_argparse(args):
|
|
ycdldb = closest_db()
|
|
|
|
videos = ycdldb.get_videos_by_sql('''
|
|
SELECT * FROM videos
|
|
LEFT JOIN channels ON channels.id = videos.author_id
|
|
WHERE is_shorts IS NULL AND duration < 62 AND state = "pending" AND channels.ignore_shorts = 1
|
|
ORDER BY published DESC
|
|
''')
|
|
videos = list(videos)
|
|
if len(videos) == 0:
|
|
log.info('No shorts candidates.')
|
|
return 0
|
|
|
|
while len(videos) > 0:
|
|
count = 0
|
|
with ycdldb.transaction:
|
|
while len(videos) > 0:
|
|
video = videos.pop()
|
|
try:
|
|
is_shorts = ycdl.ytapi.video_is_shorts(video.id)
|
|
except Exception as exc:
|
|
log.warning(traceback.format_exc())
|
|
continue
|
|
|
|
pairs = {'id': video.id, 'is_shorts': int(is_shorts)}
|
|
if is_shorts:
|
|
pairs['state'] = 'ignored'
|
|
video.state = 'ignored'
|
|
log.info('%s is shorts.', video.id)
|
|
else:
|
|
log.info('%s is not shorts.', video.id)
|
|
ycdldb.update(table=ycdl.objects.Video, pairs=pairs, where_key='id')
|
|
count += 1
|
|
|
|
# break every once in a while so the enclosing transaction
|
|
# can commit our work so far.
|
|
if count == 25:
|
|
break
|
|
|
|
def init_argparse(args):
|
|
ycdldb = ycdl.ycdldb.YCDLDB(create=True)
|
|
pipeable.stdout(ycdldb.data_directory.absolute_path)
|
|
return 0
|
|
|
|
def refresh_channels_argparse(args):
|
|
needs_commit = False
|
|
status = 0
|
|
|
|
ycdldb = closest_db()
|
|
with ycdldb.transaction:
|
|
if args.channels:
|
|
channels = [ycdldb.get_channel(c) for c in args.channels]
|
|
for channel in channels:
|
|
try:
|
|
channel.refresh(force=args.force)
|
|
needs_commit = True
|
|
except Exception as exc:
|
|
log.warning(traceback.format_exc())
|
|
status = 1
|
|
else:
|
|
excs = ycdldb.refresh_all_channels(force=args.force, skip_failures=True)
|
|
needs_commit = True
|
|
|
|
if not needs_commit:
|
|
return status
|
|
|
|
if not (args.autoyes or interactive.getpermission('Commit?')):
|
|
ycdldb.rollback()
|
|
|
|
return status
|
|
|
|
def _video_list_argparse(args):
|
|
ycdldb = closest_db()
|
|
videos = ycdldb.get_videos(channel_id=args.channel_id, state=args.state, orderby=args.orderby)
|
|
|
|
if args.limit is not None:
|
|
videos = itertools.islice(videos, args.limit)
|
|
|
|
yield from videos
|
|
|
|
def video_list_argparse(args):
|
|
for video in _video_list_argparse(args):
|
|
line = args.format.format(
|
|
author_id=video.author_id,
|
|
duration=video.duration,
|
|
id=video.id,
|
|
live_broadcast=video.live_broadcast,
|
|
published=video.published,
|
|
published_string=video.published_string,
|
|
state=video.state,
|
|
thumbnail=video.thumbnail,
|
|
title=video.title,
|
|
views=video.views,
|
|
)
|
|
pipeable.stdout(line)
|
|
|
|
return 0
|
|
|
|
@operatornotify.main_decorator(subject='ycdl_cli')
|
|
@vlogging.main_decorator
|
|
def main(argv):
|
|
parser = argparse.ArgumentParser(
|
|
description='''
|
|
This is the command-line interface for YCDL, so that you can automate your
|
|
database and integrate it into other scripts.
|
|
''',
|
|
)
|
|
subparsers = parser.add_subparsers()
|
|
|
|
################################################################################################
|
|
|
|
p_add_channel = subparsers.add_parser(
|
|
'add_channel',
|
|
aliases=['add-channel'],
|
|
description='''
|
|
Add a channel to the database.
|
|
''',
|
|
)
|
|
p_add_channel.examples = [
|
|
'UCFhXFikryT4aFcLkLw2LBLA',
|
|
'UCFhXFikryT4aFcLkLw2LBLA --automark downloaded',
|
|
'UCLx053rWZxCiYWsBETgdKrQ --name LGR',
|
|
]
|
|
p_add_channel.add_argument(
|
|
'channel_id',
|
|
)
|
|
p_add_channel.add_argument(
|
|
'--automark',
|
|
default='pending',
|
|
help='''
|
|
Set the channel's automark to this value, which should be 'pending',
|
|
'downloaded', or 'ignored'.
|
|
''',
|
|
)
|
|
p_add_channel.add_argument(
|
|
'--download_directory',
|
|
'--download-directory',
|
|
default=None,
|
|
help='''
|
|
Set the channel's download directory to this path, which must
|
|
be a directory.
|
|
''',
|
|
)
|
|
p_add_channel.add_argument(
|
|
'--name',
|
|
default=None,
|
|
help='''
|
|
Override the channel's own name with a name of your choosing.
|
|
''',
|
|
)
|
|
p_add_channel.add_argument(
|
|
'--no_videos',
|
|
'--no-videos',
|
|
dest='get_videos',
|
|
action='store_false',
|
|
help='''
|
|
By default, the channel's videos will be fetched right away. Add this
|
|
argument if you don't want to do that yet.
|
|
|
|
You should run refresh_channels later.
|
|
''',
|
|
)
|
|
p_add_channel.add_argument(
|
|
'--queuefile_extension',
|
|
'--queuefile-extension',
|
|
type=str,
|
|
default=None,
|
|
help='''
|
|
Set the queuefile extension for all videos downloaded from this channel.
|
|
''',
|
|
)
|
|
p_add_channel.add_argument(
|
|
'--yes',
|
|
dest='autoyes',
|
|
action='store_true',
|
|
help='''
|
|
Commit the database without prompting.
|
|
''',
|
|
)
|
|
p_add_channel.set_defaults(func=add_channel_argparse)
|
|
|
|
################################################################################################
|
|
|
|
p_channel_list = subparsers.add_parser(
|
|
'channel_list',
|
|
aliases=['channel-list'],
|
|
description='''
|
|
Print all channels in the database.
|
|
|
|
Note: If you want to use this in a command pipeline, please specify
|
|
--format instead of relying on the default.
|
|
''',
|
|
)
|
|
p_channel_list.examples = [
|
|
'',
|
|
['--format', '{id} automark={automark}'],
|
|
'--automark downloaded',
|
|
]
|
|
p_channel_list.add_argument(
|
|
'--format',
|
|
default='{id}:{name}',
|
|
help='''
|
|
A string like "{id}: {name}" to format the attributes of the channel.
|
|
The available attributes are id, name, automark, autorefresh,
|
|
uploads_playlist, queuefile_extension.
|
|
|
|
If you are using --channel_list as listargs for another command, then
|
|
this argument is not relevant.
|
|
''',
|
|
)
|
|
p_channel_list.add_argument(
|
|
'--automark',
|
|
help='''
|
|
Only show channels with this automark, pending, downloaded, or ignored.
|
|
''',
|
|
)
|
|
p_channel_list.set_defaults(func=channel_list_argparse)
|
|
|
|
################################################################################################
|
|
|
|
p_delete_channel = subparsers.add_parser(
|
|
'delete_channel',
|
|
aliases=['delete-channel'],
|
|
description='''
|
|
Delete a channel and all its videos from the database.
|
|
''',
|
|
)
|
|
p_delete_channel.examples = [
|
|
{'args': 'UCOYBuFGi8T3NM5fNAptCLCw', 'comment': 'Delete one channel'},
|
|
{'args': 'UCOYBuFGi8T3NM5fNAptCLCw UCmu9PVIZBk-ZCi-Sk2F2utA', 'comment': 'Delete many channels'},
|
|
{'args': '--channel-list --automark ignored', 'comment': 'Delete all channels that use the ignored automark'},
|
|
]
|
|
p_delete_channel.add_argument(
|
|
'channel_ids',
|
|
nargs='*',
|
|
help='''
|
|
One or more channel IDs to delete.
|
|
|
|
Uses pipeable to support !c clipboard, !i stdin lines of IDs.
|
|
''',
|
|
)
|
|
p_delete_channel.add_argument(
|
|
'--yes',
|
|
dest='autoyes',
|
|
action='store_true',
|
|
help='''
|
|
Commit the database without prompting.
|
|
''',
|
|
)
|
|
p_delete_channel.add_argument(
|
|
'--channel_list',
|
|
'--channel-list',
|
|
dest='channel_list_args',
|
|
nargs='...',
|
|
help='''
|
|
All remaining arguments will go to the channel_list command to generate
|
|
the list of channels to delete. Do not worry about --format.
|
|
See channel_list --help for help.
|
|
''',
|
|
)
|
|
p_delete_channel.set_defaults(func=delete_channel_argparse)
|
|
|
|
################################################################################################
|
|
|
|
p_download_video = subparsers.add_parser(
|
|
'download_video',
|
|
aliases=['download-video'],
|
|
description='''
|
|
Create the queuefiles for one or more videos.
|
|
|
|
The video will have its state set to "downloaded".
|
|
''',
|
|
)
|
|
p_download_video.examples = [
|
|
{'args': 'thOifuHs6eY', 'comment': 'Download one video'},
|
|
{'args': 'yJ-oASr_djo vHuFizITMdA --force', 'comment': 'Force download many videos'},
|
|
{'args': '--video_list --channel UCvBv3PCvD9v-IKKTkd94XPg', 'comment': 'Download all videos from this channel'},
|
|
{'args': '--force --video_list --state downloaded', 'comment': 'Force re-download all videos that have already been downloaded'},
|
|
]
|
|
p_download_video.add_argument(
|
|
'video_ids',
|
|
nargs='*',
|
|
help='''
|
|
Uses pipeable to support !c clipboard, !i stdin lines of IDs.
|
|
''',
|
|
)
|
|
p_download_video.add_argument(
|
|
'--download_directory',
|
|
'--download-directory',
|
|
default=None,
|
|
help='''
|
|
By default, the queuefile will be placed in the channel's
|
|
download_directory if it has one, or the download_directory in the
|
|
ycdl.json config file. You can pass this argument to override both
|
|
of those and use a specific directory.
|
|
''',
|
|
)
|
|
p_download_video.add_argument(
|
|
'--force',
|
|
action='store_true',
|
|
help='''
|
|
By default, a video that is already marked as downloaded will not be
|
|
downloaded again. You can add this to make the queuefiles for those
|
|
videos anyway.
|
|
''',
|
|
)
|
|
p_download_video.add_argument(
|
|
'--queuefile_extension',
|
|
'--queuefile-extension',
|
|
default=None,
|
|
help='''
|
|
By default, the queuefile extension is taken from the channel or the
|
|
config file. You can pass this argument to override both of those.
|
|
''',
|
|
)
|
|
p_download_video.add_argument(
|
|
'--yes',
|
|
dest='autoyes',
|
|
action='store_true',
|
|
help='''
|
|
Commit the database without prompting.
|
|
''',
|
|
)
|
|
p_download_video.add_argument(
|
|
'--video_list',
|
|
'--video-list',
|
|
dest='video_list_args',
|
|
nargs='...',
|
|
help='''
|
|
All remaining arguments will go to the video_list command to generate the
|
|
list of channels to delete. Do not worry about --format.
|
|
See video_list --help for help.
|
|
''',
|
|
)
|
|
p_download_video.set_defaults(func=download_video_argparse)
|
|
|
|
################################################################################################
|
|
|
|
p_ignore_shorts = subparsers.add_parser(
|
|
'ignore_shorts',
|
|
aliases=['ignore-shorts'],
|
|
description='''
|
|
Queries the Youtube API to figure out which videos are shorts, and marks
|
|
them as ignored.
|
|
''',
|
|
)
|
|
p_ignore_shorts.set_defaults(func=ignore_shorts_argparse)
|
|
|
|
################################################################################################
|
|
|
|
p_init = subparsers.add_parser(
|
|
'init',
|
|
description='''
|
|
Create a new YCDL database in the current directory.
|
|
''',
|
|
)
|
|
p_init.set_defaults(func=init_argparse)
|
|
|
|
################################################################################################
|
|
|
|
p_refresh_channels = subparsers.add_parser(
|
|
'refresh_channels',
|
|
aliases=['refresh-channels'],
|
|
description='''
|
|
Refresh some or all channels in the database.
|
|
|
|
New videos will have their state marked with the channel's automark value,
|
|
and queuefiles will be created for channels with automark=downloaded.
|
|
''',
|
|
)
|
|
p_refresh_channels.examples = [
|
|
'--force',
|
|
'--channels UC1_uAIS3r8Vu6JjXWvastJg',
|
|
]
|
|
p_refresh_channels.add_argument(
|
|
'--channels',
|
|
nargs='*',
|
|
help='''
|
|
Any number of channel IDs.
|
|
If omitted, all channels will be refreshed.
|
|
''',
|
|
)
|
|
p_refresh_channels.add_argument(
|
|
'--force',
|
|
action='store_true',
|
|
help='''
|
|
If omitted, only new videos are found.
|
|
If included, channels are refreshed completely. This may be slow and
|
|
cost a lot of API calls.
|
|
''',
|
|
)
|
|
p_refresh_channels.add_argument(
|
|
'--yes',
|
|
dest='autoyes',
|
|
action='store_true',
|
|
help='''
|
|
Commit the database without prompting.
|
|
''',
|
|
)
|
|
p_refresh_channels.set_defaults(func=refresh_channels_argparse)
|
|
|
|
################################################################################################
|
|
|
|
p_video_list = subparsers.add_parser(
|
|
'video_list',
|
|
aliases=['video-list'],
|
|
description='''
|
|
Print videos in the database.
|
|
|
|
Note: If you want to use this in a command pipeline, please specify
|
|
--format instead of relying on the default.
|
|
''',
|
|
)
|
|
p_video_list.examples = [
|
|
'--state pending --limit 100',
|
|
'--channel UCzIiTeduaanyEboRfwJJznA --orderby views',
|
|
'--channel UC6nSFpj9HTCZ5t-N3Rm3-HA --format "{thumbnail} {id}.jpg" | threaded_dl !i 1 {basename}'
|
|
]
|
|
p_video_list.add_argument(
|
|
'--channel',
|
|
dest='channel_id',
|
|
default=None,
|
|
help='''
|
|
A channel ID to list videos from.
|
|
''',
|
|
)
|
|
p_video_list.add_argument(
|
|
'--format',
|
|
default='{published_string}:{id}:{title}',
|
|
help='''
|
|
A string like "{published_string}:{id} {title}" to format the
|
|
attributes of the video. The available attributes are author_id,
|
|
duration, id, live_broadcast, published, published_string, state,
|
|
title, views, thumbnail.
|
|
''',
|
|
)
|
|
p_video_list.add_argument(
|
|
'--limit',
|
|
type=int,
|
|
default=None,
|
|
help='''
|
|
Only show up to this many results.
|
|
''',
|
|
)
|
|
p_video_list.add_argument(
|
|
'--orderby',
|
|
default=None,
|
|
help='''
|
|
Order the results by published, views, duration, or random.
|
|
''',
|
|
)
|
|
p_video_list.add_argument(
|
|
'--state',
|
|
default=None,
|
|
help='''
|
|
Only show videos with this state, pending, downloaded, or ignored.
|
|
''',
|
|
)
|
|
p_video_list.set_defaults(func=video_list_argparse)
|
|
|
|
##
|
|
|
|
def postprocessor(args):
|
|
if hasattr(args, 'video_list_args'):
|
|
args.video_list_args = p_video_list.parse_args(args.video_list_args)
|
|
if hasattr(args, 'channel_list_args'):
|
|
args.channel_list_args = p_channel_list.parse_args(args.channel_list_args)
|
|
return args
|
|
|
|
try:
|
|
return betterhelp.go(parser, argv, args_postprocessor=postprocessor)
|
|
except ycdl.exceptions.NoClosestYCDLDB as exc:
|
|
pipeable.stderr(exc.error_message)
|
|
pipeable.stderr('Try `ycdl_cli.py init` to create the database.')
|
|
return 1
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main(sys.argv[1:]))
|