Completed
Push — master ( 28863a...fcfb92 )
by De
25s
created

comics.py (1 issue)

1
#! /usr/bin/python3
2
# vim: set expandtab tabstop=4 shiftwidth=4 :
3
"""Module to retrieve webcomics"""
4
5
from comic_abstract import GenericComic, get_date_for_comic
6
import re
7
from datetime import date, timedelta
8
import datetime
9
from urlfunctions import get_soup_at_url, urljoin_wrapper,\
10
    convert_iri_to_plain_ascii_uri, load_json_at_url, urlopen_wrapper
11
import json
12
import locale
13
import urllib
14
15
DEFAULT_LOCAL = 'en_GB.UTF-8'
16
17
18
class Xkcd(GenericComic):
19
    """Class to retrieve Xkcd comics."""
20
    name = 'xkcd'
21
    long_name = 'xkcd'
22
    url = 'http://xkcd.com'
23
24
    @classmethod
25
    def get_next_comic(cls, last_comic):
26
        """Generator to get the next comic. Implementation of GenericComic's abstract method."""
27
        json_url = urljoin_wrapper(cls.url, 'info.0.json')
28
        first_num = last_comic['num'] if last_comic else 0
29
        last_num = load_json_at_url(json_url)['num']
30
31
        for num in range(first_num + 1, last_num + 1):
32
            comic = cls.get_comic_info(num)
33
            if comic is not None:
34
                yield comic
35
36
    @classmethod
37
    def get_comic_info(cls, num):
38
        """Get information about a particular comics."""
39
        if num == 404:
40
            return None
41
        json_url = urljoin_wrapper(cls.url, '%d/info.0.json' % num)
42
        comic_json = load_json_at_url(json_url)
43
        assert comic_json['num'] == num, json_url
44
        return {
45
            'json_url': json_url,
46
            'num': num,
47
            'url': urljoin_wrapper(cls.url, str(num)),
48
            'prefix': '%d-' % num,
49
            'img': [comic_json['img']],
50
            'day': int(comic_json['day']),
51
            'month': int(comic_json['month']),
52
            'year': int(comic_json['year']),
53
            'link': comic_json['link'],
54
            'news': comic_json['news'],
55
            'safe_title': comic_json['safe_title'],
56
            'transcript': comic_json['transcript'],
57
            'alt': comic_json['alt'],
58
            'title': comic_json['title'],
59
        }
60
61
62
# Helper functions corresponding to get_url_from_link/get_url_from_archive_element
63
64
65
@classmethod
66
def get_href(cls, link):
67
    """Implementation of get_url_from_link/get_url_from_archive_element."""
68
    return link['href']
69
70
71
@classmethod
72
def join_cls_url_to_href(cls, link):
73
    """Implementation of get_url_from_link/get_url_from_archive_element."""
74
    return urljoin_wrapper(cls.url, link['href'])
75
76
77
class GenericNavigableComic(GenericComic):
78
    """Generic class for "navigable" comics : with first/next arrows.
79
80
    This class applies to comic where previous and next comics can be
81
    accessed from a given comic. Once given a starting point (either
82
    the first comic or the last comic retrieved), it will handle the
83
    navigation, the retrieval of the soup object and the setting of
84
    the 'url' attribute on retrieved comics. This limits a lot the
85
    amount of boilerplate code in the different implementation classes.
86
87
    The method `get_next_comic` methods is implemented in terms of new
88
    more specialized methods to be implemented/overridden:
89
        - get_first_comic_link
90
        - get_navi_link
91
        - get_comic_info
92
        - get_url_from_link
93
    """
94
    _categories = ('NAVIGABLE', )
95
96
    @classmethod
97
    def get_first_comic_link(cls):
98
        """Get link to first comics.
99
100
        Sometimes this can be retrieved of any comic page, sometimes on
101
        the archive page, sometimes it doesn't exist at all and one has
102
        to iterate backward to find it before hardcoding the result found.
103
        """
104
        raise NotImplementedError
105
106
    @classmethod
107
    def get_navi_link(cls, last_soup, next_):
108
        """Get link to next (or previous - for dev purposes) comic."""
109
        raise NotImplementedError
110
111
    @classmethod
112
    def get_comic_info(cls, soup, link):
113
        """Get information about a particular comics."""
114
        raise NotImplementedError
115
116
    @classmethod
117
    def get_url_from_link(cls, link):
118
        """Get url corresponding to a link. Default implementation is similar to get_href."""
119
        return link['href']
120
121
    @classmethod
122
    def get_next_link(cls, last_soup):
123
        """Get link to next comic."""
124
        link = cls.get_navi_link(last_soup, True)
125
        cls.log("Next link is %s" % link)
126
        return link
127
128
    @classmethod
129
    def get_prev_link(cls, last_soup):
130
        """Get link to previous comic."""
131
        link = cls.get_navi_link(last_soup, False)
132
        cls.log("Prev link is %s" % link)
133
        return link
134
135
    @classmethod
136
    def get_next_comic(cls, last_comic):
137
        """Generic implementation of get_next_comic for navigable comics."""
138
        url = last_comic['url'] if last_comic else None
139
        cls.log("starting 'get_next_comic' from %s" % url)
140
        next_comic = \
141
            cls.get_next_link(get_soup_at_url(url)) \
142
            if url else \
143
            cls.get_first_comic_link()
144
        cls.log("next/first comic will be %s (url is %s)" % (str(next_comic), url))
145
        # cls.check_navigation(url)
146
        while next_comic:
147
            prev_url, url = url, cls.get_url_from_link(next_comic)
148
            if prev_url == url:
149
                cls.log("got same url %s" % url)
150
                break
151
            cls.log("about to get %s (%s)" % (url, str(next_comic)))
152
            soup = get_soup_at_url(url)
153
            comic = cls.get_comic_info(soup, next_comic)
154
            if comic is not None:
155
                assert 'url' not in comic
156
                comic['url'] = url
157
                yield comic
158
            next_comic = cls.get_next_link(soup)
159
            cls.log("next comic will be %s" % str(next_comic))
160
161
    @classmethod
162
    def check_first_link(cls):
163
        """Check that navigation to first comic seems to be working - for dev purposes."""
164
        cls.log("about to check first link")
165
        ok = True
166
        firstlink = cls.get_first_comic_link()
167
        if firstlink is None:
168
            print("From %s : no first link" % cls.url)
169
            ok = False
170
        else:
171
            firsturl = cls.get_url_from_link(firstlink)
172
            try:
173
                get_soup_at_url(firsturl)
174
            except urllib.error.HTTPError:
175
                print("From %s : invalid first url" % cls.url)
176
                ok = False
177
        cls.log("checked first link -> returned %d" % ok)
178
        return ok
179
180
    @classmethod
181
    def check_prev_next_links(cls, url):
182
        """Check that navigation to prev/next from a given URL seems to be working - for dev purposes."""
183
        cls.log("about to check prev/next from %s" % url)
184
        ok = True
185
        if url is None:
186
            prevlink, nextlink = None, None
187
        else:
188
            soup = get_soup_at_url(url)
189
            prevlink, nextlink = cls.get_prev_link(soup), cls.get_next_link(soup)
190
        if prevlink is None and nextlink is None:
191
            print("From %s : no previous nor next" % url)
192
            ok = False
193
        else:
194
            if prevlink:
195
                prevurl = cls.get_url_from_link(prevlink)
196
                prevsoup = get_soup_at_url(prevurl)
197
                prevnextlink = cls.get_next_link(prevsoup)
198
                prevnext = cls.get_url_from_link(prevnextlink) if prevnextlink is not None else "NO URL"
199
                if prevnext != url:
200
                    print("From %s, going backward then forward leads to %s" % (url, prevnext))
201
                    ok = False
202
            if nextlink:
203
                nexturl = cls.get_url_from_link(nextlink)
204
                if nexturl != url:
205
                    nextsoup = get_soup_at_url(nexturl)
206
                    nextprevlink = cls.get_prev_link(nextsoup)
207
                    nextprev = cls.get_url_from_link(nextprevlink) if nextprevlink is not None else "NO URL"
208
                    if nextprev != url:
209
                        print("From %s, going forward then backward leads to %s" % (url, nextprev))
210
                        ok = False
211
        cls.log("checked prev/next from %s -> returned %d" % (url, ok))
212
        return ok
213
214
    @classmethod
215
    def check_navigation(cls, url):
216
        """Check that navigation functions seem to be working - for dev purposes."""
217
        cls.log("about to check navigation from %s" % url)
218
        first = cls.check_first_link()
219
        prevnext = cls.check_prev_next_links(url)
220
        ok = first and prevnext
221
        cls.log("checked navigation from %s -> returned %d" % (url, ok))
222
        return ok
223
224
225
class GenericListableComic(GenericComic):
226
    """Generic class for "listable" comics : with a list of comics (aka 'archive')
227
228
    The method `get_next_comic` methods is implemented in terms of new
229
    more specialized methods to be implemented/overridden:
230
        - get_archive_elements
231
        - get_url_from_archive_element
232
        - get_comic_info
233
    """
234
    _categories = ('LISTABLE', )
235
236
    @classmethod
237
    def get_archive_elements(cls):
238
        """Get the archive elements (iterable)."""
239
        raise NotImplementedError
240
241
    @classmethod
242
    def get_url_from_archive_element(cls, archive_elt):
243
        """Get url corresponding to an archive element."""
244
        raise NotImplementedError
245
246
    @classmethod
247
    def get_comic_info(cls, soup, archive_elt):
248
        """Get information about a particular comics."""
249
        raise NotImplementedError
250
251
    @classmethod
252
    def get_next_comic(cls, last_comic):
253
        """Generic implementation of get_next_comic for listable comics."""
254
        waiting_for_url = last_comic['url'] if last_comic else None
255
        archive_elts = list(cls.get_archive_elements())
256
        for archive_elt in archive_elts:
257
            url = cls.get_url_from_archive_element(archive_elt)
258
            cls.log("considering %s" % url)
259
            if waiting_for_url is None:
260
                cls.log("about to get %s (%s)" % (url, str(archive_elt)))
261
                soup = get_soup_at_url(url)
262
                comic = cls.get_comic_info(soup, archive_elt)
263
                if comic is not None:
264
                    assert 'url' not in comic
265
                    comic['url'] = url
266
                    yield comic
267
            elif waiting_for_url == url:
268
                waiting_for_url = None
269
        if waiting_for_url is not None:
270
            print("Did not find %s in the %d comics: there might be a problem" %
271
                  (waiting_for_url, len(archive_elts)))
272
273
# Helper functions corresponding to get_first_comic_link/get_navi_link
274
275
276
@classmethod
277
def get_link_rel_next(cls, last_soup, next_):
278
    """Implementation of get_navi_link."""
279
    return last_soup.find('link', rel='next' if next_ else 'prev')
280
281
282
@classmethod
283
def get_a_rel_next(cls, last_soup, next_):
284
    """Implementation of get_navi_link."""
285
    return last_soup.find('a', rel='next' if next_ else 'prev')
286
287
288
@classmethod
289
def get_a_navi_navinext(cls, last_soup, next_):
290
    """Implementation of get_navi_link."""
291
    # ComicPress (WordPress plugin)
292
    return last_soup.find('a', class_='navi navi-next' if next_ else 'navi navi-prev')
293
294
295
@classmethod
296
def get_a_navi_comicnavnext_navinext(cls, last_soup, next_):
297
    """Implementation of get_navi_link."""
298
    return last_soup.find('a', class_='navi comic-nav-next navi-next' if next_ else 'navi comic-nav-previous navi-prev')
299
300
301
@classmethod
302
def get_a_comicnavbase_comicnavnext(cls, last_soup, next_):
303
    """Implementation of get_navi_link."""
304
    return last_soup.find('a', class_='comic-nav-base comic-nav-next' if next_ else 'comic-nav-base comic-nav-previous')
305
306
307
@classmethod
308
def get_a_navi_navifirst(cls):
309
    """Implementation of get_first_comic_link."""
310
    # ComicPress (WordPress plugin)
311
    return get_soup_at_url(cls.url).find('a', class_='navi navi-first')
312
313
314
@classmethod
315
def get_div_navfirst_a(cls):
316
    """Implementation of get_first_comic_link."""
317
    return get_soup_at_url(cls.url).find('div', class_="nav-first").find('a')
318
319
320
@classmethod
321
def get_a_comicnavbase_comicnavfirst(cls):
322
    """Implementation of get_first_comic_link."""
323
    return get_soup_at_url(cls.url).find('a', class_='comic-nav-base comic-nav-first')
324
325
326
@classmethod
327
def simulate_first_link(cls):
328
    """Implementation of get_first_comic_link creating a link-like object from
329
    an URL provided by the class.
330
331
    Note: The first URL can easily be found using :
332
    `get_first_comic_link = navigate_to_first_comic`.
333
    """
334
    return {'href': cls.first_url}
335
336
337
@classmethod
338
def navigate_to_first_comic(cls):
339
    """Implementation of get_first_comic_link navigating from a user provided
340
    URL to the first comic.
341
342
    Sometimes, the first comic cannot be reached directly so to start
343
    from the first comic one has to go to the previous comic until
344
    there is no previous comics. Once this URL is reached, it
345
    is better to hardcode it but for development purposes, it
346
    is convenient to have an automatic way to find it.
347
348
    Then, the URL found can easily be used via `simulate_first_link`.
349
    """
350
    try:
351
        url = cls.first_url
352
    except AttributeError:
353
        url = input("Get starting URL: ")
354
    print(url)
355
    comic = cls.get_prev_link(get_soup_at_url(url))
356
    while comic:
357
        url = cls.get_url_from_link(comic)
358
        print(url)
359
        comic = cls.get_prev_link(get_soup_at_url(url))
360
    return {'href': url}
361
362
363
class GenericEmptyComic(GenericComic):
364
    """Generic class for comics where nothing is to be done.
365
366
    It can be useful to deactivate temporarily comics that do not work
367
    properly by replacing `def MyComic(GenericWhateverComic)` with
368
    `def MyComic(GenericEmptyComic, GenericWhateverComic)`."""
369
    _categories = ('EMPTY', )
370
371
    @classmethod
372
    def get_next_comic(cls, last_comic):
373
        """Implementation of get_next_comic returning no comics."""
374
        cls.log("comic is considered as empty - returning no comic")
375
        return []
376
377
378
class GenericComicNotWorking(GenericEmptyComic):
379
    """Subclass of GenericEmptyComic used when comic is not working.
380
381
    This is more explicit than GenericEmptyComic as it hilights that
382
    only the implementation is not working and it can be fixed."""
383
    _categories = ('NOTWORKING', )
384
385
386
class GenericUnavailableComic(GenericEmptyComic):
387
    """Subclass of GenericEmptyComic used when a comic is not available.
388
389
    This is more explicit than GenericEmptyComic as it hilights that
390
    the source of the comic is not available but we expect it to be back
391
    soonish. See also GenericDeletedComic."""
392
    _categories = ('UNAVAILABLE', )
393
394
395
class GenericDeletedComic(GenericEmptyComic):
396
    """Subclass of GenericEmptyComic used when a comic does not exist anymore.
397
398
    This is more explicit than GenericEmptyComic as it hilights that
399
    the source of the comic does not exist anymore and it probably cannot
400
    be fixed. Corresponding classes are kept as we can still use the
401
    downloaded data. See also GenericUnavailableComic."""
402
    _categories = ('DELETED', )
403
404
405 View Code Duplication
class ExtraFabulousComics(GenericNavigableComic):
406
    """Class to retrieve Extra Fabulous Comics."""
407
    # Also on https://extrafabulouscomics.tumblr.com
408
    name = 'efc'
409
    long_name = 'Extra Fabulous Comics'
410
    url = 'http://extrafabulouscomics.com'
411
    _categories = ('EFC', )
412
    get_navi_link = get_link_rel_next
413
    get_first_comic_link = simulate_first_link
414
    first_url = 'http://extrafabulouscomics.com/comic/buttfly/'
415
416
    @classmethod
417
    def get_comic_info(cls, soup, link):
418
        """Get information about a particular comics."""
419
        img_src_re = re.compile('^%s/wp-content/uploads/' % cls.url)
420
        imgs = soup.find_all('img', src=img_src_re)
421
        title = soup.find('meta', property='og:title')['content']
422
        date_str = soup.find('meta', property='article:published_time')['content'][:10]
423
        day = string_to_date(date_str, "%Y-%m-%d")
424
        return {
425
            'title': title,
426
            'img': [i['src'] for i in imgs],
427
            'month': day.month,
428
            'year': day.year,
429
            'day': day.day,
430
            'prefix': title + '-'
431
        }
432
433
434 View Code Duplication
class GenericLeMondeBlog(GenericNavigableComic):
435
    """Generic class to retrieve comics from Le Monde blogs."""
436
    _categories = ('LEMONDE', 'FRANCAIS')
437
    get_navi_link = get_link_rel_next
438
    get_first_comic_link = simulate_first_link
439
    first_url = NotImplemented
440
441
    @classmethod
442
    def get_comic_info(cls, soup, link):
443
        """Get information about a particular comics."""
444
        url2 = soup.find('link', rel='shortlink')['href']
445
        title = soup.find('meta', property='og:title')['content']
446
        date_str = soup.find("span", class_="entry-date").string
447
        day = string_to_date(date_str, "%d %B %Y", "fr_FR.utf8")
448
        imgs = soup.find_all('meta', property='og:image')
449
        return {
450
            'title': title,
451
            'url2': url2,
452
            'img': [convert_iri_to_plain_ascii_uri(i['content']) for i in imgs],
453
            'month': day.month,
454
            'year': day.year,
455
            'day': day.day,
456
        }
457
458
459
class ZepWorld(GenericLeMondeBlog):
460
    """Class to retrieve Zep World comics."""
461
    name = "zep"
462
    long_name = "Zep World"
463
    url = "http://zepworld.blog.lemonde.fr"
464
    first_url = "http://zepworld.blog.lemonde.fr/2014/10/31/bientot-le-blog-de-zep/"
465
466
467
class Vidberg(GenericLeMondeBlog):
468
    """Class to retrieve Vidberg comics."""
469
    name = 'vidberg'
470
    long_name = "Vidberg - l'actu en patates"
471
    url = "http://vidberg.blog.lemonde.fr"
472
    # Not the first but I didn't find an efficient way to retrieve it
473
    first_url = "http://vidberg.blog.lemonde.fr/2012/02/09/revue-de-campagne-la-campagne-du-modem-semballe/"
474
475
476
class Plantu(GenericLeMondeBlog):
477
    """Class to retrieve Plantu comics."""
478
    name = 'plantu'
479
    long_name = "Plantu"
480
    url = "http://plantu.blog.lemonde.fr"
481
    first_url = "http://plantu.blog.lemonde.fr/2014/10/28/stress-test-a-bruxelles/"
482
483
484
class XavierGorce(GenericLeMondeBlog):
485
    """Class to retrieve Xavier Gorce comics."""
486
    name = 'gorce'
487
    long_name = "Xavier Gorce"
488
    url = "http://xaviergorce.blog.lemonde.fr"
489
    first_url = "http://xaviergorce.blog.lemonde.fr/2015/01/09/distinction/"
490
491
492
class CartooningForPeace(GenericLeMondeBlog):
493
    """Class to retrieve Cartooning For Peace comics."""
494
    name = 'forpeace'
495
    long_name = "Cartooning For Peace"
496
    url = "http://cartooningforpeace.blog.lemonde.fr"
497
    first_url = "http://cartooningforpeace.blog.lemonde.fr/2014/12/15/bado/"
498
499
500
class Aurel(GenericLeMondeBlog):
501
    """Class to retrieve Aurel comics."""
502
    name = 'aurel'
503
    long_name = "Aurel"
504
    url = "http://aurel.blog.lemonde.fr"
505
    first_url = "http://aurel.blog.lemonde.fr/2014/09/29/le-senat-repasse-a-droite/"
506
507
508
class LesCulottees(GenericLeMondeBlog):
509
    """Class to retrieve Les Culottees comics."""
510
    name = 'culottees'
511
    long_name = 'Les Culottees'
512
    url = "http://lesculottees.blog.lemonde.fr"
513
    first_url = "http://lesculottees.blog.lemonde.fr/2016/01/11/clementine-delait-femme-a-barbe/"
514
515
516
class UneAnneeAuLycee(GenericLeMondeBlog):
517
    """Class to retrieve Une Annee Au Lycee comics."""
518
    name = 'lycee'
519
    long_name = 'Une Annee au Lycee'
520
    url = 'http://uneanneeaulycee.blog.lemonde.fr'
521
    first_url = "http://uneanneeaulycee.blog.lemonde.fr/2016/06/13/la-semaine-du-bac-est-arrivee/"
522
523
524 View Code Duplication
class Rall(GenericComicNotWorking, GenericNavigableComic):
525
    """Class to retrieve Ted Rall comics."""
526
    # Also on http://www.gocomics.com/tedrall
527
    name = 'rall'
528
    long_name = "Ted Rall"
529
    url = "http://rall.com/comic"
530
    _categories = ('RALL', )
531
    get_navi_link = get_link_rel_next
532
    get_first_comic_link = simulate_first_link
533
    # Not the first but I didn't find an efficient way to retrieve it
534
    first_url = "http://rall.com/2014/01/30/los-angeles-times-cartoon-well-miss-those-california-flowers"
535
536
    @classmethod
537
    def get_comic_info(cls, soup, link):
538
        """Get information about a particular comics."""
539
        title = soup.find('meta', property='og:title')['content']
540
        author = soup.find("span", class_="author vcard").find("a").string
541
        date_str = soup.find("span", class_="entry-date").string
542
        day = string_to_date(date_str, "%B %d, %Y")
543
        desc = soup.find('meta', property='og:description')['content']
544
        imgs = soup.find('div', class_='entry-content').find_all('img')
545
        imgs = imgs[:-7]  # remove social media buttons
546
        return {
547
            'title': title,
548
            'author': author,
549
            'month': day.month,
550
            'year': day.year,
551
            'day': day.day,
552
            'description': desc,
553
            'img': [i['src'] for i in imgs],
554
        }
555
556
557
class Dilem(GenericNavigableComic):
558
    """Class to retrieve Ali Dilem comics."""
559
    name = 'dilem'
560
    long_name = 'Ali Dilem'
561
    url = 'http://information.tv5monde.com/dilem'
562
    _categories = ('FRANCAIS', )
563
    get_url_from_link = join_cls_url_to_href
564
    get_first_comic_link = simulate_first_link
565
    first_url = "http://information.tv5monde.com/dilem/2004-06-26"
566
567
    @classmethod
568
    def get_navi_link(cls, last_soup, next_):
569
        """Get link to next or previous comic."""
570
        # prev is next / next is prev
571
        li = last_soup.find('li', class_='prev' if next_ else 'next')
572
        return li.find('a') if li else None
573
574 View Code Duplication
    @classmethod
575
    def get_comic_info(cls, soup, link):
576
        """Get information about a particular comics."""
577
        short_url = soup.find('link', rel='shortlink')['href']
578
        title = soup.find('meta', attrs={'name': 'twitter:title'})['content']
579
        imgs = soup.find_all('meta', property='og:image')
580
        date_str = soup.find('span', property='dc:date')['content']
581
        date_str = date_str[:10]
582
        day = string_to_date(date_str, "%Y-%m-%d")
583
        return {
584
            'short_url': short_url,
585
            'title': title,
586
            'img': [i['content'] for i in imgs],
587
            'day': day.day,
588
            'month': day.month,
589
            'year': day.year,
590
        }
591
592
593
class SpaceAvalanche(GenericNavigableComic):
594
    """Class to retrieve Space Avalanche comics."""
595
    name = 'avalanche'
596
    long_name = 'Space Avalanche'
597
    url = 'http://www.spaceavalanche.com'
598
    get_navi_link = get_link_rel_next
599
600
    @classmethod
601
    def get_first_comic_link(cls):
602
        """Get link to first comics."""
603
        return {'href': "http://www.spaceavalanche.com/2009/02/02/irish-sea/", 'title': "Irish Sea"}
604
605
    @classmethod
606
    def get_comic_info(cls, soup, link):
607
        """Get information about a particular comics."""
608
        url_date_re = re.compile('.*/([0-9]*)/([0-9]*)/([0-9]*)/.*$')
609
        title = link['title']
610
        url = cls.get_url_from_link(link)
611
        year, month, day = [int(s)
612
                            for s in url_date_re.match(url).groups()]
613
        imgs = soup.find("div", class_="entry").find_all("img")
614
        return {
615
            'title': title,
616
            'day': day,
617
            'month': month,
618
            'year': year,
619
            'img': [i['src'] for i in imgs],
620
        }
621
622
623
class ZenPencils(GenericNavigableComic):
624
    """Class to retrieve ZenPencils comics."""
625
    # Also on http://zenpencils.tumblr.com
626
    # Also on http://www.gocomics.com/zen-pencils
627
    name = 'zenpencils'
628
    long_name = 'Zen Pencils'
629
    url = 'http://zenpencils.com'
630
    _categories = ('ZENPENCILS', )
631
    get_navi_link = get_link_rel_next
632
    get_first_comic_link = simulate_first_link
633
    first_url = "http://zenpencils.com/comic/1-ralph-waldo-emerson-make-them-cry/"
634
635
    @classmethod
636
    def get_comic_info(cls, soup, link):
637
        """Get information about a particular comics."""
638
        imgs = soup.find('div', id='comic').find_all('img')
639
        # imgs2 = soup.find_all('meta', property='og:image')
640
        post = soup.find('div', class_='post-content')
641
        author = post.find("span", class_="post-author").find("a").string
642
        title = soup.find('h2', class_='post-title').string
643
        date_str = post.find('span', class_='post-date').string
644
        day = string_to_date(date_str, "%B %d, %Y")
645
        assert imgs
646
        assert all(i['alt'] == i['title'] for i in imgs)
647
        assert all(i['alt'] in (title, "") for i in imgs)
648
        return {
649
            'title': title,
650
            'author': author,
651
            'day': day.day,
652
            'month': day.month,
653
            'year': day.year,
654
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
655
        }
656
657
658
class ItsTheTie(GenericDeletedComic, GenericNavigableComic):
659
    """Class to retrieve It's the tie comics."""
660
    # Also on http://itsthetie.tumblr.com
661
    # Also on https://tapastic.com/series/itsthetie
662
    name = 'tie'
663
    long_name = "It's the tie"
664
    url = "http://itsthetie.com"
665
    _categories = ('TIE', )
666
    get_first_comic_link = get_div_navfirst_a
667
    get_navi_link = get_a_rel_next
668
669
    @classmethod
670
    def get_comic_info(cls, soup, link):
671
        """Get information about a particular comics."""
672
        title = soup.find('h1', class_='comic-title').find('a').string
673
        date_str = soup.find('header', class_='comic-meta entry-meta').find('a').string
674
        day = string_to_date(date_str, "%B %d, %Y")
675
        # Bonus images may or may not be in meta og:image.
676
        imgs = soup.find_all('meta', property='og:image')
677
        imgs_src = [i['content'] for i in imgs]
678
        bonus = soup.find_all('img', attrs={'data-oversrc': True})
679
        bonus_src = [b['data-oversrc'] for b in bonus]
680
        all_imgs_src = imgs_src + [s for s in bonus_src if s not in imgs_src]
681
        all_imgs_src = [s for s in all_imgs_src if not s.endswith("/2016/01/bonus-panel.png")]
682
        tag_meta = soup.find('meta', property='article:tag')
683
        tags = tag_meta['content'] if tag_meta else ""
684
        return {
685
            'title': title,
686
            'month': day.month,
687
            'year': day.year,
688
            'day': day.day,
689
            'img': all_imgs_src,
690
            'tags': tags,
691
        }
692
693
694 View Code Duplication
class PenelopeBagieu(GenericNavigableComic):
695
    """Class to retrieve comics from Penelope Bagieu's blog."""
696
    name = 'bagieu'
697
    long_name = 'Ma vie est tout a fait fascinante (Bagieu)'
698
    url = 'http://www.penelope-jolicoeur.com'
699
    _categories = ('FRANCAIS', )
700
    get_navi_link = get_link_rel_next
701
    get_first_comic_link = simulate_first_link
702
    first_url = 'http://www.penelope-jolicoeur.com/2007/02/ma-vie-mon-oeuv.html'
703
704
    @classmethod
705
    def get_comic_info(cls, soup, link):
706
        """Get information about a particular comics."""
707
        date_str = soup.find('h2', class_='date-header').string
708
        day = string_to_date(date_str, "%A %d %B %Y", "fr_FR.utf8")
709
        imgs = soup.find('div', class_='entry-body').find_all('img')
710
        title = soup.find('h3', class_='entry-header').string
711
        return {
712
            'title': title,
713
            'img': [i['src'] for i in imgs],
714
            'month': day.month,
715
            'year': day.year,
716
            'day': day.day,
717
        }
718
719
720 View Code Duplication
class OneOneOneOneComic(GenericComicNotWorking, GenericNavigableComic):
721
    """Class to retrieve 1111 Comics."""
722
    # Also on http://comics1111.tumblr.com
723
    # Also on https://tapastic.com/series/1111-Comics
724
    name = '1111'
725
    long_name = '1111 Comics'
726
    url = 'http://www.1111comics.me'
727
    _categories = ('ONEONEONEONE', )
728
    get_first_comic_link = get_div_navfirst_a
729
    get_navi_link = get_link_rel_next
730
731
    @classmethod
732
    def get_comic_info(cls, soup, link):
733
        """Get information about a particular comics."""
734
        title = soup.find('h1', class_='comic-title').find('a').string
735
        date_str = soup.find('header', class_='comic-meta entry-meta').find('a').string
736
        day = string_to_date(date_str, "%B %d, %Y")
737
        imgs = soup.find_all('meta', property='og:image')
738
        return {
739
            'title': title,
740
            'month': day.month,
741
            'year': day.year,
742
            'day': day.day,
743
            'img': [i['content'] for i in imgs],
744
        }
745
746
747 View Code Duplication
class AngryAtNothing(GenericDeletedComic, GenericNavigableComic):
748
    """Class to retrieve Angry at Nothing comics."""
749
    # Also on http://tapastic.com/series/Comics-yeah-definitely-comics-
750
    # Also on http://angryatnothing.tumblr.com
751
    name = 'angry'
752
    long_name = 'Angry At Nothing'
753
    url = 'http://www.angryatnothing.net'
754
    get_first_comic_link = get_div_navfirst_a
755
    get_navi_link = get_a_rel_next
756
757
    @classmethod
758
    def get_comic_info(cls, soup, link):
759
        """Get information about a particular comics."""
760
        title = soup.find('h1', class_='comic-title').find('a').string
761
        date_str = soup.find('header', class_='comic-meta entry-meta').find('a').string
762
        day = string_to_date(date_str, "%B %d, %Y")
763
        imgs = soup.find_all('meta', property='og:image')
764
        return {
765
            'title': title,
766
            'month': day.month,
767
            'year': day.year,
768
            'day': day.day,
769
            'img': [i['content'] for i in imgs],
770
        }
771
772
773
class NeDroid(GenericNavigableComic):
774
    """Class to retrieve NeDroid comics."""
775
    name = 'nedroid'
776
    long_name = 'NeDroid'
777
    url = 'http://nedroid.com'
778
    get_first_comic_link = get_div_navfirst_a
779
    get_navi_link = get_link_rel_next
780
    get_url_from_link = join_cls_url_to_href
781
782 View Code Duplication
    @classmethod
783
    def get_comic_info(cls, soup, link):
784
        """Get information about a particular comics."""
785
        short_url_re = re.compile('^%s/\\?p=([0-9]*)' % cls.url)
786
        short_url = cls.get_url_from_link(soup.find('link', rel='shortlink'))
787
        num = int(short_url_re.match(short_url).groups()[0])
788
        imgs = soup.find('div', id='comic').find_all('img')
789
        assert len(imgs) == 1, imgs
790
        title = imgs[0]['alt']
791
        title2 = imgs[0]['title']
792
        return {
793
            'short_url': short_url,
794
            'title': title,
795
            'title2': title2,
796
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
797
            'num': num,
798
        }
799
800
801
class Garfield(GenericNavigableComic):
802
    """Class to retrieve Garfield comics."""
803
    # Also on http://www.gocomics.com/garfield
804
    name = 'garfield'
805
    long_name = 'Garfield'
806
    url = 'https://garfield.com'
807
    _categories = ('GARFIELD', )
808
    get_first_comic_link = simulate_first_link
809
    first_url = 'https://garfield.com/comic/1978/06/19'
810
811
    @classmethod
812
    def get_navi_link(cls, last_soup, next_):
813
        """Get link to next or previous comic."""
814
        return last_soup.find('a', class_='comic-arrow-right' if next_ else 'comic-arrow-left')
815
816
    @classmethod
817
    def get_comic_info(cls, soup, link):
818
        """Get information about a particular comics."""
819
        url = cls.get_url_from_link(link)
820
        date_re = re.compile('^%s/comic/([0-9]*)/([0-9]*)/([0-9]*)' % cls.url)
821
        year, month, day = [int(s) for s in date_re.match(url).groups()]
822
        imgs = soup.find('div', class_='comic-display').find_all('img', class_='img-responsive')
823
        return {
824
            'month': month,
825
            'year': year,
826
            'day': day,
827
            'img': [i['src'] for i in imgs],
828
        }
829
830
831 View Code Duplication
class Dilbert(GenericNavigableComic):
832
    """Class to retrieve Dilbert comics."""
833
    # Also on http://www.gocomics.com/dilbert-classics
834
    name = 'dilbert'
835
    long_name = 'Dilbert'
836
    url = 'http://dilbert.com'
837
    get_url_from_link = join_cls_url_to_href
838
    get_first_comic_link = simulate_first_link
839
    first_url = 'http://dilbert.com/strip/1989-04-16'
840
841
    @classmethod
842
    def get_navi_link(cls, last_soup, next_):
843
        """Get link to next or previous comic."""
844
        link = last_soup.find('div', class_='nav-comic nav-right' if next_ else 'nav-comic nav-left')
845
        return link.find('a') if link else None
846
847
    @classmethod
848
    def get_comic_info(cls, soup, link):
849
        """Get information about a particular comics."""
850
        title = soup.find('meta', property='og:title')['content']
851
        imgs = soup.find_all('meta', property='og:image')
852
        desc = soup.find('meta', property='og:description')['content']
853
        date_str = soup.find('meta', property='article:publish_date')['content']
854
        day = string_to_date(date_str, "%B %d, %Y")
855
        author = soup.find('meta', property='article:author')['content']
856
        tags = soup.find('meta', property='article:tag')['content']
857
        return {
858
            'title': title,
859
            'description': desc,
860
            'img': [i['content'] for i in imgs],
861
            'author': author,
862
            'tags': tags,
863
            'day': day.day,
864
            'month': day.month,
865
            'year': day.year
866
        }
867
868
869
class VictimsOfCircumsolar(GenericDeletedComic, GenericNavigableComic):
870
    """Class to retrieve VictimsOfCircumsolar comics."""
871
    # Also on https://victimsofcomics.tumblr.com
872
    name = 'circumsolar'
873
    long_name = 'Victims Of Circumsolar'
874
    url = 'http://www.victimsofcircumsolar.com'
875
    get_navi_link = get_a_navi_comicnavnext_navinext
876
    get_first_comic_link = simulate_first_link
877
    first_url = 'http://www.victimsofcircumsolar.com/comic/modern-addiction'
878
879
    @classmethod
880
    def get_comic_info(cls, soup, link):
881
        """Get information about a particular comics."""
882
        # Date is on the archive page
883
        title = soup.find_all('meta', property='og:title')[-1]['content']
884
        desc = soup.find_all('meta', property='og:description')[-1]['content']
885
        imgs = soup.find('div', id='comic').find_all('img')
886
        assert all(i['title'] == i['alt'] == title for i in imgs)
887
        return {
888
            'title': title,
889
            'description': desc,
890
            'img': [i['src'] for i in imgs],
891
        }
892
893
894
class ThreeWordPhrase(GenericNavigableComic):
895
    """Class to retrieve Three Word Phrase comics."""
896
    # Also on http://www.threewordphrase.tumblr.com
897
    name = 'threeword'
898
    long_name = 'Three Word Phrase'
899
    url = 'http://threewordphrase.com'
900
    get_url_from_link = join_cls_url_to_href
901
902
    @classmethod
903
    def get_first_comic_link(cls):
904
        """Get link to first comics."""
905
        return get_soup_at_url(cls.url).find('img', src='/firstlink.gif').parent
906
907
    @classmethod
908
    def get_navi_link(cls, last_soup, next_):
909
        """Get link to next or previous comic."""
910
        link = last_soup.find('img', src='/nextlink.gif' if next_ else '/prevlink.gif').parent
911
        return None if link.get('href') is None else link
912
913
    @classmethod
914
    def get_comic_info(cls, soup, link):
915
        """Get information about a particular comics."""
916
        title = soup.find('title')
917
        imgs = [img for img in soup.find_all('img')
918
                if not img['src'].endswith(
919
                    ('link.gif', '32.png', 'twpbookad.jpg',
920
                     'merchad.jpg', 'header.gif', 'tipjar.jpg'))]
921
        return {
922
            'title': title.string if title else None,
923
            'title2': '  '.join(img.get('alt') for img in imgs if img.get('alt')),
924
            'img': [urljoin_wrapper(cls.url, img['src']) for img in imgs],
925
        }
926
927
928
class DeadlyPanel(GenericComicNotWorking, GenericNavigableComic):  # Not working on my machine
929
    """Class to retrieve Deadly Panel comics."""
930
    # Also on https://tapastic.com/series/deadlypanel
931
    # Also on https://deadlypanel.tumblr.com
932
    name = 'deadly'
933
    long_name = 'Deadly Panel'
934
    url = 'http://www.deadlypanel.com'
935
    get_first_comic_link = get_a_navi_navifirst
936
    get_navi_link = get_a_navi_comicnavnext_navinext
937
938
    @classmethod
939
    def get_comic_info(cls, soup, link):
940
        """Get information about a particular comics."""
941
        imgs = soup.find('div', id='comic').find_all('img')
942
        assert all(i['alt'] == i['title'] for i in imgs)
943
        return {
944
            'img': [i['src'] for i in imgs],
945
        }
946
947
948 View Code Duplication
class TheGentlemanArmchair(GenericNavigableComic):
949
    """Class to retrieve The Gentleman Armchair comics."""
950
    name = 'gentlemanarmchair'
951
    long_name = 'The Gentleman Armchair'
952
    url = 'http://thegentlemansarmchair.com'
953
    get_first_comic_link = get_a_navi_navifirst
954
    get_navi_link = get_link_rel_next
955
956
    @classmethod
957
    def get_comic_info(cls, soup, link):
958
        """Get information about a particular comics."""
959
        title = soup.find('h2', class_='post-title').string
960
        author = soup.find("span", class_="post-author").find("a").string
961
        date_str = soup.find('span', class_='post-date').string
962
        day = string_to_date(date_str, "%B %d, %Y")
963
        imgs = soup.find('div', id='comic').find_all('img')
964
        return {
965
            'img': [i['src'] for i in imgs],
966
            'title': title,
967
            'author': author,
968
            'month': day.month,
969
            'year': day.year,
970
            'day': day.day,
971
        }
972
973
974
class ImogenQuest(GenericNavigableComic):
975
    """Class to retrieve Imogen Quest comics."""
976
    # Also on http://imoquest.tumblr.com
977
    name = 'imogen'
978
    long_name = 'Imogen Quest'
979
    url = 'http://imogenquest.net'
980
    get_first_comic_link = get_div_navfirst_a
981
    get_navi_link = get_a_rel_next
982
983
    @classmethod
984
    def get_comic_info(cls, soup, link):
985
        """Get information about a particular comics."""
986
        title = soup.find('h2', class_='post-title').string
987
        author = soup.find("span", class_="post-author").find("a").string
988
        date_str = soup.find('span', class_='post-date').string
989
        day = string_to_date(date_str, '%B %d, %Y')
990
        imgs = soup.find('div', class_='comicpane').find_all('img')
991
        assert all(i['alt'] == i['title'] for i in imgs)
992
        title2 = imgs[0]['title']
993
        return {
994
            'day': day.day,
995
            'month': day.month,
996
            'year': day.year,
997
            'img': [i['src'] for i in imgs],
998
            'title': title,
999
            'title2': title2,
1000
            'author': author,
1001
        }
1002
1003
1004 View Code Duplication
class MyExtraLife(GenericNavigableComic):
1005
    """Class to retrieve My Extra Life comics."""
1006
    name = 'extralife'
1007
    long_name = 'My Extra Life'
1008
    url = 'http://www.myextralife.com'
1009
    get_navi_link = get_link_rel_next
1010
1011
    @classmethod
1012
    def get_first_comic_link(cls):
1013
        """Get link to first comics."""
1014
        return get_soup_at_url(cls.url).find('a', class_='comic_nav_link first_comic_link')
1015
1016
    @classmethod
1017
    def get_comic_info(cls, soup, link):
1018
        """Get information about a particular comics."""
1019
        title = soup.find("h1", class_="comic_title").string
1020
        date_str = soup.find("span", class_="comic_date").string
1021
        day = string_to_date(date_str, "%B %d, %Y")
1022
        imgs = soup.find_all("img", class_="comic")
1023
        assert all(i['alt'] == i['title'] == title for i in imgs)
1024
        return {
1025
            'title': title,
1026
            'img': [i['src'] for i in imgs if i["src"]],
1027
            'day': day.day,
1028
            'month': day.month,
1029
            'year': day.year
1030
        }
1031
1032
1033
class SaturdayMorningBreakfastCereal(GenericNavigableComic):
1034
    """Class to retrieve Saturday Morning Breakfast Cereal comics."""
1035
    # Also on http://www.gocomics.com/saturday-morning-breakfast-cereal
1036
    # Also on http://smbc-comics.tumblr.com
1037
    name = 'smbc'
1038
    long_name = 'Saturday Morning Breakfast Cereal'
1039
    url = 'http://www.smbc-comics.com'
1040
    _categories = ('SMBC', )
1041
    get_navi_link = get_a_rel_next
1042
1043
    @classmethod
1044
    def get_first_comic_link(cls):
1045
        """Get link to first comics."""
1046
        return get_soup_at_url(cls.url).find('a', rel='start')
1047
1048
    @classmethod
1049
    def get_comic_info(cls, soup, link):
1050
        """Get information about a particular comics."""
1051
        image1 = soup.find('img', id='cc-comic')
1052
        image_url1 = image1['src']
1053
        aftercomic = soup.find('div', id='aftercomic')
1054
        image_url2 = aftercomic.find('img')['src'] if aftercomic else ''
1055
        imgs = [image_url1] + ([image_url2] if image_url2 else [])
1056
        date_str = soup.find('div', class_='cc-publishtime').contents[0]
1057
        day = string_to_date(date_str, "%B %d, %Y")
1058
        return {
1059
            'title': image1['title'],
1060
            'img': [convert_iri_to_plain_ascii_uri(urljoin_wrapper(cls.url, i)) for i in imgs],
1061
            'day': day.day,
1062
            'month': day.month,
1063
            'year': day.year
1064
        }
1065
1066
1067 View Code Duplication
class PerryBibleFellowship(GenericListableComic):  # Is now navigable too
1068
    """Class to retrieve Perry Bible Fellowship comics."""
1069
    name = 'pbf'
1070
    long_name = 'Perry Bible Fellowship'
1071
    url = 'http://pbfcomics.com'
1072
    get_url_from_archive_element = join_cls_url_to_href
1073
1074
    @classmethod
1075
    def get_archive_elements(cls):
1076
        soup = get_soup_at_url(cls.url)
1077
        thumbnails = soup.find('div', id='all_thumbnails')
1078
        return reversed(thumbnails.find_all('a'))
1079
1080
    @classmethod
1081
    def get_comic_info(cls, soup, link):
1082
        """Get information about a particular comics."""
1083
        name = soup.find('meta', property='og:title')['content']
1084
        imgs = soup.find_all('meta', property='og:image')
1085
        assert len(imgs) == 1, imgs
1086
        return {
1087
            'name': name,
1088
            'img': [i['content'] for i in imgs],
1089
        }
1090
1091
1092 View Code Duplication
class Mercworks(GenericDeletedComic):  # Moved to Webtoons
1093
    """Class to retrieve Mercworks comics."""
1094
    # Also on http://mercworks.tumblr.com
1095
    # Also on http://www.webtoons.com/en/comedy/mercworks/list?title_no=426
1096
    # Also on https://tapastic.com/series/MercWorks
1097
    name = 'mercworks'
1098
    long_name = 'Mercworks'
1099
    url = 'http://mercworks.net'
1100
    _categories = ('MERCWORKS', )
1101
    get_first_comic_link = get_a_comicnavbase_comicnavfirst
1102
    get_navi_link = get_link_rel_next
1103
1104
    @classmethod
1105
    def get_comic_info(cls, soup, link):
1106
        """Get information about a particular comics."""
1107
        title = soup.find('meta', property='og:title')['content']
1108
        metadesc = soup.find('meta', property='og:description')
1109
        desc = metadesc['content'] if metadesc else ""
1110
        date_str = soup.find('meta', property='article:published_time')['content'][:10]
1111
        day = string_to_date(date_str, "%Y-%m-%d")
1112
        imgs = soup.find_all('meta', property='og:image')
1113
        return {
1114
            'img': [i['content'] for i in imgs],
1115
            'title': title,
1116
            'desc': desc,
1117
            'day': day.day,
1118
            'month': day.month,
1119
            'year': day.year
1120
        }
1121
1122
1123
class BerkeleyMews(GenericListableComic):
1124
    """Class to retrieve Berkeley Mews comics."""
1125
    # Also on http://mews.tumblr.com
1126
    # Also on http://www.gocomics.com/berkeley-mews
1127
    name = 'berkeley'
1128
    long_name = 'Berkeley Mews'
1129
    url = 'http://www.berkeleymews.com'
1130
    _categories = ('BERKELEY', )
1131
    get_url_from_archive_element = get_href
1132
    comic_num_re = re.compile('%s/\\?p=([0-9]*)$' % url)
1133
1134
    @classmethod
1135
    def get_archive_elements(cls):
1136
        archive_url = urljoin_wrapper(cls.url, "?page_id=2")
1137
        return reversed(get_soup_at_url(archive_url).find_all('a', href=cls.comic_num_re))
1138
1139
    @classmethod
1140
    def get_comic_info(cls, soup, link):
1141
        """Get information about a particular comics."""
1142
        comic_date_re = re.compile('.*/([0-9]*)-([0-9]*)-([0-9]*)-.*')
1143
        url = cls.get_url_from_archive_element(link)
1144
        num = int(cls.comic_num_re.match(url).groups()[0])
1145
        img = soup.find('div', id='comic').find('img')
1146
        assert all(i['alt'] == i['title'] for i in [img])
1147
        title2 = img['title']
1148
        img_url = img['src']
1149
        year, month, day = [int(s) for s in comic_date_re.match(img_url).groups()]
1150
        return {
1151
            'num': num,
1152
            'title': link.string,
1153
            'title2': title2,
1154
            'img': [img_url],
1155
            'year': year,
1156
            'month': month,
1157
            'day': day,
1158
        }
1159
1160
1161
class GenericBouletCorp(GenericNavigableComic):
1162
    """Generic class to retrieve BouletCorp comics in different languages."""
1163
    # Also on https://bouletcorp.tumblr.com
1164
    _categories = ('BOULET', )
1165
    get_navi_link = get_link_rel_next
1166
1167
    @classmethod
1168
    def get_first_comic_link(cls):
1169
        """Get link to first comics."""
1170
        return get_soup_at_url(cls.url).find('div', id='centered_nav').find_all('a')[0]
1171
1172
    @classmethod
1173
    def get_comic_info(cls, soup, link):
1174
        """Get information about a particular comics."""
1175
        url = cls.get_url_from_link(link)
1176
        date_re = re.compile('^%s/([0-9]*)/([0-9]*)/([0-9]*)/' % cls.url)
1177
        year, month, day = [int(s) for s in date_re.match(url).groups()]
1178
        imgs = soup.find('div', id='notes').find('div', class_='storycontent').find_all('img')
1179
        texts = '  '.join(t for t in (i.get('title') for i in imgs) if t)
1180
        title = soup.find('title').string
1181
        return {
1182
            'img': [convert_iri_to_plain_ascii_uri(i['src']) for i in imgs if i.get('src') is not None],
1183
            'title': title,
1184
            'texts': texts,
1185
            'year': year,
1186
            'month': month,
1187
            'day': day,
1188
        }
1189
1190
1191
class BouletCorp(GenericBouletCorp):
1192
    """Class to retrieve BouletCorp comics."""
1193
    name = 'boulet'
1194
    long_name = 'Boulet Corp'
1195
    url = 'http://www.bouletcorp.com'
1196
    _categories = ('FRANCAIS', )
1197
1198
1199
class BouletCorpEn(GenericBouletCorp):
1200
    """Class to retrieve EnglishBouletCorp comics."""
1201
    name = 'boulet_en'
1202
    long_name = 'Boulet Corp English'
1203
    url = 'http://english.bouletcorp.com'
1204
1205
1206 View Code Duplication
class AmazingSuperPowers(GenericNavigableComic):
1207
    """Class to retrieve Amazing Super Powers comics."""
1208
    name = 'asp'
1209
    long_name = 'Amazing Super Powers'
1210
    url = 'http://www.amazingsuperpowers.com'
1211
    get_first_comic_link = get_a_navi_navifirst
1212
    get_navi_link = get_a_navi_navinext
1213
1214
    @classmethod
1215
    def get_comic_info(cls, soup, link):
1216
        """Get information about a particular comics."""
1217
        author = soup.find("span", class_="post-author").find("a").string
1218
        date_str = soup.find('span', class_='post-date').string
1219
        day = string_to_date(date_str, "%B %d, %Y")
1220
        imgs = soup.find('div', id='comic').find_all('img')
1221
        title = ' '.join(i['title'] for i in imgs)
1222
        assert all(i['alt'] == i['title'] for i in imgs)
1223
        return {
1224
            'title': title,
1225
            'author': author,
1226
            'img': [img['src'] for img in imgs],
1227
            'day': day.day,
1228
            'month': day.month,
1229
            'year': day.year
1230
        }
1231
1232
1233
class ToonHole(GenericNavigableComic):
1234
    """Class to retrieve Toon Holes comics."""
1235
    # Also on http://tapastic.com/series/TOONHOLE
1236
    name = 'toonhole'
1237
    long_name = 'Toon Hole'
1238
    url = 'http://www.toonhole.com'
1239
    get_first_comic_link = get_a_comicnavbase_comicnavfirst
1240
    get_navi_link = get_a_comicnavbase_comicnavnext
1241
1242
    @classmethod
1243
    def get_comic_info(cls, soup, link):
1244
        """Get information about a particular comics."""
1245
        date_str = soup.find('div', class_='entry-meta').contents[0].strip()
1246
        day = string_to_date(date_str, "%B %d, %Y")
1247
        imgs = soup.find('div', id='comic').find_all('img')
1248
        if imgs:
1249
            img = imgs[0]
1250
            title = img['alt']
1251
            assert img['title'] == title
1252
        else:
1253
            title = ""
1254
        return {
1255
            'title': title,
1256
            'month': day.month,
1257
            'year': day.year,
1258
            'day': day.day,
1259
            'img': [convert_iri_to_plain_ascii_uri(i['src']) for i in imgs],
1260
        }
1261
1262
1263
class Channelate(GenericNavigableComic):
1264
    """Class to retrieve Channelate comics."""
1265
    name = 'channelate'
1266
    long_name = 'Channelate'
1267
    url = 'http://www.channelate.com'
1268
    get_first_comic_link = get_div_navfirst_a
1269
    get_navi_link = get_link_rel_next
1270
    get_url_from_link = join_cls_url_to_href
1271
1272
    @classmethod
1273
    def get_comic_info(cls, soup, link):
1274
        """Get information about a particular comics."""
1275
        author = soup.find("span", class_="post-author").find("a").string
1276
        date_str = soup.find('span', class_='post-date').string
1277
        day = string_to_date(date_str, '%Y/%m/%d')
1278
        title = soup.find('meta', property='og:title')['content']
1279
        post = soup.find('div', id='comic')
1280
        imgs = post.find_all('img') if post else []
1281
        extra_url = None
1282
        extra_div = soup.find('div', id='extrapanelbutton')
1283
        if extra_div:
1284
            extra_url = extra_div.find('a')['href']
1285
            extra_soup = get_soup_at_url(extra_url)
1286
            extra_imgs = extra_soup.find_all('img', class_='extrapanelimage')
1287
            imgs.extend(extra_imgs)
1288
        return {
1289
            'url_extra': extra_url,
1290
            'title': title,
1291
            'author': author,
1292
            'month': day.month,
1293
            'year': day.year,
1294
            'day': day.day,
1295
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
1296
        }
1297
1298
1299
class CyanideAndHappiness(GenericNavigableComic):
1300
    """Class to retrieve Cyanide And Happiness comics."""
1301
    name = 'cyanide'
1302
    long_name = 'Cyanide and Happiness'
1303
    url = 'http://explosm.net'
1304
    _categories = ('NSFW', )
1305
    get_url_from_link = join_cls_url_to_href
1306
1307
    @classmethod
1308
    def get_first_comic_link(cls):
1309
        """Get link to first comics."""
1310
        return get_soup_at_url(cls.url).find('a', title='Oldest comic')
1311
1312
    @classmethod
1313
    def get_navi_link(cls, last_soup, next_):
1314
        """Get link to next or previous comic."""
1315
        link = last_soup.find('a', class_='nav-next' if next_ else 'nav-previous')
1316
        return None if link.get('href') is None else link
1317
1318
    @classmethod
1319
    def get_comic_info(cls, soup, link):
1320
        """Get information about a particular comics."""
1321
        url2 = soup.find('meta', property='og:url')['content']
1322
        num = int(url2.split('/')[-2])
1323
        date_str, _, author = soup.find('div', id='comic-author').text.strip().partition('\nby ')
1324
        day = string_to_date(date_str, '%Y.%m.%d')
1325
        imgs = soup.find_all('img', id='main-comic')
1326
        return {
1327
            'num': num,
1328
            'author': author,
1329
            'month': day.month,
1330
            'year': day.year,
1331
            'day': day.day,
1332
            'prefix': '%d-' % num,
1333
            'img': [convert_iri_to_plain_ascii_uri(urljoin_wrapper(cls.url, i['src'])) for i in imgs]
1334
        }
1335
1336
1337
class MrLovenstein(GenericComic):
1338
    """Class to retrieve Mr Lovenstein comics."""
1339
    # Also on https://tapastic.com/series/MrLovenstein
1340
    name = 'mrlovenstein'
1341
    long_name = 'Mr. Lovenstein'
1342
    url = 'http://www.mrlovenstein.com'
1343
1344
    @classmethod
1345
    def get_next_comic(cls, last_comic):
1346
        """Generator to get the next comic. Implementation of GenericComic's abstract method."""
1347
        # TODO: more info from http://www.mrlovenstein.com/archive
1348
        comic_num_re = re.compile('^/comic/([0-9]*)$')
1349
        nums = [int(comic_num_re.match(link['href']).groups()[0])
1350
                for link in get_soup_at_url(cls.url).find_all('a', href=comic_num_re)]
1351
        first, last = min(nums), max(nums)
1352
        if last_comic:
1353
            first = last_comic['num'] + 1
1354
        for num in range(first, last + 1):
1355
            url = urljoin_wrapper(cls.url, '/comic/%d' % num)
1356
            soup = get_soup_at_url(url)
1357
            imgs = list(
1358
                reversed(soup.find_all('img', src=re.compile('^/images/comics/'))))
1359
            description = soup.find('meta', attrs={'name': 'description'})['content']
1360
            yield {
1361
                'url': url,
1362
                'num': num,
1363
                'texts': '  '.join(t for t in (i.get('title') for i in imgs) if t),
1364
                'img': [urljoin_wrapper(url, i['src']) for i in imgs],
1365
                'description': description,
1366
            }
1367
1368
1369
class DinosaurComics(GenericListableComic):
1370
    """Class to retrieve Dinosaur Comics comics."""
1371
    name = 'dinosaur'
1372
    long_name = 'Dinosaur Comics'
1373
    url = 'http://www.qwantz.com'
1374
    get_url_from_archive_element = get_href
1375
    comic_link_re = re.compile('^%s/index.php\\?comic=([0-9]*)$' % url)
1376
1377
    @classmethod
1378
    def get_archive_elements(cls):
1379
        archive_url = urljoin_wrapper(cls.url, 'archive.php')
1380
        # first link is random -> skip it
1381
        return reversed(get_soup_at_url(archive_url).find_all('a', href=cls.comic_link_re)[1:])
1382
1383
    @classmethod
1384
    def get_comic_info(cls, soup, link):
1385
        """Get information about a particular comics."""
1386
        url = cls.get_url_from_archive_element(link)
1387
        num = int(cls.comic_link_re.match(url).groups()[0])
1388
        date_str = link.string
1389
        text = link.next_sibling.string
1390
        day = string_to_date(remove_st_nd_rd_th_from_date(date_str), "%B %d, %Y")
1391
        comic_img_re = re.compile('^%s/comics/' % cls.url)
1392
        img = soup.find('img', src=comic_img_re)
1393
        return {
1394
            'month': day.month,
1395
            'year': day.year,
1396
            'day': day.day,
1397
            'img': [img.get('src')],
1398
            'title': img.get('title'),
1399
            'text': text,
1400
            'num': num,
1401
        }
1402
1403
1404
class ButterSafe(GenericListableComic):
1405
    """Class to retrieve Butter Safe comics."""
1406
    name = 'butter'
1407 View Code Duplication
    long_name = 'ButterSafe'
1408
    url = 'http://buttersafe.com'
1409
    get_url_from_archive_element = get_href
1410
    comic_link_re = re.compile('^%s/([0-9]*)/([0-9]*)/([0-9]*)/.*' % url)
1411
1412
    @classmethod
1413
    def get_archive_elements(cls):
1414
        archive_url = urljoin_wrapper(cls.url, 'archive/')
1415
        return reversed(get_soup_at_url(archive_url).find_all('a', href=cls.comic_link_re))
1416
1417
    @classmethod
1418
    def get_comic_info(cls, soup, link):
1419
        """Get information about a particular comics."""
1420
        url = cls.get_url_from_archive_element(link)
1421
        title = link.string
1422
        year, month, day = [int(s) for s in cls.comic_link_re.match(url).groups()]
1423
        img = soup.find('div', id='comic').find('img')
1424
        assert img['alt'] == title
1425
        return {
1426
            'title': title,
1427
            'day': day,
1428
            'month': month,
1429
            'year': year,
1430
            'img': [img['src']],
1431
        }
1432
1433
1434
class CalvinAndHobbes(GenericComic):
1435
    """Class to retrieve Calvin and Hobbes comics."""
1436
    # Also on http://www.gocomics.com/calvinandhobbes/
1437
    name = 'calvin'
1438
    long_name = 'Calvin and Hobbes'
1439
    # This is not through any official webpage but eh...
1440
    url = 'http://marcel-oehler.marcellosendos.ch/comics/ch/'
1441
1442
    @classmethod
1443
    def get_next_comic(cls, last_comic):
1444
        """Generator to get the next comic. Implementation of GenericComic's abstract method."""
1445
        last_date = get_date_for_comic(
1446
            last_comic) if last_comic else date(1985, 11, 1)
1447
        link_re = re.compile('^([0-9]*)/([0-9]*)/')
1448
        img_re = re.compile('')
1449
        for link in get_soup_at_url(cls.url).find_all('a', href=link_re):
1450
            url = link['href']
1451
            year, month = link_re.match(url).groups()
1452
            if date(int(year), int(month), 1) + timedelta(days=31) >= last_date:
1453
                img_re = re.compile('^%s%s([0-9]*)' % (year, month))
1454
                month_url = urljoin_wrapper(cls.url, url)
1455
                for img in get_soup_at_url(month_url).find_all('img', src=img_re):
1456
                    img_src = img['src']
1457
                    day = int(img_re.match(img_src).groups()[0])
1458
                    comic_date = date(int(year), int(month), day)
1459
                    if comic_date > last_date:
1460
                        yield {
1461
                            'url': month_url,
1462
                            'year': int(year),
1463
                            'month': int(month),
1464
                            'day': int(day),
1465
                            'img': ['%s%s/%s/%s' % (cls.url, year, month, img_src)],
1466
                        }
1467
                        last_date = comic_date
1468
1469
1470
class AbstruseGoose(GenericListableComic):
1471
    """Class to retrieve AbstruseGoose Comics."""
1472
    name = 'abstruse'
1473 View Code Duplication
    long_name = 'Abstruse Goose'
1474
    url = 'http://abstrusegoose.com'
1475
    get_url_from_archive_element = get_href
1476
    comic_url_re = re.compile('^%s/([0-9]*)$' % url)
1477
    comic_img_re = re.compile('^%s/strips/.*' % url)
1478
1479
    @classmethod
1480
    def get_archive_elements(cls):
1481
        archive_url = urljoin_wrapper(cls.url, 'archive')
1482
        return get_soup_at_url(archive_url).find_all('a', href=cls.comic_url_re)
1483
1484
    @classmethod
1485
    def get_comic_info(cls, soup, archive_elt):
1486
        comic_url = cls.get_url_from_archive_element(archive_elt)
1487
        num = int(cls.comic_url_re.match(comic_url).groups()[0])
1488
        imgs = soup.find_all('img', src=cls.comic_img_re)
1489
        return {
1490
            'num': num,
1491
            'title': archive_elt.string,
1492
            'img': [i['src'] for i in imgs],
1493
        }
1494
1495
1496
class PhDComics(GenericNavigableComic):
1497
    """Class to retrieve PHD Comics."""
1498
    name = 'phd'
1499
    long_name = 'PhD Comics'
1500
    url = 'http://phdcomics.com/comics/archive.php'
1501
1502
    @classmethod
1503
    def get_first_comic_link(cls):
1504
        """Get link to first comics."""
1505
        soup = get_soup_at_url(cls.url)
1506
        img = soup.find('img', src='http://phdcomics.com/comics/images/first_button.gif')
1507
        return None if img is None else img.parent
1508
1509
    @classmethod
1510
    def get_navi_link(cls, last_soup, next_):
1511
        """Get link to next or previous comic."""
1512
        url = 'http://phdcomics.com/comics/images/%s_button.gif' % ('next' if next_ else 'prev')
1513
        img = last_soup.find('img', src=url)
1514
        return None if img is None else img.parent
1515
1516
    @classmethod
1517
    def get_comic_info(cls, soup, link):
1518
        """Get information about a particular comics."""
1519
        title = soup.find('meta', attrs={'name': 'twitter:title'})['content']
1520
        imgs = soup.find_all('meta', property='og:image')
1521
        return {
1522
            'img': [i['content'] for i in imgs],
1523
            'title': title,
1524
        }
1525
1526
1527
class Quarktees(GenericNavigableComic):
1528
    """Class to retrieve the Quarktees comics."""
1529
    name = 'quarktees'
1530
    long_name = 'Quarktees'
1531
    url = 'http://www.quarktees.com/blogs/news'
1532
    get_url_from_link = join_cls_url_to_href
1533
    get_first_comic_link = simulate_first_link
1534
    first_url = 'http://www.quarktees.com/blogs/news/12486621-coming-soon'
1535
1536
    @classmethod
1537
    def get_navi_link(cls, last_soup, next_):
1538
        """Get link to next or previous comic."""
1539
        return last_soup.find('a', id='article-next' if next_ else 'article-prev')
1540
1541
    @classmethod
1542
    def get_comic_info(cls, soup, link):
1543
        """Get information about a particular comics."""
1544
        title = soup.find('meta', property='og:title')['content']
1545
        article = soup.find('div', class_='single-article')
1546
        imgs = article.find_all('img')
1547
        return {
1548
            'title': title,
1549
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
1550
        }
1551
1552
1553
class OverCompensating(GenericNavigableComic):
1554
    """Class to retrieve the Over Compensating comics."""
1555
    name = 'compensating'
1556
    long_name = 'Over Compensating'
1557
    url = 'http://www.overcompensating.com'
1558
    get_url_from_link = join_cls_url_to_href
1559
1560
    @classmethod
1561
    def get_first_comic_link(cls):
1562
        """Get link to first comics."""
1563
        return get_soup_at_url(cls.url).find('a', href=re.compile('comic=1$'))
1564
1565
    @classmethod
1566
    def get_navi_link(cls, last_soup, next_):
1567
        """Get link to next or previous comic."""
1568
        return last_soup.find('a', title='next comic' if next_ else 'go back already')
1569
1570
    @classmethod
1571
    def get_comic_info(cls, soup, link):
1572
        """Get information about a particular comics."""
1573
        img_src_re = re.compile('^/oc/comics/.*')
1574
        comic_num_re = re.compile('.*comic=([0-9]*)$')
1575
        comic_url = cls.get_url_from_link(link)
1576
        num = int(comic_num_re.match(comic_url).groups()[0])
1577
        img = soup.find('img', src=img_src_re)
1578
        return {
1579
            'num': num,
1580
            'img': [urljoin_wrapper(comic_url, img['src'])],
1581
            'title': img.get('title')
1582
        }
1583
1584
1585
class Oglaf(GenericNavigableComic):
1586
    """Class to retrieve Oglaf comics."""
1587
    name = 'oglaf'
1588
    long_name = 'Oglaf [NSFW]'
1589
    url = 'http://oglaf.com'
1590
    _categories = ('NSFW', )
1591
    get_url_from_link = join_cls_url_to_href
1592
1593
    @classmethod
1594
    def get_first_comic_link(cls):
1595
        """Get link to first comics."""
1596
        return get_soup_at_url(cls.url).find("div", id="st").parent
1597
1598
    @classmethod
1599
    def get_navi_link(cls, last_soup, next_):
1600
        """Get link to next or previous comic."""
1601
        div = last_soup.find("div", id="nx" if next_ else "pvs")
1602
        return div.parent if div else None
1603
1604
    @classmethod
1605
    def get_comic_info(cls, soup, link):
1606
        """Get information about a particular comics."""
1607
        title = soup.find('title').string
1608
        title_imgs = soup.find('div', id='tt').find_all('img')
1609
        assert len(title_imgs) == 1, title_imgs
1610
        strip_imgs = soup.find_all('img', id='strip')
1611
        assert len(strip_imgs) == 1, strip_imgs
1612
        imgs = title_imgs + strip_imgs
1613
        desc = ' '.join(i['title'] for i in imgs)
1614
        return {
1615
            'title': title,
1616
            'img': [i['src'] for i in imgs],
1617
            'description': desc,
1618
        }
1619
1620
1621
class ScandinaviaAndTheWorld(GenericNavigableComic):
1622
    """Class to retrieve Scandinavia And The World comics."""
1623
    name = 'satw'
1624
    long_name = 'Scandinavia And The World'
1625
    url = 'http://satwcomic.com'
1626
    get_first_comic_link = simulate_first_link
1627
    first_url = 'http://satwcomic.com/sweden-denmark-and-norway'
1628
1629
    @classmethod
1630
    def get_navi_link(cls, last_soup, next_):
1631
        """Get link to next or previous comic."""
1632
        return last_soup.find('a', accesskey='n' if next_ else 'p')
1633
1634
    @classmethod
1635
    def get_comic_info(cls, soup, link):
1636
        """Get information about a particular comics."""
1637
        title = soup.find('meta', attrs={'name': 'twitter:label1'})['content']
1638
        desc = soup.find('meta', property='og:description')['content']
1639
        imgs = soup.find_all('img', itemprop="image")
1640
        return {
1641
            'title': title,
1642
            'description': desc,
1643
            'img': [i['src'] for i in imgs],
1644
        }
1645
1646
1647
class SomethingOfThatIlk(GenericDeletedComic):
1648
    """Class to retrieve the Something Of That Ilk comics."""
1649
    name = 'somethingofthatilk'
1650
    long_name = 'Something Of That Ilk'
1651
    url = 'http://www.somethingofthatilk.com'
1652
1653
1654
class MonkeyUser(GenericNavigableComic):
1655
    """Class to retrieve Monkey User comics."""
1656
    name = 'monkeyuser'
1657
    long_name = 'Monkey User'
1658
    url = 'http://www.monkeyuser.com'
1659
    get_first_comic_link = simulate_first_link
1660
    first_url = 'http://www.monkeyuser.com/2016/project-lifecycle/'
1661
    get_url_from_link = join_cls_url_to_href
1662
1663
    @classmethod
1664
    def get_navi_link(cls, last_soup, next_):
1665
        """Get link to next or previous comic."""
1666
        div = last_soup.find('div', title='next' if next_ else 'previous')
1667
        return None if div is None else div.find('a')
1668
1669
    @classmethod
1670
    def get_comic_info(cls, soup, link):
1671
        """Get information about a particular comics."""
1672
        title = soup.find('meta', property='og:title')['content']
1673
        desc = soup.find('meta', property='og:description')['content']
1674
        imgs = soup.find_all('meta', property='og:image')
1675
        date_str = soup.find('span', class_='post-date').find('time').string
1676
        day = string_to_date(date_str, "%d %b %Y")
1677
        return {
1678
            'month': day.month,
1679
            'year': day.year,
1680
            'day': day.day,
1681
            'img': [i['content'] for i in imgs],
1682
            'title': title,
1683
            'description': desc,
1684
        }
1685
1686
1687
class InfiniteMonkeyBusiness(GenericNavigableComic):
1688
    """Class to retrieve InfiniteMonkeyBusiness comics."""
1689
    name = 'monkey'
1690
    long_name = 'Infinite Monkey Business'
1691
    url = 'http://infinitemonkeybusiness.net'
1692
    get_navi_link = get_a_navi_comicnavnext_navinext
1693
    get_first_comic_link = simulate_first_link
1694
    first_url = 'http://infinitemonkeybusiness.net/comic/pillory/'
1695
1696
    @classmethod
1697
    def get_comic_info(cls, soup, link):
1698
        """Get information about a particular comics."""
1699
        title = soup.find('meta', property='og:title')['content']
1700
        imgs = soup.find('div', id='comic').find_all('img')
1701
        return {
1702
            'title': title,
1703
            'img': [i['src'] for i in imgs],
1704
        }
1705
1706
1707
class Wondermark(GenericListableComic):
1708
    """Class to retrieve the Wondermark comics."""
1709
    name = 'wondermark'
1710
    long_name = 'Wondermark'
1711
    url = 'http://wondermark.com'
1712
    get_url_from_archive_element = get_href
1713
1714
    @classmethod
1715
    def get_archive_elements(cls):
1716
        archive_url = urljoin_wrapper(cls.url, 'archive/')
1717
        return reversed(get_soup_at_url(archive_url).find_all('a', rel='bookmark'))
1718
1719
    @classmethod
1720
    def get_comic_info(cls, soup, link):
1721
        """Get information about a particular comics."""
1722
        date_str = soup.find('div', class_='postdate').find('em').string
1723
        day = string_to_date(remove_st_nd_rd_th_from_date(date_str), "%B %d, %Y")
1724
        div = soup.find('div', id='comic')
1725
        if div:
1726
            img = div.find('img')
1727
            img_src = [img['src']]
1728
            alt = img['alt']
1729
            assert alt == img['title']
1730
            title = soup.find('meta', property='og:title')['content']
1731
        else:
1732
            img_src = []
1733
            alt = ''
1734
            title = ''
1735
        return {
1736
            'month': day.month,
1737
            'year': day.year,
1738
            'day': day.day,
1739
            'img': img_src,
1740
            'title': title,
1741
            'alt': alt,
1742
            'tags': ' '.join(t.string for t in soup.find('div', class_='postmeta').find_all('a', rel='tag')),
1743
        }
1744
1745
1746
class WarehouseComic(GenericNavigableComic):
1747
    """Class to retrieve Warehouse Comic comics."""
1748
    name = 'warehouse'
1749
    long_name = 'Warehouse Comic'
1750
    url = 'http://warehousecomic.com'
1751
    get_first_comic_link = get_a_navi_navifirst
1752
    get_navi_link = get_link_rel_next
1753
1754
    @classmethod
1755
    def get_comic_info(cls, soup, link):
1756
        """Get information about a particular comics."""
1757
        title = soup.find('h2', class_='post-title').string
1758
        date_str = soup.find('span', class_='post-date').string
1759
        day = string_to_date(date_str, "%B %d, %Y")
1760
        imgs = soup.find('div', id='comic').find_all('img')
1761
        return {
1762
            'img': [i['src'] for i in imgs],
1763
            'title': title,
1764
            'day': day.day,
1765
            'month': day.month,
1766
            'year': day.year,
1767
        }
1768
1769
1770
class JustSayEh(GenericNavigableComic):
1771
    """Class to retrieve Just Say Eh comics."""
1772
    # Also on http//tapastic.com/series/Just-Say-Eh
1773
    name = 'justsayeh'
1774
    long_name = 'Just Say Eh'
1775
    url = 'http://www.justsayeh.com'
1776
    get_first_comic_link = get_a_navi_navifirst
1777
    get_navi_link = get_a_navi_comicnavnext_navinext
1778
1779
    @classmethod
1780
    def get_comic_info(cls, soup, link):
1781
        """Get information about a particular comics."""
1782
        title = soup.find('h2', class_='post-title').string
1783
        imgs = soup.find("div", id="comic").find_all("img")
1784
        assert all(i['alt'] == i['title'] for i in imgs)
1785
        alt = imgs[0]['alt']
1786
        return {
1787
            'img': [i['src'] for i in imgs],
1788
            'title': title,
1789
            'alt': alt,
1790
        }
1791
1792
1793
class MouseBearComedy(GenericComicNotWorking):  # Website has changed
1794
    """Class to retrieve Mouse Bear Comedy comics."""
1795
    # Also on http://mousebearcomedy.tumblr.com
1796 View Code Duplication
    name = 'mousebear'
1797
    long_name = 'Mouse Bear Comedy'
1798
    url = 'http://www.mousebearcomedy.com'
1799
    get_first_comic_link = get_a_navi_navifirst
1800
    get_navi_link = get_a_navi_comicnavnext_navinext
1801
1802
    @classmethod
1803
    def get_comic_info(cls, soup, link):
1804
        """Get information about a particular comics."""
1805
        title = soup.find('h2', class_='post-title').string
1806
        author = soup.find("span", class_="post-author").find("a").string
1807
        date_str = soup.find("span", class_="post-date").string
1808
        day = string_to_date(date_str, '%B %d, %Y')
1809
        imgs = soup.find("div", id="comic").find_all("img")
1810
        assert all(i['alt'] == i['title'] == title for i in imgs)
1811
        return {
1812
            'day': day.day,
1813
            'month': day.month,
1814
            'year': day.year,
1815
            'img': [i['src'] for i in imgs],
1816
            'title': title,
1817
            'author': author,
1818
        }
1819
1820
1821
class BigFootJustice(GenericNavigableComic):
1822
    """Class to retrieve Big Foot Justice comics."""
1823
    # Also on http://tapastic.com/series/bigfoot-justice
1824 View Code Duplication
    name = 'bigfoot'
1825
    long_name = 'Big Foot Justice'
1826
    url = 'http://bigfootjustice.com'
1827
    get_first_comic_link = get_a_navi_navifirst
1828
    get_navi_link = get_a_navi_comicnavnext_navinext
1829
1830
    @classmethod
1831
    def get_comic_info(cls, soup, link):
1832
        """Get information about a particular comics."""
1833
        imgs = soup.find('div', id='comic').find_all('img')
1834
        assert all(i['title'] == i['alt'] for i in imgs)
1835
        title = ' '.join(i['title'] for i in imgs)
1836
        return {
1837
            'img': [i['src'] for i in imgs],
1838
            'title': title,
1839
        }
1840
1841
1842
class RespawnComic(GenericNavigableComic):
1843
    """Class to retrieve Respawn Comic."""
1844
    # Also on https://respawncomic.tumblr.com
1845
    name = 'respawn'
1846
    long_name = 'Respawn Comic'
1847
    url = 'http://respawncomic.com '
1848
    _categories = ('RESPAWN', )
1849
    get_navi_link = get_a_rel_next
1850
    get_first_comic_link = simulate_first_link
1851
    first_url = 'http://respawncomic.com/comic/c0001/'
1852
1853 View Code Duplication
    @classmethod
1854
    def get_comic_info(cls, soup, link):
1855
        """Get information about a particular comics."""
1856
        title = soup.find('meta', property='og:title')['content']
1857
        author = soup.find('meta', attrs={'name': 'shareaholic:article_author_name'})['content']
1858
        date_str = soup.find('meta', attrs={'name': 'shareaholic:article_published_time'})['content']
1859
        date_str = date_str[:10]
1860
        day = string_to_date(date_str, "%Y-%m-%d")
1861
        imgs = soup.find_all('meta', property='og:image')
1862
        skip_imgs = {
1863
            'http://respawncomic.com/wp-content/uploads/2016/03/site/HAROLD2.png',
1864
            'http://respawncomic.com/wp-content/uploads/2016/03/site/DEVA.png'
1865
        }
1866
        return {
1867
            'title': title,
1868
            'author': author,
1869
            'day': day.day,
1870
            'month': day.month,
1871
            'year': day.year,
1872
            'img': [i['content'] for i in imgs if i['content'] not in skip_imgs],
1873
        }
1874
1875
1876
class SafelyEndangered(GenericNavigableComic):
1877
    """Class to retrieve Safely Endangered comics."""
1878
    # Also on http://tumblr.safelyendangered.com
1879 View Code Duplication
    name = 'endangered'
1880
    long_name = 'Safely Endangered'
1881
    url = 'http://www.safelyendangered.com'
1882
    get_navi_link = get_link_rel_next
1883
    get_first_comic_link = simulate_first_link
1884
    first_url = 'http://www.safelyendangered.com/comic/ignored/'
1885
1886
    @classmethod
1887
    def get_comic_info(cls, soup, link):
1888
        """Get information about a particular comics."""
1889
        title = soup.find('h2', class_='post-title').string
1890
        date_str = soup.find('span', class_='post-date').string
1891
        day = string_to_date(date_str, '%B %d, %Y')
1892
        imgs = soup.find('div', id='comic').find_all('img')
1893
        alt = imgs[0]['alt']
1894
        assert all(i['alt'] == i['title'] for i in imgs)
1895
        return {
1896
            'day': day.day,
1897
            'month': day.month,
1898
            'year': day.year,
1899
            'img': [i['src'] for i in imgs],
1900
            'title': title,
1901
            'alt': alt,
1902
        }
1903
1904
1905
class PicturesInBoxes(GenericNavigableComic):
1906
    """Class to retrieve Pictures In Boxes comics."""
1907
    # Also on https://picturesinboxescomic.tumblr.com
1908 View Code Duplication
    name = 'picturesinboxes'
1909
    long_name = 'Pictures in Boxes'
1910
    url = 'http://www.picturesinboxes.com'
1911
    get_navi_link = get_a_navi_navinext
1912
    get_first_comic_link = simulate_first_link
1913
    first_url = 'http://www.picturesinboxes.com/2013/10/26/tetris/'
1914
1915
    @classmethod
1916
    def get_comic_info(cls, soup, link):
1917
        """Get information about a particular comics."""
1918
        title = soup.find('h2', class_='post-title').string
1919
        author = soup.find("span", class_="post-author").find("a").string
1920
        date_str = soup.find('span', class_='post-date').string
1921
        day = string_to_date(date_str, '%B %d, %Y')
1922
        imgs = soup.find('div', class_='comicpane').find_all('img')
1923
        assert imgs
1924
        assert all(i['title'] == i['alt'] == title for i in imgs)
1925
        return {
1926
            'day': day.day,
1927
            'month': day.month,
1928
            'year': day.year,
1929
            'img': [i['src'] for i in imgs],
1930
            'title': title,
1931
            'author': author,
1932
        }
1933
1934
1935
class Penmen(GenericComicNotWorking, GenericNavigableComic):
1936
    """Class to retrieve Penmen comics."""
1937
    name = 'penmen'
1938 View Code Duplication
    long_name = 'Penmen'
1939
    url = 'http://penmen.com'
1940
    get_navi_link = get_link_rel_next
1941
    get_first_comic_link = simulate_first_link
1942
    first_url = 'http://penmen.com/index.php/2016/09/12/penmen-announces-grin-big-brand-clothing/'
1943
1944
    @classmethod
1945
    def get_comic_info(cls, soup, link):
1946
        """Get information about a particular comics."""
1947
        title = soup.find('title').string
1948
        imgs = soup.find('div', class_='entry-content').find_all('img')
1949
        short_url = soup.find('link', rel='shortlink')['href']
1950
        tags = ' '.join(t.string for t in soup.find_all('a', rel='tag'))
1951
        date_str = soup.find('time')['datetime'][:10]
1952
        day = string_to_date(date_str, "%Y-%m-%d")
1953
        return {
1954
            'title': title,
1955
            'short_url': short_url,
1956
            'img': [i['src'] for i in imgs],
1957
            'tags': tags,
1958
            'month': day.month,
1959
            'year': day.year,
1960
            'day': day.day,
1961
        }
1962
1963
1964
class TheDoghouseDiaries(GenericDeletedComic, GenericNavigableComic):
1965
    """Class to retrieve The Dog House Diaries comics."""
1966
    name = 'doghouse'
1967
    long_name = 'The Dog House Diaries'
1968
    url = 'http://thedoghousediaries.com'
1969
1970
    @classmethod
1971
    def get_first_comic_link(cls):
1972
        """Get link to first comics."""
1973
        return get_soup_at_url(cls.url).find('a', id='firstlink')
1974
1975
    @classmethod
1976
    def get_navi_link(cls, last_soup, next_):
1977
        """Get link to next or previous comic."""
1978
        return last_soup.find('a', id='nextlink' if next_ else 'previouslink')
1979
1980
    @classmethod
1981
    def get_comic_info(cls, soup, link):
1982
        """Get information about a particular comics."""
1983
        comic_img_re = re.compile('^dhdcomics/.*')
1984
        img = soup.find('img', src=comic_img_re)
1985
        comic_url = cls.get_url_from_link(link)
1986
        return {
1987
            'title': soup.find('h2', id='titleheader').string,
1988
            'title2': soup.find('div', id='subtext').string,
1989
            'alt': img.get('title'),
1990
            'img': [urljoin_wrapper(comic_url, img['src'].strip())],
1991
            'num': int(comic_url.split('/')[-1]),
1992
        }
1993
1994
1995
class InvisibleBread(GenericListableComic):
1996
    """Class to retrieve Invisible Bread comics."""
1997
    # Also on http://www.gocomics.com/invisible-bread
1998
    name = 'invisiblebread'
1999
    long_name = 'Invisible Bread'
2000
    url = 'http://invisiblebread.com'
2001
2002
    @classmethod
2003
    def get_archive_elements(cls):
2004
        archive_url = urljoin_wrapper(cls.url, 'archives/')
2005
        return reversed(get_soup_at_url(archive_url).find_all('td', class_='archive-title'))
2006
2007
    @classmethod
2008
    def get_url_from_archive_element(cls, td):
2009
        return td.find('a')['href']
2010
2011
    @classmethod
2012
    def get_comic_info(cls, soup, td):
2013
        """Get information about a particular comics."""
2014
        url = cls.get_url_from_archive_element(td)
2015
        title = td.find('a').string
2016
        month_and_day = td.previous_sibling.string
2017
        link_re = re.compile('^%s/([0-9]+)/' % cls.url)
2018
        year = link_re.match(url).groups()[0]
2019
        date_str = month_and_day + ' ' + year
2020
        day = string_to_date(date_str, '%b %d %Y')
2021
        imgs = [soup.find('div', id='comic').find('img')]
2022
        assert len(imgs) == 1, imgs
2023
        assert all(i['title'] == i['alt'] == title for i in imgs)
2024
        return {
2025
            'month': day.month,
2026
            'year': day.year,
2027
            'day': day.day,
2028
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
2029
            'title': title,
2030
        }
2031
2032
2033
class DiscoBleach(GenericDeletedComic):
2034
    """Class to retrieve Disco Bleach Comics."""
2035
    name = 'discobleach'
2036
    long_name = 'Disco Bleach'
2037
    url = 'http://discobleach.com'
2038
2039
2040
class TubeyToons(GenericDeletedComic):
2041
    """Class to retrieve TubeyToons comics."""
2042
    # Also on http://tapastic.com/series/Tubey-Toons
2043
    # Also on https://tubeytoons.tumblr.com
2044
    name = 'tubeytoons'
2045
    long_name = 'Tubey Toons'
2046
    url = 'http://tubeytoons.com'
2047
    _categories = ('TUNEYTOONS', )
2048
2049
2050
class CompletelySeriousComics(GenericNavigableComic):
2051
    """Class to retrieve Completely Serious comics."""
2052
    name = 'completelyserious'
2053 View Code Duplication
    long_name = 'Completely Serious Comics'
2054
    url = 'http://completelyseriouscomics.com'
2055
    get_first_comic_link = get_a_navi_navifirst
2056
    get_navi_link = get_a_navi_navinext
2057
2058
    @classmethod
2059
    def get_comic_info(cls, soup, link):
2060
        """Get information about a particular comics."""
2061
        title = soup.find('h2', class_='post-title').string
2062
        author = soup.find('span', class_='post-author').contents[1].string
2063
        date_str = soup.find('span', class_='post-date').string
2064
        day = string_to_date(date_str, '%B %d, %Y')
2065
        imgs = soup.find('div', class_='comicpane').find_all('img')
2066
        assert imgs
2067
        alt = imgs[0]['title']
2068
        assert all(i['title'] == i['alt'] == alt for i in imgs)
2069
        return {
2070
            'month': day.month,
2071
            'year': day.year,
2072
            'day': day.day,
2073
            'img': [i['src'] for i in imgs],
2074
            'title': title,
2075
            'alt': alt,
2076
            'author': author,
2077
        }
2078
2079
2080
class PoorlyDrawnLines(GenericListableComic):
2081
    """Class to retrieve Poorly Drawn Lines comics."""
2082
    # Also on http://pdlcomics.tumblr.com
2083 View Code Duplication
    name = 'poorlydrawn'
2084
    long_name = 'Poorly Drawn Lines'
2085
    url = 'https://www.poorlydrawnlines.com'
2086
    _categories = ('POORLYDRAWN', )
2087
    get_url_from_archive_element = get_href
2088
2089
    @classmethod
2090
    def get_comic_info(cls, soup, link):
2091
        """Get information about a particular comics."""
2092
        imgs = soup.find('div', class_='post').find_all('img')
2093
        assert len(imgs) <= 1, imgs
2094
        return {
2095
            'img': [i['src'] for i in imgs],
2096
            'title': imgs[0].get('title', "") if imgs else "",
2097
        }
2098
2099
    @classmethod
2100
    def get_archive_elements(cls):
2101
        archive_url = urljoin_wrapper(cls.url, 'archive')
2102
        url_re = re.compile('^%s/comic/.' % cls.url)
2103
        return reversed(get_soup_at_url(archive_url).find_all('a', href=url_re))
2104
2105
2106
class LoadingComics(GenericNavigableComic):
2107
    """Class to retrieve Loading Artist comics."""
2108
    name = 'loadingartist'
2109
    long_name = 'Loading Artist'
2110
    url = 'http://www.loadingartist.com/latest'
2111
2112
    @classmethod
2113
    def get_first_comic_link(cls):
2114
        """Get link to first comics."""
2115
        return get_soup_at_url(cls.url).find('a', title="First")
2116
2117
    @classmethod
2118
    def get_navi_link(cls, last_soup, next_):
2119
        """Get link to next or previous comic."""
2120
        return last_soup.find('a', title='Next' if next_ else 'Previous')
2121
2122
    @classmethod
2123
    def get_comic_info(cls, soup, link):
2124
        """Get information about a particular comics."""
2125
        title = soup.find('h1').string
2126
        date_str = soup.find('span', class_='date').string.strip()
2127
        day = string_to_date(date_str, "%B %d, %Y")
2128
        imgs = soup.find('div', class_='comic').find_all('img', alt='', title='')
2129
        return {
2130
            'title': title,
2131
            'img': [i['src'] for i in imgs],
2132
            'month': day.month,
2133
            'year': day.year,
2134
            'day': day.day,
2135
        }
2136
2137
2138
class ChuckleADuck(GenericNavigableComic):
2139
    """Class to retrieve Chuckle-A-Duck comics."""
2140
    name = 'chuckleaduck'
2141
    long_name = 'Chuckle-A-duck'
2142
    url = 'http://chuckleaduck.com'
2143
    get_first_comic_link = get_div_navfirst_a
2144
    get_navi_link = get_link_rel_next
2145
2146
    @classmethod
2147
    def get_comic_info(cls, soup, link):
2148
        """Get information about a particular comics."""
2149
        date_str = soup.find('span', class_='post-date').string
2150
        day = string_to_date(remove_st_nd_rd_th_from_date(date_str), "%B %d, %Y")
2151
        author = soup.find('span', class_='post-author').string
2152
        div = soup.find('div', id='comic')
2153
        imgs = div.find_all('img') if div else []
2154
        title = imgs[0]['title'] if imgs else ""
2155
        assert all(i['title'] == i['alt'] == title for i in imgs)
2156
        return {
2157
            'month': day.month,
2158
            'year': day.year,
2159
            'day': day.day,
2160
            'img': [i['src'] for i in imgs],
2161
            'title': title,
2162
            'author': author,
2163
        }
2164
2165
2166
class DepressedAlien(GenericNavigableComic):
2167
    """Class to retrieve Depressed Alien Comics."""
2168
    name = 'depressedalien'
2169
    long_name = 'Depressed Alien'
2170
    url = 'http://depressedalien.com'
2171
    get_url_from_link = join_cls_url_to_href
2172
2173
    @classmethod
2174
    def get_first_comic_link(cls):
2175
        """Get link to first comics."""
2176
        return get_soup_at_url(cls.url).find('img', attrs={'name': 'beginArrow'}).parent
2177
2178
    @classmethod
2179
    def get_navi_link(cls, last_soup, next_):
2180
        """Get link to next or previous comic."""
2181
        return last_soup.find('img', attrs={'name': 'rightArrow' if next_ else 'leftArrow'}).parent
2182
2183
    @classmethod
2184
    def get_comic_info(cls, soup, link):
2185
        """Get information about a particular comics."""
2186
        title = soup.find('meta', attrs={'name': 'twitter:title'})['content']
2187
        imgs = soup.find_all('meta', property='og:image')
2188
        return {
2189
            'title': title,
2190
            'img': [i['content'] for i in imgs],
2191
        }
2192
2193
2194
class TurnOffUs(GenericListableComic):
2195
    """Class to retrieve TurnOffUs comics."""
2196
    name = 'turnoffus'
2197 View Code Duplication
    long_name = 'Turn Off Us'
2198
    url = 'http://turnoff.us'
2199
    get_url_from_archive_element = join_cls_url_to_href
2200
2201
    @classmethod
2202
    def get_archive_elements(cls):
2203
        archive_url = urljoin_wrapper(cls.url, 'all')
2204
        post_list = get_soup_at_url(archive_url).find('ul', class_='post-list')
2205
        return reversed(post_list.find_all('a', class_='post-link'))
2206
2207
    @classmethod
2208
    def get_comic_info(cls, soup, archive_elt):
2209
        """Get information about a particular comics."""
2210
        title = soup.find('meta', property='og:title')['content']
2211
        imgs = soup.find_all('meta', property='og:image')
2212
        return {
2213
            'title': title,
2214
            'img': [i['content'] for i in imgs],
2215
        }
2216
2217
2218
class ThingsInSquares(GenericListableComic):
2219
    """Class to retrieve Things In Squares comics."""
2220
    # This can be retrieved in other languages
2221
    # Also on https://tapastic.com/series/Things-in-Squares
2222
    name = 'squares'
2223
    long_name = 'Things in squares'
2224
    url = 'http://www.thingsinsquares.com'
2225
2226
    @classmethod
2227
    def get_comic_info(cls, soup, tr):
2228
        """Get information about a particular comics."""
2229
        _, td2, td3 = tr.find_all('td')
2230
        a = td2.find('a')
2231
        date_str = td3.string
2232
        day = string_to_date(date_str, "%m.%d.%y")
2233
        title = a.string
2234
        title2 = soup.find('meta', property='og:title')['content']
2235
        desc = soup.find('meta', property='og:description')
2236
        description = desc['content'] if desc else ''
2237
        tags = ' '.join(t['content'] for t in soup.find_all('meta', property='article:tag'))
2238
        imgs = soup.find_all('meta', property='og:image')
2239
        return {
2240
            'day': day.day,
2241
            'month': day.month,
2242
            'year': day.year,
2243
            'title': title,
2244
            'title2': title2,
2245
            'description': description,
2246
            'tags': tags,
2247
            'img': [i['content'] for i in imgs],
2248
        }
2249
2250
    @classmethod
2251
    def get_url_from_archive_element(cls, tr):
2252
        _, td2, __ = tr.find_all('td')
2253
        return td2.find('a')['href']
2254
2255
    @classmethod
2256
    def get_archive_elements(cls):
2257
        archive_url = urljoin_wrapper(cls.url, 'archive-2')
2258
        return reversed(get_soup_at_url(archive_url).find('tbody').find_all('tr'))
2259
2260
2261
class HappleTea(GenericNavigableComic):
2262
    """Class to retrieve Happle Tea Comics."""
2263
    name = 'happletea'
2264 View Code Duplication
    long_name = 'Happle Tea'
2265
    url = 'http://www.happletea.com'
2266
    get_first_comic_link = get_a_navi_navifirst
2267
    get_navi_link = get_link_rel_next
2268
2269
    @classmethod
2270
    def get_comic_info(cls, soup, link):
2271
        """Get information about a particular comics."""
2272
        imgs = soup.find('div', id='comic').find_all('img')
2273
        post = soup.find('div', class_='post-content')
2274
        title = post.find('h2', class_='post-title').string
2275
        author = post.find('a', rel='author').string
2276
        date_str = post.find('span', class_='post-date').string
2277
        day = string_to_date(date_str, "%B %d, %Y")
2278
        assert all(i['alt'] == i['title'] for i in imgs)
2279
        return {
2280
            'title': title,
2281
            'img': [i['src'] for i in imgs],
2282
            'alt': ''.join(i['alt'] for i in imgs),
2283
            'month': day.month,
2284
            'year': day.year,
2285
            'day': day.day,
2286
            'author': author,
2287
        }
2288
2289
2290
class RockPaperScissors(GenericNavigableComic):
2291
    """Class to retrieve Rock Paper Scissors comics."""
2292
    name = 'rps'
2293
    long_name = 'Rock Paper Scissors'
2294
    url = 'http://rps-comics.com'
2295
    get_first_comic_link = get_a_navi_navifirst
2296
    get_navi_link = get_link_rel_next
2297
2298
    @classmethod
2299
    def get_comic_info(cls, soup, link):
2300
        """Get information about a particular comics."""
2301
        title = soup.find('title').string
2302
        imgs = soup.find_all('meta', property='og:image')
2303
        short_url = soup.find('link', rel='shortlink')['href']
2304
        transcript = soup.find('div', id='transcript-content').string
2305
        return {
2306
            'title': title,
2307
            'transcript': transcript,
2308
            'short_url': short_url,
2309
            'img': [i['content'] for i in imgs],
2310
        }
2311
2312
2313
class FatAwesomeComics(GenericNavigableComic):
2314
    """Class to retrieve Fat Awesome Comics."""
2315
    # Also on http://fatawesomecomedy.tumblr.com
2316
    name = 'fatawesome'
2317
    long_name = 'Fat Awesome'
2318
    url = 'http://fatawesome.com/comics'
2319
    get_navi_link = get_a_rel_next
2320
    get_first_comic_link = simulate_first_link
2321
    first_url = 'http://fatawesome.com/shortbus/'
2322
2323
    @classmethod
2324
    def get_comic_info(cls, soup, link):
2325
        """Get information about a particular comics."""
2326
        title = soup.find('meta', attrs={'name': 'twitter:title'})['content']
2327
        description = soup.find('meta', attrs={'name': 'description'})['content']
2328
        tags_prop = soup.find('meta', property='article:tag')
2329
        tags = tags_prop['content'] if tags_prop else ""
2330
        date_str = soup.find('meta', property='article:published_time')['content'][:10]
2331
        day = string_to_date(date_str, "%Y-%m-%d")
2332
        imgs = soup.find_all('img', attrs={'data-recalc-dims': "1"})
2333
        assert len(imgs) == 1, imgs
2334
        return {
2335
            'title': title,
2336
            'description': description,
2337
            'tags': tags,
2338
            'alt': "".join(i['alt'] for i in imgs),
2339
            'img': [i['src'].rsplit('?', 1)[0] for i in imgs],
2340
            'month': day.month,
2341
            'year': day.year,
2342
            'day': day.day,
2343
        }
2344
2345
2346
class PeterLauris(GenericNavigableComic):
2347
    """Class to retrieve Peter Lauris comics."""
2348
    name = 'peterlauris'
2349 View Code Duplication
    long_name = 'Peter Lauris'
2350
    url = 'http://peterlauris.com/comics'
2351
    get_navi_link = get_a_rel_next
2352
    get_first_comic_link = simulate_first_link
2353
    first_url = 'http://peterlauris.com/comics/just-in-case/'
2354
2355
    @classmethod
2356
    def get_comic_info(cls, soup, link):
2357
        """Get information about a particular comics."""
2358
        title = soup.find('meta', attrs={'name': 'twitter:title'})['content']
2359
        date_str = soup.find('meta', property='article:published_time')['content'][:10]
2360
        day = string_to_date(date_str, "%Y-%m-%d")
2361
        imgs = soup.find_all('meta', property='og:image')
2362
        return {
2363
            'title': title,
2364
            'img': [i['content'] for i in imgs],
2365
            'month': day.month,
2366
            'year': day.year,
2367
            'day': day.day,
2368
        }
2369
2370
2371
class JuliasDrawings(GenericListableComic):
2372
    """Class to retrieve Julia's Drawings."""
2373
    name = 'julia'
2374 View Code Duplication
    long_name = "Julia's Drawings"
2375
    url = 'https://drawings.jvns.ca'
2376
    get_url_from_archive_element = get_href
2377
2378
    @classmethod
2379
    def get_archive_elements(cls):
2380
        div = get_soup_at_url(cls.url).find('div', class_='drawings')
2381
        return reversed(div.find_all('a'))
2382
2383
    @classmethod
2384
    def get_comic_info(cls, soup, archive_elt):
2385
        """Get information about a particular comics."""
2386
        date_str = soup.find('meta', property='og:article:published_time')['content'][:10]
2387
        day = string_to_date(date_str, "%Y-%m-%d")
2388
        title = soup.find('h3', class_='p-post-title').string
2389
        imgs = soup.find('section', class_='post-content').find_all('img')
2390
        return {
2391
            'title': title,
2392
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
2393
            'month': day.month,
2394
            'year': day.year,
2395
            'day': day.day,
2396
        }
2397
2398
2399
class AnythingComic(GenericListableComic):
2400
    """Class to retrieve Anything Comics."""
2401
    # Also on http://tapastic.com/series/anything
2402
    name = 'anythingcomic'
2403
    long_name = 'Anything Comic'
2404
    url = 'http://www.anythingcomic.com'
2405
2406
    @classmethod
2407
    def get_archive_elements(cls):
2408
        archive_url = urljoin_wrapper(cls.url, 'archive/')
2409
        # The first 2 <tr>'s do not correspond to comics
2410
        return get_soup_at_url(archive_url).find('table', id='chapter_table').find_all('tr')[2:]
2411
2412
    @classmethod
2413
    def get_url_from_archive_element(cls, tr):
2414
        """Get url corresponding to an archive element."""
2415
        _, td_comic, td_date, _ = tr.find_all('td')
2416
        link = td_comic.find('a')
2417
        return urljoin_wrapper(cls.url, link['href'])
2418
2419
    @classmethod
2420
    def get_comic_info(cls, soup, tr):
2421
        """Get information about a particular comics."""
2422
        td_num, td_comic, td_date, _ = tr.find_all('td')
2423
        num = int(td_num.string)
2424
        link = td_comic.find('a')
2425
        title = link.string
2426
        imgs = soup.find_all('img', id='comic_image')
2427
        date_str = td_date.string
2428
        day = string_to_date(remove_st_nd_rd_th_from_date(date_str), "%B %d, %Y, %I:%M %p")
2429
        assert len(imgs) == 1, imgs
2430
        assert all(i.get('alt') == i.get('title') for i in imgs)
2431
        return {
2432
            'num': num,
2433
            'title': title,
2434
            'alt': imgs[0].get('alt', ''),
2435
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
2436
            'month': day.month,
2437
            'year': day.year,
2438
            'day': day.day,
2439
        }
2440
2441
2442
class LonnieMillsap(GenericNavigableComic):
2443
    """Class to retrieve Lonnie Millsap's comics."""
2444
    name = 'millsap'
2445 View Code Duplication
    long_name = 'Lonnie Millsap'
2446
    url = 'http://www.lonniemillsap.com'
2447
    get_navi_link = get_link_rel_next
2448
    get_first_comic_link = simulate_first_link
2449
    first_url = 'http://www.lonniemillsap.com/?p=42'
2450
2451
    @classmethod
2452
    def get_comic_info(cls, soup, link):
2453
        """Get information about a particular comics."""
2454
        title = soup.find('h2', class_='post-title').string
2455
        post = soup.find('div', class_='post-content')
2456
        author = post.find("span", class_="post-author").find("a").string
2457
        date_str = post.find("span", class_="post-date").string
2458
        day = string_to_date(date_str, "%B %d, %Y")
2459
        imgs = post.find("div", class_="entry").find_all("img")
2460
        return {
2461
            'title': title,
2462
            'author': author,
2463
            'img': [i['src'] for i in imgs],
2464
            'month': day.month,
2465
            'year': day.year,
2466
            'day': day.day,
2467
        }
2468
2469
2470
class LinsEditions(GenericDeletedComic):  # Permanently moved to warandpeas
2471
    """Class to retrieve L.I.N.S. Editions comics."""
2472
    # Also on https://linscomics.tumblr.com
2473
    # Now on https://warandpeas.com
2474
    name = 'lins'
2475
    long_name = 'L.I.N.S. Editions'
2476
    url = 'https://linsedition.com'
2477
    _categories = ('WARANDPEAS', 'LINS')
2478
2479
2480
class WarAndPeas(GenericNavigableComic):
2481
    """Class to retrieve War And Peas comics."""
2482
    name = 'warandpeas'
2483 View Code Duplication
    long_name = 'War And Peas'
2484
    url = 'https://warandpeas.com'
2485
    get_navi_link = get_link_rel_next
2486
    get_first_comic_link = simulate_first_link
2487
    first_url = 'https://warandpeas.com/2011/11/07/565/'
2488
    _categories = ('WARANDPEAS', 'LINS')
2489
2490
    @classmethod
2491
    def get_comic_info(cls, soup, link):
2492
        """Get information about a particular comics."""
2493
        title = soup.find('meta', property='og:title')['content']
2494
        imgs = soup.find_all('meta', property='og:image')
2495
        date_str = soup.find('meta', property='article:published_time')['content'][:10]
2496
        day = string_to_date(date_str, "%Y-%m-%d")
2497
        return {
2498
            'title': title,
2499
            'img': [i['content'] for i in imgs],
2500
            'month': day.month,
2501
            'year': day.year,
2502
            'day': day.day,
2503
        }
2504
2505
2506
class ThorsThundershack(GenericNavigableComic):
2507
    """Class to retrieve Thor's Thundershack comics."""
2508
    # Also on http://tapastic.com/series/Thors-Thundershac
2509
    name = 'thor'
2510
    long_name = 'Thor\'s Thundershack'
2511
    url = 'http://www.thorsthundershack.com'
2512
    _categories = ('THOR', )
2513
    get_url_from_link = join_cls_url_to_href
2514
2515
    @classmethod
2516
    def get_first_comic_link(cls):
2517
        """Get link to first comics."""
2518
        return get_soup_at_url(cls.url).find('a', class_='first navlink')
2519
2520
    @classmethod
2521
    def get_navi_link(cls, last_soup, next_):
2522
        """Get link to next or previous comic."""
2523
        for link in last_soup.find_all('a', rel='next' if next_ else 'prev'):
2524
            if link['href'] != '/comic':
2525
                return link
2526
        return None
2527
2528
    @classmethod
2529
    def get_comic_info(cls, soup, link):
2530
        """Get information about a particular comics."""
2531
        title = soup.find('meta', attrs={'name': 'description'})["content"]
2532
        description = soup.find('div', itemprop='articleBody').text
2533
        author = soup.find('span', itemprop='author copyrightHolder').string
2534
        imgs = soup.find_all('img', itemprop='image')
2535
        assert all(i['title'] == i['alt'] for i in imgs)
2536
        alt = imgs[0]['alt'] if imgs else ""
2537
        date_str = soup.find('time', itemprop='datePublished')["datetime"]
2538
        day = string_to_date(date_str, "%Y-%m-%d %H:%M:%S")
2539
        return {
2540
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
2541
            'month': day.month,
2542
            'year': day.year,
2543
            'day': day.day,
2544
            'author': author,
2545
            'title': title,
2546
            'alt': alt,
2547
            'description': description,
2548
        }
2549
2550
2551
class GerbilWithAJetpack(GenericNavigableComic):
2552
    """Class to retrieve GerbilWithAJetpack comics."""
2553
    name = 'gerbil'
2554 View Code Duplication
    long_name = 'Gerbil With A Jetpack'
2555
    url = 'http://gerbilwithajetpack.com'
2556
    get_first_comic_link = get_a_navi_navifirst
2557
    get_navi_link = get_a_rel_next
2558
2559
    @classmethod
2560
    def get_comic_info(cls, soup, link):
2561
        """Get information about a particular comics."""
2562
        title = soup.find('h2', class_='post-title').string
2563
        author = soup.find("span", class_="post-author").find("a").string
2564
        date_str = soup.find("span", class_="post-date").string
2565
        day = string_to_date(date_str, "%B %d, %Y")
2566
        imgs = soup.find("div", id="comic").find_all("img")
2567
        alt = imgs[0]['alt']
2568
        assert all(i['alt'] == i['title'] == alt for i in imgs)
2569
        return {
2570
            'img': [i['src'] for i in imgs],
2571
            'title': title,
2572
            'alt': alt,
2573
            'author': author,
2574
            'day': day.day,
2575
            'month': day.month,
2576
            'year': day.year
2577
        }
2578
2579
2580
class EveryDayBlues(GenericDeletedComic, GenericNavigableComic):
2581
    """Class to retrieve EveryDayBlues Comics."""
2582
    name = "blues"
2583 View Code Duplication
    long_name = "Every Day Blues"
2584
    url = "http://everydayblues.net"
2585
    get_first_comic_link = get_a_navi_navifirst
2586
    get_navi_link = get_link_rel_next
2587
2588
    @classmethod
2589
    def get_comic_info(cls, soup, link):
2590
        """Get information about a particular comics."""
2591
        title = soup.find("h2", class_="post-title").string
2592
        author = soup.find("span", class_="post-author").find("a").string
2593
        date_str = soup.find("span", class_="post-date").string
2594
        day = string_to_date(date_str, "%d. %B %Y", "de_DE.utf8")
2595
        imgs = soup.find("div", id="comic").find_all("img")
2596
        assert all(i['alt'] == i['title'] == title for i in imgs)
2597
        assert len(imgs) <= 1, imgs
2598
        return {
2599
            'img': [i['src'] for i in imgs],
2600
            'title': title,
2601
            'author': author,
2602
            'day': day.day,
2603
            'month': day.month,
2604
            'year': day.year
2605
        }
2606
2607
2608
class BiterComics(GenericNavigableComic):
2609
    """Class to retrieve Biter Comics."""
2610
    name = "biter"
2611 View Code Duplication
    long_name = "Biter Comics"
2612
    url = "http://www.bitercomics.com"
2613
    get_first_comic_link = get_a_navi_navifirst
2614
    get_navi_link = get_link_rel_next
2615
2616
    @classmethod
2617
    def get_comic_info(cls, soup, link):
2618
        """Get information about a particular comics."""
2619
        title = soup.find("h1", class_="entry-title").string
2620
        author = soup.find("span", class_="author vcard").find("a").string
2621
        date_str = soup.find("span", class_="entry-date").string
2622
        day = string_to_date(date_str, "%B %d, %Y")
2623
        imgs = soup.find("div", id="comic").find_all("img")
2624
        assert all(i['alt'] == i['title'] for i in imgs)
2625
        assert len(imgs) == 1, imgs
2626
        alt = imgs[0]['alt']
2627
        return {
2628
            'img': [i['src'] for i in imgs],
2629
            'title': title,
2630
            'alt': alt,
2631
            'author': author,
2632
            'day': day.day,
2633
            'month': day.month,
2634
            'year': day.year
2635
        }
2636
2637
2638
class TheAwkwardYeti(GenericNavigableComic):
2639
    """Class to retrieve The Awkward Yeti comics."""
2640
    # Also on http://www.gocomics.com/the-awkward-yeti
2641 View Code Duplication
    # Also on http://larstheyeti.tumblr.com
2642
    # Also on https://tapastic.com/series/TheAwkwardYeti
2643
    name = 'yeti'
2644
    long_name = 'The Awkward Yeti'
2645
    url = 'http://theawkwardyeti.com'
2646
    _categories = ('YETI', )
2647
    get_first_comic_link = get_a_navi_navifirst
2648
    get_navi_link = get_link_rel_next
2649
2650
    @classmethod
2651
    def get_comic_info(cls, soup, link):
2652
        """Get information about a particular comics."""
2653
        title = soup.find('h2', class_='post-title').string
2654
        date_str = soup.find("span", class_="post-date").string
2655
        day = string_to_date(date_str, "%B %d, %Y")
2656
        imgs = soup.find("div", id="comic").find_all("img")
2657
        assert all(idx > 0 or i['alt'] == i['title'] for idx, i in enumerate(imgs))
2658
        return {
2659
            'img': [i['src'] for i in imgs],
2660
            'title': title,
2661
            'day': day.day,
2662
            'month': day.month,
2663
            'year': day.year
2664
        }
2665
2666
2667
class PleasantThoughts(GenericNavigableComic):
2668
    """Class to retrieve Pleasant Thoughts comics."""
2669
    name = 'pleasant'
2670
    long_name = 'Pleasant Thoughts'
2671
    url = 'http://pleasant-thoughts.com'
2672
    get_first_comic_link = get_a_navi_navifirst
2673
    get_navi_link = get_link_rel_next
2674
2675
    @classmethod
2676
    def get_comic_info(cls, soup, link):
2677
        """Get information about a particular comics."""
2678
        post = soup.find('div', class_='post-content')
2679
        title = post.find('h2', class_='post-title').string
2680
        imgs = post.find("div", class_="entry").find_all("img")
2681
        return {
2682
            'title': title,
2683
            'img': [i['src'] for i in imgs],
2684
        }
2685
2686
2687
class MisterAndMe(GenericNavigableComic):
2688
    """Class to retrieve Mister & Me Comics."""
2689
    # Also on http://www.gocomics.com/mister-and-me
2690
    # Also on https://tapastic.com/series/Mister-and-Me
2691
    name = 'mister'
2692
    long_name = 'Mister & Me'
2693
    url = 'http://www.mister-and-me.com'
2694
    get_first_comic_link = get_a_comicnavbase_comicnavfirst
2695
    get_navi_link = get_link_rel_next
2696
2697
    @classmethod
2698
    def get_comic_info(cls, soup, link):
2699
        """Get information about a particular comics."""
2700
        title = soup.find('h2', class_='post-title').string
2701
        author = soup.find("span", class_="post-author").find("a").string
2702
        date_str = soup.find("span", class_="post-date").string
2703
        day = string_to_date(date_str, "%B %d, %Y")
2704
        imgs = soup.find("div", id="comic").find_all("img")
2705
        assert all(i['alt'] == i['title'] for i in imgs)
2706
        assert len(imgs) <= 1, imgs
2707
        alt = imgs[0]['alt'] if imgs else ""
2708
        return {
2709
            'img': [i['src'] for i in imgs],
2710
            'title': title,
2711
            'alt': alt,
2712
            'author': author,
2713
            'day': day.day,
2714
            'month': day.month,
2715
            'year': day.year
2716
        }
2717
2718
2719
class LastPlaceComics(GenericNavigableComic):
2720
    """Class to retrieve Last Place Comics."""
2721
    name = 'lastplace'
2722
    long_name = 'Last Place Comics'
2723
    url = "http://lastplacecomics.com"
2724
    get_first_comic_link = get_a_comicnavbase_comicnavfirst
2725
    get_navi_link = get_link_rel_next
2726
2727
    @classmethod
2728
    def get_comic_info(cls, soup, link):
2729
        """Get information about a particular comics."""
2730
        title = soup.find('h2', class_='post-title').string
2731
        author = soup.find("span", class_="post-author").find("a").string
2732
        date_str = soup.find("span", class_="post-date").string
2733
        day = string_to_date(date_str, "%B %d, %Y")
2734
        imgs = soup.find("div", id="comic").find_all("img")
2735
        assert all(i['alt'] == i['title'] for i in imgs)
2736
        assert len(imgs) <= 1, imgs
2737
        alt = imgs[0]['alt'] if imgs else ""
2738
        return {
2739
            'img': [i['src'] for i in imgs],
2740
            'title': title,
2741
            'alt': alt,
2742
            'author': author,
2743
            'day': day.day,
2744
            'month': day.month,
2745
            'year': day.year
2746
        }
2747
2748
2749
class TalesOfAbsurdity(GenericNavigableComic):
2750
    """Class to retrieve Tales Of Absurdity comics."""
2751
    # Also on http://tapastic.com/series/Tales-Of-Absurdity
2752
    # Also on http://talesofabsurdity.tumblr.com
2753
    name = 'absurdity'
2754
    long_name = 'Tales of Absurdity'
2755
    url = 'http://talesofabsurdity.com'
2756
    _categories = ('ABSURDITY', )
2757
    get_first_comic_link = get_a_navi_navifirst
2758
    get_navi_link = get_a_navi_comicnavnext_navinext
2759
2760
    @classmethod
2761
    def get_comic_info(cls, soup, link):
2762
        """Get information about a particular comics."""
2763
        title = soup.find('h2', class_='post-title').string
2764
        author = soup.find("span", class_="post-author").find("a").string
2765
        date_str = soup.find("span", class_="post-date").string
2766
        day = string_to_date(date_str, "%B %d, %Y")
2767
        imgs = soup.find("div", id="comic").find_all("img")
2768
        assert all(i['alt'] == i['title'] for i in imgs)
2769
        alt = imgs[0]['alt'] if imgs else ""
2770
        return {
2771
            'img': [i['src'] for i in imgs],
2772
            'title': title,
2773
            'alt': alt,
2774
            'author': author,
2775
            'day': day.day,
2776
            'month': day.month,
2777
            'year': day.year
2778
        }
2779
2780
2781
class EndlessOrigami(GenericComicNotWorking, GenericNavigableComic):  # Nav not working
2782
    """Class to retrieve Endless Origami Comics."""
2783
    name = "origami"
2784
    long_name = "Endless Origami"
2785
    url = "http://endlessorigami.com"
2786
    get_first_comic_link = get_a_navi_navifirst
2787
    get_navi_link = get_link_rel_next
2788
2789
    @classmethod
2790
    def get_comic_info(cls, soup, link):
2791
        """Get information about a particular comics."""
2792
        title = soup.find('h2', class_='post-title').string
2793
        author = soup.find("span", class_="post-author").find("a").string
2794
        date_str = soup.find("span", class_="post-date").string
2795
        day = string_to_date(date_str, "%B %d, %Y")
2796
        imgs = soup.find("div", id="comic").find_all("img")
2797
        assert all(i['alt'] == i['title'] for i in imgs)
2798
        alt = imgs[0]['alt'] if imgs else ""
2799
        return {
2800
            'img': [i['src'] for i in imgs],
2801
            'title': title,
2802
            'alt': alt,
2803
            'author': author,
2804
            'day': day.day,
2805
            'month': day.month,
2806
            'year': day.year
2807
        }
2808
2809
2810
class PlanC(GenericNavigableComic):
2811
    """Class to retrieve Plan C comics."""
2812
    name = 'planc'
2813
    long_name = 'Plan C'
2814
    url = 'http://www.plancomic.com'
2815
    get_first_comic_link = get_a_navi_navifirst
2816
    get_navi_link = get_a_navi_comicnavnext_navinext
2817
2818
    @classmethod
2819
    def get_comic_info(cls, soup, link):
2820
        """Get information about a particular comics."""
2821
        title = soup.find('h2', class_='post-title').string
2822
        date_str = soup.find("span", class_="post-date").string
2823
        day = string_to_date(date_str, "%B %d, %Y")
2824
        imgs = soup.find('div', id='comic').find_all('img')
2825
        return {
2826
            'title': title,
2827
            'img': [i['src'] for i in imgs],
2828
            'month': day.month,
2829
            'year': day.year,
2830
            'day': day.day,
2831
        }
2832
2833
2834
class BuniComic(GenericNavigableComic):
2835
    """Class to retrieve Buni Comics."""
2836
    name = 'buni'
2837 View Code Duplication
    long_name = 'BuniComics'
2838
    url = 'http://www.bunicomic.com'
2839
    get_first_comic_link = get_a_comicnavbase_comicnavfirst
2840
    get_navi_link = get_link_rel_next
2841
2842
    @classmethod
2843
    def get_comic_info(cls, soup, link):
2844
        """Get information about a particular comics."""
2845
        imgs = soup.find('div', id='comic').find_all('img')
2846
        assert all(i['alt'] == i['title'] for i in imgs)
2847
        assert len(imgs) == 1, imgs
2848
        return {
2849
            'img': [i['src'] for i in imgs],
2850
            'title': imgs[0]['title'],
2851
        }
2852
2853
2854
class GenericCommitStrip(GenericNavigableComic):
2855
    """Generic class to retrieve Commit Strips in different languages."""
2856
    get_navi_link = get_a_rel_next
2857 View Code Duplication
    get_first_comic_link = simulate_first_link
2858
    first_url = NotImplemented
2859
2860
    @classmethod
2861
    def get_comic_info(cls, soup, link):
2862
        """Get information about a particular comics."""
2863
        desc = soup.find('meta', property='og:description')['content']
2864
        title = soup.find('meta', property='og:title')['content']
2865
        imgs = soup.find('div', class_='entry-content').find_all('img')
2866
        title2 = ' '.join(i.get('title', '') for i in imgs)
2867
        return {
2868
            'title': title,
2869
            'title2': title2,
2870
            'description': desc,
2871
            'img': [urljoin_wrapper(cls.url, convert_iri_to_plain_ascii_uri(i['src'])) for i in imgs],
2872
        }
2873
2874
2875
class CommitStripFr(GenericCommitStrip):
2876
    """Class to retrieve Commit Strips in French."""
2877
    name = 'commit_fr'
2878
    long_name = 'Commit Strip (Fr)'
2879
    url = 'http://www.commitstrip.com/fr'
2880
    _categories = ('FRANCAIS', )
2881
    first_url = 'http://www.commitstrip.com/fr/2012/02/22/interview/'
2882
2883
2884
class CommitStripEn(GenericCommitStrip):
2885
    """Class to retrieve Commit Strips in English."""
2886
    name = 'commit_en'
2887
    long_name = 'Commit Strip (En)'
2888
    url = 'http://www.commitstrip.com/en'
2889
    first_url = 'http://www.commitstrip.com/en/2012/02/22/interview/'
2890
2891
2892
class GenericBoumerie(GenericNavigableComic):
2893
    """Generic class to retrieve Boumeries comics in different languages."""
2894
    # Also on http://boumeries.tumblr.com
2895 View Code Duplication
    get_first_comic_link = get_a_navi_navifirst
2896
    get_navi_link = get_link_rel_next
2897
    date_format = NotImplemented
2898
    lang = NotImplemented
2899
2900
    @classmethod
2901
    def get_comic_info(cls, soup, link):
2902
        """Get information about a particular comics."""
2903
        title = soup.find('h2', class_='post-title').string
2904
        short_url = soup.find('link', rel='shortlink')['href']
2905
        author = soup.find("span", class_="post-author").find("a").string
2906
        date_str = soup.find('span', class_='post-date').string
2907
        day = string_to_date(date_str, cls.date_format, cls.lang)
2908
        imgs = soup.find('div', id='comic').find_all('img')
2909
        assert all(i['alt'] == i['title'] for i in imgs)
2910
        return {
2911
            'short_url': short_url,
2912
            'img': [i['src'] for i in imgs],
2913
            'title': title,
2914
            'author': author,
2915
            'month': day.month,
2916
            'year': day.year,
2917
            'day': day.day,
2918
        }
2919
2920
2921
class BoumerieEn(GenericBoumerie):
2922
    """Class to retrieve Boumeries comics in English."""
2923
    name = 'boumeries_en'
2924
    long_name = 'Boumeries (En)'
2925
    url = 'http://comics.boumerie.com'
2926
    _categories = ('BOUMERIES', )
2927
    date_format = "%B %d, %Y"
2928
    lang = 'en_GB.UTF-8'
2929
2930
2931
class BoumerieFr(GenericBoumerie):
2932
    """Class to retrieve Boumeries comics in French."""
2933
    name = 'boumeries_fr'
2934
    long_name = 'Boumeries (Fr)'
2935
    url = 'http://bd.boumerie.com'
2936
    _categories = ('BOUMERIES', 'FRANCAIS')
2937
    date_format = "%B %d, %Y"  # Used to be "%A, %d %B %Y"
2938
    lang = "fr_FR.utf8"
2939
2940
2941
class UnearthedComics(GenericNavigableComic):
2942
    """Class to retrieve Unearthed comics."""
2943
    # Also on http://tapastic.com/series/UnearthedComics
2944 View Code Duplication
    # Also on https://unearthedcomics.tumblr.com
2945
    name = 'unearthed'
2946
    long_name = 'Unearthed Comics'
2947
    url = 'http://unearthedcomics.com'
2948
    _categories = ('UNEARTHED', )
2949
    get_navi_link = get_link_rel_next
2950
    get_first_comic_link = simulate_first_link
2951
    first_url = 'http://unearthedcomics.com/comics/world-with-turn-signals/'
2952
2953
    @classmethod
2954
    def get_comic_info(cls, soup, link):
2955
        """Get information about a particular comics."""
2956
        short_url = soup.find('link', rel='shortlink')['href']
2957
        title_elt = soup.find('h1') or soup.find('h2')
2958
        title = title_elt.string if title_elt else ""
2959
        desc = soup.find('meta', property='og:description')
2960
        date_str = soup.find('time', class_='published updated hidden')['datetime']
2961
        day = string_to_date(date_str, "%Y-%m-%d")
2962
        post = soup.find('div', class_="entry content entry-content type-portfolio")
2963
        imgs = post.find_all('img')
2964
        return {
2965
            'title': title,
2966
            'description': desc,
2967
            'url2': short_url,
2968
            'img': [i['src'] for i in imgs],
2969
            'month': day.month,
2970
            'year': day.year,
2971
            'day': day.day,
2972
        }
2973
2974
2975
class Optipess(GenericNavigableComic):
2976
    """Class to retrieve Optipess comics."""
2977
    name = 'optipess'
2978
    long_name = 'Optipess'
2979
    url = 'http://www.optipess.com'
2980
    get_first_comic_link = get_a_navi_navifirst
2981
    get_navi_link = get_link_rel_next
2982
2983
    @classmethod
2984
    def get_comic_info(cls, soup, link):
2985
        """Get information about a particular comics."""
2986
        title = soup.find('h2', class_='post-title').string
2987
        author = soup.find("span", class_="post-author").find("a").string
2988
        comic = soup.find('div', id='comic')
2989
        imgs = comic.find_all('img') if comic else []
2990
        alt = imgs[0]['title'] if imgs else ""
2991
        assert all(i['alt'] == i['title'] == alt for i in imgs)
2992
        date_str = soup.find('span', class_='post-date').string
2993
        day = string_to_date(date_str, "%B %d, %Y")
2994
        return {
2995
            'title': title,
2996
            'alt': alt,
2997
            'author': author,
2998
            'img': [i['src'] for i in imgs],
2999
            'month': day.month,
3000
            'year': day.year,
3001
            'day': day.day,
3002
        }
3003
3004
3005
class PainTrainComic(GenericNavigableComic):
3006
    """Class to retrieve Pain Train Comics."""
3007
    name = 'paintrain'
3008
    long_name = 'Pain Train Comics'
3009
    url = 'http://paintraincomic.com'
3010
    get_first_comic_link = get_a_navi_navifirst
3011
    get_navi_link = get_link_rel_next
3012
3013
    @classmethod
3014
    def get_comic_info(cls, soup, link):
3015
        """Get information about a particular comics."""
3016
        title = soup.find('h2', class_='post-title').string
3017
        short_url = soup.find('link', rel='shortlink')['href']
3018
        short_url_re = re.compile('^%s/\\?p=([0-9]*)' % cls.url)
3019
        num = int(short_url_re.match(short_url).groups()[0])
3020
        imgs = soup.find('div', id='comic').find_all('img')
3021
        alt = imgs[0]['title']
3022
        assert all(i['alt'] == i['title'] == alt for i in imgs)
3023
        date_str = soup.find('span', class_='post-date').string
3024
        day = string_to_date(date_str, "%d/%m/%Y")
3025
        return {
3026
            'short_url': short_url,
3027
            'num': num,
3028
            'img': [i['src'] for i in imgs],
3029
            'month': day.month,
3030
            'year': day.year,
3031
            'day': day.day,
3032
            'alt': alt,
3033
            'title': title,
3034
        }
3035
3036
3037
class MoonBeard(GenericNavigableComic):
3038
    """Class to retrieve MoonBeard comics."""
3039
    # Also on http://squireseses.tumblr.com
3040
    # Also on http://www.webtoons.com/en/comedy/moon-beard/list?title_no=471
3041
    name = 'moonbeard'
3042
    long_name = 'Moon Beard'
3043
    url = 'http://moonbeard.com'
3044
    _categories = ('MOONBEARD', )
3045
    get_first_comic_link = get_a_navi_navifirst
3046
    get_navi_link = get_a_navi_navinext
3047
3048
    @classmethod
3049
    def get_comic_info(cls, soup, link):
3050
        """Get information about a particular comics."""
3051
        title = soup.find('h2', class_='post-title').string
3052
        short_url = soup.find('link', rel='shortlink')['href']
3053
        short_url_re = re.compile('^%s/\\?p=([0-9]*)' % cls.url)
3054
        num = int(short_url_re.match(short_url).groups()[0])
3055
        imgs = soup.find('div', id='comic').find_all('img')
3056
        alt = imgs[0]['title']
3057
        assert all(i['alt'] == i['title'] == alt for i in imgs)
3058
        date_str = soup.find('span', class_='post-date').string
3059
        day = string_to_date(date_str, "%B %d, %Y")
3060
        tags = ' '.join(t['content'] for t in soup.find_all('meta', property='article:tag'))
3061
        author = soup.find('span', class_='post-author').string
3062
        return {
3063
            'short_url': short_url,
3064
            'num': num,
3065
            'img': [i['src'] for i in imgs],
3066
            'month': day.month,
3067
            'year': day.year,
3068
            'day': day.day,
3069
            'title': title,
3070
            'tags': tags,
3071
            'alt': alt,
3072
            'author': author,
3073
        }
3074
3075
3076
class SystemComic(GenericNavigableComic):
3077
    """Class to retrieve System Comic."""
3078
    name = 'system'
3079
    long_name = 'System Comic'
3080
    url = 'http://www.systemcomic.com'
3081
    get_navi_link = get_a_rel_next
3082
3083
    @classmethod
3084
    def get_first_comic_link(cls):
3085
        """Get link to first comics."""
3086
        return get_soup_at_url(cls.url).find('li', class_='first').find('a')
3087
3088
    @classmethod
3089
    def get_comic_info(cls, soup, link):
3090
        """Get information about a particular comics."""
3091
        title = soup.find('meta', property='og:title')['content']
3092
        desc = soup.find('meta', property='og:description')['content']
3093
        date_str = soup.find('time')["datetime"]
3094
        day = string_to_date(date_str, "%Y-%m-%d")
3095
        imgs = soup.find('figure').find_all('img')
3096
        return {
3097
            'title': title,
3098
            'description': desc,
3099
            'day': day.day,
3100
            'month': day.month,
3101
            'year': day.year,
3102
            'img': [i['src'] for i in imgs],
3103
        }
3104
3105
3106
class LittleLifeLines(GenericNavigableComic):
3107
    """Class to retrieve Little Life Lines comics."""
3108
    # Also on https://little-life-lines.tumblr.com
3109 View Code Duplication
    name = 'life'
3110
    long_name = 'Little Life Lines'
3111
    url = 'http://www.littlelifelines.com'
3112
    get_url_from_link = join_cls_url_to_href
3113
    get_first_comic_link = simulate_first_link
3114
    first_url = 'http://www.littlelifelines.com/comics/well-done'
3115
3116
    @classmethod
3117
    def get_navi_link(cls, last_soup, next_):
3118
        """Get link to next or previous comic."""
3119
        # prev is next / next is prev
3120
        li = last_soup.find('li', class_='prev' if next_ else 'next')
3121
        return li.find('a') if li else None
3122
3123
    @classmethod
3124
    def get_comic_info(cls, soup, link):
3125
        """Get information about a particular comics."""
3126
        title = soup.find('meta', property='og:title')['content']
3127
        desc = soup.find('meta', property='og:description')['content']
3128
        date_str = soup.find('time', class_='published')['datetime']
3129
        day = string_to_date(date_str, "%Y-%m-%d")
3130
        author = soup.find('a', rel='author').string
3131
        div_content = soup.find('div', class_="body entry-content")
3132
        imgs = div_content.find_all('img')
3133
        imgs = [i for i in imgs if i.get('src') is not None]
3134
        alt = imgs[0]['alt']
3135
        return {
3136
            'title': title,
3137
            'alt': alt,
3138
            'description': desc,
3139
            'author': author,
3140
            'day': day.day,
3141
            'month': day.month,
3142
            'year': day.year,
3143
            'img': [i['src'] for i in imgs],
3144
        }
3145
3146
3147
class GenericWordPressInkblot(GenericNavigableComic):
3148
    """Generic class to retrieve comics using WordPress with Inkblot."""
3149
    get_navi_link = get_link_rel_next
3150
3151
    @classmethod
3152
    def get_first_comic_link(cls):
3153
        """Get link to first comics."""
3154
        return get_soup_at_url(cls.url).find('a', class_='webcomic-link webcomic1-link first-webcomic-link first-webcomic1-link')
3155
3156
    @classmethod
3157
    def get_comic_info(cls, soup, link):
3158
        """Get information about a particular comics."""
3159
        title = soup.find('meta', property='og:title')['content']
3160
        imgs = soup.find('div', class_='webcomic-image').find_all('img')
3161
        date_str = soup.find('meta', property='article:published_time')['content'][:10]
3162
        day = string_to_date(date_str, "%Y-%m-%d")
3163
        return {
3164
            'title': title,
3165
            'day': day.day,
3166
            'month': day.month,
3167
            'year': day.year,
3168
            'img': [i['src'] for i in imgs],
3169
        }
3170
3171
3172
class EverythingsStupid(GenericWordPressInkblot):
3173
    """Class to retrieve Everything's stupid Comics."""
3174
    # Also on http://tapastic.com/series/EverythingsStupid
3175
    # Also on http://www.webtoons.com/en/challenge/everythings-stupid/list?title_no=14591
3176
    # Also on http://everythingsstupidcomics.tumblr.com
3177
    name = 'stupid'
3178
    long_name = "Everything's Stupid"
3179
    url = 'http://everythingsstupid.net'
3180
3181
3182
class TheIsmComics(GenericDeletedComic, GenericWordPressInkblot):
3183
    """Class to retrieve The Ism Comics."""
3184
    # Also on https://tapastic.com/series/TheIsm (?)
3185
    name = 'theism'
3186
    long_name = "The Ism"
3187
    url = 'http://www.theism-comics.com'
3188
3189
3190
class WoodenPlankStudios(GenericWordPressInkblot):
3191
    """Class to retrieve Wooden Plank Studios comics."""
3192
    name = 'woodenplank'
3193
    long_name = 'Wooden Plank Studios'
3194
    url = 'http://woodenplankstudios.com'
3195
3196
3197
class ElectricBunnyComic(GenericNavigableComic):
3198
    """Class to retrieve Electric Bunny Comics."""
3199
    # Also on http://electricbunnycomics.tumblr.com
3200
    name = 'bunny'
3201
    long_name = 'Electric Bunny Comic'
3202
    url = 'http://www.electricbunnycomics.com/View/Comic/153/Welcome+to+Hell'
3203
    get_url_from_link = join_cls_url_to_href
3204
3205
    @classmethod
3206
    def get_first_comic_link(cls):
3207
        """Get link to first comics."""
3208
        return get_soup_at_url(cls.url).find('img', alt='First').parent
3209
3210
    @classmethod
3211
    def get_navi_link(cls, last_soup, next_):
3212
        """Get link to next or previous comic."""
3213
        img = last_soup.find('img', alt='Next' if next_ else 'Back')
3214
        return img.parent if img else None
3215
3216
    @classmethod
3217
    def get_comic_info(cls, soup, link):
3218
        """Get information about a particular comics."""
3219
        title = soup.find('meta', property='og:title')['content']
3220
        imgs = soup.find_all('meta', property='og:image')
3221
        return {
3222
            'title': title,
3223
            'img': [i['content'] for i in imgs],
3224
        }
3225
3226
3227
class SheldonComics(GenericNavigableComic):
3228
    """Class to retrieve Sheldon comics."""
3229
    # Also on http://www.gocomics.com/sheldon
3230
    name = 'sheldon'
3231
    long_name = 'Sheldon Comics'
3232
    url = 'http://www.sheldoncomics.com'
3233
3234
    @classmethod
3235
    def get_first_comic_link(cls):
3236
        """Get link to first comics."""
3237
        return get_soup_at_url(cls.url).find("a", id="nav-first")
3238
3239
    @classmethod
3240
    def get_navi_link(cls, last_soup, next_):
3241
        """Get link to next or previous comic."""
3242
        for link in last_soup.find_all("a", id="nav-next" if next_ else "nav-prev"):
3243
            if link['href'] != 'http://www.sheldoncomics.com':
3244
                return link
3245
        return None
3246
3247
    @classmethod
3248
    def get_comic_info(cls, soup, link):
3249
        """Get information about a particular comics."""
3250
        imgs = soup.find("div", id="comic-foot").find_all("img")
3251
        assert all(i['alt'] == i['title'] for i in imgs)
3252
        assert len(imgs) == 1, imgs
3253
        title = imgs[0]['title']
3254
        return {
3255
            'title': title,
3256
            'img': [i['src'] for i in imgs],
3257
        }
3258
3259
3260
class ManVersusManatee(GenericNavigableComic):
3261
    """Class to retrieve Man Versus Manatee comics."""
3262
    url = 'http://manvsmanatee.com'
3263 View Code Duplication
    name = 'manvsmanatee'
3264
    long_name = 'Man Versus Manatee'
3265
    get_first_comic_link = get_a_comicnavbase_comicnavfirst
3266
    get_navi_link = get_a_comicnavbase_comicnavnext
3267
3268
    @classmethod
3269
    def get_comic_info(cls, soup, link):
3270
        """Get information about a particular comics."""
3271
        title = soup.find('h2', class_='post-title').string
3272
        imgs = soup.find('div', id='comic').find_all('img')
3273
        date_str = soup.find('span', class_='post-date').string
3274
        day = string_to_date(date_str, "%B %d, %Y")
3275
        return {
3276
            'img': [i['src'] for i in imgs],
3277
            'title': title,
3278
            'month': day.month,
3279
            'year': day.year,
3280
            'day': day.day,
3281
        }
3282
3283
3284
class TheMeerkatguy(GenericNavigableComic):
3285
    """Class to retrieve The Meerkatguy comics."""
3286
    long_name = 'The Meerkatguy'
3287
    url = 'http://www.themeerkatguy.com'
3288
    name = 'meerkatguy'
3289
    get_first_comic_link = get_a_comicnavbase_comicnavfirst
3290
    get_navi_link = get_a_comicnavbase_comicnavnext
3291
3292
    @classmethod
3293
    def get_comic_info(cls, soup, link):
3294
        """Get information about a particular comics."""
3295
        title = soup.find('title').string
3296
        imgs = soup.find_all('meta', property='og:image')
3297
        return {
3298
            'img': [i['content'] for i in imgs],
3299
            'title': title,
3300
        }
3301
3302
3303
class Ubertool(GenericNavigableComic):
3304
    """Class to retrieve Ubertool comics."""
3305
    # Also on https://ubertool.tumblr.com
3306 View Code Duplication
    # Also on https://tapastic.com/series/ubertool
3307
    name = 'ubertool'
3308
    long_name = 'Ubertool'
3309
    url = 'http://ubertoolcomic.com'
3310
    _categories = ('UBERTOOL', )
3311
    get_first_comic_link = get_a_comicnavbase_comicnavfirst
3312
    get_navi_link = get_a_comicnavbase_comicnavnext
3313
3314
    @classmethod
3315
    def get_comic_info(cls, soup, link):
3316
        """Get information about a particular comics."""
3317
        title = soup.find('h2', class_='post-title').string
3318
        date_str = soup.find('span', class_='post-date').string
3319
        day = string_to_date(date_str, "%B %d, %Y")
3320
        imgs = soup.find('div', id='comic').find_all('img')
3321
        return {
3322
            'img': [i['src'] for i in imgs],
3323
            'title': title,
3324
            'month': day.month,
3325
            'year': day.year,
3326
            'day': day.day,
3327
        }
3328
3329
3330
class EarthExplodes(GenericNavigableComic):
3331
    """Class to retrieve The Earth Explodes comics."""
3332
    name = 'earthexplodes'
3333
    long_name = 'The Earth Explodes'
3334
    url = 'http://www.earthexplodes.com'
3335
    get_url_from_link = join_cls_url_to_href
3336
    get_first_comic_link = simulate_first_link
3337
    first_url = 'http://www.earthexplodes.com/comics/000/'
3338
3339
    @classmethod
3340
    def get_navi_link(cls, last_soup, next_):
3341
        """Get link to next or previous comic."""
3342
        return last_soup.find('a', id='next' if next_ else 'prev')
3343
3344
    @classmethod
3345
    def get_comic_info(cls, soup, link):
3346
        """Get information about a particular comics."""
3347
        title = soup.find('title').string
3348
        imgs = soup.find('div', id='image').find_all('img')
3349
        alt = imgs[0].get('title', '')
3350
        return {
3351
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
3352
            'title': title,
3353
            'alt': alt,
3354
        }
3355
3356
3357
class PomComics(GenericNavigableComic):
3358
    """Class to retrieve PomComics."""
3359
    name = 'pom'
3360 View Code Duplication
    long_name = 'Pom Comics / Piece of Me'
3361
    url = 'http://www.pomcomic.com'
3362
    get_url_from_link = join_cls_url_to_href
3363
3364
    @classmethod
3365
    def get_first_comic_link(cls):
3366
        """Get link to first comics."""
3367
        return get_soup_at_url(cls.url).find('a', class_='btn-first')
3368
3369
    @classmethod
3370
    def get_navi_link(cls, last_soup, next_):
3371
        """Get link to next or previous comic."""
3372
        return last_soup.find('a', class_='btn-next' if next_ else 'btn-prev')
3373
3374
    @classmethod
3375
    def get_comic_info(cls, soup, link):
3376
        """Get information about a particular comics."""
3377
        title = soup.find('h1').string
3378
        desc = soup.find('meta', property='og:description')['content']
3379
        tags = soup.find('meta', attrs={'name': 'keywords'})['content']
3380
        imgs = soup.find('div', class_='comic').find_all('img')
3381
        return {
3382
            'title': title,
3383
            'desc': desc,
3384
            'tags': tags,
3385
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
3386
        }
3387
3388
3389
class CubeDrone(GenericComicNotWorking, GenericNavigableComic):  # Website has changed
3390
    """Class to retrieve Cube Drone comics."""
3391
    name = 'cubedrone'
3392
    long_name = 'Cube Drone'
3393
    url = 'http://cube-drone.com/comics'
3394
    get_url_from_link = join_cls_url_to_href
3395
3396
    @classmethod
3397
    def get_first_comic_link(cls):
3398
        """Get link to first comics."""
3399
        return get_soup_at_url(cls.url).find('span', class_='glyphicon glyphicon-backward').parent
3400
3401
    @classmethod
3402
    def get_navi_link(cls, last_soup, next_):
3403
        """Get link to next or previous comic."""
3404
        class_ = 'glyphicon glyphicon-chevron-' + ('right' if next_ else 'left')
3405
        return last_soup.find('span', class_=class_).parent
3406
3407
    @classmethod
3408
    def get_comic_info(cls, soup, link):
3409
        """Get information about a particular comics."""
3410
        title = soup.find('meta', attrs={'name': 'twitter:title'})['content']
3411
        url2 = soup.find('meta', attrs={'name': 'twitter:url'})['content']
3412
        # date_str = soup.find('h2', class_='comic_title').find('small').string
3413
        # day = string_to_date(date_str, "%B %d, %Y, %I:%M %p")
3414
        imgs = soup.find_all('img', class_='comic img-responsive')
3415
        title2 = imgs[0]['title']
3416
        alt = imgs[0]['alt']
3417
        return {
3418
            'url2': url2,
3419
            'title': title,
3420
            'title2': title2,
3421
            'alt': alt,
3422
            'img': [i['src'] for i in imgs],
3423
        }
3424
3425
3426
class MakeItStoopid(GenericDeletedComic, GenericNavigableComic):
3427
    """Class to retrieve Make It Stoopid Comics."""
3428
    name = 'stoopid'
3429
    long_name = 'Make it stoopid'
3430
    url = 'http://makeitstoopid.com/comic.php'
3431
3432
    @classmethod
3433
    def get_nav(cls, soup):
3434
        """Get the navigation elements from soup object."""
3435
        cnav = soup.find_all(class_='cnav')
3436
        nav1, nav2 = cnav[:5], cnav[5:]
3437
        assert nav1 == nav2
3438
        # begin, prev, archive, next_, end = nav1
3439
        return [None if i.get('href') is None else i for i in nav1]
3440
3441
    @classmethod
3442
    def get_first_comic_link(cls):
3443
        """Get link to first comics."""
3444
        return cls.get_nav(get_soup_at_url(cls.url))[0]
3445
3446
    @classmethod
3447
    def get_navi_link(cls, last_soup, next_):
3448
        """Get link to next or previous comic."""
3449
        return cls.get_nav(last_soup)[3 if next_ else 1]
3450
3451
    @classmethod
3452
    def get_comic_info(cls, soup, link):
3453
        """Get information about a particular comics."""
3454
        title = link['title']
3455
        imgs = soup.find_all('img', id='comicimg')
3456
        return {
3457
            'title': title,
3458
            'img': [i['src'] for i in imgs],
3459
        }
3460
3461
3462
class OffTheLeashDog(GenericNavigableComic):
3463
    """Class to retrieve Off The Leash Dog comics."""
3464
    # Also on http://rupertfawcettsdoggyblog.tumblr.com
3465
    # Also on http://www.rupertfawcettcartoons.com
3466
    name = 'offtheleash'
3467
    long_name = 'Off The Leash Dog'
3468
    url = 'http://offtheleashdogcartoons.com'
3469
    _categories = ('FAWCETT', )
3470
    get_navi_link = get_a_rel_next
3471
    get_first_comic_link = simulate_first_link
3472
    first_url = 'http://offtheleashdogcartoons.com/uncategorized/can-i-help-you/'
3473
3474
    @classmethod
3475
    def get_comic_info(cls, soup, link):
3476
        """Get information about a particular comics."""
3477
        title = soup.find("h1", class_="entry-title").string
3478
        imgs = soup.find('div', class_='entry-content').find_all('img')
3479
        return {
3480
            'title': title,
3481
            'img': [i['src'] for i in imgs],
3482
        }
3483
3484
3485
class MacadamValley(GenericNavigableComic):
3486
    """Class to retrieve Macadam Valley comics."""
3487
    name = 'macadamvalley'
3488 View Code Duplication
    long_name = 'Macadam Valley'
3489
    url = 'http://macadamvalley.com'
3490
    get_navi_link = get_a_rel_next
3491
    get_first_comic_link = simulate_first_link
3492
    first_url = 'http://macadamvalley.com/le-debut-de-la-fin/'
3493
3494
    @classmethod
3495
    def get_comic_info(cls, soup, link):
3496
        """Get information about a particular comics."""
3497
        title = soup.find("h1", class_="entry-title").string
3498
        img = soup.find('div', class_='entry-content').find('img')
3499
        date_str = soup.find('time', class_='entry-date')['datetime']
3500
        date_str = date_str[:10]
3501
        day = string_to_date(date_str, "%Y-%m-%d")
3502
        author = soup.find('a', rel='author').string
3503
        return {
3504
            'title': title,
3505
            'img': [i['src'] for i in [img]],
3506
            'day': day.day,
3507
            'month': day.month,
3508
            'year': day.year,
3509
            'author': author,
3510
        }
3511
3512
3513
class MarketoonistComics(GenericNavigableComic):
3514
    """Class to retrieve Marketoonist Comics."""
3515
    name = 'marketoonist'
3516 View Code Duplication
    long_name = 'Marketoonist'
3517
    url = 'https://marketoonist.com/cartoons'
3518
    get_first_comic_link = simulate_first_link
3519
    get_navi_link = get_link_rel_next
3520
    first_url = 'https://marketoonist.com/2002/10/the-8-types-of-brand-managers-2.html'
3521
3522
    @classmethod
3523
    def get_comic_info(cls, soup, link):
3524
        """Get information about a particular comics."""
3525
        imgs = soup.find_all('meta', property='og:image')
3526
        date_str = soup.find('meta', property='article:published_time')['content'][:10]
3527
        day = string_to_date(date_str, "%Y-%m-%d")
3528
        title = soup.find('meta', property='og:title')['content']
3529
        return {
3530
            'img': [i['content'] for i in imgs],
3531
            'day': day.day,
3532
            'month': day.month,
3533
            'year': day.year,
3534
            'title': title,
3535
        }
3536
3537
3538
class ConsoliaComics(GenericNavigableComic):
3539
    """Class to retrieve Consolia comics."""
3540
    name = 'consolia'
3541 View Code Duplication
    long_name = 'consolia'
3542
    url = 'https://consolia-comic.com'
3543
    get_url_from_link = join_cls_url_to_href
3544
3545
    @classmethod
3546
    def get_first_comic_link(cls):
3547
        """Get link to first comics."""
3548
        return get_soup_at_url(cls.url).find('a', class_='first')
3549
3550
    @classmethod
3551
    def get_navi_link(cls, last_soup, next_):
3552
        """Get link to next or previous comic."""
3553
        return last_soup.find('a', class_='next' if next_ else 'prev')
3554
3555
    @classmethod
3556
    def get_comic_info(cls, soup, link):
3557
        """Get information about a particular comics."""
3558
        title = soup.find('meta', property='og:title')['content']
3559
        date_str = soup.find('time')["datetime"]
3560
        day = string_to_date(date_str, "%Y-%m-%d")
3561
        imgs = soup.find_all('meta', property='og:image')
3562
        return {
3563
            'title': title,
3564
            'img': [i['content'] for i in imgs],
3565
            'day': day.day,
3566
            'month': day.month,
3567
            'year': day.year,
3568
        }
3569
3570
3571
class GenericBlogspotComic(GenericNavigableComic):
3572
    """Generic class to retrieve comics from Blogspot."""
3573
    get_first_comic_link = simulate_first_link
3574
    first_url = NotImplemented
3575
    _categories = ('BLOGSPOT', )
3576
3577
    @classmethod
3578
    def get_navi_link(cls, last_soup, next_):
3579
        """Get link to next or previous comic."""
3580
        return last_soup.find('a', id='Blog1_blog-pager-newer-link' if next_ else 'Blog1_blog-pager-older-link')
3581
3582
3583
class TuMourrasMoinsBete(GenericBlogspotComic):
3584
    """Class to retrieve Tu Mourras Moins Bete comics."""
3585
    name = 'mourrasmoinsbete'
3586 View Code Duplication
    long_name = 'Tu Mourras Moins Bete'
3587
    url = 'http://tumourrasmoinsbete.blogspot.fr'
3588
    _categories = ('FRANCAIS', )
3589
    first_url = 'http://tumourrasmoinsbete.blogspot.fr/2008/06/essai.html'
3590
3591
    @classmethod
3592
    def get_comic_info(cls, soup, link):
3593
        """Get information about a particular comics."""
3594
        title = soup.find('title').string
3595
        imgs = soup.find('div', itemprop='description articleBody').find_all('img')
3596
        author = soup.find('span', itemprop='author').string
3597
        return {
3598
            'img': [i['src'] for i in imgs],
3599
            'author': author,
3600
            'title': title,
3601
        }
3602
3603
3604
class Octopuns(GenericBlogspotComic):
3605
    """Class to retrieve Octopuns comics."""
3606
    # Also on http://octopuns.tumblr.com
3607 View Code Duplication
    name = 'octopuns'
3608
    long_name = 'Octopuns'
3609
    url = 'http://www.octopuns.net'  # or http://octopuns.blogspot.fr/
3610
    first_url = 'http://octopuns.blogspot.com/2010/12/17122010-always-read-label.html'
3611
3612
    @classmethod
3613
    def get_comic_info(cls, soup, link):
3614
        """Get information about a particular comics."""
3615
        title = soup.find('h3', class_='post-title entry-title').string
3616
        date_str = soup.find('h2', class_='date-header').string
3617
        day = string_to_date(date_str, "%A, %B %d, %Y")
3618
        imgs = soup.find_all('link', rel='image_src')
3619
        return {
3620
            'img': [i['href'] for i in imgs],
3621
            'title': title,
3622
            'day': day.day,
3623
            'month': day.month,
3624
            'year': day.year,
3625
        }
3626
3627
3628
class GeekAndPoke(GenericNavigableComic):
3629
    """Class to retrieve Geek And Poke comics."""
3630
    name = 'geek'
3631
    long_name = 'Geek And Poke'
3632
    url = 'http://geek-and-poke.com'
3633
    get_url_from_link = join_cls_url_to_href
3634
    get_first_comic_link = simulate_first_link
3635
    first_url = 'http://geek-and-poke.com/geekandpoke/2006/8/27/a-new-place-for-a-not-so-old-blog.html'
3636
3637
    @classmethod
3638
    def get_navi_link(cls, last_soup, next_):
3639
        """Get link to next or previous comic."""
3640
        return last_soup.find('a', class_='prev-item' if next_ else 'next-item')
3641
3642
    @classmethod
3643
    def get_comic_info(cls, soup, link):
3644
        """Get information about a particular comics."""
3645
        title = soup.find('meta', property='og:title')['content']
3646
        desc = soup.find('meta', property='og:description')
3647
        desc_str = "" if desc is None else desc['content']
3648
        date_str = soup.find('time', class_='published')['datetime']
3649
        day = string_to_date(date_str, "%Y-%m-%d")
3650
        author = soup.find('a', rel='author').string
3651
        div_content = (soup.find('div', class_="body entry-content") or
3652
                       soup.find('div', class_="special-content"))
3653
        imgs = div_content.find_all('img')
3654
        imgs = [i for i in imgs if i.get('src') is not None]
3655
        assert all('title' not in i or i['alt'] == i['title'] for i in imgs)
3656
        alt = imgs[0].get('alt', "") if imgs else []
3657
        return {
3658
            'title': title,
3659
            'alt': alt,
3660
            'description': desc_str,
3661
            'author': author,
3662
            'day': day.day,
3663
            'month': day.month,
3664
            'year': day.year,
3665
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
3666
        }
3667
3668
3669
class GloryOwlComix(GenericBlogspotComic):
3670
    """Class to retrieve Glory Owl comics."""
3671
    name = 'gloryowl'
3672 View Code Duplication
    long_name = 'Glory Owl'
3673
    url = 'http://gloryowlcomix.blogspot.fr'
3674
    _categories = ('NSFW', 'FRANCAIS')
3675
    first_url = 'http://gloryowlcomix.blogspot.fr/2013/02/1_7.html'
3676
3677
    @classmethod
3678
    def get_comic_info(cls, soup, link):
3679
        """Get information about a particular comics."""
3680
        title = soup.find('title').string
3681
        imgs = soup.find_all('link', rel='image_src')
3682
        author = soup.find('a', rel='author').string
3683
        return {
3684
            'img': [i['href'] for i in imgs],
3685
            'author': author,
3686
            'title': title,
3687
        }
3688
3689
3690
class GenericSquareSpace(GenericNavigableComic):
3691
    """Generic class to retrieve comics using SquareSpace."""
3692
    _categories = ('SQUARESPACE', )
3693 View Code Duplication
    get_url_from_link = join_cls_url_to_href
3694
    get_first_comic_link = simulate_first_link
3695
3696
    @classmethod
3697
    def get_navi_link(cls, last_soup, next_):
3698
        """Get link to next or previous comic."""
3699
        return last_soup.find('a', id='prevLink' if next_ else 'nextLink')
3700
3701
    @classmethod
3702
    def get_images(cls, soup):
3703
        """Get image URLs for a comic."""
3704
        raise NotImplementedError
3705
3706
    @classmethod
3707
    def get_comic_info(cls, soup, link):
3708
        """Get information about a particular comics."""
3709
        title = soup.find('meta', property='og:title')['content']
3710
        desc = soup.find('meta', property='og:description')['content']
3711
        date_str = soup.find('time', itemprop='datePublished')["datetime"]
3712
        day = string_to_date(date_str, "%Y-%m-%d")
3713
        author = soup.find('a', rel='author').string
3714
        return {
3715
            'title': title,
3716
            'img': cls.get_images(soup),
3717
            'month': day.month,
3718
            'year': day.year,
3719
            'day': day.day,
3720
            'author': author,
3721
            'description': desc,
3722
        }
3723
3724
3725
class AtRandomComics(GenericSquareSpace):
3726
    """Class to retrieve At Random Comics."""
3727
    name = 'atrandom'
3728
    long_name = 'At Random Comics'
3729
    url = 'http://www.atrandomcomics.com'
3730
    first_url = 'http://www.atrandomcomics.com/at-random-comics-home/2015/5/5/can-of-worms'
3731
3732
    @classmethod
3733
    def get_images(cls, soup):
3734
        """Get image URLs for a comic."""
3735
        imgs = soup.find_all('meta', property='og:image')
3736
        return [i['content'] for i in imgs]
3737
3738
3739
class NothingSuspicious(GenericSquareSpace):
3740
    """Class to retrieve Nothing Suspicious comics."""
3741
    name = 'nothingsuspicious'
3742
    long_name = 'Nothing Suspicious'
3743
    url = 'https://nothingsuspicio.us'
3744
    first_url = 'https://nothingsuspicio.us/?offset=1483592400908'
3745
3746
    @classmethod
3747
    def get_images(cls, soup):
3748
        """Get image URLs for a comic."""
3749
        imgs = soup.find('div', class_='content-wrapper').find('img')
3750
        return [i['src'] for i in [imgs]]
3751
3752
3753
class DeathBulge(GenericComic):
3754
    """Class to retrieve the DeathBulge comics."""
3755
    name = 'deathbulge'
3756
    long_name = 'Death Bulge'
3757
    url = 'http://www.deathbulge.com'
3758
3759
    @classmethod
3760
    def get_next_comic(cls, last_comic):
3761
        """Generator to get the next comic. Implementation of GenericComic's abstract method."""
3762
        json_url = urljoin_wrapper(cls.url, 'api/comics/1')
3763
        json = load_json_at_url(json_url)
3764
        pagination = json['pagination_links']
3765
        first_num = last_comic['num'] if last_comic else pagination['first']
3766
        last_num = pagination['last']
3767
        for num in range(first_num + 1, last_num):
3768
            json_url = urljoin_wrapper(cls.url, 'api/comics/%d' % num)
3769
            json = load_json_at_url(json_url)
3770
            pagination = json['pagination_links']
3771
            comic_json = json['comic']
3772
            date_str = comic_json['timestamp'][:10]
3773
            day = string_to_date(date_str, "%Y-%m-%d")
3774
            comic_id = comic_json['id']  # not exactly 'num' o_O
3775
            yield {
3776
                'json_url': json_url,
3777
                'num': comic_id,
3778
                'url': urljoin_wrapper(cls.url, 'comics/%d' % num),
3779
                'alt': comic_json['alt_text'],
3780
                'title': comic_json['title'],
3781
                'img': [urljoin_wrapper(cls.url, comic_json['comic'])],
3782
                'month': day.month,
3783
                'year': day.year,
3784
                'day': day.day,
3785
            }
3786
3787
3788
class GenericTumblrV1(GenericComic):
3789
    """Generic class to retrieve comics from Tumblr using the V1 API."""
3790
    _categories = ('TUMBLR', )
3791
3792
    @classmethod
3793
    def get_next_comic(cls, last_comic):
3794
        """Generic implementation of get_next_comic for Tumblr comics."""
3795
        for p in cls.get_posts(last_comic):
3796
            comic = cls.get_comic_info(p)
3797
            if comic is not None:
3798
                yield comic
3799
3800
    @classmethod
3801
    def check_url(cls, url):
3802
        if not url.startswith(cls.url):
3803
            print("url '%s' does not start with '%s'" % (url, cls.url))
3804
        return url
3805
3806
    @classmethod
3807
    def get_url_from_post(cls, post):
3808
        return cls.check_url(post['url'])
3809
3810
    @classmethod
3811
    def get_api_url(cls):
3812
        return urljoin_wrapper(cls.url, '/api/read/')
3813
3814
    @classmethod
3815
    def get_api_url_for_id(cls, tumblr_id):
3816
        return cls.get_api_url() + '?id=%d' % (tumblr_id)
3817
3818
    @classmethod
3819
    def get_comic_info(cls, post):
3820
        """Get information about a particular comics."""
3821
        type_ = post['type']
3822
        if type_ != 'photo':
3823
            return None
3824
        tumblr_id = int(post['id'])
3825
        api_url = cls.get_api_url_for_id(tumblr_id)
3826
        day = datetime.datetime.fromtimestamp(int(post['unix-timestamp'])).date()
3827
        caption = post.find('photo-caption')
3828
        title = caption.string if caption else ""
3829
        tags = ' '.join(t.string for t in post.find_all('tag'))
3830
        # Photos may appear in 'photo' tags and/or straight in the post
3831
        photo_tags = post.find_all('photo')
3832
        if not photo_tags:
3833
            photo_tags = [post]
3834
        # Images are in multiple resolutions - taking the first one
3835
        imgs = [photo.find('photo-url') for photo in photo_tags]
3836
        return {
3837
            'url': cls.get_url_from_post(post),
3838
            'url2': post['url-with-slug'],
3839
            'day': day.day,
3840
            'month': day.month,
3841
            'year': day.year,
3842
            'title': title,
3843
            'tags': tags,
3844
            'img': [i.string for i in imgs],
3845
            'tumblr-id': tumblr_id,
3846
            'api_url': api_url,
3847
        }
3848
3849
    @classmethod
3850
    def get_posts(cls, last_comic, nb_post_per_call=10):
3851
        """Get posts using API. nb_post_per_call is max 50.
3852
3853
        Posts are retrieved from newer to older as per the tumblr v1 api
3854
        but are returned in chronological order."""
3855
        waiting_for_id = last_comic['tumblr-id'] if last_comic else None
3856
        posts_acc = []
3857
        if last_comic is not None:
3858
            # cls.check_url(last_comic['url'])
3859
            cls.check_url(last_comic['api_url'])
3860
            # Sometimes, tumblr posts are deleted. When previous post is deleted, we
3861
            # might end up spending a lot of time looking for something that
3862
            # doesn't exist. Failing early and clearly might be a better option.
3863
            last_api_url = cls.get_api_url_for_id(waiting_for_id)
3864
            try:
3865
                get_soup_at_url(last_api_url)
3866
            except urllib.error.HTTPError:
3867
                try:
3868
                    get_soup_at_url(cls.url)
3869
                except urllib.error.HTTPError:
3870
                    print("Did not find previous post nor main url %s" % cls.url)
3871
                else:
3872
                    print("Did not find previous post %s : it might have been deleted" % last_api_url)
3873
                return reversed(posts_acc)
3874
        api_url = cls.get_api_url()
3875
        posts = get_soup_at_url(api_url).find('posts')
3876
        start, total = int(posts['start']), int(posts['total'])
3877
        assert start == 0
3878
        for starting_num in range(0, total, nb_post_per_call):
3879
            api_url2 = api_url + '?start=%d&num=%d' % (starting_num, nb_post_per_call)
3880
            posts2 = get_soup_at_url(api_url2).find('posts')
3881
            start2, total2 = int(posts2['start']), int(posts2['total'])
3882
            assert starting_num == start2, "%d != %d" % (starting_num, start2)
3883
            # This may happen and should be handled in the future
3884
            assert total == total2, "%d != %d" % (total, total2)
3885
            for p in posts2.find_all('post'):
3886
                tumblr_id = int(p['id'])
3887
                if waiting_for_id and waiting_for_id == tumblr_id:
3888
                    return reversed(posts_acc)
3889
                posts_acc.append(p)
3890
        if waiting_for_id is None:
3891
            return reversed(posts_acc)
3892
        print("Did not find %s : there might be a problem" % waiting_for_id)
3893
        return []
3894
3895
3896
class SaturdayMorningBreakfastCerealTumblr(GenericTumblrV1):
3897
    """Class to retrieve Saturday Morning Breakfast Cereal comics."""
3898
    # Also on http://www.gocomics.com/saturday-morning-breakfast-cereal
3899
    # Also on http://www.smbc-comics.com
3900
    name = 'smbc-tumblr'
3901
    long_name = 'Saturday Morning Breakfast Cereal (from Tumblr)'
3902
    url = 'http://smbc-comics.tumblr.com'
3903
    _categories = ('SMBC', )
3904
3905
3906
class AHammADay(GenericTumblrV1):
3907
    """Class to retrieve class A Hamm A Day comics."""
3908
    name = 'hamm'
3909
    long_name = 'A Hamm A Day'
3910
    url = 'http://www.ahammaday.com'
3911
3912
3913
class IrwinCardozo(GenericTumblrV1):
3914
    """Class to retrieve Irwin Cardozo Comics."""
3915
    name = 'irwinc'
3916
    long_name = 'Irwin Cardozo'
3917
    url = 'http://irwincardozocomics.tumblr.com'
3918
3919
3920
class AccordingToDevin(GenericTumblrV1):
3921
    """Class to retrieve According To Devin comics."""
3922
    name = 'devin'
3923
    long_name = 'According To Devin'
3924
    url = 'http://accordingtodevin.tumblr.com'
3925
3926
3927
class ItsTheTieTumblr(GenericTumblrV1):
3928
    """Class to retrieve It's the tie comics."""
3929
    # Also on http://itsthetie.com
3930
    # Also on https://tapastic.com/series/itsthetie
3931
    name = 'tie-tumblr'
3932
    long_name = "It's the tie (from Tumblr)"
3933
    url = "http://itsthetie.tumblr.com"
3934
    _categories = ('TIE', )
3935
3936
3937
class OctopunsTumblr(GenericTumblrV1):
3938
    """Class to retrieve Octopuns comics."""
3939
    # Also on http://www.octopuns.net
3940
    name = 'octopuns-tumblr'
3941
    long_name = 'Octopuns (from Tumblr)'
3942
    url = 'http://octopuns.tumblr.com'
3943
3944
3945
class PicturesInBoxesTumblr(GenericTumblrV1):
3946
    """Class to retrieve Pictures In Boxes comics."""
3947
    # Also on http://www.picturesinboxes.com
3948
    name = 'picturesinboxes-tumblr'
3949
    long_name = 'Pictures in Boxes (from Tumblr)'
3950
    url = 'https://picturesinboxescomic.tumblr.com'
3951
3952
3953
class TubeyToonsTumblr(GenericTumblrV1):
3954
    """Class to retrieve TubeyToons comics."""
3955
    # Also on http://tapastic.com/series/Tubey-Toons
3956
    # Also on http://tubeytoons.com
3957
    name = 'tubeytoons-tumblr'
3958
    long_name = 'Tubey Toons (from Tumblr)'
3959
    url = 'https://tubeytoons.tumblr.com'
3960
    _categories = ('TUNEYTOONS', )
3961
3962
3963
class UnearthedComicsTumblr(GenericTumblrV1):
3964
    """Class to retrieve Unearthed comics."""
3965
    # Also on http://tapastic.com/series/UnearthedComics
3966
    # Also on http://unearthedcomics.com
3967
    name = 'unearthed-tumblr'
3968
    long_name = 'Unearthed Comics (from Tumblr)'
3969
    url = 'https://unearthedcomics.tumblr.com'
3970
    _categories = ('UNEARTHED', )
3971
3972
3973
class PieComic(GenericTumblrV1):
3974
    """Class to retrieve Pie Comic comics."""
3975
    name = 'pie'
3976
    long_name = 'Pie Comic'
3977
    url = "http://piecomic.tumblr.com"
3978
3979
3980
class MrEthanDiamond(GenericTumblrV1):
3981
    """Class to retrieve Mr Ethan Diamond comics."""
3982
    name = 'diamond'
3983
    long_name = 'Mr Ethan Diamond'
3984
    url = 'http://mrethandiamond.tumblr.com'
3985
3986
3987
class Flocci(GenericTumblrV1):
3988
    """Class to retrieve floccinaucinihilipilification comics."""
3989
    name = 'flocci'
3990
    long_name = 'floccinaucinihilipilification'
3991
    url = "http://floccinaucinihilipilificationa.tumblr.com"
3992
3993
3994
class UpAndOut(GenericTumblrV1):
3995
    """Class to retrieve Up & Out comics."""
3996
    # Also on http://tapastic.com/series/UP-and-OUT
3997
    name = 'upandout'
3998
    long_name = 'Up And Out (from Tumblr)'
3999
    url = 'http://upandoutcomic.tumblr.com'
4000
4001
4002
class Pundemonium(GenericTumblrV1):
4003
    """Class to retrieve Pundemonium comics."""
4004
    name = 'pundemonium'
4005
    long_name = 'Pundemonium'
4006
    url = 'http://monstika.tumblr.com'
4007
4008
4009
class PoorlyDrawnLinesTumblr(GenericTumblrV1):
4010
    """Class to retrieve Poorly Drawn Lines comics."""
4011
    # Also on http://poorlydrawnlines.com
4012
    name = 'poorlydrawn-tumblr'
4013
    long_name = 'Poorly Drawn Lines (from Tumblr)'
4014
    url = 'http://pdlcomics.tumblr.com'
4015
    _categories = ('POORLYDRAWN', )
4016
4017
4018
class PearShapedComics(GenericTumblrV1):
4019
    """Class to retrieve Pear Shaped Comics."""
4020
    name = 'pearshaped'
4021
    long_name = 'Pear-Shaped Comics'
4022
    url = 'http://pearshapedcomics.com'
4023
4024
4025
class PondScumComics(GenericTumblrV1):
4026
    """Class to retrieve Pond Scum Comics."""
4027
    name = 'pond'
4028
    long_name = 'Pond Scum'
4029
    url = 'http://pondscumcomic.tumblr.com'
4030
4031
4032
class MercworksTumblr(GenericTumblrV1):
4033
    """Class to retrieve Mercworks comics."""
4034
    # Also on http://mercworks.net
4035
    # Also on http://www.webtoons.com/en/comedy/mercworks/list?title_no=426
4036
    # Also on https://tapastic.com/series/MercWorks
4037
    name = 'mercworks-tumblr'
4038
    long_name = 'Mercworks (from Tumblr)'
4039
    url = 'http://mercworks.tumblr.com'
4040
    _categories = ('MERCWORKS', )
4041
4042
4043
class OwlTurdTumblr(GenericTumblrV1):
4044
    """Class to retrieve Owl Turd / Shen comix."""
4045
    # Also on https://tapas.io/series/Shen-Comix
4046
    name = 'owlturd-tumblr'
4047
    long_name = 'Owl Turd / Shen Comix (from Tumblr)'
4048
    url = 'http://shencomix.com'
4049
    _categories = ('OWLTURD', 'SHENCOMIX')
4050
4051
4052
class VectorBelly(GenericTumblrV1):
4053
    """Class to retrieve Vector Belly comics."""
4054
    # Also on http://vectorbelly.com
4055
    name = 'vector'
4056
    long_name = 'Vector Belly'
4057
    url = 'http://vectorbelly.tumblr.com'
4058
4059
4060
class GoneIntoRapture(GenericTumblrV1):
4061
    """Class to retrieve Gone Into Rapture comics."""
4062
    # Also on http://goneintorapture.tumblr.com
4063
    # Also on http://tapastic.com/series/Goneintorapture
4064
    name = 'rapture'
4065
    long_name = 'Gone Into Rapture'
4066
    url = 'http://goneintorapture.com'
4067
4068
4069
class TheOatmealTumblr(GenericTumblrV1):
4070
    """Class to retrieve The Oatmeal comics."""
4071
    # Also on http://theoatmeal.com
4072
    name = 'oatmeal-tumblr'
4073
    long_name = 'The Oatmeal (from Tumblr)'
4074
    url = 'http://oatmeal.tumblr.com'
4075
4076
4077
class HeckIfIKnowComicsTumblr(GenericTumblrV1):
4078
    """Class to retrieve Heck If I Know Comics."""
4079
    # Also on http://tapastic.com/series/Regular
4080
    name = 'heck-tumblr'
4081
    long_name = 'Heck if I Know comics (from Tumblr)'
4082
    url = 'http://heckifiknowcomics.com'
4083
4084
4085
class MyJetPack(GenericTumblrV1):
4086
    """Class to retrieve My Jet Pack comics."""
4087
    name = 'jetpack'
4088
    long_name = 'My Jet Pack'
4089
    url = 'http://myjetpack.tumblr.com'
4090
4091
4092
class CheerUpEmoKidTumblr(GenericTumblrV1):
4093
    """Class to retrieve CheerUpEmoKid comics."""
4094
    # Also on http://www.cheerupemokid.com
4095
    # Also on http://tapastic.com/series/CUEK
4096
    name = 'cuek-tumblr'
4097
    long_name = 'Cheer Up Emo Kid (from Tumblr)'
4098
    url = 'https://enzocomics.tumblr.com'
4099
4100
4101
class ForLackOfABetterComic(GenericTumblrV1):
4102
    """Class to retrieve For Lack Of A Better Comics."""
4103
    # Also on http://forlackofabettercomic.com
4104
    name = 'lack'
4105
    long_name = 'For Lack Of A Better Comic'
4106
    url = 'http://forlackofabettercomic.tumblr.com'
4107
4108
4109
class ZenPencilsTumblr(GenericTumblrV1):
4110
    """Class to retrieve ZenPencils comics."""
4111
    # Also on http://zenpencils.com
4112
    # Also on http://www.gocomics.com/zen-pencils
4113
    name = 'zenpencils-tumblr'
4114
    long_name = 'Zen Pencils (from Tumblr)'
4115
    url = 'http://zenpencils.tumblr.com'
4116
    _categories = ('ZENPENCILS', )
4117
4118
4119
class ThreeWordPhraseTumblr(GenericTumblrV1):
4120
    """Class to retrieve Three Word Phrase comics."""
4121
    # Also on http://threewordphrase.com
4122
    name = 'threeword-tumblr'
4123
    long_name = 'Three Word Phrase (from Tumblr)'
4124
    url = 'http://threewordphrase.tumblr.com'
4125
4126
4127
class TimeTrabbleTumblr(GenericTumblrV1):
4128
    """Class to retrieve Time Trabble comics."""
4129
    # Also on http://timetrabble.com
4130
    name = 'timetrabble-tumblr'
4131
    long_name = 'Time Trabble (from Tumblr)'
4132
    url = 'http://timetrabble.tumblr.com'
4133
4134
4135
class SafelyEndangeredTumblr(GenericTumblrV1):
4136
    """Class to retrieve Safely Endangered comics."""
4137
    # Also on http://www.safelyendangered.com
4138
    name = 'endangered-tumblr'
4139
    long_name = 'Safely Endangered (from Tumblr)'
4140
    url = 'http://tumblr.safelyendangered.com'
4141
4142
4143
class MouseBearComedyTumblr(GenericTumblrV1):
4144
    """Class to retrieve Mouse Bear Comedy comics."""
4145
    # Also on http://www.mousebearcomedy.com
4146
    name = 'mousebear-tumblr'
4147
    long_name = 'Mouse Bear Comedy (from Tumblr)'
4148
    url = 'http://mousebearcomedy.tumblr.com'
4149
4150
4151
class BouletCorpTumblr(GenericTumblrV1):
4152
    """Class to retrieve BouletCorp comics."""
4153
    # Also on http://www.bouletcorp.com
4154
    name = 'boulet-tumblr'
4155
    long_name = 'Boulet Corp (from Tumblr)'
4156
    url = 'https://bouletcorp.tumblr.com'
4157
    _categories = ('BOULET', )
4158
4159
4160
class TheAwkwardYetiTumblr(GenericTumblrV1):
4161
    """Class to retrieve The Awkward Yeti comics."""
4162
    # Also on http://www.gocomics.com/the-awkward-yeti
4163
    # Also on http://theawkwardyeti.com
4164
    # Also on https://tapastic.com/series/TheAwkwardYeti
4165
    name = 'yeti-tumblr'
4166
    long_name = 'The Awkward Yeti (from Tumblr)'
4167
    url = 'http://larstheyeti.tumblr.com'
4168
    _categories = ('YETI', )
4169
4170
4171
class NellucNhoj(GenericTumblrV1):
4172
    """Class to retrieve NellucNhoj comics."""
4173
    name = 'nhoj'
4174
    long_name = 'Nelluc Nhoj'
4175
    url = 'http://nellucnhoj.com'
4176
4177
4178
class DownTheUpwardSpiralTumblr(GenericTumblrV1):
4179
    """Class to retrieve Down The Upward Spiral comics."""
4180
    # Also on http://www.downtheupwardspiral.com
4181
    # Also on https://tapastic.com/series/Down-the-Upward-Spiral
4182
    name = 'spiral-tumblr'
4183
    long_name = 'Down the Upward Spiral (from Tumblr)'
4184
    url = 'http://downtheupwardspiral.tumblr.com'
4185
4186
4187
class AsPerUsualTumblr(GenericTumblrV1):
4188
    """Class to retrieve As Per Usual comics."""
4189
    # Also on https://tapastic.com/series/AsPerUsual
4190
    name = 'usual-tumblr'
4191
    long_name = 'As Per Usual (from Tumblr)'
4192
    url = 'http://as-per-usual.tumblr.com'
4193
    categories = ('DAMILEE', )
4194
4195
4196
class HotComicsForCoolPeopleTumblr(GenericTumblrV1):
4197
    """Class to retrieve Hot Comics For Cool People."""
4198
    # Also on https://tapastic.com/series/Hot-Comics-For-Cool-People
4199
    # Also on http://hotcomics.biz (links to tumblr)
4200
    # Also on http://hcfcp.com (links to tumblr)
4201
    name = 'hotcomics-tumblr'
4202
    long_name = 'Hot Comics For Cool People (from Tumblr)'
4203
    url = 'http://hotcomicsforcoolpeople.tumblr.com'
4204
    categories = ('DAMILEE', )
4205
4206
4207
class OneOneOneOneComicTumblr(GenericTumblrV1):
4208
    """Class to retrieve 1111 Comics."""
4209
    # Also on http://www.1111comics.me
4210
    # Also on https://tapastic.com/series/1111-Comics
4211
    name = '1111-tumblr'
4212
    long_name = '1111 Comics (from Tumblr)'
4213
    url = 'http://comics1111.tumblr.com'
4214
    _categories = ('ONEONEONEONE', )
4215
4216
4217
class JhallComicsTumblr(GenericTumblrV1):
4218
    """Class to retrieve Jhall Comics."""
4219
    # Also on http://jhallcomics.com
4220
    name = 'jhall-tumblr'
4221
    long_name = 'Jhall Comics (from Tumblr)'
4222
    url = 'http://jhallcomics.tumblr.com'
4223
4224
4225
class BerkeleyMewsTumblr(GenericTumblrV1):
4226
    """Class to retrieve Berkeley Mews comics."""
4227
    # Also on http://www.gocomics.com/berkeley-mews
4228
    # Also on http://www.berkeleymews.com
4229
    name = 'berkeley-tumblr'
4230
    long_name = 'Berkeley Mews (from Tumblr)'
4231
    url = 'http://mews.tumblr.com'
4232
    _categories = ('BERKELEY', )
4233
4234
4235
class JoanCornellaTumblr(GenericTumblrV1):
4236
    """Class to retrieve Joan Cornella comics."""
4237
    # Also on http://joancornella.net
4238
    name = 'cornella-tumblr'
4239
    long_name = 'Joan Cornella (from Tumblr)'
4240
    url = 'http://cornellajoan.tumblr.com'
4241
4242
4243
class RespawnComicTumblr(GenericTumblrV1):
4244
    """Class to retrieve Respawn Comic."""
4245
    # Also on http://respawncomic.com
4246
    name = 'respawn-tumblr'
4247
    long_name = 'Respawn Comic (from Tumblr)'
4248
    url = 'https://respawncomic.tumblr.com'
4249
4250
4251
class ChrisHallbeckTumblr(GenericTumblrV1):
4252
    """Class to retrieve Chris Hallbeck comics."""
4253
    # Also on https://tapastic.com/ChrisHallbeck
4254
    # Also on http://maximumble.com
4255
    # Also on http://minimumble.com
4256
    # Also on http://thebookofbiff.com
4257
    name = 'hallbeck-tumblr'
4258
    long_name = 'Chris Hallback (from Tumblr)'
4259
    url = 'https://chrishallbeck.tumblr.com'
4260
    _categories = ('HALLBACK', )
4261
4262
4263
class ComicNuggets(GenericTumblrV1):
4264
    """Class to retrieve Comic Nuggets."""
4265
    name = 'nuggets'
4266
    long_name = 'Comic Nuggets'
4267
    url = 'http://comicnuggets.com'
4268
4269
4270
class PigeonGazetteTumblr(GenericTumblrV1):
4271
    """Class to retrieve The Pigeon Gazette comics."""
4272
    # Also on https://tapastic.com/series/The-Pigeon-Gazette
4273
    name = 'pigeon-tumblr'
4274
    long_name = 'The Pigeon Gazette (from Tumblr)'
4275
    url = 'http://thepigeongazette.tumblr.com'
4276
4277
4278
class CancerOwl(GenericTumblrV1):
4279
    """Class to retrieve Cancer Owl comics."""
4280
    # Also on http://cancerowl.com
4281
    name = 'cancerowl-tumblr'
4282
    long_name = 'Cancer Owl (from Tumblr)'
4283
    url = 'http://cancerowl.tumblr.com'
4284
4285
4286
class FowlLanguageTumblr(GenericTumblrV1):
4287
    """Class to retrieve Fowl Language comics."""
4288
    # Also on http://www.fowllanguagecomics.com
4289
    # Also on http://tapastic.com/series/Fowl-Language-Comics
4290
    # Also on http://www.gocomics.com/fowl-language
4291
    name = 'fowllanguage-tumblr'
4292
    long_name = 'Fowl Language Comics (from Tumblr)'
4293
    url = 'http://fowllanguagecomics.tumblr.com'
4294
    _categories = ('FOWLLANGUAGE', )
4295
4296
4297
class TheOdd1sOutTumblr(GenericTumblrV1):
4298
    """Class to retrieve The Odd 1s Out comics."""
4299
    # Also on http://theodd1sout.com
4300
    # Also on https://tapastic.com/series/Theodd1sout
4301
    name = 'theodd-tumblr'
4302
    long_name = 'The Odd 1s Out (from Tumblr)'
4303
    url = 'http://theodd1sout.tumblr.com'
4304
4305
4306
class TheUnderfoldTumblr(GenericTumblrV1):
4307
    """Class to retrieve The Underfold comics."""
4308
    # Also on http://theunderfold.com
4309
    name = 'underfold-tumblr'
4310
    long_name = 'The Underfold (from Tumblr)'
4311
    url = 'http://theunderfold.tumblr.com'
4312
4313
4314
class LolNeinTumblr(GenericTumblrV1):
4315
    """Class to retrieve Lol Nein comics."""
4316
    # Also on http://lolnein.com
4317
    name = 'lolnein-tumblr'
4318
    long_name = 'Lol Nein (from Tumblr)'
4319
    url = 'http://lolneincom.tumblr.com'
4320
4321
4322
class FatAwesomeComicsTumblr(GenericTumblrV1):
4323
    """Class to retrieve Fat Awesome Comics."""
4324
    # Also on http://fatawesome.com/comics
4325
    name = 'fatawesome-tumblr'
4326
    long_name = 'Fat Awesome (from Tumblr)'
4327
    url = 'http://fatawesomecomedy.tumblr.com'
4328
4329
4330
class TheWorldIsFlatTumblr(GenericTumblrV1):
4331
    """Class to retrieve The World Is Flat Comics."""
4332
    # Also on https://tapastic.com/series/The-World-is-Flat
4333
    name = 'flatworld-tumblr'
4334
    long_name = 'The World Is Flat (from Tumblr)'
4335
    url = 'http://theworldisflatcomics.com'
4336
4337
4338
class DorrisMc(GenericTumblrV1):
4339
    """Class to retrieve Dorris Mc Comics"""
4340
    # Also on http://www.gocomics.com/dorris-mccomics
4341
    name = 'dorrismc'
4342
    long_name = 'Dorris Mc'
4343
    url = 'http://dorrismccomics.com'
4344
4345
4346
class LeleozTumblr(GenericDeletedComic, GenericTumblrV1):
4347
    """Class to retrieve Leleoz comics."""
4348
    # Also on https://tapastic.com/series/Leleoz
4349
    name = 'leleoz-tumblr'
4350
    long_name = 'Leleoz (from Tumblr)'
4351
    url = 'http://leleozcomics.tumblr.com'
4352
4353
4354
class MoonBeardTumblr(GenericTumblrV1):
4355
    """Class to retrieve MoonBeard comics."""
4356
    # Also on http://moonbeard.com
4357
    # Also on http://www.webtoons.com/en/comedy/moon-beard/list?title_no=471
4358
    name = 'moonbeard-tumblr'
4359
    long_name = 'Moon Beard (from Tumblr)'
4360
    url = 'http://squireseses.tumblr.com'
4361
    _categories = ('MOONBEARD', )
4362
4363
4364
class AComik(GenericTumblrV1):
4365
    """Class to retrieve A Comik"""
4366
    name = 'comik'
4367
    long_name = 'A Comik'
4368
    url = 'http://acomik.com'
4369
4370
4371
class ClassicRandy(GenericTumblrV1):
4372
    """Class to retrieve Classic Randy comics."""
4373
    name = 'randy'
4374
    long_name = 'Classic Randy'
4375
    url = 'http://classicrandy.tumblr.com'
4376
4377
4378
class DagssonTumblr(GenericTumblrV1):
4379
    """Class to retrieve Dagsson comics."""
4380
    # Also on http://www.dagsson.com
4381
    name = 'dagsson-tumblr'
4382
    long_name = 'Dagsson Hugleikur (from Tumblr)'
4383
    url = 'https://hugleikurdagsson.tumblr.com'
4384
4385
4386
class LinsEditionsTumblr(GenericTumblrV1):
4387
    """Class to retrieve L.I.N.S. Editions comics."""
4388
    # Also on https://linsedition.com
4389
    # Now on http://warandpeas.tumblr.com
4390
    name = 'lins-tumblr'
4391
    long_name = 'L.I.N.S. Editions (from Tumblr)'
4392
    url = 'https://linscomics.tumblr.com'
4393
    _categories = ('WARANDPEAS', 'LINS')
4394
4395
4396
class WarAndPeasTumblr(GenericTumblrV1):
4397
    """Class to retrieve War And Peas comics."""
4398
    # Was on https://linscomics.tumblr.com
4399
    name = 'warandpeas-tumblr'
4400
    long_name = 'War And Peas (from Tumblr)'
4401
    url = 'http://warandpeas.tumblr.com'
4402
    _categories = ('WARANDPEAS', 'LINS')
4403
4404
4405
class OrigamiHotDish(GenericTumblrV1):
4406
    """Class to retrieve Origami Hot Dish comics."""
4407
    name = 'origamihotdish'
4408
    long_name = 'Origami Hot Dish'
4409
    url = 'http://origamihotdish.com'
4410
4411
4412
class HitAndMissComicsTumblr(GenericTumblrV1):
4413
    """Class to retrieve Hit and Miss Comics."""
4414
    name = 'hitandmiss'
4415
    long_name = 'Hit and Miss Comics'
4416
    url = 'https://hitandmisscomics.tumblr.com'
4417
4418
4419
class HMBlanc(GenericTumblrV1):
4420
    """Class to retrieve HM Blanc comics."""
4421
    name = 'hmblanc'
4422
    long_name = 'HM Blanc'
4423
    url = 'http://hmblanc.tumblr.com'
4424
4425
4426
class TalesOfAbsurdityTumblr(GenericTumblrV1):
4427
    """Class to retrieve Tales Of Absurdity comics."""
4428
    # Also on http://talesofabsurdity.com
4429
    # Also on http://tapastic.com/series/Tales-Of-Absurdity
4430
    name = 'absurdity-tumblr'
4431
    long_name = 'Tales of Absurdity (from Tumblr)'
4432
    url = 'http://talesofabsurdity.tumblr.com'
4433
    _categories = ('ABSURDITY', )
4434
4435
4436
class RobbieAndBobby(GenericTumblrV1):
4437
    """Class to retrieve Robbie And Bobby comics."""
4438
    # Also on http://robbieandbobby.com
4439
    name = 'robbie-tumblr'
4440
    long_name = 'Robbie And Bobby (from Tumblr)'
4441
    url = 'http://robbieandbobby.tumblr.com'
4442
4443
4444
class ElectricBunnyComicTumblr(GenericTumblrV1):
4445
    """Class to retrieve Electric Bunny Comics."""
4446
    # Also on http://www.electricbunnycomics.com/View/Comic/153/Welcome+to+Hell
4447
    name = 'bunny-tumblr'
4448
    long_name = 'Electric Bunny Comic (from Tumblr)'
4449
    url = 'http://electricbunnycomics.tumblr.com'
4450
4451
4452
class Hoomph(GenericTumblrV1):
4453
    """Class to retrieve Hoomph comics."""
4454
    name = 'hoomph'
4455
    long_name = 'Hoomph'
4456
    url = 'http://hoom.ph'
4457
4458
4459
class BFGFSTumblr(GenericTumblrV1):
4460
    """Class to retrieve BFGFS comics."""
4461
    # Also on https://tapastic.com/series/BFGFS
4462
    # Also on http://bfgfs.com
4463
    name = 'bfgfs-tumblr'
4464
    long_name = 'BFGFS (from Tumblr)'
4465
    url = 'https://bfgfs.tumblr.com'
4466
4467
4468
class DoodleForFood(GenericTumblrV1):
4469
    """Class to retrieve Doodle For Food comics."""
4470
    # Also on https://tapastic.com/series/Doodle-for-Food
4471
    name = 'doodle'
4472
    long_name = 'Doodle For Food'
4473
    url = 'http://www.doodleforfood.com'
4474
4475
4476
class CassandraCalinTumblr(GenericTumblrV1):
4477
    """Class to retrieve C. Cassandra comics."""
4478
    # Also on http://cassandracalin.com
4479
    # Also on https://tapastic.com/series/C-Cassandra-comics
4480
    name = 'cassandra-tumblr'
4481
    long_name = 'Cassandra Calin (from Tumblr)'
4482
    url = 'http://c-cassandra.tumblr.com'
4483
4484
4485
class DougWasTaken(GenericTumblrV1):
4486
    """Class to retrieve Doug Was Taken comics."""
4487
    name = 'doug'
4488
    long_name = 'Doug Was Taken'
4489
    url = 'https://dougwastaken.tumblr.com'
4490
4491
4492
class MandatoryRollerCoaster(GenericTumblrV1):
4493
    """Class to retrieve Mandatory Roller Coaster comics."""
4494
    name = 'rollercoaster'
4495
    long_name = 'Mandatory Roller Coaster'
4496
    url = 'http://mandatoryrollercoaster.com'
4497
4498
4499
class CEstPasEnRegardantSesPompes(GenericTumblrV1):
4500
    """Class to retrieve C'Est Pas En Regardant Ses Pompes (...)  comics."""
4501
    name = 'cperspqccltt'
4502
    long_name = 'C Est Pas En Regardant Ses Pompes (...)'
4503
    url = 'http://marcoandco.tumblr.com'
4504
4505
4506
class TheGrohlTroll(GenericTumblrV1):
4507
    """Class to retrieve The Grohl Troll comics."""
4508
    name = 'grohltroll'
4509
    long_name = 'The Grohl Troll'
4510
    url = 'http://thegrohltroll.com'
4511
4512
4513
class WebcomicName(GenericTumblrV1):
4514
    """Class to retrieve Webcomic Name comics."""
4515
    name = 'webcomicname'
4516
    long_name = 'Webcomic Name'
4517
    url = 'http://webcomicname.com'
4518
4519
4520
class BooksOfAdam(GenericTumblrV1):
4521
    """Class to retrieve Books of Adam comics."""
4522
    # Also on http://www.booksofadam.com
4523
    name = 'booksofadam'
4524
    long_name = 'Books of Adam'
4525
    url = 'http://booksofadam.tumblr.com'
4526
4527
4528
class HarkAVagrant(GenericTumblrV1):
4529
    """Class to retrieve Hark A Vagrant comics."""
4530
    # Also on http://www.harkavagrant.com
4531
    name = 'hark-tumblr'
4532
    long_name = 'Hark A Vagrant (from Tumblr)'
4533
    url = 'http://beatonna.tumblr.com'
4534
4535
4536
class OurSuperAdventureTumblr(GenericTumblrV1):
4537
    """Class to retrieve Our Super Adventure comics."""
4538
    # Also on https://tapastic.com/series/Our-Super-Adventure
4539
    # Also on http://www.oursuperadventure.com
4540
    # http://sarahgraley.com
4541
    name = 'superadventure-tumblr'
4542
    long_name = 'Our Super Adventure (from Tumblr)'
4543
    url = 'http://sarahssketchbook.tumblr.com'
4544
4545
4546
class JakeLikesOnions(GenericTumblrV1):
4547
    """Class to retrieve Jake Likes Onions comics."""
4548
    name = 'jake'
4549
    long_name = 'Jake Likes Onions'
4550
    url = 'http://jakelikesonions.com'
4551
4552
4553
class InYourFaceCakeTumblr(GenericTumblrV1):
4554
    """Class to retrieve In Your Face Cake comics."""
4555
    # Also on https://tapas.io/series/In-Your-Face-Cake
4556
    name = 'inyourfacecake-tumblr'
4557
    long_name = 'In Your Face Cake (from Tumblr)'
4558
    url = 'https://in-your-face-cake.tumblr.com'
4559
    _categories = ('INYOURFACECAKE', )
4560
4561
4562
class Robospunk(GenericTumblrV1):
4563
    """Class to retrieve Robospunk comics."""
4564
    name = 'robospunk'
4565
    long_name = 'Robospunk'
4566
    url = 'http://robospunk.com'
4567
4568
4569
class BananaTwinky(GenericTumblrV1):
4570
    """Class to retrieve Banana Twinky comics."""
4571
    name = 'banana'
4572
    long_name = 'Banana Twinky'
4573
    url = 'https://bananatwinky.tumblr.com'
4574
4575
4576
class YesterdaysPopcornTumblr(GenericTumblrV1):
4577
    """Class to retrieve Yesterday's Popcorn comics."""
4578
    # Also on http://www.yesterdayspopcorn.com
4579
    # Also on https://tapastic.com/series/Yesterdays-Popcorn
4580
    name = 'popcorn-tumblr'
4581
    long_name = 'Yesterday\'s Popcorn (from Tumblr)'
4582
    url = 'http://yesterdayspopcorn.tumblr.com'
4583
4584
4585
class TwistedDoodles(GenericTumblrV1):
4586
    """Class to retrieve Twisted Doodles comics."""
4587
    name = 'twisted'
4588
    long_name = 'Twisted Doodles'
4589
    url = 'http://www.twisteddoodles.com'
4590
4591
4592
class UbertoolTumblr(GenericTumblrV1):
4593
    """Class to retrieve Ubertool comics."""
4594
    # Also on http://ubertoolcomic.com
4595
    # Also on https://tapastic.com/series/ubertool
4596
    name = 'ubertool-tumblr'
4597
    long_name = 'Ubertool (from Tumblr)'
4598
    url = 'https://ubertool.tumblr.com'
4599
    _categories = ('UBERTOOL', )
4600
4601
4602
class LittleLifeLinesTumblr(GenericDeletedComic, GenericTumblrV1):
4603
    """Class to retrieve Little Life Lines comics."""
4604
    # Also on http://www.littlelifelines.com
4605
    name = 'life-tumblr'
4606
    long_name = 'Little Life Lines (from Tumblr)'
4607
    url = 'https://little-life-lines.tumblr.com'
4608
4609
4610
class TheyCanTalk(GenericTumblrV1):
4611
    """Class to retrieve They Can Talk comics."""
4612
    name = 'theycantalk'
4613
    long_name = 'They Can Talk'
4614
    url = 'http://theycantalk.com'
4615
4616
4617
class Will5NeverCome(GenericTumblrV1):
4618
    """Class to retrieve Will 5:00 Never Come comics."""
4619
    name = 'will5'
4620
    long_name = 'Will 5:00 Never Come ?'
4621
    url = 'http://will5nevercome.com'
4622
4623
4624
class Sephko(GenericTumblrV1):
4625
    """Class to retrieve Sephko Comics."""
4626
    # Also on http://www.sephko.com
4627
    name = 'sephko'
4628
    long_name = 'Sephko'
4629
    url = 'https://sephko.tumblr.com'
4630
4631
4632
class BlazersAtDawn(GenericTumblrV1):
4633
    """Class to retrieve Blazers At Dawn Comics."""
4634
    name = 'blazers'
4635
    long_name = 'Blazers At Dawn'
4636
    url = 'http://blazersatdawn.tumblr.com'
4637
4638
4639
class ArtByMoga(GenericEmptyComic, GenericTumblrV1):  # Deactivated because it downloads too many things
4640
    """Class to retrieve Art By Moga Comics."""
4641
    name = 'moga'
4642
    long_name = 'Art By Moga'
4643
    url = 'http://artbymoga.tumblr.com'
4644
4645
4646
class VerbalVomitTumblr(GenericTumblrV1):
4647
    """Class to retrieve Verbal Vomit comics."""
4648
    # Also on http://www.verbal-vomit.com
4649
    name = 'vomit-tumblr'
4650
    long_name = 'Verbal Vomit (from Tumblr)'
4651
    url = 'http://verbalvomits.tumblr.com'
4652
4653
4654
class LibraryComic(GenericTumblrV1):
4655
    """Class to retrieve LibraryComic."""
4656
    # Also on http://librarycomic.com
4657
    name = 'library-tumblr'
4658
    long_name = 'LibraryComic (from Tumblr)'
4659
    url = 'https://librarycomic.tumblr.com'
4660
4661
4662
class TizzyStitchBirdTumblr(GenericTumblrV1):
4663
    """Class to retrieve Tizzy Stitch Bird comics."""
4664
    # Also on http://tizzystitchbird.com
4665
    # Also on https://tapastic.com/series/TizzyStitchbird
4666
    # Also on http://www.webtoons.com/en/challenge/tizzy-stitchbird/list?title_no=50082
4667
    name = 'tizzy-tumblr'
4668
    long_name = 'Tizzy Stitch Bird (from Tumblr)'
4669
    url = 'http://tizzystitchbird.tumblr.com'
4670
4671
4672
class VictimsOfCircumsolarTumblr(GenericTumblrV1):
4673
    """Class to retrieve VictimsOfCircumsolar comics."""
4674
    # Also on http://www.victimsofcircumsolar.com
4675
    name = 'circumsolar-tumblr'
4676
    long_name = 'Victims Of Circumsolar (from Tumblr)'
4677
    url = 'https://victimsofcomics.tumblr.com'
4678
4679
4680
class RockPaperCynicTumblr(GenericTumblrV1):
4681
    """Class to retrieve RockPaperCynic comics."""
4682
    # Also on http://www.rockpapercynic.com
4683
    # Also on https://tapastic.com/series/rockpapercynic
4684
    name = 'rpc-tumblr'
4685
    long_name = 'Rock Paper Cynic (from Tumblr)'
4686
    url = 'http://rockpapercynic.tumblr.com'
4687
4688
4689
class DeadlyPanelTumblr(GenericTumblrV1):
4690
    """Class to retrieve Deadly Panel comics."""
4691
    # Also on http://www.deadlypanel.com
4692
    # Also on https://tapastic.com/series/deadlypanel
4693
    name = 'deadly-tumblr'
4694
    long_name = 'Deadly Panel (from Tumblr)'
4695
    url = 'https://deadlypanel.tumblr.com'
4696
4697
4698
class CatanaComics(GenericComicNotWorking):  # Not a Tumblr anymore ?
4699
    """Class to retrieve Catana comics."""
4700
    name = 'catana'
4701
    long_name = 'Catana'
4702
    url = 'http://www.catanacomics.com'
4703
4704
4705
class AngryAtNothingTumblr(GenericTumblrV1):
4706
    """Class to retrieve Angry at Nothing comics."""
4707
    # Also on http://www.angryatnothing.net
4708
    # Also on http://tapastic.com/series/Comics-yeah-definitely-comics-
4709
    name = 'angry-tumblr'
4710
    long_name = 'Angry At Nothing (from Tumblr)'
4711
    url = 'http://angryatnothing.tumblr.com'
4712
4713
4714
class ShanghaiTango(GenericTumblrV1):
4715
    """Class to retrieve Shanghai Tango comic."""
4716
    name = 'tango'
4717
    long_name = 'Shanghai Tango'
4718
    url = 'http://tango2010weibo.tumblr.com'
4719
4720
4721
class OffTheLeashDogTumblr(GenericTumblrV1):
4722
    """Class to retrieve Off The Leash Dog comics."""
4723
    # Also on http://offtheleashdogcartoons.com
4724
    # Also on http://www.rupertfawcettcartoons.com
4725
    name = 'offtheleash-tumblr'
4726
    long_name = 'Off The Leash Dog (from Tumblr)'
4727
    url = 'http://rupertfawcettsdoggyblog.tumblr.com'
4728
    _categories = ('FAWCETT', )
4729
4730
4731
class ImogenQuestTumblr(GenericTumblrV1):
4732
    """Class to retrieve Imogen Quest comics."""
4733
    # Also on http://imogenquest.net
4734
    name = 'imogen-tumblr'
4735
    long_name = 'Imogen Quest (from Tumblr)'
4736
    url = 'http://imoquest.tumblr.com'
4737
4738
4739
class Shitfest(GenericTumblrV1):
4740
    """Class to retrieve Shitfest comics."""
4741
    name = 'shitfest'
4742
    long_name = 'Shitfest'
4743
    url = 'http://shitfestcomic.com'
4744
4745
4746
class IceCreamSandwichComics(GenericTumblrV1):
4747
    """Class to retrieve Ice Cream Sandwich Comics."""
4748
    name = 'icecream'
4749
    long_name = 'Ice Cream Sandwich Comics'
4750
    url = 'http://icecreamsandwichcomics.com'
4751
4752
4753
class Dustinteractive(GenericTumblrV1):
4754
    """Class to retrieve Dustinteractive comics."""
4755
    name = 'dustinteractive'
4756
    long_name = 'Dustinteractive'
4757
    url = 'http://dustinteractive.com'
4758
4759
4760
class StickyCinemaFloor(GenericTumblrV1):
4761
    """Class to retrieve Sticky Cinema Floor comics."""
4762
    name = 'stickycinema'
4763
    long_name = 'Sticky Cinema Floor'
4764
    url = 'https://stickycinemafloor.tumblr.com'
4765
4766
4767
class IncidentalComicsTumblr(GenericTumblrV1):
4768
    """Class to retrieve Incidental Comics."""
4769
    # Also on http://www.incidentalcomics.com
4770
    name = 'incidental-tumblr'
4771
    long_name = 'Incidental Comics (from Tumblr)'
4772
    url = 'http://incidentalcomics.tumblr.com'
4773
4774
4775
class APleasantWasteOfTimeTumblr(GenericTumblrV1):
4776
    """Class to retrieve A Pleasant Waste Of Time comics."""
4777
    # Also on https://tapas.io/series/A-Pleasant-
4778
    name = 'pleasant-waste-tumblr'
4779
    long_name = 'A Pleasant Waste Of Time (from Tumblr)'
4780
    url = 'https://artjcf.tumblr.com'
4781
    _categories = ('WASTE', )
4782
4783
4784
class HorovitzComicsTumblr(GenericTumblrV1):
4785
    """Class to retrieve Horovitz new comics."""
4786
    # Also on http://www.horovitzcomics.com
4787
    name = 'horovitz-tumblr'
4788
    long_name = 'Horovitz (from Tumblr)'
4789
    url = 'https://horovitzcomics.tumblr.com'
4790
    _categories = ('HOROVITZ', )
4791
4792
4793
class DeepDarkFearsTumblr(GenericTumblrV1):
4794
    """Class to retrieve DeepvDarkvFears comics."""
4795
    name = 'deep-dark-fears-tumblr'
4796
    long_name = 'Deep Dark Fears (from Tumblr)'
4797
    url = 'http://deep-dark-fears.tumblr.com'
4798
4799
4800
class DakotaMcDadzean(GenericTumblrV1):
4801
    """Class to retrieve Dakota McDadzean comics."""
4802
    name = 'dakota'
4803
    long_name = 'Dakota McDadzean'
4804
    url = 'http://dakotamcfadzean.tumblr.com'
4805
4806
4807
class ExtraFabulousComicsTumblr(GenericTumblrV1):
4808
    """Class to retrieve Extra Fabulous Comics."""
4809
    # Also on http://extrafabulouscomics.com
4810
    name = 'efc-tumblr'
4811
    long_name = 'Extra Fabulous Comics (from Tumblr)'
4812
    url = 'https://extrafabulouscomics.tumblr.com'
4813
    _categories = ('EFC', )
4814
4815
4816
class AlexLevesque(GenericTumblrV1):
4817
    """Class to retrieve AlexLevesque comics."""
4818
    name = 'alevesque'
4819
    long_name = 'Alex Levesque'
4820
    url = 'http://alexlevesque.com'
4821
    _categories = ('FRANCAIS', )
4822
4823
4824
class JamesOfNoTradesTumblr(GenericTumblrV1):
4825
    """Class to retrieve JamesOfNoTrades comics."""
4826
    # Also on http://jamesofnotrades.com
4827
    # Also on http://www.webtoons.com/en/challenge/james-of-no-trades/list?title_no=43422
4828
    # Also on https://tapas.io/series/James-of-No-Trades
4829
    name = 'jamesofnotrades-tumblr'
4830
    long_name = 'James Of No Trades (from Tumblr)'
4831
    url = 'http://jamesfregan.tumblr.com'
4832
    _categories = ('JAMESOFNOTRADES', )
4833
4834
4835
class InfiniteGuff(GenericTumblrV1):
4836
    """Class to retrieve Infinite Guff comics."""
4837
    name = 'infiniteguff'
4838
    long_name = 'Infinite Guff'
4839
    url = 'http://infiniteguff.com'
4840
4841
4842
class SkeletonClaw(GenericTumblrV1):
4843
    """Class to retrieve Skeleton Claw comics."""
4844
    name = 'skeletonclaw'
4845
    long_name = 'Skeleton Claw'
4846
    url = 'http://skeletonclaw.com'
4847
4848
4849
class MrsFrolleinTumblr(GenericTumblrV1):
4850
    """Class to retrieve Mrs Frollein comics."""
4851
    # Also on http://www.webtoons.com/en/challenge/mrsfrollein/list?title_no=51710
4852
    name = 'frollein'
4853
    long_name = 'Mrs Frollein (from Tumblr)'
4854
    url = 'https://mrsfrollein.tumblr.com'
4855
4856
4857
class GoodBearComicsTumblr(GenericTumblrV1):
4858
    """Class to retrieve GoodBearComics."""
4859
    # Also on https://goodbearcomics.com
4860
    name = 'goodbear-tumblr'
4861
    long_name = 'Good Bear Comics (from Tumblr)'
4862
    url = 'https://goodbearcomics.tumblr.com'
4863
4864
4865
class BrooklynCartoonsTumblr(GenericTumblrV1):
4866
    """Class to retrieve Brooklyn Cartoons."""
4867
    # Also on https://www.brooklyncartoons.com
4868
    # Also on https://www.instagram.com/brooklyncartoons
4869
    name = 'brooklyn-tumblr'
4870
    long_name = 'Brooklyn Cartoons (from Tumblr)'
4871
    url = 'http://brooklyncartoons.tumblr.com'
4872
4873
4874
class GemmaCorrellTumblr(GenericTumblrV1):
4875
    # Also on http://www.gemmacorrell.com/portfolio/comics/
4876
    name = 'gemma-tumblr'
4877
    long_name = 'Gemma Correll (from Tumblr)'
4878
    url = 'http://gemmacorrell.tumblr.com'
4879
4880
4881
class RobotatertotTumblr(GenericTumblrV1):
4882
    """Class to retrieve Robotatertot comics."""
4883
    # Also on https://www.instagram.com/robotatertotcomics
4884
    name = 'robotatertot-tumblr'
4885
    long_name = 'Robotatertot (from Tumblr)'
4886
    url = 'https://robotatertot.tumblr.com'
4887
4888
4889
class HuffyPenguin(GenericTumblrV1):
4890
    """Class to retrieve Huffy Penguin comics."""
4891
    name = 'huffypenguin'
4892
    long_name = 'Huffy Penguin'
4893
    url = 'http://huffy-penguin.tumblr.com'
4894
4895
4896
class CowardlyComicsTumblr(GenericTumblrV1):
4897
    """Class to retrieve Cowardly Comics."""
4898
    # Also on https://tapas.io/series/CowardlyComics
4899
    # Also on http://www.webtoons.com/en/challenge/cowardly-comics/list?title_no=65893
4900
    name = 'cowardly-tumblr'
4901
    long_name = 'Cowardly Comics (from Tumblr)'
4902
    url = 'http://cowardlycomics.tumblr.com'
4903
4904
4905
class Caw4hwTumblr(GenericTumblrV1):
4906
    """Class to retrieve Caw4hw comics."""
4907
    # Also on https://tapas.io/series/CAW4HW
4908
    name = 'caw4hw-tumblr'
4909
    long_name = 'Caw4hw (from Tumblr)'
4910
    url = 'https://caw4hw.tumblr.com'
4911
4912
4913
class WeFlapsTumblr(GenericTumblrV1):
4914
    """Class to retrieve WeFlaps comics."""
4915
    name = 'weflaps-tumblr'
4916
    long_name = 'We Flaps (from Tumblr)'
4917
    url = 'https://weflaps.tumblr.com'
4918
4919
4920
class TheseInsideJokesTumblr(GenericTumblrV1):
4921
    """Class to retrieve These Inside Jokes comics."""
4922
    # Also on http://www.theseinsidejokes.com
4923
    name = 'theseinsidejokes-tumblr'
4924
    long_name = 'These Inside Jokes (from Tumblr)'
4925
    url = 'http://theseinsidejokes.tumblr.com'
4926
4927
4928
class RustledJimmies(GenericTumblrV1):
4929
    """Class to retrieve Rustled Jimmies comics."""
4930
    name = 'restled'
4931
    long_name = 'Rustled Jimmies'
4932
    url = 'http://rustledjimmies.net'
4933
4934
4935
class SinewynTumblr(GenericTumblrV1):
4936
    """Class to retrieve Sinewyn comics."""
4937
    # Also on https://sinewyn.wordpress.com
4938
    name = 'sinewyn-tumblr'
4939
    long_name = 'Sinewyn (from Tumblr)'
4940
    url = 'https://sinewyn.tumblr.com'
4941
4942
4943
class ItFoolsAMonster(GenericTumblrV1):
4944
    """Class to retrieve It Fools A Monster comics."""
4945
    name = 'itfoolsamonster'
4946
    long_name = 'It Fools A Monster'
4947
    url = 'http://itfoolsamonster.com'
4948
4949
4950
class BoumeriesTumblr(GenericTumblrV1):
4951
    """Class to retrieve Boumeries comics."""
4952
    # Also on http://bd.boumerie.com
4953
    # Also on http://comics.boumerie.com
4954
    name = 'boumeries-tumblr'
4955
    long_name = 'Boumeries (from Tumblr)'
4956
    url = 'http://boumeries.tumblr.com/'
4957
    _categories = ('BOUMERIES', )
4958
4959
4960
class InfiniteImmortalBensTumblr(GenericTumblrV1):
4961
    """Class to retrieve Infinite Immortal Bens comics."""
4962
    # Also on http://www.webtoons.com/en/challenge/infinite-immortal-bens/list?title_no=32847
4963
    # Also on https://tapas.io/series/Infinite-Immortal-Bens
4964
    url = 'https://infiniteimmortalbens.tumblr.com'
4965
    name = 'infiniteimmortal-tumblr'
4966
    long_name = 'Infinite Immortal Bens (from Tumblr)'
4967
    _categories = ('INFINITEIMMORTAL', )
4968
4969
4970
class CheeseCornzTumblr(GenericTumblrV1):
4971
    """Class to retrieve Cheese Cornz comics."""
4972
    name = 'cheesecornz-tumblr'
4973
    long_name = 'Cheese Cornz (from Tumblr)'
4974
    url = 'https://cheesecornz.tumblr.com'
4975
4976
4977
class CinismoIlustrado(GenericTumblrV1):
4978
    """Class to retrieve CinismoIlustrado comics."""
4979
    name = 'cinismo'
4980
    long_name = 'Cinismo Ilustrado'
4981
    url = 'http://cinismoilustrado.com'
4982
    _categories = ('ESPANOL', )
4983
4984
4985
class EatMyPaintTumblr(GenericTumblrV1):
4986
    """Class to retrieve Eat My Paint comics."""
4987
    # Also on https://tapas.io/series/eatmypaint
4988
    name = 'eatmypaint-tumblr'
4989
    long_name = 'Eat My Paint (from Tumblr)'
4990
    url = 'https://eatmypaint.tumblr.com'
4991
    _categories = ('EATMYPAINT', )
4992
4993
4994
class AnomalyTownFromTumblr(GenericTumblrV1):
4995
    """Class to retrieve Anomaly Town."""
4996
    name = 'anomalytown-tumblr'
4997
    long_name = 'Anomaly Town (from Tumblr)'
4998
    url = 'https://anomalytown.tumblr.com'
4999
5000
5001
class HorovitzComics(GenericDeletedComic, GenericListableComic):
5002
    """Generic class to handle the logic common to the different comics from Horovitz."""
5003
    # Also on https://horovitzcomics.tumblr.com
5004
    url = 'http://www.horovitzcomics.com'
5005
    _categories = ('HOROVITZ', )
5006
    img_re = re.compile('.*comics/([0-9]*)/([0-9]*)/([0-9]*)/.*$')
5007
    link_re = NotImplemented
5008
    get_url_from_archive_element = join_cls_url_to_href
5009
5010
    @classmethod
5011
    def get_comic_info(cls, soup, link):
5012
        """Get information about a particular comics."""
5013 View Code Duplication
        href = link['href']
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
5014
        num = int(cls.link_re.match(href).groups()[0])
5015
        title = link.string
5016
        imgs = soup.find_all('img', id='comic')
5017
        assert len(imgs) == 1, imgs
5018
        year, month, day = [int(s)
5019
                            for s in cls.img_re.match(imgs[0]['src']).groups()]
5020
        return {
5021
            'title': title,
5022
            'day': day,
5023
            'month': month,
5024
            'year': year,
5025
            'img': [i['src'] for i in imgs],
5026
            'num': num,
5027
        }
5028
5029
    @classmethod
5030
    def get_archive_elements(cls):
5031
        archive_url = 'http://www.horovitzcomics.com/comics/archive/'
5032
        return reversed(get_soup_at_url(archive_url).find_all('a', href=cls.link_re))
5033
5034
5035
class HorovitzNew(HorovitzComics):
5036
    """Class to retrieve Horovitz new comics."""
5037
    name = 'horovitznew'
5038
    long_name = 'Horovitz New'
5039
    link_re = re.compile('^/comics/new/([0-9]+)$')
5040
5041
5042
class HorovitzClassic(HorovitzComics):
5043
    """Class to retrieve Horovitz classic comics."""
5044
    name = 'horovitzclassic'
5045
    long_name = 'Horovitz Classic'
5046
    link_re = re.compile('^/comics/classic/([0-9]+)$')
5047
5048
5049
class GenericGoComic(GenericNavigableComic):
5050
    """Generic class to handle the logic common to comics from gocomics.com."""
5051
    _categories = ('GOCOMIC', )
5052
5053
    @classmethod
5054
    def get_first_comic_link(cls):
5055
        """Get link to first comics."""
5056
        div = get_soup_at_url(cls.url).find('div', class_='gc-deck gc-deck--cta-1')
5057
        return div.find('a')
5058
5059
    @classmethod
5060
    def get_navi_link(cls, last_soup, next_):
5061
        """Get link to next or previous comic."""
5062
        PREV = 'fa btn btn-outline-default btn-circle fa-caret-left sm js-previous-comic '
5063
        NEXT = 'fa btn btn-outline-default btn-circle fa-caret-right sm '
5064
        return last_soup.find('a', class_=NEXT if next_ else PREV)
5065
5066
    @classmethod
5067
    def get_url_from_link(cls, link):
5068
        gocomics = 'http://www.gocomics.com'
5069
        return urljoin_wrapper(gocomics, link['href'])
5070
5071
    @classmethod
5072
    def get_comic_info(cls, soup, link):
5073
        """Get information about a particular comics."""
5074
        date_str = soup.find('meta', property='article:published_time')['content']
5075
        day = string_to_date(date_str, "%Y-%m-%d")
5076
        imgs = soup.find_all('meta', property='og:image')
5077
        author = soup.find('meta', property='article:author')['content']
5078
        tags = soup.find('meta', property='article:tag')['content']
5079
        return {
5080
            'day': day.day,
5081
            'month': day.month,
5082
            'year': day.year,
5083
            'img': [i['content'] for i in imgs],
5084
            'author': author,
5085
            'tags': tags,
5086
        }
5087
5088
5089
class PearlsBeforeSwine(GenericGoComic):
5090
    """Class to retrieve Pearls Before Swine comics."""
5091
    name = 'pearls'
5092
    long_name = 'Pearls Before Swine'
5093
    url = 'http://www.gocomics.com/pearlsbeforeswine'
5094
5095
5096
class Peanuts(GenericGoComic):
5097
    """Class to retrieve Peanuts comics."""
5098
    name = 'peanuts'
5099
    long_name = 'Peanuts'
5100
    url = 'http://www.gocomics.com/peanuts'
5101
5102
5103
class MattWuerker(GenericGoComic):
5104
    """Class to retrieve Matt Wuerker comics."""
5105
    name = 'wuerker'
5106
    long_name = 'Matt Wuerker'
5107
    url = 'http://www.gocomics.com/mattwuerker'
5108
5109
5110
class TomToles(GenericGoComic):
5111
    """Class to retrieve Tom Toles comics."""
5112
    name = 'toles'
5113
    long_name = 'Tom Toles'
5114
    url = 'http://www.gocomics.com/tomtoles'
5115
5116
5117
class BreakOfDay(GenericGoComic):
5118
    """Class to retrieve Break Of Day comics."""
5119
    name = 'breakofday'
5120
    long_name = 'Break Of Day'
5121
    url = 'http://www.gocomics.com/break-of-day'
5122
5123
5124
class Brevity(GenericGoComic):
5125
    """Class to retrieve Brevity comics."""
5126
    name = 'brevity'
5127
    long_name = 'Brevity'
5128
    url = 'http://www.gocomics.com/brevity'
5129
5130
5131
class MichaelRamirez(GenericGoComic):
5132
    """Class to retrieve Michael Ramirez comics."""
5133
    name = 'ramirez'
5134
    long_name = 'Michael Ramirez'
5135
    url = 'http://www.gocomics.com/michaelramirez'
5136
5137
5138
class MikeLuckovich(GenericGoComic):
5139
    """Class to retrieve Mike Luckovich comics."""
5140
    name = 'luckovich'
5141
    long_name = 'Mike Luckovich'
5142
    url = 'http://www.gocomics.com/mikeluckovich'
5143
5144
5145
class JimBenton(GenericGoComic):
5146
    """Class to retrieve Jim Benton comics."""
5147
    # Also on http://jimbenton.tumblr.com
5148
    name = 'benton'
5149
    long_name = 'Jim Benton'
5150
    url = 'http://www.gocomics.com/jim-benton-cartoons'
5151
5152
5153
class TheArgyleSweater(GenericGoComic):
5154
    """Class to retrieve the Argyle Sweater comics."""
5155
    name = 'argyle'
5156
    long_name = 'Argyle Sweater'
5157
    url = 'http://www.gocomics.com/theargylesweater'
5158
5159
5160
class SunnyStreet(GenericGoComic):
5161
    """Class to retrieve Sunny Street comics."""
5162
    # Also on http://www.sunnystreetcomics.com
5163
    name = 'sunny'
5164
    long_name = 'Sunny Street'
5165
    url = 'http://www.gocomics.com/sunny-street'
5166
5167
5168
class OffTheMark(GenericGoComic):
5169
    """Class to retrieve Off The Mark comics."""
5170
    # Also on https://www.offthemark.com
5171
    name = 'offthemark'
5172
    long_name = 'Off The Mark'
5173
    url = 'http://www.gocomics.com/offthemark'
5174
5175
5176
class WuMo(GenericGoComic):
5177
    """Class to retrieve WuMo comics."""
5178
    # Also on http://wumo.com
5179
    name = 'wumo'
5180
    long_name = 'WuMo'
5181
    url = 'http://www.gocomics.com/wumo'
5182
5183
5184
class LunarBaboon(GenericGoComic):
5185
    """Class to retrieve Lunar Baboon comics."""
5186
    # Also on http://www.lunarbaboon.com
5187
    # Also on https://tapastic.com/series/Lunarbaboon
5188
    name = 'lunarbaboon'
5189
    long_name = 'Lunar Baboon'
5190
    url = 'http://www.gocomics.com/lunarbaboon'
5191
5192
5193
class SandersenGocomic(GenericGoComic):
5194
    """Class to retrieve Sarah Andersen comics."""
5195
    # Also on http://sarahcandersen.com
5196
    # Also on http://tapastic.com/series/Doodle-Time
5197
    name = 'sandersen-goc'
5198
    long_name = 'Sarah Andersen (from GoComics)'
5199
    url = 'http://www.gocomics.com/sarahs-scribbles'
5200
5201
5202
class SaturdayMorningBreakfastCerealGoComic(GenericGoComic):
5203
    """Class to retrieve Saturday Morning Breakfast Cereal comics."""
5204
    # Also on http://smbc-comics.tumblr.com
5205
    # Also on http://www.smbc-comics.com
5206
    name = 'smbc-goc'
5207
    long_name = 'Saturday Morning Breakfast Cereal (from GoComics)'
5208
    url = 'http://www.gocomics.com/saturday-morning-breakfast-cereal'
5209
    _categories = ('SMBC', )
5210
5211
5212
class CalvinAndHobbesGoComic(GenericGoComic):
5213
    """Class to retrieve Calvin and Hobbes comics."""
5214
    # From gocomics, not http://marcel-oehler.marcellosendos.ch/comics/ch/
5215
    name = 'calvin-goc'
5216
    long_name = 'Calvin and Hobbes (from GoComics)'
5217
    url = 'http://www.gocomics.com/calvinandhobbes'
5218
5219
5220
class RallGoComic(GenericGoComic):
5221
    """Class to retrieve Ted Rall comics."""
5222
    # Also on http://rall.com/comic
5223
    name = 'rall-goc'
5224
    long_name = "Ted Rall (from GoComics)"
5225
    url = "http://www.gocomics.com/ted-rall"
5226
    _categories = ('RALL', )
5227
5228
5229
class TheAwkwardYetiGoComic(GenericGoComic):
5230
    """Class to retrieve The Awkward Yeti comics."""
5231
    # Also on http://larstheyeti.tumblr.com
5232
    # Also on http://theawkwardyeti.com
5233
    # Also on https://tapastic.com/series/TheAwkwardYeti
5234
    name = 'yeti-goc'
5235
    long_name = 'The Awkward Yeti (from GoComics)'
5236
    url = 'http://www.gocomics.com/the-awkward-yeti'
5237
    _categories = ('YETI', )
5238
5239
5240
class BerkeleyMewsGoComics(GenericGoComic):
5241
    """Class to retrieve Berkeley Mews comics."""
5242
    # Also on http://mews.tumblr.com
5243
    # Also on http://www.berkeleymews.com
5244
    name = 'berkeley-goc'
5245
    long_name = 'Berkeley Mews (from GoComics)'
5246
    url = 'http://www.gocomics.com/berkeley-mews'
5247
    _categories = ('BERKELEY', )
5248
5249
5250
class SheldonGoComics(GenericGoComic):
5251
    """Class to retrieve Sheldon comics."""
5252
    # Also on http://www.sheldoncomics.com
5253
    name = 'sheldon-goc'
5254
    long_name = 'Sheldon Comics (from GoComics)'
5255
    url = 'http://www.gocomics.com/sheldon'
5256
5257
5258
class FowlLanguageGoComics(GenericGoComic):
5259
    """Class to retrieve Fowl Language comics."""
5260
    # Also on http://www.fowllanguagecomics.com
5261
    # Also on http://tapastic.com/series/Fowl-Language-Comics
5262
    # Also on http://fowllanguagecomics.tumblr.com
5263
    name = 'fowllanguage-goc'
5264
    long_name = 'Fowl Language Comics (from GoComics)'
5265
    url = 'http://www.gocomics.com/fowl-language'
5266
    _categories = ('FOWLLANGUAGE', )
5267
5268
5269
class NickAnderson(GenericGoComic):
5270
    """Class to retrieve Nick Anderson comics."""
5271
    name = 'nickanderson'
5272
    long_name = 'Nick Anderson'
5273
    url = 'http://www.gocomics.com/nickanderson'
5274
5275
5276
class GarfieldGoComics(GenericGoComic):
5277
    """Class to retrieve Garfield comics."""
5278
    # Also on http://garfield.com
5279
    name = 'garfield-goc'
5280
    long_name = 'Garfield (from GoComics)'
5281
    url = 'http://www.gocomics.com/garfield'
5282
    _categories = ('GARFIELD', )
5283
5284
5285
class DorrisMcGoComics(GenericGoComic):
5286
    """Class to retrieve Dorris Mc Comics"""
5287
    # Also on http://dorrismccomics.com
5288
    name = 'dorrismc-goc'
5289
    long_name = 'Dorris Mc (from GoComics)'
5290
    url = 'http://www.gocomics.com/dorris-mccomics'
5291
5292
5293
class FoxTrot(GenericGoComic):
5294
    """Class to retrieve FoxTrot comics."""
5295
    name = 'foxtrot'
5296
    long_name = 'FoxTrot'
5297
    url = 'http://www.gocomics.com/foxtrot'
5298
5299
5300
class FoxTrotClassics(GenericGoComic):
5301
    """Class to retrieve FoxTrot Classics comics."""
5302
    name = 'foxtrot-classics'
5303
    long_name = 'FoxTrot Classics'
5304
    url = 'http://www.gocomics.com/foxtrotclassics'
5305
5306
5307
class MisterAndMeGoComics(GenericDeletedComic, GenericGoComic):
5308
    """Class to retrieve Mister & Me Comics."""
5309
    # Also on http://www.mister-and-me.com
5310
    # Also on https://tapastic.com/series/Mister-and-Me
5311
    name = 'mister-goc'
5312
    long_name = 'Mister & Me (from GoComics)'
5313
    url = 'http://www.gocomics.com/mister-and-me'
5314
5315
5316
class NonSequitur(GenericGoComic):
5317
    """Class to retrieve Non Sequitur (Wiley Miller) comics."""
5318
    name = 'nonsequitur'
5319
    long_name = 'Non Sequitur'
5320
    url = 'http://www.gocomics.com/nonsequitur'
5321
5322
5323
class JoeyAlisonSayers(GenericGoComic):
5324
    """Class to retrieve Joey Alison Sayers comics."""
5325
    name = 'joeyalison'
5326
    long_name = 'Joey Alison Sayers (from GoComics)'
5327
    url = 'http://www.gocomics.com/joey-alison-sayers-comics'
5328
5329
5330
class SavageChickenGoComics(GenericGoComic):
5331
    """Class to retrieve Savage Chicken comics."""
5332
    # Also on http://www.savagechickens.com
5333
    name = 'savage-goc'
5334
    long_name = 'Savage Chicken (from GoComics)'
5335
    url = 'http://www.gocomics.com/savage-chickens'
5336
5337
5338
class GenericTapasticComic(GenericListableComic):
5339
    """Generic class to handle the logic common to comics from tapastic.com."""
5340
    _categories = ('TAPASTIC', )
5341
5342
    @classmethod
5343
    def get_comic_info(cls, soup, archive_elt):
5344
        """Get information about a particular comics."""
5345
        timestamp = int(archive_elt['publishDate']) / 1000.0
5346
        day = datetime.datetime.fromtimestamp(timestamp).date()
5347
        imgs = soup.find_all('img', class_='art-image')
5348
        if not imgs:
5349
            # print("Comic %s is being uploaded, retry later" % cls.get_url_from_archive_element(archive_elt))
5350
            return None
5351
        assert len(imgs) > 0, imgs
5352
        return {
5353
            'day': day.day,
5354
            'year': day.year,
5355
            'month': day.month,
5356
            'img': [i['src'] for i in imgs],
5357
            'title': archive_elt['title'],
5358
        }
5359
5360
    @classmethod
5361
    def get_url_from_archive_element(cls, archive_elt):
5362
        return 'http://tapastic.com/episode/' + str(archive_elt['id'])
5363
5364
    @classmethod
5365
    def get_archive_elements(cls):
5366
        pref, suff = 'episodeList : ', ','
5367
        # Information is stored in the javascript part
5368
        # I don't know the clean way to get it so this is the ugly way.
5369
        string = [s[len(pref):-len(suff)] for s in (s.decode('utf-8').strip() for s in urlopen_wrapper(cls.url).readlines()) if s.startswith(pref) and s.endswith(suff)][0]
5370
        return json.loads(string)
5371
5372
5373
class VegetablesForDessert(GenericTapasticComic):
5374
    """Class to retrieve Vegetables For Dessert comics."""
5375
    # Also on http://vegetablesfordessert.tumblr.com
5376
    name = 'vegetables'
5377
    long_name = 'Vegetables For Dessert'
5378
    url = 'http://tapastic.com/series/vegetablesfordessert'
5379
5380
5381
class FowlLanguageTapa(GenericTapasticComic):
5382
    """Class to retrieve Fowl Language comics."""
5383
    # Also on http://www.fowllanguagecomics.com
5384
    # Also on http://fowllanguagecomics.tumblr.com
5385
    # Also on http://www.gocomics.com/fowl-language
5386
    name = 'fowllanguage-tapa'
5387
    long_name = 'Fowl Language Comics (from Tapastic)'
5388
    url = 'http://tapastic.com/series/Fowl-Language-Comics'
5389
    _categories = ('FOWLLANGUAGE', )
5390
5391
5392
class OscillatingProfundities(GenericTapasticComic):
5393
    """Class to retrieve Oscillating Profundities comics."""
5394
    name = 'oscillating'
5395
    long_name = 'Oscillating Profundities'
5396
    url = 'http://tapastic.com/series/oscillatingprofundities'
5397
5398
5399
class ZnoflatsComics(GenericTapasticComic):
5400
    """Class to retrieve Znoflats comics."""
5401
    name = 'znoflats'
5402
    long_name = 'Znoflats Comics'
5403
    url = 'http://tapastic.com/series/Znoflats-Comics'
5404
5405
5406
class SandersenTapastic(GenericTapasticComic):
5407
    """Class to retrieve Sarah Andersen comics."""
5408
    # Also on http://sarahcandersen.com
5409
    # Also on http://www.gocomics.com/sarahs-scribbles
5410
    name = 'sandersen-tapa'
5411
    long_name = 'Sarah Andersen (from Tapastic)'
5412
    url = 'http://tapastic.com/series/Doodle-Time'
5413
5414
5415
class TubeyToonsTapastic(GenericTapasticComic):
5416
    """Class to retrieve TubeyToons comics."""
5417
    # Also on http://tubeytoons.com
5418
    # Also on https://tubeytoons.tumblr.com
5419
    name = 'tubeytoons-tapa'
5420
    long_name = 'Tubey Toons (from Tapastic)'
5421
    url = 'http://tapastic.com/series/Tubey-Toons'
5422
    _categories = ('TUNEYTOONS', )
5423
5424
5425
class AnythingComicTapastic(GenericTapasticComic):
5426
    """Class to retrieve Anything Comics."""
5427
    # Also on http://www.anythingcomic.com
5428
    name = 'anythingcomic-tapa'
5429
    long_name = 'Anything Comic (from Tapastic)'
5430
    url = 'http://tapastic.com/series/anything'
5431
5432
5433
class UnearthedComicsTapastic(GenericTapasticComic):
5434
    """Class to retrieve Unearthed comics."""
5435
    # Also on http://unearthedcomics.com
5436
    # Also on https://unearthedcomics.tumblr.com
5437
    name = 'unearthed-tapa'
5438
    long_name = 'Unearthed Comics (from Tapastic)'
5439
    url = 'http://tapastic.com/series/UnearthedComics'
5440
    _categories = ('UNEARTHED', )
5441
5442
5443
class EverythingsStupidTapastic(GenericTapasticComic):
5444
    """Class to retrieve Everything's stupid Comics."""
5445
    # Also on http://www.webtoons.com/en/challenge/everythings-stupid/list?title_no=14591
5446
    # Also on http://everythingsstupid.net
5447
    name = 'stupid-tapa'
5448
    long_name = "Everything's Stupid (from Tapastic)"
5449
    url = 'http://tapastic.com/series/EverythingsStupid'
5450
5451
5452
class JustSayEhTapastic(GenericTapasticComic):
5453
    """Class to retrieve Just Say Eh comics."""
5454
    # Also on http://www.justsayeh.com
5455
    name = 'justsayeh-tapa'
5456
    long_name = 'Just Say Eh (from Tapastic)'
5457
    url = 'http://tapastic.com/series/Just-Say-Eh'
5458
5459
5460
class ThorsThundershackTapastic(GenericTapasticComic):
5461
    """Class to retrieve Thor's Thundershack comics."""
5462
    # Also on http://www.thorsthundershack.com
5463
    name = 'thor-tapa'
5464
    long_name = 'Thor\'s Thundershack (from Tapastic)'
5465
    url = 'http://tapastic.com/series/Thors-Thundershac'
5466
    _categories = ('THOR', )
5467
5468
5469
class OwlTurdTapastic(GenericTapasticComic):
5470
    """Class to retrieve Owl Turd / Shen comix."""
5471
    # Also on http://shencomix.com
5472
    name = 'owlturd-tapa'
5473
    long_name = 'Owl Turd / Shen Comix (from Tapastic)'
5474
    url = 'https://tapas.io/series/Shen-Comix'
5475
    _categories = ('OWLTURD', 'SHENCOMIX')
5476
5477
5478
class GoneIntoRaptureTapastic(GenericTapasticComic):
5479
    """Class to retrieve Gone Into Rapture comics."""
5480
    # Also on http://goneintorapture.tumblr.com
5481
    # Also on http://goneintorapture.com
5482
    name = 'rapture-tapa'
5483
    long_name = 'Gone Into Rapture (from Tapastic)'
5484
    url = 'http://tapastic.com/series/Goneintorapture'
5485
5486
5487
class HeckIfIKnowComicsTapa(GenericTapasticComic):
5488
    """Class to retrieve Heck If I Know Comics."""
5489
    # Also on http://heckifiknowcomics.com
5490
    name = 'heck-tapa'
5491
    long_name = 'Heck if I Know comics (from Tapastic)'
5492
    url = 'http://tapastic.com/series/Regular'
5493
5494
5495
class CheerUpEmoKidTapa(GenericTapasticComic):
5496
    """Class to retrieve CheerUpEmoKid comics."""
5497
    # Also on http://www.cheerupemokid.com
5498
    # Also on https://enzocomics.tumblr.com
5499
    name = 'cuek-tapa'
5500
    long_name = 'Cheer Up Emo Kid (from Tapastic)'
5501
    url = 'http://tapastic.com/series/CUEK'
5502
5503
5504
class BigFootJusticeTapa(GenericTapasticComic):
5505
    """Class to retrieve Big Foot Justice comics."""
5506
    # Also on http://bigfootjustice.com
5507
    name = 'bigfoot-tapa'
5508
    long_name = 'Big Foot Justice (from Tapastic)'
5509
    url = 'http://tapastic.com/series/bigfoot-justice'
5510
5511
5512
class UpAndOutTapa(GenericTapasticComic):
5513
    """Class to retrieve Up & Out comics."""
5514
    # Also on http://upandoutcomic.tumblr.com
5515
    name = 'upandout-tapa'
5516
    long_name = 'Up And Out (from Tapastic)'
5517
    url = 'http://tapastic.com/series/UP-and-OUT'
5518
5519
5520
class ToonHoleTapa(GenericTapasticComic):
5521
    """Class to retrieve Toon Holes comics."""
5522
    # Also on http://www.toonhole.com
5523
    name = 'toonhole-tapa'
5524
    long_name = 'Toon Hole (from Tapastic)'
5525
    url = 'http://tapastic.com/series/TOONHOLE'
5526
5527
5528
class AngryAtNothingTapa(GenericTapasticComic):
5529
    """Class to retrieve Angry at Nothing comics."""
5530
    # Also on http://www.angryatnothing.net
5531
    # Also on http://angryatnothing.tumblr.com
5532
    name = 'angry-tapa'
5533
    long_name = 'Angry At Nothing (from Tapastic)'
5534
    url = 'http://tapastic.com/series/Comics-yeah-definitely-comics-'
5535
5536
5537
class LeleozTapa(GenericTapasticComic):
5538
    """Class to retrieve Leleoz comics."""
5539
    # Also on http://leleozcomics.tumblr.com
5540
    name = 'leleoz-tapa'
5541
    long_name = 'Leleoz (from Tapastic)'
5542
    url = 'https://tapastic.com/series/Leleoz'
5543
5544
5545
class TheAwkwardYetiTapa(GenericTapasticComic):
5546
    """Class to retrieve The Awkward Yeti comics."""
5547
    # Also on http://www.gocomics.com/the-awkward-yeti
5548
    # Also on http://theawkwardyeti.com
5549
    # Also on http://larstheyeti.tumblr.com
5550
    name = 'yeti-tapa'
5551
    long_name = 'The Awkward Yeti (from Tapastic)'
5552
    url = 'https://tapastic.com/series/TheAwkwardYeti'
5553
    _categories = ('YETI', )
5554
5555
5556
class AsPerUsualTapa(GenericTapasticComic):
5557
    """Class to retrieve As Per Usual comics."""
5558
    # Also on http://as-per-usual.tumblr.com
5559
    name = 'usual-tapa'
5560
    long_name = 'As Per Usual (from Tapastic)'
5561
    url = 'https://tapastic.com/series/AsPerUsual'
5562
    categories = ('DAMILEE', )
5563
5564
5565
class HotComicsForCoolPeopleTapa(GenericTapasticComic):
5566
    """Class to retrieve Hot Comics For Cool People."""
5567
    # Also on http://hotcomicsforcoolpeople.tumblr.com
5568
    # Also on http://hotcomics.biz (links to tumblr)
5569
    # Also on http://hcfcp.com (links to tumblr)
5570
    name = 'hotcomics-tapa'
5571
    long_name = 'Hot Comics For Cool People (from Tapastic)'
5572
    url = 'https://tapastic.com/series/Hot-Comics-For-Cool-People'
5573
    categories = ('DAMILEE', )
5574
5575
5576
class OneOneOneOneComicTapa(GenericTapasticComic):
5577
    """Class to retrieve 1111 Comics."""
5578
    # Also on http://www.1111comics.me
5579
    # Also on http://comics1111.tumblr.com
5580
    name = '1111-tapa'
5581
    long_name = '1111 Comics (from Tapastic)'
5582
    url = 'https://tapastic.com/series/1111-Comics'
5583
    _categories = ('ONEONEONEONE', )
5584
5585
5586
class TumbleDryTapa(GenericTapasticComic):
5587
    """Class to retrieve Tumble Dry comics."""
5588
    # Also on http://tumbledrycomics.com
5589
    name = 'tumbledry-tapa'
5590
    long_name = 'Tumblr Dry (from Tapastic)'
5591
    url = 'https://tapastic.com/series/TumbleDryComics'
5592
5593
5594
class DeadlyPanelTapa(GenericTapasticComic):
5595
    """Class to retrieve Deadly Panel comics."""
5596
    # Also on http://www.deadlypanel.com
5597
    # Also on https://deadlypanel.tumblr.com
5598
    name = 'deadly-tapa'
5599
    long_name = 'Deadly Panel (from Tapastic)'
5600
    url = 'https://tapastic.com/series/deadlypanel'
5601
5602
5603
class ChrisHallbeckMaxiTapa(GenericTapasticComic):
5604
    """Class to retrieve Chris Hallbeck comics."""
5605
    # Also on https://chrishallbeck.tumblr.com
5606
    # Also on http://maximumble.com
5607
    name = 'hallbeckmaxi-tapa'
5608
    long_name = 'Chris Hallback - Maximumble (from Tapastic)'
5609
    url = 'https://tapastic.com/series/Maximumble'
5610
    _categories = ('HALLBACK', )
5611
5612
5613
class ChrisHallbeckMiniTapa(GenericDeletedComic, GenericTapasticComic):
5614
    """Class to retrieve Chris Hallbeck comics."""
5615
    # Also on https://chrishallbeck.tumblr.com
5616
    # Also on http://minimumble.com
5617
    name = 'hallbeckmini-tapa'
5618
    long_name = 'Chris Hallback - Minimumble (from Tapastic)'
5619
    url = 'https://tapastic.com/series/Minimumble'
5620
    _categories = ('HALLBACK', )
5621
5622
5623
class ChrisHallbeckBiffTapa(GenericDeletedComic, GenericTapasticComic):
5624
    """Class to retrieve Chris Hallbeck comics."""
5625
    # Also on https://chrishallbeck.tumblr.com
5626
    # Also on http://thebookofbiff.com
5627
    name = 'hallbeckbiff-tapa'
5628
    long_name = 'Chris Hallback - The Book of Biff (from Tapastic)'
5629
    url = 'https://tapastic.com/series/Biff'
5630
    _categories = ('HALLBACK', )
5631
5632
5633
class RandoWisTapa(GenericTapasticComic):
5634
    """Class to retrieve RandoWis comics."""
5635
    # Also on https://randowis.com
5636
    name = 'randowis-tapa'
5637
    long_name = 'RandoWis (from Tapastic)'
5638
    url = 'https://tapastic.com/series/RandoWis'
5639
5640
5641
class PigeonGazetteTapa(GenericTapasticComic):
5642
    """Class to retrieve The Pigeon Gazette comics."""
5643
    # Also on http://thepigeongazette.tumblr.com
5644
    name = 'pigeon-tapa'
5645
    long_name = 'The Pigeon Gazette (from Tapastic)'
5646
    url = 'https://tapastic.com/series/The-Pigeon-Gazette'
5647
5648
5649
class TheOdd1sOutTapa(GenericTapasticComic):
5650
    """Class to retrieve The Odd 1s Out comics."""
5651
    # Also on http://theodd1sout.com
5652
    # Also on http://theodd1sout.tumblr.com
5653
    name = 'theodd-tapa'
5654
    long_name = 'The Odd 1s Out (from Tapastic)'
5655
    url = 'https://tapastic.com/series/Theodd1sout'
5656
5657
5658
class TheWorldIsFlatTapa(GenericTapasticComic):
5659
    """Class to retrieve The World Is Flat Comics."""
5660
    # Also on http://theworldisflatcomics.tumblr.com
5661
    name = 'flatworld-tapa'
5662
    long_name = 'The World Is Flat (from Tapastic)'
5663
    url = 'https://tapastic.com/series/The-World-is-Flat'
5664
5665
5666
class MisterAndMeTapa(GenericTapasticComic):
5667
    """Class to retrieve Mister & Me Comics."""
5668
    # Also on http://www.mister-and-me.com
5669
    # Also on http://www.gocomics.com/mister-and-me
5670
    name = 'mister-tapa'
5671
    long_name = 'Mister & Me (from Tapastic)'
5672
    url = 'https://tapastic.com/series/Mister-and-Me'
5673
5674
5675
class TalesOfAbsurdityTapa(GenericDeletedComic, GenericTapasticComic):
5676
    """Class to retrieve Tales Of Absurdity comics."""
5677
    # Also on http://talesofabsurdity.com
5678
    # Also on http://talesofabsurdity.tumblr.com
5679
    name = 'absurdity-tapa'
5680
    long_name = 'Tales of Absurdity (from Tapastic)'
5681
    url = 'http://tapastic.com/series/Tales-Of-Absurdity'
5682
    _categories = ('ABSURDITY', )
5683
5684
5685
class BFGFSTapa(GenericTapasticComic):
5686
    """Class to retrieve BFGFS comics."""
5687
    # Also on http://bfgfs.com
5688
    # Also on https://bfgfs.tumblr.com
5689
    name = 'bfgfs-tapa'
5690
    long_name = 'BFGFS (from Tapastic)'
5691
    url = 'https://tapastic.com/series/BFGFS'
5692
5693
5694
class DoodleForFoodTapa(GenericTapasticComic):
5695
    """Class to retrieve Doodle For Food comics."""
5696
    # Also on http://www.doodleforfood.com
5697
    name = 'doodle-tapa'
5698
    long_name = 'Doodle For Food (from Tapastic)'
5699
    url = 'https://tapastic.com/series/Doodle-for-Food'
5700
5701
5702
class MrLovensteinTapa(GenericTapasticComic):
5703
    """Class to retrieve Mr Lovenstein comics."""
5704
    # Also on  https://tapastic.com/series/MrLovenstein
5705
    name = 'mrlovenstein-tapa'
5706
    long_name = 'Mr. Lovenstein (from Tapastic)'
5707
    url = 'https://tapastic.com/series/MrLovenstein'
5708
5709
5710
class CassandraCalinTapa(GenericTapasticComic):
5711
    """Class to retrieve C. Cassandra comics."""
5712
    # Also on http://cassandracalin.com
5713
    # Also on http://c-cassandra.tumblr.com
5714
    name = 'cassandra-tapa'
5715
    long_name = 'Cassandra Calin (from Tapastic)'
5716
    url = 'https://tapastic.com/series/C-Cassandra-comics'
5717
5718
5719
class WafflesAndPancakes(GenericTapasticComic):
5720
    """Class to retrieve Waffles And Pancakes comics."""
5721
    # Also on http://wandpcomic.com
5722
    name = 'waffles'
5723
    long_name = 'Waffles And Pancakes'
5724
    url = 'https://tapastic.com/series/Waffles-and-Pancakes'
5725
5726
5727
class YesterdaysPopcornTapastic(GenericTapasticComic):
5728
    """Class to retrieve Yesterday's Popcorn comics."""
5729
    # Also on http://www.yesterdayspopcorn.com
5730
    # Also on http://yesterdayspopcorn.tumblr.com
5731
    name = 'popcorn-tapa'
5732
    long_name = 'Yesterday\'s Popcorn (from Tapastic)'
5733
    url = 'https://tapastic.com/series/Yesterdays-Popcorn'
5734
5735
5736
class OurSuperAdventureTapastic(GenericDeletedComic, GenericTapasticComic):
5737
    """Class to retrieve Our Super Adventure comics."""
5738
    # Also on http://www.oursuperadventure.com
5739
    # http://sarahssketchbook.tumblr.com
5740
    # http://sarahgraley.com
5741
    name = 'superadventure-tapastic'
5742
    long_name = 'Our Super Adventure (from Tapastic)'
5743
    url = 'https://tapastic.com/series/Our-Super-Adventure'
5744
5745
5746
class NamelessPCs(GenericTapasticComic):
5747
    """Class to retrieve Nameless PCs comics."""
5748
    # Also on http://namelesspcs.com
5749
    name = 'namelesspcs-tapa'
5750
    long_name = 'NamelessPCs (from Tapastic)'
5751
    url = 'https://tapastic.com/series/NamelessPC'
5752
5753
5754
class DownTheUpwardSpiralTapa(GenericTapasticComic):
5755
    """Class to retrieve Down The Upward Spiral comics."""
5756
    # Also on http://www.downtheupwardspiral.com
5757
    # Also on http://downtheupwardspiral.tumblr.com
5758
    name = 'spiral-tapa'
5759
    long_name = 'Down the Upward Spiral (from Tapastic)'
5760
    url = 'https://tapastic.com/series/Down-the-Upward-Spiral'
5761
5762
5763
class UbertoolTapa(GenericTapasticComic):
5764
    """Class to retrieve Ubertool comics."""
5765
    # Also on http://ubertoolcomic.com
5766
    # Also on https://ubertool.tumblr.com
5767
    name = 'ubertool-tapa'
5768
    long_name = 'Ubertool (from Tapastic)'
5769
    url = 'https://tapastic.com/series/ubertool'
5770
    _categories = ('UBERTOOL', )
5771
5772
5773
class BarteNerdsTapa(GenericDeletedComic, GenericTapasticComic):
5774
    """Class to retrieve BarteNerds comics."""
5775
    # Also on http://www.bartenerds.com
5776
    name = 'bartenerds-tapa'
5777
    long_name = 'BarteNerds (from Tapastic)'
5778
    url = 'https://tapastic.com/series/BarteNERDS'
5779
5780
5781
class SmallBlueYonderTapa(GenericTapasticComic):
5782
    """Class to retrieve Small Blue Yonder comics."""
5783
    # Also on http://www.smallblueyonder.com
5784
    name = 'smallblue-tapa'
5785
    long_name = 'Small Blue Yonder (from Tapastic)'
5786
    url = 'https://tapastic.com/series/Small-Blue-Yonder'
5787
5788
5789
class TizzyStitchBirdTapa(GenericTapasticComic):
5790
    """Class to retrieve Tizzy Stitch Bird comics."""
5791
    # Also on http://tizzystitchbird.com
5792
    # Also on http://tizzystitchbird.tumblr.com
5793
    # Also on http://www.webtoons.com/en/challenge/tizzy-stitchbird/list?title_no=50082
5794
    name = 'tizzy-tapa'
5795
    long_name = 'Tizzy Stitch Bird (from Tapastic)'
5796
    url = 'https://tapastic.com/series/TizzyStitchbird'
5797
5798
5799
class RockPaperCynicTapa(GenericTapasticComic):
5800
    """Class to retrieve RockPaperCynic comics."""
5801
    # Also on http://www.rockpapercynic.com
5802
    # Also on http://rockpapercynic.tumblr.com
5803
    name = 'rpc-tapa'
5804
    long_name = 'Rock Paper Cynic (from Tapastic)'
5805
    url = 'https://tapastic.com/series/rockpapercynic'
5806
5807
5808
class IsItCanonTapa(GenericTapasticComic):
5809
    """Class to retrieve Is It Canon comics."""
5810
    # Also on http://www.isitcanon.com
5811
    name = 'canon-tapa'
5812
    long_name = 'Is It Canon (from Tapastic)'
5813
    url = 'http://tapastic.com/series/isitcanon'
5814
5815
5816
class ItsTheTieTapa(GenericTapasticComic):
5817
    """Class to retrieve It's the tie comics."""
5818
    # Also on http://itsthetie.com
5819
    # Also on http://itsthetie.tumblr.com
5820
    name = 'tie-tapa'
5821
    long_name = "It's the tie (from Tapastic)"
5822
    url = "https://tapastic.com/series/itsthetie"
5823
    _categories = ('TIE', )
5824
5825
5826
class JamesOfNoTradesTapa(GenericTapasticComic):
5827
    """Class to retrieve JamesOfNoTrades comics."""
5828
    # Also on http://jamesofnotrades.com
5829
    # Also on http://www.webtoons.com/en/challenge/james-of-no-trades/list?title_no=43422
5830
    # Also on http://jamesfregan.tumblr.com
5831
    name = 'jamesofnotrades-tapa'
5832
    long_name = 'James Of No Trades (from Tapastic)'
5833
    url = 'https://tapas.io/series/James-of-No-Trades'
5834
    _categories = ('JAMESOFNOTRADES', )
5835
5836
5837
class MomentumTapa(GenericTapasticComic):
5838
    """Class to retrieve Momentum comics."""
5839
    # Also on http://www.momentumcomic.com
5840
    name = 'momentum-tapa'
5841
    long_name = 'Momentum (from Tapastic)'
5842
    url = 'https://tapastic.com/series/momentum'
5843
5844
5845
class InYourFaceCakeTapa(GenericTapasticComic):
5846
    """Class to retrieve In Your Face Cake comics."""
5847
    # Also on https://in-your-face-cake.tumblr.com
5848
    name = 'inyourfacecake-tapa'
5849
    long_name = 'In Your Face Cake (from Tapastic)'
5850
    url = 'https://tapas.io/series/In-Your-Face-Cake'
5851
    _categories = ('INYOURFACECAKE', )
5852
5853
5854
class CowardlyComicsTapa(GenericTapasticComic):
5855
    """Class to retrieve Cowardly Comics."""
5856
    # Also on http://cowardlycomics.tumblr.com
5857
    # Also on http://www.webtoons.com/en/challenge/cowardly-comics/list?title_no=65893
5858
    name = 'cowardly-tapa'
5859
    long_name = 'Cowardly Comics (from Tapastic)'
5860
    url = 'https://tapas.io/series/CowardlyComics'
5861
5862
5863
class Caw4hwTapa(GenericTapasticComic):
5864
    """Class to retrieve Caw4hw comics."""
5865
    # Also on https://caw4hw.tumblr.com
5866
    name = 'caw4hw-tapa'
5867
    long_name = 'Caw4hw (from Tapastic)'
5868
    url = 'https://tapas.io/series/CAW4HW'
5869
5870
5871
class DontBeDadTapa(GenericTapasticComic):
5872
    """Class to retrieve Don't Be Dad comics."""
5873
    # Also on https://dontbedad.com/
5874
    # Also on http://www.webtoons.com/en/challenge/dontbedad/list?title_no=123074
5875
    name = 'dontbedad-tapa'
5876
    long_name = "Don't Be Dad (from Tapastic)"
5877
    url = 'https://tapas.io/series/DontBeDad-Comics'
5878
5879
5880
class APleasantWasteOfTimeTapa(GenericTapasticComic):
5881
    """Class to retrieve A Pleasant Waste Of Time comics."""
5882
    # Also on https://artjcf.tumblr.com
5883
    name = 'pleasant-waste-tapa'
5884
    long_name = 'A Pleasant Waste Of Time (from Tapastic)'
5885
    url = 'https://tapas.io/series/A-Pleasant-'
5886
    _categories = ('WASTE', )
5887
5888
5889
class InfiniteImmortalBensTapa(GenericTapasticComic):
5890
    """Class to retrieve Infinite Immortal Bens comics."""
5891
    # Also on http://www.webtoons.com/en/challenge/infinite-immortal-bens/list?title_no=32847
5892
    # Also on https://infiniteimmortalbens.tumblr.com
5893
    url = 'https://tapas.io/series/Infinite-Immortal-Bens'
5894
    name = 'infiniteimmortal-tapa'
5895
    long_name = 'Infinite Immortal Bens (from Tapastic)'
5896
    _categories = ('INFINITEIMMORTAL', )
5897
5898
5899
class EatMyPaintTapa(GenericTapasticComic):
5900
    """Class to retrieve Eat My Paint comics."""
5901
    # Also on https://eatmypaint.tumblr.com
5902
    name = 'eatmypaint-tapa'
5903
    long_name = 'Eat My Paint (from Tapastic)'
5904
    url = 'https://tapas.io/series/eatmypaint'
5905
    _categories = ('EATMYPAINT', )
5906
5907
5908
class MercworksTapa(GenericTapasticComic):
5909
    """Class to retrieve Mercworks comics."""
5910
    # Also on http://mercworks.net
5911
    # Also on http://www.webtoons.com/en/comedy/mercworks/list?title_no=426
5912
    # Also on http://mercworks.tumblr.com
5913
    name = 'mercworks-tapa'
5914
    long_name = 'Mercworks (from Tapastic)'
5915
    url = 'https://tapastic.com/series/MercWorks'
5916
    _categories = ('MERCWORKS', )
5917
5918
5919
class AbsurdoLapin(GenericNavigableComic):
5920
    """Class to retrieve Absurdo Lapin comics."""
5921
    name = 'absurdo'
5922
    long_name = 'Absurdo'
5923
    url = 'https://absurdo.lapin.org'
5924
    get_url_from_link = join_cls_url_to_href
5925
5926
    @classmethod
5927
    def get_nav(cls, soup):
5928
        """Get the navigation elements from soup object."""
5929
        cont = soup.find('div', id='content')
5930
        _, b2 = cont.find_all('div', class_='buttons')
5931
        # prev, first, last, next
5932
        return [li.find('a') for li in b2.find_all('li')]
5933
5934
    @classmethod
5935
    def get_first_comic_link(cls):
5936
        """Get link to first comics."""
5937
        return cls.get_nav(get_soup_at_url(cls.url))[1]
5938
5939
    @classmethod
5940
    def get_navi_link(cls, last_soup, next_):
5941
        """Get link to next or previous comic."""
5942
        return cls.get_nav(last_soup)[3 if next_ else 0]
5943
5944
    @classmethod
5945
    def get_comic_info(cls, soup, link):
5946
        """Get information about a particular comics."""
5947
        author = soup.find('meta', attrs={'name': 'author'})['content']
5948
        tags = soup.find('meta', attrs={'name': 'keywords'})['content']
5949
        title = soup.find('title').string
5950
        imgs = soup.find('div', id='content').find_all('img')
5951
        return {
5952
            'title': title,
5953
            'img': [urljoin_wrapper(cls.url, i['src']) for i in imgs],
5954
            'tags': tags,
5955
            'author': author,
5956
        }
5957
5958
5959
def get_subclasses(klass):
5960
    """Gets the list of direct/indirect subclasses of a class"""
5961
    subclasses = klass.__subclasses__()
5962
    for derived in list(subclasses):
5963
        subclasses.extend(get_subclasses(derived))
5964
    return subclasses
5965
5966
5967
def remove_st_nd_rd_th_from_date(string):
5968
    """Function to transform 1st/2nd/3rd/4th in a parsable date format."""
5969
    # Hackish way to convert string with numeral "1st"/"2nd"/etc to date
5970
    return (string.replace('st', '')
5971
            .replace('nd', '')
5972
            .replace('rd', '')
5973
            .replace('th', '')
5974
            .replace('Augu', 'August'))
5975
5976
5977
def string_to_date(string, date_format, local=DEFAULT_LOCAL):
5978
    """Function to convert string to date object.
5979
    Wrapper around datetime.datetime.strptime."""
5980
    # format described in https://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
5981
    prev_locale = locale.setlocale(locale.LC_ALL)
5982
    if local != prev_locale:
5983
        locale.setlocale(locale.LC_ALL, local)
5984
    ret = datetime.datetime.strptime(string, date_format).date()
5985
    if local != prev_locale:
5986
        locale.setlocale(locale.LC_ALL, prev_locale)
5987
    return ret
5988
5989
5990
COMICS = set(get_subclasses(GenericComic))
5991
VALID_COMICS = [c for c in COMICS if c.name is not None]
5992
COMIC_NAMES = {c.name: c for c in VALID_COMICS}
5993
assert len(VALID_COMICS) == len(COMIC_NAMES)
5994
CLASS_NAMES = {c.__name__ for c in VALID_COMICS}
5995
assert len(VALID_COMICS) == len(CLASS_NAMES)
5996