forked from wummel/dosage
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dosage
executable file
·330 lines (290 loc) · 11.6 KB
/
dosage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
#!/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):
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(), key=lambda s: s.getName())
if columnList:
num = doColumnList(scrapers)
else:
num = doSingleList(scrapers, verbose=verbose)
out.info(u'%d supported comics.' % num)
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."""
for num, scraperobj in enumerate(scrapers):
if verbose:
displayComicHelp(scraperobj)
else:
out.info(getScraperName(scraperobj))
return num
def doColumnList(scrapers):
"""Get list of scraper names with multiple names per line."""
screenWidth = get_columns(sys.stdout)
# limit name length so at least two columns are there
limit = (screenWidth // 2) - 8
names = [getScraperName(scraperobj, limit=limit) 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
TAG_ADULT = "adult"
TAG_LANG = "lang"
def getScraperName(scraperobj, limit=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))
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()