rewrite: cli rewrite
parent
757dbb941c
commit
527c547b77
|
@ -1 +1 @@
|
||||||
__version__ = '3.6.3'
|
__version__ = '4.0.0.r1'
|
||||||
|
|
|
@ -1,363 +1,54 @@
|
||||||
import click
|
import click
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import importlib
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from anime_downloader import session
|
|
||||||
from anime_downloader.sites import get_anime_class
|
|
||||||
from anime_downloader.players.mpv import mpv
|
|
||||||
from anime_downloader.__version__ import __version__
|
from anime_downloader.__version__ import __version__
|
||||||
|
|
||||||
from anime_downloader import util
|
|
||||||
from anime_downloader.config import Config
|
from anime_downloader.config import Config
|
||||||
from anime_downloader import watch as _watch
|
from anime_downloader import util
|
||||||
|
|
||||||
echo = click.echo
|
echo = click.echo
|
||||||
|
|
||||||
|
|
||||||
@click.group(context_settings=Config.CONTEXT_SETTINGS)
|
class CLIClass(click.MultiCommand):
|
||||||
|
|
||||||
|
def list_commands(self, ctx):
|
||||||
|
commands_dir = os.path.join(os.path.dirname(__file__), 'commands')
|
||||||
|
rv = []
|
||||||
|
for filename in os.listdir(commands_dir):
|
||||||
|
if filename.endswith('.py'):
|
||||||
|
rv.append(filename[:-3])
|
||||||
|
rv.sort()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def get_command(self, ctx, name):
|
||||||
|
command = importlib.import_module(
|
||||||
|
"anime_downloader.commands.{}".format(name))
|
||||||
|
return command.command
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(cls=CLIClass, context_settings=Config.CONTEXT_SETTINGS)
|
||||||
@click.version_option(version=__version__)
|
@click.version_option(version=__version__)
|
||||||
def cli():
|
@click.option(
|
||||||
|
'--log-level', '-ll',
|
||||||
|
type=click.Choice(['ERROR', 'WARNING', 'INFO', 'DEBUG']),
|
||||||
|
default='INFO',
|
||||||
|
help="Log Level"
|
||||||
|
)
|
||||||
|
def cli(log_level):
|
||||||
"""Anime Downloader
|
"""Anime Downloader
|
||||||
|
|
||||||
Download or watch your favourite anime
|
Download or watch your favourite anime
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
|
||||||
# NOTE: Don't put defaults here. Add them to the dict in config
|
|
||||||
@cli.command()
|
|
||||||
@click.argument('anime_url')
|
|
||||||
@click.option(
|
|
||||||
'--episodes', '-e', 'episode_range', metavar='<int>:<int>',
|
|
||||||
help="Range of anime you want to download in the form <start>:<end>")
|
|
||||||
@click.option(
|
|
||||||
'--url', '-u', type=bool, is_flag=True,
|
|
||||||
help="If flag is set, prints the stream url instead of downloading")
|
|
||||||
@click.option(
|
|
||||||
'--play', 'player', metavar='PLAYER',
|
|
||||||
help="Streams in the specified player")
|
|
||||||
@click.option(
|
|
||||||
'--skip-download', is_flag=True,
|
|
||||||
help="Retrieve without downloading")
|
|
||||||
@click.option(
|
|
||||||
'--download-dir', metavar='PATH',
|
|
||||||
help="Specifiy the directory to download to")
|
|
||||||
@click.option(
|
|
||||||
'--quality', '-q', type=click.Choice(['360p', '480p', '720p', '1080p']),
|
|
||||||
help='Specify the quality of episode. Default-720p')
|
|
||||||
@click.option(
|
|
||||||
'--fallback-qualities', '-fq', cls=util.ClickListOption,
|
|
||||||
help='Specifiy the order of fallback qualities as a list.')
|
|
||||||
@click.option(
|
|
||||||
'--force-download', '-f', is_flag=True,
|
|
||||||
help='Force downloads even if file exists')
|
|
||||||
@click.option(
|
|
||||||
'--log-level', '-ll', 'log_level',
|
|
||||||
type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR']),
|
|
||||||
help='Sets the level of logger')
|
|
||||||
@click.option(
|
|
||||||
'--file-format', '-ff', default='{anime_title}/{anime_title}_{ep_no}',
|
|
||||||
help='Format for how the files to be downloaded be named.',
|
|
||||||
metavar='FORMAT STRING'
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'--provider',
|
|
||||||
help='The anime provider (website) for search.',
|
|
||||||
type=click.Choice(['9anime', 'kissanime', 'twist.moe', 'animepahe', 'kisscartoon', 'masterani', 'gogoanime'])
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'--external-downloader', '-xd',
|
|
||||||
help='Use an external downloader command to download. '
|
|
||||||
'Use "{aria2}" to use aria2 as downloader. See github wiki.',
|
|
||||||
metavar='DOWNLOAD COMMAND'
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'--chunk-size',
|
|
||||||
help='Chunk size for downloading in chunks(in MB). Use this if you '
|
|
||||||
'experience throttling.',
|
|
||||||
type=int
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'--disable-ssl',
|
|
||||||
is_flag=True,
|
|
||||||
help='Disable verifying the SSL certificate, if flag is set'
|
|
||||||
)
|
|
||||||
@click.pass_context
|
|
||||||
def dl(ctx, anime_url, episode_range, url, player, skip_download, quality,
|
|
||||||
force_download, log_level, download_dir, file_format, provider,
|
|
||||||
external_downloader, chunk_size, disable_ssl, fallback_qualities):
|
|
||||||
""" Download the anime using the url or search for it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
util.setup_logger(log_level)
|
util.setup_logger(log_level)
|
||||||
util.print_info(__version__)
|
|
||||||
|
|
||||||
cls = get_anime_class(anime_url)
|
|
||||||
|
|
||||||
disable_ssl = cls and cls.__name__ == 'Masterani' or disable_ssl
|
|
||||||
session.get_session().verify = not disable_ssl
|
|
||||||
|
|
||||||
if not cls:
|
|
||||||
anime_url = util.search(anime_url, provider)
|
|
||||||
cls = get_anime_class(anime_url)
|
|
||||||
|
|
||||||
|
def main():
|
||||||
try:
|
try:
|
||||||
# Remove any trailing slashes of url
|
cli()
|
||||||
anime_url = anime_url.rstrip('/')
|
|
||||||
anime = cls(anime_url, quality=quality,
|
|
||||||
fallback_qualities=fallback_qualities)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if log_level != 'DEBUG':
|
if 'DEBUG' in sys.argv:
|
||||||
echo(click.style(str(e), fg='red'))
|
|
||||||
else:
|
|
||||||
raise
|
raise
|
||||||
return
|
click.echo(click.style('ERROR:', fg='black', bg='red') +
|
||||||
|
' '+click.style(str(e), fg='red'))
|
||||||
logging.info('Found anime: {}'.format(anime.title))
|
|
||||||
|
|
||||||
animes = util.parse_ep_str(anime, episode_range)
|
|
||||||
|
|
||||||
if url or player:
|
|
||||||
skip_download = True
|
|
||||||
|
|
||||||
if download_dir and not skip_download:
|
|
||||||
logging.info('Downloading to {}'.format(os.path.abspath(download_dir)))
|
|
||||||
|
|
||||||
for episode in animes:
|
|
||||||
if url:
|
|
||||||
util.print_episodeurl(episode)
|
|
||||||
|
|
||||||
if player:
|
|
||||||
util.play_episode(episode, player=player)
|
|
||||||
|
|
||||||
if not skip_download:
|
|
||||||
if external_downloader:
|
|
||||||
logging.info('Downloading episode {} of {}'.format(
|
|
||||||
episode.ep_no, anime.title)
|
|
||||||
)
|
|
||||||
util.external_download(external_downloader, episode,
|
|
||||||
file_format, path=download_dir)
|
|
||||||
continue
|
|
||||||
if chunk_size is not None:
|
|
||||||
chunk_size *= 1e6
|
|
||||||
chunk_size = int(chunk_size)
|
|
||||||
episode.download(force=force_download,
|
|
||||||
path=download_dir,
|
|
||||||
format=file_format,
|
|
||||||
range_size=chunk_size)
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.argument('anime_name', required=False)
|
|
||||||
@click.option(
|
|
||||||
'--new', '-n', type=bool, is_flag=True,
|
|
||||||
help="Create a new anime to watch")
|
|
||||||
@click.option(
|
|
||||||
'--list', '-l', '_list', type=bool, is_flag=True,
|
|
||||||
help="List all animes in watch list")
|
|
||||||
@click.option(
|
|
||||||
'--remove', '-r', 'remove', type=bool, is_flag=True,
|
|
||||||
help="Remove the specified anime")
|
|
||||||
@click.option(
|
|
||||||
'--update-all', '-u', 'update_all', type=bool, is_flag=True,
|
|
||||||
help="Update the episodes of all anime in your list"
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'--quality', '-q', type=click.Choice(['360p', '480p', '720p', '1080p']),
|
|
||||||
help='Specify the quality of episode.')
|
|
||||||
@click.option(
|
|
||||||
'--download-dir', metavar='PATH',
|
|
||||||
help="Specify the directory to download to")
|
|
||||||
@click.option(
|
|
||||||
'--provider',
|
|
||||||
help='The anime provider (website) for search.',
|
|
||||||
type=click.Choice(['9anime', 'kissanime', 'twist.moe', 'kisscartoon', 'masterani'])
|
|
||||||
)
|
|
||||||
|
|
||||||
@click.option(
|
|
||||||
'--log-level', '-ll', 'log_level',
|
|
||||||
type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR']),
|
|
||||||
help='Sets the level of logger', default='INFO')
|
|
||||||
def watch(anime_name, new, update_all, _list, quality, log_level, remove,
|
|
||||||
download_dir, provider):
|
|
||||||
"""
|
|
||||||
With watch you can keep track of any anime you watch.
|
|
||||||
|
|
||||||
Available Commands after selection of an anime:\n
|
|
||||||
set : set episodes_done, provider and title.
|
|
||||||
Ex: set episodes_done=3\n
|
|
||||||
remove : remove selected anime from watch list\n
|
|
||||||
update : Update the episodes of the currrent anime\n
|
|
||||||
watch : Watch selected anime\n
|
|
||||||
download : Download episodes of selected anime
|
|
||||||
"""
|
|
||||||
util.setup_logger(log_level)
|
|
||||||
util.print_info(__version__)
|
|
||||||
|
|
||||||
watcher = _watch.Watcher()
|
|
||||||
|
|
||||||
if new:
|
|
||||||
if anime_name:
|
|
||||||
query = anime_name
|
|
||||||
else:
|
|
||||||
query = click.prompt('Enter a anime name or url', type=str)
|
|
||||||
|
|
||||||
url = util.search(query, provider)
|
|
||||||
|
|
||||||
watcher.new(url)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
if remove:
|
|
||||||
anime = watcher.get(anime_name)
|
|
||||||
if anime and click.confirm(
|
|
||||||
"Remove '{}'".format(anime.title), abort=True
|
|
||||||
):
|
|
||||||
watcher.remove(anime)
|
|
||||||
else:
|
|
||||||
logging.error("Couldn't find '{}'. "
|
|
||||||
"Use a better search term.".format(anime_name))
|
|
||||||
sys.exit(1)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
if update_all:
|
|
||||||
animes = watcher.anime_list()
|
|
||||||
for anime in animes:
|
|
||||||
watcher.update_anime(anime)
|
|
||||||
|
|
||||||
if _list:
|
|
||||||
list_animes(watcher, quality, download_dir)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
if anime_name:
|
|
||||||
anime = watcher.get(anime_name)
|
|
||||||
if not anime:
|
|
||||||
logging.error(
|
|
||||||
"Couldn't find '{}'."
|
|
||||||
"Use a better search term.".format(anime_name))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
anime.quality = quality
|
|
||||||
|
|
||||||
logging.info('Found {}'.format(anime.title))
|
|
||||||
watch_anime(watcher, anime)
|
|
||||||
|
|
||||||
|
|
||||||
def list_animes(watcher, quality, download_dir):
|
|
||||||
watcher.list()
|
|
||||||
inp = click.prompt('Select an anime', default=1)
|
|
||||||
try:
|
|
||||||
anime = watcher.get(int(inp)-1)
|
|
||||||
except IndexError:
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Make the selected anime first result
|
|
||||||
watcher.update(anime)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
click.clear()
|
|
||||||
click.secho('Title: ' + click.style(anime.title,
|
|
||||||
fg='green', bold=True))
|
|
||||||
click.echo('episodes_done: {}'.format(click.style(
|
|
||||||
str(anime.episodes_done), bold=True, fg='yellow')))
|
|
||||||
click.echo('Length: {}'.format(len(anime)))
|
|
||||||
click.echo('Provider: {}'.format(anime.sitename))
|
|
||||||
|
|
||||||
meta = ''
|
|
||||||
for k, v in anime.meta.items():
|
|
||||||
meta += '{}: {}\n'.format(k, click.style(str(v), bold=True))
|
|
||||||
click.echo(meta)
|
|
||||||
|
|
||||||
click.echo('Available Commands: set, remove, update, watch,'
|
|
||||||
' download.\n')
|
|
||||||
|
|
||||||
inp = click.prompt('Press q to exit', default='q').strip()
|
|
||||||
|
|
||||||
# TODO: A better way to handle commands. Use regex. Refractor to class?
|
|
||||||
# Decorator?
|
|
||||||
if inp == 'q':
|
|
||||||
break
|
|
||||||
elif inp == 'remove':
|
|
||||||
watcher.remove(anime)
|
|
||||||
break
|
|
||||||
elif inp == 'update':
|
|
||||||
watcher.update_anime(anime)
|
|
||||||
elif inp == 'watch':
|
|
||||||
anime.quality = quality
|
|
||||||
watch_anime(watcher, anime)
|
|
||||||
sys.exit(0)
|
|
||||||
elif inp.startswith('download'):
|
|
||||||
try:
|
|
||||||
inp = inp.split('download ')[1]
|
|
||||||
except IndexError:
|
|
||||||
inp = ':'
|
|
||||||
inp = str(anime.episodes_done+1) + \
|
|
||||||
inp if inp.startswith(':') else inp
|
|
||||||
inp = inp+str(len(anime)) if inp.endswith(':') else inp
|
|
||||||
|
|
||||||
anime = util.split_anime(anime, inp)
|
|
||||||
|
|
||||||
if not download_dir:
|
|
||||||
download_dir = Config['dl']['download_dir']
|
|
||||||
|
|
||||||
for episode in anime:
|
|
||||||
episode.download(force=False,
|
|
||||||
path=Config['dl']['download_dir'],
|
|
||||||
format=Config['dl']['file_format'])
|
|
||||||
elif inp.startswith('set '):
|
|
||||||
inp = inp.split('set ')[-1]
|
|
||||||
key, val = [v.strip() for v in inp.split('=')]
|
|
||||||
key = key.lower()
|
|
||||||
|
|
||||||
if key == 'title':
|
|
||||||
watcher.remove(anime)
|
|
||||||
setattr(anime, key, val)
|
|
||||||
watcher.add(anime)
|
|
||||||
elif key == 'episodes_done':
|
|
||||||
setattr(anime, key, int(val))
|
|
||||||
watcher.update(anime)
|
|
||||||
elif key == 'provider':
|
|
||||||
url = util.search(anime.title, val)
|
|
||||||
watcher.remove(anime)
|
|
||||||
newanime = watcher.new(url)
|
|
||||||
newanime.episodes_done = anime.episodes_done
|
|
||||||
newanime._timestamp = anime._timestamp
|
|
||||||
watcher.update(newanime)
|
|
||||||
anime = newanime
|
|
||||||
|
|
||||||
|
|
||||||
def watch_anime(watcher, anime):
|
|
||||||
to_watch = anime[anime.episodes_done:]
|
|
||||||
logging.debug('Sliced epiosdes: {}'.format(to_watch._episode_urls))
|
|
||||||
|
|
||||||
while anime.episodes_done < len(anime):
|
|
||||||
episode = anime[anime.episodes_done]
|
|
||||||
anime.episodes_done += 1
|
|
||||||
watcher.update(anime)
|
|
||||||
for tries in range(5):
|
|
||||||
logging.info(
|
|
||||||
'Playing episode {}'.format(episode.ep_no)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
player = mpv(episode.source().stream_url)
|
|
||||||
except Exception as e:
|
|
||||||
anime.episodes_done -= 1
|
|
||||||
watcher.update(anime)
|
|
||||||
logging.error(str(e))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
returncode = player.play()
|
|
||||||
|
|
||||||
if returncode == player.STOP:
|
|
||||||
sys.exit(0)
|
|
||||||
elif returncode == player.CONNECT_ERR:
|
|
||||||
logging.warning("Couldn't connect. Retrying. "
|
|
||||||
"Attempt #{}".format(tries+1))
|
|
||||||
continue
|
|
||||||
elif returncode == player.PREV:
|
|
||||||
anime.episodes_done -= 2
|
|
||||||
watcher.update(anime)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
import click
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from anime_downloader import util
|
||||||
|
from anime_downloader import session
|
||||||
|
from anime_downloader.sites import get_anime_class
|
||||||
|
from anime_downloader.__version__ import __version__
|
||||||
|
|
||||||
|
logger = logging.Logger(__name__)
|
||||||
|
|
||||||
|
echo = click.echo
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: Don't put defaults here. Add them to the dict in config
|
||||||
|
@click.command()
|
||||||
|
@click.argument('anime_url')
|
||||||
|
@click.option(
|
||||||
|
'--episodes', '-e', 'episode_range', metavar='<int>:<int>',
|
||||||
|
help="Range of anime you want to download in the form <start>:<end>")
|
||||||
|
@click.option(
|
||||||
|
'--url', '-u', type=bool, is_flag=True,
|
||||||
|
help="If flag is set, prints the stream url instead of downloading")
|
||||||
|
@click.option(
|
||||||
|
'--play', 'player', metavar='PLAYER',
|
||||||
|
help="Streams in the specified player")
|
||||||
|
@click.option(
|
||||||
|
'--skip-download', is_flag=True,
|
||||||
|
help="Retrieve without downloading")
|
||||||
|
@click.option(
|
||||||
|
'--download-dir', metavar='PATH',
|
||||||
|
help="Specifiy the directory to download to")
|
||||||
|
@click.option(
|
||||||
|
'--quality', '-q', type=click.Choice(['360p', '480p', '720p', '1080p']),
|
||||||
|
help='Specify the quality of episode. Default-720p')
|
||||||
|
@click.option(
|
||||||
|
'--fallback-qualities', '-fq', cls=util.ClickListOption,
|
||||||
|
help='Specifiy the order of fallback qualities as a list.')
|
||||||
|
@click.option(
|
||||||
|
'--force-download', '-f', is_flag=True,
|
||||||
|
help='Force downloads even if file exists')
|
||||||
|
@click.option(
|
||||||
|
'--file-format', '-ff', default='{anime_title}/{anime_title}_{ep_no}',
|
||||||
|
help='Format for how the files to be downloaded be named.',
|
||||||
|
metavar='FORMAT STRING'
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--provider',
|
||||||
|
help='The anime provider (website) for search.',
|
||||||
|
type=click.Choice(['9anime', 'kissanime', 'twist.moe', 'animepahe', 'kisscartoon', 'masterani', 'gogoanime'])
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--external-downloader', '-xd',
|
||||||
|
help='Use an external downloader command to download. '
|
||||||
|
'Use "{aria2}" to use aria2 as downloader. See github wiki.',
|
||||||
|
metavar='DOWNLOAD COMMAND'
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--chunk-size',
|
||||||
|
help='Chunk size for downloading in chunks(in MB). Use this if you '
|
||||||
|
'experience throttling.',
|
||||||
|
type=int
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--disable-ssl',
|
||||||
|
is_flag=True,
|
||||||
|
help='Disable verifying the SSL certificate, if flag is set'
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def command(ctx, anime_url, episode_range, url, player, skip_download, quality,
|
||||||
|
force_download, download_dir, file_format, provider,
|
||||||
|
external_downloader, chunk_size, disable_ssl, fallback_qualities):
|
||||||
|
""" Download the anime using the url or search for it.
|
||||||
|
"""
|
||||||
|
util.print_info(__version__)
|
||||||
|
|
||||||
|
# TODO: Replace by factory
|
||||||
|
cls = get_anime_class(anime_url)
|
||||||
|
|
||||||
|
disable_ssl = cls and cls.__name__ == 'Masterani' or disable_ssl
|
||||||
|
session.get_session().verify = not disable_ssl
|
||||||
|
|
||||||
|
if not cls:
|
||||||
|
anime_url = util.search(anime_url, provider)
|
||||||
|
cls = get_anime_class(anime_url)
|
||||||
|
|
||||||
|
anime = cls(anime_url, quality=quality,
|
||||||
|
fallback_qualities=fallback_qualities)
|
||||||
|
|
||||||
|
logging.info('Found anime: {}'.format(anime.title))
|
||||||
|
|
||||||
|
animes = util.parse_ep_str(anime, episode_range)
|
||||||
|
|
||||||
|
if url or player:
|
||||||
|
skip_download = True
|
||||||
|
|
||||||
|
if download_dir and not skip_download:
|
||||||
|
logging.info('Downloading to {}'.format(os.path.abspath(download_dir)))
|
||||||
|
|
||||||
|
for episode in animes:
|
||||||
|
if url:
|
||||||
|
util.print_episodeurl(episode)
|
||||||
|
|
||||||
|
if player:
|
||||||
|
util.play_episode(episode, player=player)
|
||||||
|
|
||||||
|
if not skip_download:
|
||||||
|
if external_downloader:
|
||||||
|
logging.info('Downloading episode {} of {}'.format(
|
||||||
|
episode.ep_no, anime.title)
|
||||||
|
)
|
||||||
|
util.external_download(external_downloader, episode,
|
||||||
|
file_format, path=download_dir)
|
||||||
|
continue
|
||||||
|
if chunk_size is not None:
|
||||||
|
chunk_size *= 1e6
|
||||||
|
chunk_size = int(chunk_size)
|
||||||
|
episode.download(force=force_download,
|
||||||
|
path=download_dir,
|
||||||
|
format=file_format,
|
||||||
|
range_size=chunk_size)
|
||||||
|
print()
|
|
@ -0,0 +1,221 @@
|
||||||
|
import click
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from anime_downloader import util
|
||||||
|
from anime_downloader.__version__ import __version__
|
||||||
|
from anime_downloader.players.mpv import mpv
|
||||||
|
from anime_downloader import watch as _watch
|
||||||
|
from anime_downloader.config import Config
|
||||||
|
|
||||||
|
logger = logging.Logger(__name__)
|
||||||
|
|
||||||
|
echo = click.echo
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument('anime_name', required=False)
|
||||||
|
@click.option(
|
||||||
|
'--new', '-n', type=bool, is_flag=True,
|
||||||
|
help="Create a new anime to watch")
|
||||||
|
@click.option(
|
||||||
|
'--list', '-l', '_list', type=bool, is_flag=True,
|
||||||
|
help="List all animes in watch list")
|
||||||
|
@click.option(
|
||||||
|
'--remove', '-r', 'remove', type=bool, is_flag=True,
|
||||||
|
help="Remove the specified anime")
|
||||||
|
@click.option(
|
||||||
|
'--update-all', '-u', 'update_all', type=bool, is_flag=True,
|
||||||
|
help="Update the episodes of all anime in your list"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--quality', '-q', type=click.Choice(['360p', '480p', '720p', '1080p']),
|
||||||
|
help='Specify the quality of episode.')
|
||||||
|
@click.option(
|
||||||
|
'--download-dir', metavar='PATH',
|
||||||
|
help="Specify the directory to download to")
|
||||||
|
@click.option(
|
||||||
|
'--provider',
|
||||||
|
help='The anime provider (website) for search.',
|
||||||
|
type=click.Choice(['9anime', 'kissanime', 'twist.moe', 'kisscartoon', 'masterani'])
|
||||||
|
)
|
||||||
|
def command(anime_name, new, update_all, _list, quality, remove,
|
||||||
|
download_dir, provider):
|
||||||
|
"""
|
||||||
|
With watch you can keep track of any anime you watch.
|
||||||
|
|
||||||
|
Available Commands after selection of an anime:\n
|
||||||
|
set : set episodes_done, provider and title.
|
||||||
|
Ex: set episodes_done=3\n
|
||||||
|
remove : remove selected anime from watch list\n
|
||||||
|
update : Update the episodes of the currrent anime\n
|
||||||
|
watch : Watch selected anime\n
|
||||||
|
download : Download episodes of selected anime
|
||||||
|
"""
|
||||||
|
util.print_info(__version__)
|
||||||
|
|
||||||
|
watcher = _watch.Watcher()
|
||||||
|
|
||||||
|
if new:
|
||||||
|
if anime_name:
|
||||||
|
query = anime_name
|
||||||
|
else:
|
||||||
|
query = click.prompt('Enter a anime name or url', type=str)
|
||||||
|
|
||||||
|
url = util.search(query, provider)
|
||||||
|
|
||||||
|
watcher.new(url)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if remove:
|
||||||
|
anime = watcher.get(anime_name)
|
||||||
|
if anime and click.confirm(
|
||||||
|
"Remove '{}'".format(anime.title), abort=True
|
||||||
|
):
|
||||||
|
watcher.remove(anime)
|
||||||
|
else:
|
||||||
|
logging.error("Couldn't find '{}'. "
|
||||||
|
"Use a better search term.".format(anime_name))
|
||||||
|
sys.exit(1)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if update_all:
|
||||||
|
animes = watcher.anime_list()
|
||||||
|
for anime in animes:
|
||||||
|
watcher.update_anime(anime)
|
||||||
|
|
||||||
|
if _list:
|
||||||
|
list_animes(watcher, quality, download_dir)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if anime_name:
|
||||||
|
anime = watcher.get(anime_name)
|
||||||
|
if not anime:
|
||||||
|
logging.error(
|
||||||
|
"Couldn't find '{}'."
|
||||||
|
"Use a better search term.".format(anime_name))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
anime.quality = quality
|
||||||
|
|
||||||
|
logging.info('Found {}'.format(anime.title))
|
||||||
|
watch_anime(watcher, anime)
|
||||||
|
|
||||||
|
|
||||||
|
def list_animes(watcher, quality, download_dir):
|
||||||
|
watcher.list()
|
||||||
|
inp = click.prompt('Select an anime', default=1)
|
||||||
|
try:
|
||||||
|
anime = watcher.get(int(inp)-1)
|
||||||
|
except IndexError:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Make the selected anime first result
|
||||||
|
watcher.update(anime)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
click.clear()
|
||||||
|
click.secho('Title: ' + click.style(anime.title,
|
||||||
|
fg='green', bold=True))
|
||||||
|
click.echo('episodes_done: {}'.format(click.style(
|
||||||
|
str(anime.episodes_done), bold=True, fg='yellow')))
|
||||||
|
click.echo('Length: {}'.format(len(anime)))
|
||||||
|
click.echo('Provider: {}'.format(anime.sitename))
|
||||||
|
|
||||||
|
meta = ''
|
||||||
|
for k, v in anime.meta.items():
|
||||||
|
meta += '{}: {}\n'.format(k, click.style(str(v), bold=True))
|
||||||
|
click.echo(meta)
|
||||||
|
|
||||||
|
click.echo('Available Commands: set, remove, update, watch,'
|
||||||
|
' download.\n')
|
||||||
|
|
||||||
|
inp = click.prompt('Press q to exit', default='q').strip()
|
||||||
|
|
||||||
|
# TODO: A better way to handle commands. Use regex. Refractor to class?
|
||||||
|
# Decorator?
|
||||||
|
if inp == 'q':
|
||||||
|
break
|
||||||
|
elif inp == 'remove':
|
||||||
|
watcher.remove(anime)
|
||||||
|
break
|
||||||
|
elif inp == 'update':
|
||||||
|
watcher.update_anime(anime)
|
||||||
|
elif inp == 'watch':
|
||||||
|
anime.quality = quality
|
||||||
|
watch_anime(watcher, anime)
|
||||||
|
sys.exit(0)
|
||||||
|
elif inp.startswith('download'):
|
||||||
|
try:
|
||||||
|
inp = inp.split('download ')[1]
|
||||||
|
except IndexError:
|
||||||
|
inp = ':'
|
||||||
|
inp = str(anime.episodes_done+1) + \
|
||||||
|
inp if inp.startswith(':') else inp
|
||||||
|
inp = inp+str(len(anime)) if inp.endswith(':') else inp
|
||||||
|
|
||||||
|
anime = util.split_anime(anime, inp)
|
||||||
|
|
||||||
|
if not download_dir:
|
||||||
|
download_dir = Config['dl']['download_dir']
|
||||||
|
|
||||||
|
for episode in anime:
|
||||||
|
episode.download(force=False,
|
||||||
|
path=Config['dl']['download_dir'],
|
||||||
|
format=Config['dl']['file_format'])
|
||||||
|
elif inp.startswith('set '):
|
||||||
|
inp = inp.split('set ')[-1]
|
||||||
|
key, val = [v.strip() for v in inp.split('=')]
|
||||||
|
key = key.lower()
|
||||||
|
|
||||||
|
if key == 'title':
|
||||||
|
watcher.remove(anime)
|
||||||
|
setattr(anime, key, val)
|
||||||
|
watcher.add(anime)
|
||||||
|
elif key == 'episodes_done':
|
||||||
|
setattr(anime, key, int(val))
|
||||||
|
watcher.update(anime)
|
||||||
|
elif key == 'provider':
|
||||||
|
url = util.search(anime.title, val)
|
||||||
|
watcher.remove(anime)
|
||||||
|
newanime = watcher.new(url)
|
||||||
|
newanime.episodes_done = anime.episodes_done
|
||||||
|
newanime._timestamp = anime._timestamp
|
||||||
|
watcher.update(newanime)
|
||||||
|
anime = newanime
|
||||||
|
|
||||||
|
|
||||||
|
def watch_anime(watcher, anime):
|
||||||
|
to_watch = anime[anime.episodes_done:]
|
||||||
|
logging.debug('Sliced epiosdes: {}'.format(to_watch._episode_urls))
|
||||||
|
|
||||||
|
while anime.episodes_done < len(anime):
|
||||||
|
episode = anime[anime.episodes_done]
|
||||||
|
anime.episodes_done += 1
|
||||||
|
watcher.update(anime)
|
||||||
|
for tries in range(5):
|
||||||
|
logging.info(
|
||||||
|
'Playing episode {}'.format(episode.ep_no)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
player = mpv(episode.source().stream_url)
|
||||||
|
except Exception as e:
|
||||||
|
anime.episodes_done -= 1
|
||||||
|
watcher.update(anime)
|
||||||
|
logging.error(str(e))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
returncode = player.play()
|
||||||
|
|
||||||
|
if returncode == player.STOP:
|
||||||
|
sys.exit(0)
|
||||||
|
elif returncode == player.CONNECT_ERR:
|
||||||
|
logging.warning("Couldn't connect. Retrying. "
|
||||||
|
"Attempt #{}".format(tries+1))
|
||||||
|
continue
|
||||||
|
elif returncode == player.PREV:
|
||||||
|
anime.episodes_done -= 2
|
||||||
|
watcher.update(anime)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
|
@ -15,7 +15,6 @@ DEFAULT_CONFIG = {
|
||||||
'quality': '720p',
|
'quality': '720p',
|
||||||
'fallback_qualities': ['720p', '480p', '360p'],
|
'fallback_qualities': ['720p', '480p', '360p'],
|
||||||
'force_download': False,
|
'force_download': False,
|
||||||
'log_level': 'INFO',
|
|
||||||
'file_format': '{anime_title}/{anime_title}_{ep_no}',
|
'file_format': '{anime_title}/{anime_title}_{ep_no}',
|
||||||
'provider': '9anime',
|
'provider': '9anime',
|
||||||
'external_downloader': '',
|
'external_downloader': '',
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import cfscrape
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from anime_downloader.sites.anime import Anime
|
|
||||||
from anime_downloader.const import get_random_header
|
|
||||||
from anime_downloader.session import get_session
|
|
||||||
|
|
||||||
scraper = get_session(cfscrape.create_scraper())
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAnimeCF(Anime):
|
|
||||||
def get_data(self):
|
|
||||||
headers = get_random_header()
|
|
||||||
if hasattr(self, '_referer'):
|
|
||||||
headers['referer'] = self._referer
|
|
||||||
|
|
||||||
r = scraper.get(self.url, headers=get_random_header())
|
|
||||||
soup = BeautifulSoup(r.text, 'html.parser')
|
|
||||||
|
|
||||||
self._scrape_metadata(soup)
|
|
||||||
|
|
||||||
self._episode_urls = self._scrape_episodes(soup)
|
|
||||||
self._len = len(self._episode_urls)
|
|
||||||
|
|
||||||
logging.debug('EPISODE IDS: length: {}, ids: {}'.format(
|
|
||||||
self._len, self._episode_urls))
|
|
||||||
|
|
||||||
self._episode_urls = [(no+1, id) for no, id in
|
|
||||||
enumerate(self._episode_urls)]
|
|
||||||
|
|
||||||
return self._episode_urls
|
|
|
@ -10,11 +10,15 @@ import errno
|
||||||
import time
|
import time
|
||||||
import ast
|
import ast
|
||||||
import math
|
import math
|
||||||
|
import coloredlogs
|
||||||
|
|
||||||
from anime_downloader import session
|
from anime_downloader import session
|
||||||
from anime_downloader.sites import get_anime_class
|
from anime_downloader.sites import get_anime_class
|
||||||
from anime_downloader.const import desktop_headers
|
from anime_downloader.const import desktop_headers
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def check_in_path(app):
|
def check_in_path(app):
|
||||||
"""
|
"""
|
||||||
Checks to see if the given app exists on the path
|
Checks to see if the given app exists on the path
|
||||||
|
@ -23,19 +27,15 @@ def check_in_path(app):
|
||||||
"""
|
"""
|
||||||
return shutil.which(app) is not None
|
return shutil.which(app) is not None
|
||||||
|
|
||||||
|
|
||||||
def setup_logger(log_level):
|
def setup_logger(log_level):
|
||||||
if log_level == 'DEBUG':
|
if log_level == 'DEBUG':
|
||||||
format = '%(levelname)s %(name)s: %(message)s'
|
format = '%(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s'
|
||||||
else:
|
else:
|
||||||
format = click.style('anime', fg='green') + ': %(message)s'
|
format = click.style('anime', fg='green') + ': %(message)s'
|
||||||
|
|
||||||
logging.basicConfig(
|
logger = logging.getLogger("anime_downloader")
|
||||||
level=logging.getLevelName(log_level),
|
coloredlogs.install(level=log_level, fmt=format, logger=logger)
|
||||||
format=format
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger('urllib3.connectionpool')
|
|
||||||
logger.setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
|
|
||||||
def format_search_results(search_results):
|
def format_search_results(search_results):
|
||||||
|
@ -60,12 +60,12 @@ def search(query, provider):
|
||||||
try:
|
try:
|
||||||
search_results = cls.search(query)
|
search_results = cls.search(query)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(click.style(str(e), fg='red'))
|
logger.error(click.style(str(e), fg='red'))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
click.echo(format_search_results(search_results))
|
click.echo(format_search_results(search_results))
|
||||||
|
|
||||||
if not search_results:
|
if not search_results:
|
||||||
logging.error('No such Anime found. Please ensure correct spelling.')
|
logger.error('No such Anime found. Please ensure correct spelling.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
val = click.prompt('Enter the anime no: ', type=int, default=1)
|
val = click.prompt('Enter the anime no: ', type=int, default=1)
|
||||||
|
@ -74,12 +74,12 @@ def search(query, provider):
|
||||||
url = search_results[val-1].url
|
url = search_results[val-1].url
|
||||||
title = search_results[val-1].title
|
title = search_results[val-1].title
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logging.error('Only maximum of {} search results are allowed.'
|
logger.error('Only maximum of {} search results are allowed.'
|
||||||
' Please input a number less than {}'.format(
|
' Please input a number less than {}'.format(
|
||||||
len(search_results), len(search_results)+1))
|
len(search_results), len(search_results)+1))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
logging.info('Selected {}'.format(title))
|
logger.info('Selected {}'.format(title))
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
@ -141,17 +141,17 @@ def play_episode(episode, *, player):
|
||||||
|
|
||||||
|
|
||||||
def print_info(version):
|
def print_info(version):
|
||||||
logging.info('anime-downloader {}'.format(version))
|
logger.info('anime-downloader {}'.format(version))
|
||||||
logging.debug('Platform: {}'.format(platform.platform()))
|
logger.debug('Platform: {}'.format(platform.platform()))
|
||||||
logging.debug('Python {}'.format(platform.python_version()))
|
logger.debug('Python {}'.format(platform.python_version()))
|
||||||
|
|
||||||
|
|
||||||
def get_json(url, params=None):
|
def get_json(url, params=None):
|
||||||
logging.debug('API call URL: {} with params {!r}'.format(url, params))
|
logger.debug('API call URL: {} with params {!r}'.format(url, params))
|
||||||
res = session.get_session().get(url, headers=desktop_headers, params=params)
|
res = session.get_session().get(url, headers=desktop_headers, params=params)
|
||||||
logging.debug('URL: {}'.format(res.url))
|
logger.debug('URL: {}'.format(res.url))
|
||||||
data = res.json()
|
data = res.json()
|
||||||
logging.debug('Returned data: {}'.format(data))
|
logger.debug('Returned data: {}'.format(data))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ Base classes
|
||||||
|
|
||||||
.. automodule:: anime_downloader.sites.anime
|
.. automodule:: anime_downloader.sites.anime
|
||||||
|
|
||||||
.. autoclass:: anime_downloader.sites.anime.BaseAnime
|
.. autoclass:: anime_downloader.sites.anime.Anime
|
||||||
:members: search, get_data, _scarpe_episodes, _scrape_metadata
|
:members: search, get_data, _scrape_episodes, _scrape_metadata
|
||||||
|
|
||||||
.. autoclass:: anime_downloader.sites.anime.SearchResult
|
.. autoclass:: anime_downloader.sites.anime.SearchResult
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -25,6 +25,7 @@ setup(
|
||||||
'requests>=2.18.4',
|
'requests>=2.18.4',
|
||||||
'Click>=6.7',
|
'Click>=6.7',
|
||||||
'fuzzywuzzy>=0.16.0',
|
'fuzzywuzzy>=0.16.0',
|
||||||
|
'coloredlogs>=10.0'
|
||||||
],
|
],
|
||||||
tests_require=[
|
tests_require=[
|
||||||
'pytest',
|
'pytest',
|
||||||
|
@ -36,6 +37,6 @@ setup(
|
||||||
long_description_content_type='text/markdown',
|
long_description_content_type='text/markdown',
|
||||||
entry_points='''
|
entry_points='''
|
||||||
[console_scripts]
|
[console_scripts]
|
||||||
anime=anime_downloader.cli:cli
|
anime=anime_downloader.cli:main
|
||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue