dosage/dosage
Tobias Gruetzmacher e92a3fb3a1 New feature: Comic modules ca be "disabled".
This is modeled parallel to the "adult" feature, except the user can't
override it via the command line. Each comic module can override the
classmethod getDisabledReasons and give the user a reason why this
module is disabled. The user can see the reason in the comic list (-l or
--singlelist) and the comic module refuses to run, showing the same
message.

This is currently used to disable modules that use the _ParserScraper if
the LXML python module is missing.
2014-10-13 21:43:46 +02:00

343 lines
12 KiB
Python
Executable file

#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
# Copyright (C) 2004-2005 Tristan Seligmann and Jonathan Jacobs
# Copyright (C) 2012-2014 Bastian Kleineidam
# ___
# / \___ ___ __ _ __ _ ___
# / /\ / _ \/ __|/ _` |/ _` |/ _ \
# / /_// (_) \__ \ (_| | (_| | __/
# /___,' \___/|___/\__,_|\__, |\___|
# |___/
from __future__ import division, print_function
import sys
import os
import argparse
import pydoc
from io import StringIO
from dosagelib import events, configuration, singleton, director
from dosagelib.output import out
from dosagelib.util import internal_error, strlimit, getLangName
from dosagelib.ansicolor import get_columns
class ArgumentParser(argparse.ArgumentParser):
"""Custom argument parser."""
def print_help(self, file=None):
"""Paginate help message on TTYs."""
msg = self.format_help()
if file is None:
file = sys.stdout
if hasattr(file, "isatty") and file.isatty():
pydoc.pager(msg)
else:
print(msg, file=file)
Examples = """\
EXAMPLES
List available comics (ca. 3000 at the moment):
dosage -l
Get the latest comic of for example CalvinAndHobbes and save it in the "Comics"
directory:
dosage CalvinAndHobbes
If you already have downloaded several comics and want to get the latest
strips of all of them:
dosage --continue @
"""
def setupOptions():
"""Construct option parser.
@return: new option parser
@rtype argparse.ArgumentParser
"""
kwargs = dict(
description = "A comic downloader and archiver.",
epilog = Examples,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser = ArgumentParser(**kwargs)
parser.add_argument('-v', '--verbose', action='count', default=0, help='provides verbose output, use multiple times for more verbosity')
parser.add_argument('-n', '--numstrips', action='store', type=int, default=0, help='traverse and retrieve the given number of comic strips; use --all to retrieve all comic strips')
parser.add_argument('-a', '--all', action='store_true', help='traverse and retrieve all comic strips')
parser.add_argument('-c', '--continue', action='store_true', dest='cont', help='traverse and retrieve comic strips until an existing one is found')
parser.add_argument('-b', '--basepath', action='store', default='Comics', help='set the path to create invidivual comic directories in, default is Comics', metavar='PATH')
parser.add_argument('--baseurl', action='store', help='the base URL of your comics directory (for RSS, HTML, etc.); this should correspond to --base-path', metavar='PATH')
parser.add_argument('-l', '--list', action='store_true', help='list available comic modules')
parser.add_argument('--singlelist', action='store_true', help='list available comic modules in a single list')
parser.add_argument('--version', action='store_true', help='display the version number')
parser.add_argument('--vote', action='store_true', help='vote for the selected comics')
parser.add_argument('-m', '--modulehelp', action='store_true', help='display help for comic modules')
parser.add_argument('-t', '--timestamps', action='store_true', help='print timestamps for all output at any info level')
parser.add_argument('-o', '--output', action='append', dest='handler', choices=events.getHandlerNames(), help='sets output handlers for downloaded comics')
parser.add_argument('--adult', action='store_true', help='confirms that you are old enough to view adult content')
# used for development testing prev/next matching
parser.add_argument('--dry-run', action='store_true', help=argparse.SUPPRESS)
# multimatch is only used for development, eg. testing if all comics of a scripted plugin are working
parser.add_argument('--multimatch', action='store_true', help=argparse.SUPPRESS)
parser.add_argument('comic', nargs='*', help='comic module name (including case insensitive substrings)')
try:
import argcomplete
argcomplete.autocomplete(parser)
except ImportError:
pass
return parser
def displayVersion(verbose):
"""Display application name, version, copyright and license."""
print(configuration.App)
print(configuration.Copyright)
print(configuration.Freeware)
print("For support see", configuration.SupportUrl)
if verbose:
# search for updates
from dosagelib.updater import check_update
result, value = check_update()
if result:
if value:
version, url = value
if url is None:
# current version is newer than online version
text = ('Detected local or development version %(currentversion)s. '
'Available version of %(app)s is %(version)s.')
else:
# display update link
text = ('A new version %(version)s of %(app)s is '
'available at %(url)s.')
attrs = dict(version=version, app=configuration.AppName,
url=url, currentversion=configuration.Version)
print(text % attrs)
else:
if value is None:
value = 'invalid update file syntax'
text = ('An error occured while checking for an '
'update of %(app)s: %(error)s.')
attrs = dict(error=value, app=configuration.AppName)
print(text % attrs)
return 0
def setOutputInfo(options):
"""Set global output level and timestamp option."""
out.level = 0
out.level += options.verbose
out.timestamps = options.timestamps
# debug urllib3
#from requests.packages.urllib3 import add_stderr_logger
#add_stderr_logger()
def displayHelp(options):
"""Print help for comic strips."""
errors = 0
try:
for scraperobj in director.getScrapers(options.comic, options.basepath, listing=True):
errors += displayComicHelp(scraperobj)
except ValueError as msg:
out.exception(msg)
return 2
return errors
def displayComicHelp(scraperobj):
"""Print description and help for a comic."""
orig_context = out.context
out.context = getScraperName(scraperobj)
try:
out.info(u"URL: " + scraperobj.url)
if scraperobj.description:
out.info(u"Description: " + scraperobj.description)
if scraperobj.lang:
out.info(u"Language: " + getLangName(scraperobj.lang))
if scraperobj.genres:
out.info(u"Genres: " + ", ".join(scraperobj.genres))
if scraperobj.help:
for line in scraperobj.help.splitlines():
out.info(line)
return 0
except ValueError as msg:
out.exception(msg)
return 1
finally:
out.context = orig_context
def voteComics(options):
"""Vote for comics."""
errors = 0
try:
for scraperobj in director.getScrapers(options.comic, options.basepath, options.adult, options.multimatch):
errors += voteComic(scraperobj)
except ValueError as msg:
out.exception(msg)
errors += 1
return errors
def voteComic(scraperobj):
"""Vote for given comic scraper."""
errors = 0
orig_context = out.context
out.context = getScraperName(scraperobj)
try:
name = scraperobj.getName()
answer = scraperobj.vote()
out.debug(u'Vote answer %r' % answer)
if answer == 'counted':
url = configuration.Url + 'comics/%s.html' % name.replace('/', '_')
out.info(u'Vote submitted. Votes are updated regularly at %s.' % url)
elif answer == 'no':
out.info(u'Vote not submitted - your vote has already been submitted before.')
elif answer == 'noname':
out.warn(u'The comic %s cannot be voted.' % name)
else:
out.warn(u'Error submitting vote parameters: %r' % answer)
except Exception as msg:
out.exception(msg)
errors += 1
finally:
out.context = orig_context
return errors
def run(options):
"""Execute comic commands."""
setOutputInfo(options)
# ensure only one instance of dosage is running
me = singleton.SingleInstance()
if options.version:
return displayVersion(options.verbose)
if options.list:
return doList()
if options.singlelist:
return doList(columnList=False, verbose=options.verbose)
# after this a list of comic strips is needed
if not options.comic:
out.warn(u'No comics specified, bailing out!')
return 1
if options.modulehelp:
return displayHelp(options)
if options.vote:
return voteComics(options)
return director.getComics(options)
def doList(columnList=True, verbose=False):
"""List available comics."""
orig_context = out.context
out.context = u''
try:
page = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
if page:
fd = StringIO(u'')
out.setStream(fd)
out.info(u'Available comic scrapers:')
out.info(u'Comics tagged with [%s] require age confirmation with the --adult option.' % TAG_ADULT)
out.info(u'Non-english comics are tagged with [%s].' % TAG_LANG)
scrapers = sorted(director.getAllScrapers(listing=True), key=lambda s: s.getName())
if columnList:
num, disabled = doColumnList(scrapers)
else:
num, disabled = doSingleList(scrapers, verbose=verbose)
out.info(u'%d supported comics.' % num)
if disabled:
out.info('')
out.info(u'Some comics are disabled, they are tagged with [%s:REASON], where REASON is one of:' % TAG_DISABLED)
for k in disabled:
out.info(u' %-10s %s' % (k, disabled[k]))
if page:
pydoc.pager(fd.getvalue())
return 0
finally:
out.context = orig_context
def doSingleList(scrapers, verbose=False):
"""Get list of scraper names, one per line."""
disabled = {}
for num, scraperobj in enumerate(scrapers):
if verbose:
displayComicHelp(scraperobj)
else:
out.info(getScraperName(scraperobj, reasons=disabled))
return num, disabled
def doColumnList(scrapers):
"""Get list of scraper names with multiple names per line."""
disabled = {}
screenWidth = get_columns(sys.stdout)
# limit name length so at least two columns are there
limit = (screenWidth // 2) - 8
names = [getScraperName(scraperobj, limit=limit, reasons=disabled) for scraperobj in scrapers]
num = len(names)
maxlen = max(len(name) for name in names)
namesPerLine = max(screenWidth // (maxlen + 1), 1)
while names:
out.info(u''.join(name.ljust(maxlen) for name in names[:namesPerLine]))
del names[:namesPerLine]
return num, disabled
TAG_ADULT = "adult"
TAG_LANG = "lang"
TAG_DISABLED = "dis"
def getScraperName(scraperobj, limit=None, reasons=None):
"""Get comic scraper name."""
tags = []
if scraperobj.adult:
tags.append(TAG_ADULT)
if scraperobj.lang != "en":
tags.append("%s:%s" % (TAG_LANG, scraperobj.lang))
disabled = scraperobj.getDisabledReasons()
if disabled:
reasons.update(disabled)
for reason in disabled:
tags.append("%s:%s" % (TAG_DISABLED, reason))
if tags:
suffix = " [" + ", ".join(tags) + "]"
else:
suffix = ""
name = scraperobj.getName()
if limit is not None:
name = strlimit(name, limit)
return name + suffix
def main():
"""Parse options and execute commands."""
try:
options = setupOptions().parse_args()
options.basepath = os.path.expanduser(options.basepath)
res = run(options)
except KeyboardInterrupt:
print("Aborted.")
res = 1
except Exception:
internal_error()
res = 2
return res
def profile():
"""Profile the loading of all scrapers."""
import cProfile
cProfile.run("scraper.get_scraperclasses()", "dosage.prof")
def viewprof():
"""View profile stats."""
import pstats
stats = pstats.Stats("dosage.prof")
stats.strip_dirs().sort_stats("cumulative").print_stats(100)
if __name__ == '__main__':
sys.exit(main())
#profile()
#viewprof()