diff options
Diffstat (limited to 'dotfiles/system/.config/calibre')
17 files changed, 1294 insertions, 10329 deletions
| diff --git a/dotfiles/system/.config/calibre/custom_recipes/The Economist_1001.recipe b/dotfiles/system/.config/calibre/custom_recipes/The Economist_1001.recipe new file mode 100644 index 0000000..bcb8364 --- /dev/null +++ b/dotfiles/system/.config/calibre/custom_recipes/The Economist_1001.recipe @@ -0,0 +1,684 @@ +#!/usr/bin/env  python +# License: GPLv3 Copyright: 2008, Kovid Goyal <kovid at kovidgoyal.net> + +import json +import re +import time +from collections import defaultdict +from datetime import datetime, timedelta +from urllib.parse import quote, urlencode +from uuid import uuid4 + +from html5_parser import parse +from lxml import etree + +from calibre import replace_entities +from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString, Tag +from calibre.ptempfile import PersistentTemporaryFile +from calibre.scraper.simple import read_url +from calibre.utils.date import parse_only_date +from calibre.web.feeds.news import BasicNewsRecipe + + +def E(parent, name, text='', **attrs): +    ans = parent.makeelement(name, **attrs) +    ans.text = text +    parent.append(ans) +    return ans + + +def process_node(node, html_parent): +    ntype = node.get('type') +    if ntype == 'tag': +        c = html_parent.makeelement(node['name']) +        c.attrib.update({k: v or '' for k, v in node.get('attribs', {}).items()}) +        html_parent.append(c) +        for nc in node.get('children', ()): +            process_node(nc, c) +    elif ntype == 'text': +        text = node.get('data') +        if text: +            text = replace_entities(text) +            if len(html_parent): +                t = html_parent[-1] +                t.tail = (t.tail or '') + text +            else: +                html_parent.text = (html_parent.text or '') + text + + +def safe_dict(data, *names): +    ans = data +    for x in names: +        ans = ans.get(x) or {} +    return ans + + +class JSONHasNoContent(ValueError): +    pass + + +def load_article_from_json(raw, root): +    # open('/t/raw.json', 'w').write(raw) +    data = json.loads(raw) +    body = root.xpath('//body')[0] +    article = E(body, 'article') +    E(article, 'div', data['flyTitle'], style='color: red; font-size:small; font-weight:bold;') +    E(article, 'h1', data['title'], title=safe_dict(data, 'url', 'canonical') or '') +    E(article, 'div', data['rubric'], style='font-style: italic; color:#202020;') +    try: +        date = data['dateModified'] +    except Exception: +        date = data['datePublished'] +    dt = datetime.fromisoformat(date[:-1]) + timedelta(seconds=time.timezone) +    dt = dt.strftime('%b %d, %Y %I:%M %p') +    if data['dateline'] is None: +        E(article, 'p', dt, style='color: gray; font-size:small;') +    else: +        E(article, 'p', dt + ' | ' + (data['dateline']), style='color: gray; font-size:small;') +    main_image_url = safe_dict(data, 'image', 'main', 'url').get('canonical') +    if main_image_url: +        div = E(article, 'div') +        try: +            E(div, 'img', src=main_image_url) +        except Exception: +            pass +    for node in data.get('text') or (): +        process_node(node, article) + + +def process_web_list(li_node): +    li_html = '' +    for li in li_node['items']: +        if li.get('textHtml'): +            li_html += f'<li>{li.get("textHtml")}</li>' +        else: +            li_html +=  f'<li>{li.get("text", "")}</li>' +    return li_html + + +def process_info_box(bx): +    info = '' +    for x in safe_dict(bx, 'components'): +        info += f'<blockquote>{process_web_node(x)}</blockquote>' +    return info + + +def process_web_node(node): +    ntype = node.get('type', '') +    if ntype == 'CROSSHEAD': +        if node.get('textHtml'): +            return f'<h4>{node.get("textHtml")}</h4>' +        return f'<h4>{node.get("text", "")}</h4>' +    elif ntype in ['PARAGRAPH', 'BOOK_INFO']: +        if node.get('textHtml'): +            return f'<p>{node.get("textHtml")}</p>' +        return f'<p>{node.get("text", "")}</p>' +    elif ntype == 'IMAGE': +        alt = '' if node.get('altText') is None else node.get('altText') +        cap = '' +        if node.get('caption'): +            if node['caption'].get('textHtml') is not None: +                cap = node['caption']['textHtml'] +        return f'<div><img src="{node["url"]}" title="{alt}"></div><div style="text-align:center; font-size:small;">{cap}</div>' +    elif ntype == 'PULL_QUOTE': +        if node.get('textHtml'): +            return f'<blockquote>{node.get("textHtml")}</blockquote>' +        return f'<blockquote>{node.get("text", "")}</blockquote>' +    elif ntype == 'DIVIDER': +        return '<hr>' +    elif ntype == 'INFOGRAPHIC': +        if node.get('fallback'): +            return process_web_node(node['fallback']) +    elif ntype == 'INFOBOX': +        return process_info_box(node) +    elif ntype == 'UNORDERED_LIST': +        if node.get('items'): +            return process_web_list(node) +    elif ntype: +        print('** ', ntype) +    return '' + + +def load_article_from_web_json(raw): +    # open('/t/raw.json', 'w').write(raw) +    body = '' +    try: +        data = json.loads(raw)['props']['pageProps']['cp2Content'] +    except Exception: +        data = json.loads(raw)['props']['pageProps']['content'] +    body += f'<div style="color: red; font-size:small; font-weight:bold;">{data.get("flyTitle", "")}</div>' +    body += f'<h1>{data["headline"]}</h1>' +    if data.get('rubric') and data.get('rubric') is not None: +        body += f'<div style="font-style: italic; color:#202020;">{data.get("rubric", "")}</div>' +    try: +        date = data['dateModified'] +    except Exception: +        date = data['datePublished'] +    dt = datetime.fromisoformat(date[:-1]) + timedelta(seconds=time.timezone) +    dt = dt.strftime('%b %d, %Y %I:%M %p') +    if data.get('dateline') is None: +        body += f'<p style="color: gray; font-size: small;">{dt}</p>' +    else: +        body += f'<p style="color: gray; font-size: small;">{dt + " | " + (data["dateline"])}</p>' +    main_image_url = safe_dict(data, 'leadComponent') or '' +    if main_image_url: +        body += process_web_node(data['leadComponent']) +    for node in data.get('body'): +        body += process_web_node(node) +    return '<html><body><article>' + body + '</article></body></html>' + + +def cleanup_html_article(root): +    main = root.xpath('//main')[0] +    body = root.xpath('//body')[0] +    for child in tuple(body): +        body.remove(child) +    body.append(main) +    main.set('id', '') +    main.tag = 'article' +    for x in root.xpath('//*[@style]'): +        x.set('style', '') +    for x in root.xpath('//button'): +        x.getparent().remove(x) + + +def classes(classes): +    q = frozenset(classes.split(' ')) +    return dict(attrs={ +        'class': lambda x: x and frozenset(x.split()).intersection(q)}) + + +def new_tag(soup, name, attrs=()): +    impl = getattr(soup, 'new_tag', None) +    if impl is not None: +        return impl(name, attrs=dict(attrs)) +    return Tag(soup, name, attrs=attrs or None) + + +class NoArticles(Exception): +    pass + + +def process_url(url): +    if url.startswith('/'): +        url = 'https://www.economist.com' + url +    return url + + +class Economist(BasicNewsRecipe): +    title = 'The Economist' +    language = 'en_GB' +    encoding = 'utf-8' +    masthead_url = 'https://www.livemint.com/lm-img/dev/economist-logo-oneline.png' + +    __author__ = 'Kovid Goyal' +    description = ( +        'Global news and current affairs from a European' +        ' perspective. Best downloaded on Friday mornings (GMT)' +    ) +    extra_css = ''' +        em { color:#202020; } +        img {display:block; margin:0 auto;} +    ''' +    oldest_article = 7.0 +    resolve_internal_links = True +    remove_tags = [ +        dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent', 'aside', 'footer', 'svg']), +        dict(attrs={'aria-label': 'Article Teaser'}), +        dict(attrs={'id': 'player'}), +        dict(attrs={ +                'class': [ +                    'dblClkTrk', 'ec-article-info', 'share_inline_header', +                    'related-items', 'main-content-container', 'ec-topic-widget', +                    'teaser', 'blog-post__bottom-panel-bottom', 'blog-post__comments-label', +                    'blog-post__foot-note', 'blog-post__sharebar', 'blog-post__bottom-panel', +                    'newsletter-form', 'share-links-header', 'teaser--wrapped', 'latest-updates-panel__container', +                    'latest-updates-panel__article-link', 'blog-post__section' +                ] +            } +        ), +        dict(attrs={ +                'class': lambda x: x and 'blog-post__siblings-list-aside' in x.split()}), +        dict(attrs={'id': lambda x: x and 'gpt-ad-slot' in x}), +        classes( +            'share-links-header teaser--wrapped latest-updates-panel__container' +            ' latest-updates-panel__article-link blog-post__section newsletter-form blog-post__bottom-panel' +        ) +    ] +    keep_only_tags = [dict(name='article', id=lambda x: not x)] +    no_stylesheets = True +    remove_attributes = ['data-reactid', 'width', 'height'] +    # economist.com has started throttling after about 60% of the total has +    # downloaded with connection reset by peer (104) errors. +    delay = 3 +    browser_type = 'webengine' +    from_archive = True +    recipe_specific_options = { +        'date': { +            'short': 'The date of the edition to download (YYYY-MM-DD format)', +            'long': 'For example, 2024-07-19', +        }, +        'res': { +            'short': 'For hi-res images, select a resolution from the\nfollowing options: 834, 960, 1096, 1280, 1424', +            'long': 'This is useful for non e-ink devices, and for a lower file size\nthan the default, use from 480, 384, 360, 256.', +            'default': '600', +        }, +        'de': { +            'short': 'Web Edition', +            'long': 'Yes/No. Digital Edition does not skip some articles based on your location.', +            'default': 'No', +        } +    } + +    def __init__(self, *args, **kwargs): +        BasicNewsRecipe.__init__(self, *args, **kwargs) +        d = self.recipe_specific_options.get('de') +        if d and isinstance(d, str): +            if d.lower().strip() == 'yes': +                self.from_archive = True + +    needs_subscription = False + +    def get_browser(self, *args, **kwargs): +        if self.from_archive: +            kwargs['user_agent'] = ( +                'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36 Liskov' +            ) +            br = BasicNewsRecipe.get_browser(self, *args, **kwargs) +        else: +            kwargs['user_agent'] = 'TheEconomist-Lamarr-android' +            br = BasicNewsRecipe.get_browser(self, *args, **kwargs) +            br.addheaders += [ +                ('accept', '*/*'), +                ('content-type', 'application/json'), +                ('apollographql-client-name', 'mobile-app-apollo'), +                ('apollographql-client-version', '3.50.0'), +                ('x-request-id', str(uuid4())), +            ] +        return br + +    def publication_date(self): +        edition_date = self.recipe_specific_options.get('date') +        if edition_date and isinstance(edition_date, str): +            return parse_only_date(edition_date, as_utc=False) +        try: +            url = self.browser.open('https://www.economist.com/printedition').geturl() +        except Exception as e: +            self.log('Failed to fetch publication date with error: ' + str(e)) +            return super().publication_date() +        return parse_only_date(url.split('/')[-1], as_utc=False) + +    def economist_test_article(self): +        return [('Articles', [{'title':'test', +            'url':'https://www.economist.com/leaders/2025/03/13/americas-bullied-allies-need-to-toughen-up' +        }])] + +    def economist_return_index(self, ans): +        if not ans: +            raise NoArticles( +                'Could not find any articles, either the ' +                'economist.com server is having trouble and you should ' +                'try later or the website format has changed and the ' +                'recipe needs to be updated.' +            ) +        return ans + +    def get_content_id(self, ed_date): +        id_query = { +            'query': 'query EditionsQuery($from:Int$size:Int$ref:String!){section:canonical(ref:$ref){...EditionFragment __typename}}fragment EditionFragment on Content{hasPart(from:$from size:$size sort:"datePublished:desc"){total parts{id datePublished image{...ImageCoverFragment __typename}__typename}__typename}__typename}fragment ImageCoverFragment on Media{cover{headline width height url{canonical __typename}regionsAllowed __typename}__typename}',  # noqa: E501 +            'operationName': 'EditionsQuery', +            'variables':'{"from":0,"size":24,"ref":"/content/d06tg8j85rifiq3oo544c6b9j61dno2n"}', +        } +        id_url = 'https://cp2-graphql-gateway.p.aws.economist.com/graphql?' + urlencode(id_query, safe='()!', quote_via=quote) +        raw_id_data = self.index_to_soup(id_url, raw=True) +        data = json.loads(raw_id_data)['data']['section']['hasPart']['parts'] +        for x in data: +            if ed_date in x['datePublished']: +                return x['id'] +        return None + +    def parse_index(self): +        if self.from_archive: +            return self.parse_web_index() +        edition_date = self.recipe_specific_options.get('date') +        # return self.economist_test_article() +        # url = 'https://www.economist.com/weeklyedition/archive' +        query = { +            'query': 'query LatestWeeklyAutoEditionQuery($ref:String!){canonical(ref:$ref){hasPart(from:0 size:1 sort:"datePublished:desc"){parts{...WeeklyEditionFragment __typename}__typename}__typename}}fragment WeeklyEditionFragment on Content{id type datePublished image{...ImageCoverFragment __typename}url{canonical __typename}hasPart(size:100 sort:"publication.context.position"){parts{...ArticleFragment __typename}__typename}__typename}fragment ArticleFragment on Content{ad{grapeshot{channels{name __typename}__typename}__typename}articleSection{internal{id title:headline __typename}__typename}audio{main{id duration(format:"seconds")source:channel{id __typename}url{canonical __typename}__typename}__typename}byline dateline dateModified datePublished dateRevised flyTitle:subheadline id image{...ImageInlineFragment ...ImageMainFragment ...ImagePromoFragment __typename}print{title:headline flyTitle:subheadline rubric:description section{id title:headline __typename}__typename}publication{id tegID title:headline flyTitle:subheadline datePublished regionsAllowed url{canonical __typename}__typename}rubric:description source:channel{id __typename}tegID text(format:"json")title:headline type url{canonical __typename}topic contentIdentity{forceAppWebview mediaType articleType __typename}__typename}fragment ImageInlineFragment on Media{inline{url{canonical __typename}width height __typename}__typename}fragment ImageMainFragment on Media{main{url{canonical __typename}width height __typename}__typename}fragment ImagePromoFragment on Media{promo{url{canonical __typename}id width height __typename}__typename}fragment ImageCoverFragment on Media{cover{headline width height url{canonical __typename}regionsAllowed __typename}__typename}',  # noqa: E501 +            'operationName': 'LatestWeeklyAutoEditionQuery', +            'variables': '{"ref":"/content/d06tg8j85rifiq3oo544c6b9j61dno2n"}', +        } +        if edition_date and isinstance(edition_date, str): +            content_id = self.get_content_id(edition_date) +            if content_id: +                query = { +                    'query': 'query SpecificWeeklyEditionQuery($path:String!){section:canonical(ref:$path){...WeeklyEditionFragment __typename}}fragment WeeklyEditionFragment on Content{id type datePublished image{...ImageCoverFragment __typename}url{canonical __typename}hasPart(size:100 sort:"publication.context.position"){parts{...ArticleFragment __typename}__typename}__typename}fragment ArticleFragment on Content{ad{grapeshot{channels{name __typename}__typename}__typename}articleSection{internal{id title:headline __typename}__typename}audio{main{id duration(format:"seconds")source:channel{id __typename}url{canonical __typename}__typename}__typename}byline dateline dateModified datePublished dateRevised flyTitle:subheadline id image{...ImageInlineFragment ...ImageMainFragment ...ImagePromoFragment __typename}print{title:headline flyTitle:subheadline rubric:description section{id title:headline __typename}__typename}publication{id tegID title:headline flyTitle:subheadline datePublished regionsAllowed url{canonical __typename}__typename}rubric:description source:channel{id __typename}tegID text(format:"json")title:headline type url{canonical __typename}topic contentIdentity{forceAppWebview mediaType articleType __typename}__typename}fragment ImageInlineFragment on Media{inline{url{canonical __typename}width height __typename}__typename}fragment ImageMainFragment on Media{main{url{canonical __typename}width height __typename}__typename}fragment ImagePromoFragment on Media{promo{url{canonical __typename}id width height __typename}__typename}fragment ImageCoverFragment on Media{cover{headline width height url{canonical __typename}regionsAllowed __typename}__typename}',  # noqa: E501 +                    'operationName': 'SpecificWeeklyEditionQuery', +                    'variables': '{{"path":"{}"}}'.format(content_id), +                } +        url = 'https://cp2-graphql-gateway.p.aws.economist.com/graphql?' + urlencode(query, safe='()!', quote_via=quote) +        try: +            if edition_date and isinstance(edition_date, str): +                if not content_id: +                    self.log(edition_date, ' not found, trying web edition.') +                    self.from_archive = True +                    return self.parse_web_index() +            raw = self.index_to_soup(url, raw=True) +        except Exception: +            self.log('Digital Edition Server is not reachable, try again after some time.') +            self.from_archive = True +            return self.parse_web_index() +        ans = self.economist_parse_index(raw) +        return self.economist_return_index(ans) + +    def economist_parse_index(self, raw): +        # edition_date = self.recipe_specific_options.get('date') +        # if edition_date and isinstance(edition_date, str): +        #     data = json.loads(raw)['data']['section'] +        # else: +        #     data = json.loads(raw)['data']['canonical']['hasPart']['parts'][0] +        try: +            data = json.loads(raw)['data']['section'] +        except KeyError: +            data = json.loads(raw)['data']['canonical']['hasPart']['parts'][0] +        dt = datetime.fromisoformat(data['datePublished'][:-1]) + timedelta(seconds=time.timezone) +        dt = dt.strftime('%b %d, %Y') +        self.timefmt = ' [' + dt + ']' +        # get local issue cover, title +        try: +            region = json.loads(self.index_to_soup('https://geolocation-db.com/json', raw=True))['country_code'] +        except Exception: +            region = '' +        for cov in data['image']['cover']: +            if region in cov['regionsAllowed']: +                self.description = cov['headline'] +                self.cover_url = cov['url']['canonical'].replace('economist.com/', +                    'economist.com/cdn-cgi/image/width=960,quality=80,format=auto/') +                break +        else: +            self.description = data['image']['cover'][0]['headline'] +            self.cover_url = data['image']['cover'][0]['url']['canonical'].replace('economist.com/', +            'economist.com/cdn-cgi/image/width=960,quality=80,format=auto/') +        self.log('Got cover:', self.cover_url, '\n', self.description) + +        feeds_dict = defaultdict(list) +        for part in safe_dict(data, 'hasPart', 'parts'): +            try: +                section = part['articleSection']['internal'][0]['title'] +            except Exception: +                section = safe_dict(part, 'print', 'section', 'title') or 'section' +            if section not in feeds_dict: +                self.log(section) +            title = safe_dict(part, 'title') +            desc = safe_dict(part, 'rubric') or '' +            sub = safe_dict(part, 'flyTitle') or '' +            if sub and section != sub: +                desc = sub + ' :: ' + desc +            pt = PersistentTemporaryFile('.html') +            pt.write(json.dumps(part).encode('utf-8')) +            pt.close() +            url = 'file:///' + pt.name +            feeds_dict[section].append({'title': title, 'url': url, 'description': desc}) +            self.log('\t', title, '\n\t\t', desc) +        return list(feeds_dict.items()) + +    def populate_article_metadata(self, article, soup, first): +        if not self.from_archive: +            article.url = soup.find('h1')['title'] + +    def preprocess_html(self, soup): +        width = '600' +        w = self.recipe_specific_options.get('res') +        if w and isinstance(w, str): +            width = w +        for img in soup.findAll('img', src=True): +            qua = 'economist.com/cdn-cgi/image/width=' + width + ',quality=80,format=auto/' +            img['src'] = img['src'].replace('economist.com/', qua) +        return soup + +    def preprocess_raw_html(self, raw, url): +        if self.from_archive: +            return self.preprocess_raw_web_html(raw, url) + +        # open('/t/raw.html', 'wb').write(raw.encode('utf-8')) + +        body = '<html><body><article></article></body></html>' +        root = parse(body) +        load_article_from_json(raw, root) + +        if '/interactive/' in url: +            return ('<html><body><article><h1>' + root.xpath('//h1')[0].text + '</h1><em>' +                    'This article is supposed to be read in a browser.' +                    '</em></article></body></html>') + +        for div in root.xpath('//div[@class="lazy-image"]'): +            noscript = list(div.iter('noscript')) +            if noscript and noscript[0].text: +                img = list(parse(noscript[0].text).iter('img')) +                if img: +                    p = noscript[0].getparent() +                    idx = p.index(noscript[0]) +                    p.insert(idx, p.makeelement('img', src=img[0].get('src'))) +                    p.remove(noscript[0]) +        for x in root.xpath('//*[name()="script" or name()="style" or name()="source" or name()="meta"]'): +            x.getparent().remove(x) +        # the economist uses <small> for small caps with a custom font +        for init in root.xpath('//span[@data-caps="initial"]'): +            init.set('style', 'font-weight:bold;') +        for x in root.xpath('//small'): +            if x.text and len(x) == 0: +                x.text = x.text.upper() +                x.tag = 'span' +                x.set('style', 'font-variant: small-caps') +        for h2 in root.xpath('//h2'): +            h2.tag = 'h4' +        for x in root.xpath('//figcaption'): +            x.set('style', 'text-align:center; font-size:small;') +        for x in root.xpath('//cite'): +            x.tag = 'blockquote' +            x.set('style', 'color:#404040;') +        raw = etree.tostring(root, encoding='unicode') +        return raw + +    def parse_index_from_printedition(self): +        # return self.economist_test_article() +        edition_date = self.recipe_specific_options.get('date') +        if edition_date and isinstance(edition_date, str): +            url = 'https://www.economist.com/weeklyedition/' + edition_date +            self.timefmt = ' [' + edition_date + ']' +        else: +            url = 'https://www.economist.com/printedition' +        # raw = open('/t/raw.html').read() +        raw = self.index_to_soup(url, raw=True) +        # with open('/t/raw.html', 'wb') as f: +        #     f.write(raw) +        soup = self.index_to_soup(raw) +        # nav = soup.find(attrs={'class':'navigation__wrapper'}) +        # if nav is not None: +        #     a = nav.find('a', href=lambda x: x and '/printedition/' in x) +        #     if a is not None: +        #         self.log('Following nav link to current edition', a['href']) +        #         soup = self.index_to_soup(process_url(a['href'])) +        ans = self.economist_parse_index(soup) +        if not ans: +            raise NoArticles( +                'Could not find any articles, either the ' +                'economist.com server is having trouble and you should ' +                'try later or the website format has changed and the ' +                'recipe needs to be updated.' +            ) +        return ans + +    def eco_find_image_tables(self, soup): +        for x in soup.findAll('table', align=['right', 'center']): +            if len(x.findAll('font')) in (1, 2) and len(x.findAll('img')) == 1: +                yield x + +    def postprocess_html(self, soup, first): +        for img in soup.findAll('img', srcset=True): +            del img['srcset'] +        for table in list(self.eco_find_image_tables(soup)): +            caption = table.find('font') +            img = table.find('img') +            div = new_tag(soup, 'div') +            div['style'] = 'text-align:left;font-size:70%' +            ns = NavigableString(self.tag_to_string(caption)) +            div.insert(0, ns) +            div.insert(1, new_tag(soup, 'br')) +            del img['width'] +            del img['height'] +            img.extract() +            div.insert(2, img) +            table.replaceWith(div) +        return soup + +    def canonicalize_internal_url(self, url, is_link=True): +        if url.endswith('/print'): +            url = url.rpartition('/')[0] +        return BasicNewsRecipe.canonicalize_internal_url(self, url, is_link=is_link) + +    # archive code +    def parse_web_index(self): +        edition_date = self.recipe_specific_options.get('date') +        # return self.economist_test_article() +        if edition_date and isinstance(edition_date, str): +            url = 'https://www.economist.com/weeklyedition/' + edition_date +            self.timefmt = ' [' + edition_date + ']' +        else: +            url = 'https://www.economist.com/weeklyedition' +        soup = self.index_to_soup(url) +        ans = self.economist_parse_web_index(soup) +        return self.economist_return_index(ans) + +    def economist_parse_web_index(self, soup): +        script_tag = soup.find('script', id='__NEXT_DATA__') +        if script_tag is not None: +            data = json.loads(script_tag.string) +            # open('/t/raw.json', 'w').write(json.dumps(data, indent=2, sort_keys=True)) +            self.description = safe_dict(data, 'props', 'pageProps', 'content', 'headline') +            self.timefmt = ' [' + safe_dict(data, 'props', 'pageProps', 'content', 'formattedIssueDate') + ']' +            self.cover_url = safe_dict(data, 'props', 'pageProps', 'content', 'cover', 'url').replace( +                'economist.com/', 'economist.com/cdn-cgi/image/width=960,quality=80,format=auto/').replace('SQ_', '') +            self.log('Got cover:', self.cover_url) + +            feeds = [] + +            for part in safe_dict( +                data, 'props', 'pageProps', 'content', 'headerSections' +            ) + safe_dict(data, 'props', 'pageProps', 'content', 'sections'): +                section = safe_dict(part, 'name') or '' +                if not section: +                    continue +                self.log(section) + +                articles = [] + +                for ar in part['articles']: +                    title = safe_dict(ar, 'headline') or '' +                    url = process_url(safe_dict(ar, 'url') or '') +                    if not title or not url: +                        continue +                    desc = safe_dict(ar, 'rubric') or '' +                    sub = safe_dict(ar, 'flyTitle') or '' +                    if sub and section != sub: +                        desc = sub + ' :: ' + desc +                    self.log('\t', title, '\n\t', desc, '\n\t\t', url) +                    articles.append({'title': title, 'url': url, 'description': desc}) +                feeds.append((section, articles)) +            return feeds +        else: +            return [] + +    def preprocess_raw_web_html(self, raw, url): +        # open('/t/raw.html', 'wb').write(raw.encode('utf-8')) +        root_ = parse(raw) +        if '/interactive/' in url: +            return ('<html><body><article><h1>' + root_.xpath('//h1')[0].text + '</h1><em>' +                    'This article is supposed to be read in a browser' +                    '</em></article></body></html>') + +        script = root_.xpath('//script[@id="__NEXT_DATA__"]') + +        html = load_article_from_web_json(script[0].text) + +        root = parse(html) +        for div in root.xpath('//div[@class="lazy-image"]'): +            noscript = list(div.iter('noscript')) +            if noscript and noscript[0].text: +                img = list(parse(noscript[0].text).iter('img')) +                if img: +                    p = noscript[0].getparent() +                    idx = p.index(noscript[0]) +                    p.insert(idx, p.makeelement('img', src=img[0].get('src'))) +                    p.remove(noscript[0]) +        for x in root.xpath('//*[name()="script" or name()="style" or name()="source" or name()="meta"]'): +            x.getparent().remove(x) +        # the economist uses <small> for small caps with a custom font +        for init in root.xpath('//span[@data-caps="initial"]'): +            init.set('style', 'font-weight:bold;') +        for x in root.xpath('//small'): +            if x.text and len(x) == 0: +                x.text = x.text.upper() +                x.tag = 'span' +                x.set('style', 'font-variant: small-caps') +        for h2 in root.xpath('//h2'): +            h2.tag = 'h4' +        for x in root.xpath('//figcaption'): +            x.set('style', 'text-align:center; font-size:small;') +        for x in root.xpath('//cite'): +            x.tag = 'blockquote' +            x.set('style', 'color:#404040;') +        raw = etree.tostring(root, encoding='unicode') +        return raw + +        raw_ar = read_url([], 'https://archive.is/latest/' + url) +        archive = BeautifulSoup(str(raw_ar)) +        art = archive.find('article') +        if art: +            bdy = art.findAll('section') +            if len(bdy) != 0: +                content = bdy[-1] +            else: +                content = archive.find('div', attrs={'itemprop':'text'}) +            soup = BeautifulSoup(raw) +            article = soup.find('section', attrs={'id':'body'}) +            if not article: +                article = soup.find('div', attrs={'itemprop':'text'}) +                if not article: +                    article = soup.find(attrs={'itemprop':'blogPost'}) +            if article and content: +                self.log('**fetching archive content') +                article.append(content) + +                div = soup.findAll(attrs={'style': lambda x: x and x.startswith( +                        ('color:rgb(13, 13, 13);', 'color: rgb(18, 18, 18);') +                    )}) +                for p in div: +                    p.name = 'p' +                return str(soup) +            return raw +        return raw + +    def preprocess_web_html(self, soup): +        for img in soup.findAll('img', attrs={'old-src':True}): +            img['src'] = img['old-src'] +        for a in soup.findAll('a', href=True): +            a['href'] = 'http' + a['href'].split('http')[-1] +        for fig in soup.findAll('figure'): +            fig['class'] = 'sub' +        for sty in soup.findAll(attrs={'style':True}): +            del sty['style'] +        width = '600' +        w = self.recipe_specific_options.get('res') +        if w and isinstance(w, str): +            width = w +        for img in soup.findAll('img', src=True): +            if '/cdn-cgi/image/' not in img['src']: +                qua = 'economist.com/cdn-cgi/image/width=' + width + ',quality=80,format=auto/' +                img['src'] = img['src'].replace('economist.com/', qua) +            else: +                img['src'] = re.sub(r'width=\d+', 'width=' + width, img['src']) +        return soup + + +calibre_most_common_ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
\ No newline at end of file diff --git a/dotfiles/system/.config/calibre/custom_recipes/The New York Times_1000.recipe b/dotfiles/system/.config/calibre/custom_recipes/The New York Times_1000.recipe new file mode 100644 index 0000000..63df3fd --- /dev/null +++ b/dotfiles/system/.config/calibre/custom_recipes/The New York Times_1000.recipe @@ -0,0 +1,368 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> + +from __future__ import absolute_import, division, print_function, unicode_literals + +import datetime +import json +import re + +import mechanize + +from calibre import strftime +from calibre.ebooks.BeautifulSoup import Tag +from calibre.utils.date import strptime +from calibre.web.feeds.news import BasicNewsRecipe +from polyglot.urllib import urlencode + +is_web_edition = False +use_wayback_machine = False + +# This is an Apollo persisted query hash which you can get +# from looking at the XHR requests made by: https://www.nytimes.com/section/todayspaper +# or by https://www.nytimes.com/section/world +persistedQuery = '1f99120a11e94dd62a9474f68ee1255537ee3cf7eac20a0377819edb2fa1fef7' + +# The sections to download when downloading the web edition, comment out +# the section you are not interested in +web_sections = [ +    ('World', 'world'), +    ('U.S.', 'us'), +    ('Politics', 'politics'), +    # ('New York', 'nyregion'), +    ('Business', 'business'), +    ('Technology', 'technology'), +    # ('Sports', 'sports'), +    ('Science', 'science'), +    ('Health', 'health'), +    ('Opinion', 'opinion'), +    # ('Arts', 'arts'), +    # ('Books', 'books'), +    # ('Movies', 'movies'), +    # ('Music', 'arts/music'), +    # ('Television', 'arts/television'), +    # ('Style', 'style'), +    # ('Dining & Wine', 'food'), +    # ('Fashion & Style', 'fashion'), +    # ('Home & Garden', 'garden'), +    ('Travel', 'travel'), +    # ('Education', 'education'), +    #  ('Multimedia', 'multimedia'), +    # ('Obituaries', 'obituaries'), +    # ('Sunday Magazine', 'magazine') +] +# web_sections = [ ('Business', 'business'), ] +url_date_pat = re.compile(r'/(2\d\d\d)/(\d\d)/(\d\d)/') + + +def date_from_url(url): +    m = url_date_pat.search(url) +    if m is not None: +        return datetime.date(*map(int, m.groups())) + + +def format_date(d): +    try: +        return strftime(' [%a, %d %b %Y]', d) +    except Exception: +        return strftime(' [%Y/%m/%d]', d) + + +def classes(classes): +    q = frozenset(classes.split(' ')) +    return dict(attrs={ +        'class': lambda x: x and frozenset(x.split()).intersection(q)}) + + +def new_tag(soup, name, attrs=()): +    impl = getattr(soup, 'new_tag', None) +    if impl is not None: +        return impl(name, attrs=dict(attrs)) +    return Tag(soup, name, attrs=attrs or None) + + +class NewYorkTimes(BasicNewsRecipe): +    if is_web_edition: +        title = 'The New York Times (Web)' +        description = ( +            'New York Times (Web). You can edit the recipe to remove sections you are not interested in. ' +            'Use advanced menu to make changes to fetch Todays Paper' +        ) +    else: +        title = 'The New York Times' +        description = ( +            'New York Times. Todays Paper ' +            'Use advanced menu to make changes to fetch Web Edition' +        ) +    encoding = 'utf-8' +    __author__ = 'Kovid Goyal' +    language = 'en_US' +    ignore_duplicate_articles = {'title', 'url'} +    no_stylesheets = True +    oldest_web_edition_article = 7  # days + +    extra_css = ''' +        .byl, .time { font-size:small; color:#202020; } +        .cap { font-size:small; text-align:center; } +        .cred { font-style:italic; font-size:small; } +        em, blockquote { color: #202020; } +        .sc { font-variant: small-caps; } +        .lbl { font-size:small; color:#404040; } +        img { display:block; margin:0 auto; } +    ''' + +    @property +    def nyt_parser(self): +        ans = getattr(self, '_nyt_parser', None) +        if ans is None: +            from calibre.live import load_module +            self._nyt_parser = ans = load_module('calibre.web.site_parsers.nytimes') +        return ans + +    def get_nyt_page(self, url, skip_wayback=False): +        if use_wayback_machine and not skip_wayback: +            from calibre import browser +            return self.nyt_parser.download_url(url, browser()) +        return self.index_to_soup(url, raw=True) + +    def preprocess_raw_html(self, raw_html, url): +        cleaned = self.nyt_parser.clean_js_json(raw_html) +        return self.nyt_parser.extract_html(self.index_to_soup(cleaned), url) + +    articles_are_obfuscated = use_wayback_machine + +    if use_wayback_machine: +        def get_obfuscated_article(self, url): +            from calibre.ptempfile import PersistentTemporaryFile +            with PersistentTemporaryFile() as tf: +                tf.write(self.get_nyt_page(url)) +            return tf.name + +    recipe_specific_options = { +        'web': { +            'short': 'Type in yes, if you want ' + ('Todays Paper' if is_web_edition else 'Web Edition'), +            'default': 'Web Edition' if is_web_edition else 'Todays Paper', +        }, +        'days': { +            'short': 'Oldest article to download from this news source. In days ', +            'long': 'For example, 1, gives you articles from the past 24 hours\n(Works only for Web_Edition)', +            'default': str(oldest_web_edition_article) +        }, +        'date': { +            'short': 'The date of the edition to download (YYYY/MM/DD format)\nUsed to fetch past editions of NYT newspaper', +            'long': 'For example, 2024/07/16' +        }, +        'res': { +            'short': ( +                'For hi-res images, select a resolution from the following\noptions: ' +                'popup, jumbo, mobileMasterAt3x, superJumbo' +            ), +            'long': ( +                'This is useful for non e-ink devices, and for a lower file size\nthan ' +                'the default, use mediumThreeByTwo440, mediumThreeByTwo225, articleInline.' +            ), +        }, +        'comp': { +            'short': 'Compress News Images?', +            'long': 'enter yes', +            'default': 'no' +        } +    } + +    def __init__(self, *args, **kwargs): +        BasicNewsRecipe.__init__(self, *args, **kwargs) +        c = self.recipe_specific_options.get('comp') +        d = self.recipe_specific_options.get('days') +        w = self.recipe_specific_options.get('web') +        self.is_web_edition = is_web_edition +        if w and isinstance(w, str): +            if w == 'yes': +                self.is_web_edition = not is_web_edition +        if d and isinstance(d, str): +            self.oldest_web_edition_article = float(d) +        if c and isinstance(c, str): +            if c.lower() == 'yes': +                self.compress_news_images = True + +    def read_todays_paper(self): +        pdate = self.recipe_specific_options.get('date') +        templ = 'https://www.nytimes.com/issue/todayspaper/{}/todays-new-york-times' +        if pdate and isinstance(pdate, str): +            return pdate, self.index_to_soup(templ.format(pdate)) +        # Cant figure out how to get the date so just try todays and yesterdays dates +        date = datetime.date.today() +        pdate = date.strftime('%Y/%m/%d') +        try: +            soup = self.index_to_soup(templ.format(pdate)) +        except Exception as e: +            if getattr(e, 'code', None) == 404: +                date -= datetime.timedelta(days=1) +                pdate = date.strftime('%Y/%m/%d') +                soup = self.index_to_soup(templ.format(pdate)) +            else: +                raise +        self.log("Using today's paper from:", pdate) +        return pdate, soup + +    def read_nyt_metadata(self): +        pdate, soup = self.read_todays_paper() +        date = strptime(pdate, '%Y/%m/%d', assume_utc=False, as_utc=False) +        self.cover_url = 'https://static01.nyt.com/images/{}/nytfrontpage/scan.jpg'.format(pdate) +        self.timefmt = strftime(' [%d %b, %Y]', date) +        self.nytimes_publication_date = pdate +        script = soup.findAll('script', text=lambda x: x and 'window.__preloadedData' in x)[0] +        script = type(u'')(script) +        raw_json = script[script.find('{'):script.rfind(';')].strip().rstrip(';')  # }} +        clean_json = self.nyt_parser.clean_js_json(raw_json) +        self.nytimes_graphql_config = json.loads(clean_json)['config'] +        return soup + +    def nyt_graphql_query(self, qid, operationName='CollectionsQuery'): +        query = { +            'operationName': operationName, +            'variables': json.dumps({ +                'id': qid, +                'first': 10, +                'exclusionMode': 'HIGHLIGHTS_AND_EMBEDDED', +                'isFetchMore':False, +                'isTranslatable':False, +                'isEspanol':False, +                'highlightsListUri':'nyt://per/personalized-list/__null__', +                'highlightsListFirst':0, +                'hasHighlightsList':False +            }, separators=',:'), +            'extensions': json.dumps({ +                'persistedQuery': { +                    'version':1, +                    'sha256Hash': persistedQuery, +                }, +            }, separators=',:') +        } +        url = self.nytimes_graphql_config['gqlUrlClient'] + '?' + urlencode(query) +        br = self.browser +        # br.set_debug_http(True) +        headers = dict(self.nytimes_graphql_config['gqlRequestHeaders']) +        headers['Accept'] = 'application/json' +        req = mechanize.Request(url, headers=headers) +        raw = br.open(req).read() +        # open('/t/raw.json', 'wb').write(raw) +        return json.loads(raw) + +    def parse_todays_page(self): +        self.read_nyt_metadata() +        query_id = '/issue/todayspaper/{}/todays-new-york-times'.format(self.nytimes_publication_date) +        data = self.nyt_graphql_query(query_id) +        return parse_todays_page(data, self.log) + +    def parse_web_sections(self): +        self.read_nyt_metadata() +        feeds = [] +        for section_title, slug in web_sections: +            query_id = '/section/' + slug +            try: +                data = self.nyt_graphql_query(query_id) +                self.log('Section:', section_title) +                articles = parse_web_section(data, log=self.log, title=section_title) +            except Exception as e: +                self.log('Failed to parse section:', section_title, 'with error:', e) +                articles = [] +            if articles: +                feeds.append((section_title, articles)) +            else: +                # open('/t/raw.json', 'w').write(json.dumps(data, indent=2)) +                self.log('  No articles found in section:', section_title) +            if self.test and len(feeds) >= self.test[0]: +                break +        return feeds + +    def parse_index(self): +        # return [('All articles', [ +        #     {'title': 'XXXXX', 'url': 'https://www.nytimes.com/2020/11/27/world/americas/coronavirus-migrants-venezuela.html'}, +        # ])] +        if self.is_web_edition: +            return self.parse_web_sections() +        return self.parse_todays_page() + +    def get_browser(self, *args, **kwargs): +        kwargs['user_agent'] = 'User-Agent: Mozilla/5.0 (compatible; archive.org_bot; Wayback Machine Live Record; +http://archive.org/details/archive.org_bot)' +        br = BasicNewsRecipe.get_browser(self, *args, **kwargs) +        return br + +    def preprocess_html(self, soup): +        w = self.recipe_specific_options.get('res') +        if w and isinstance(w, str): +            res = '-' + w +            for img in soup.findAll('img', attrs={'src':True}): +                if '-article' in img['src']: +                    ext = img['src'].split('?')[0].split('.')[-1] +                    img['src'] = img['src'].rsplit('-article', 1)[0] + res + '.' + ext +        for c in soup.findAll('div', attrs={'class':'cap'}): +            for p in c.findAll(['p', 'div']): +                p.name = 'span' +        return soup + +    def get_article_url(self, article): +        url = BasicNewsRecipe.get_article_url(self, article) +        if not re.search(r'/video/|/athletic/|/card/', url): +            return url +        self.log('\tSkipping ', url) + + +def asset_to_article(asset): +    title = asset['headline']['default'] +    return {'title': title, 'url': asset['url'], 'description': asset['summary']} + + +def parse_todays_page(data, log=print): +    containers = data['data']['legacyCollection']['groupings'][0]['containers'] +    feeds = [] +    for cont in containers: +        if cont['__typename'] != 'LegacyCollectionContainer': +            continue +        section_name = cont['label'].strip() +        if not section_name: +            continue +        log(section_name) +        articles = [] +        for rel in cont['relations']: +            if rel.get('__typename') == 'LegacyCollectionRelation': +                asset = rel['asset'] +                if asset['__typename'] == 'Article': +                    articles.append(asset_to_article(asset)) +                    log(' ', articles[-1]['title'] + ':', articles[-1]['url']) +        if articles: +            feeds.append((section_name, articles)) +    return feeds + + +def parse_web_section(data, log=print, title=''): +    articles = [] +    try: +        containers = data['data']['legacyCollection']['collectionsPage'] +        if containers.get('embeddedCollections'): +            containers = containers['embeddedCollections'] +        else: +            containers = [containers] +    except Exception as e: +        log('Failed to parse web section', title, 'with error:', e) +        return articles +    for cont in containers: +        for s in cont['stream']['edges']: +            asset = s['node'] +            if asset['__typename'] == 'Article': +                articles.append(asset_to_article(asset)) +                log(' ', articles[-1]['title'] + ':', articles[-1]['url']) +    return articles + + +if __name__ == '__main__': +    import sys +    data = json.loads(open(sys.argv[-1], 'rb').read()) +    if is_web_edition: +        parse_web_section(data) +    else: +        parse_todays_page(data) + + +calibre_most_common_ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
\ No newline at end of file diff --git a/dotfiles/system/.config/calibre/custom_recipes/index.json b/dotfiles/system/.config/calibre/custom_recipes/index.json new file mode 100644 index 0000000..64e4acc --- /dev/null +++ b/dotfiles/system/.config/calibre/custom_recipes/index.json @@ -0,0 +1,10 @@ +{ +  "1000": [ +    "The New York Times", +    "The New York Times_1000.recipe" +  ], +  "1001": [ +    "The Economist", +    "The Economist_1001.recipe" +  ] +}
\ No newline at end of file diff --git a/dotfiles/system/.config/calibre/customize.py.json b/dotfiles/system/.config/calibre/customize.py.json index d092cb3..d69a479 100644 --- a/dotfiles/system/.config/calibre/customize.py.json +++ b/dotfiles/system/.config/calibre/customize.py.json @@ -6,39 +6,20 @@    "enabled_plugins": {      "__class__": "set",      "__value__": [ -<<<<<<< Updated upstream        "Big Book Search", -      "KoboTouch", -      "KoboTouchExtended", -      "Kobo Reader Device Interface",        "Google Images" -======= -      "Kobo Reader Device Interface", -      "KoboTouchExtended", -      "Big Book Search", -      "Google Images", -      "KoboTouch" ->>>>>>> Stashed changes      ]    },    "filetype_mapping": {},    "plugin_customization": {},    "plugins": {      "Apple Books covers": "/home/cjennings/.config/calibre/plugins/Apple Books covers.zip", -    "Barnes & Noble": "/home/cjennings/.config/calibre/plugins/Barnes & Noble.zip", -    "Clean Comments": "/home/cjennings/.config/calibre/plugins/Clean Comments.zip", -    "Comments Cleaner": "/home/cjennings/.config/calibre/plugins/Comments Cleaner.zip",      "Extract ISBN": "/home/cjennings/.config/calibre/plugins/Extract ISBN.zip",      "Favourites Menu": "/home/cjennings/.config/calibre/plugins/Favourites Menu.zip",      "Find Duplicates": "/home/cjennings/.config/calibre/plugins/Find Duplicates.zip", -    "KePub Metadata Reader": "/home/cjennings/.config/calibre/plugins/KePub Metadata Reader.zip", -    "KePub Metadata Writer": "/home/cjennings/.config/calibre/plugins/KePub Metadata Writer.zip",      "Kindle hi-res covers": "/home/cjennings/.config/calibre/plugins/Kindle hi-res covers.zip", -    "Kobo Books": "/home/cjennings/.config/calibre/plugins/Kobo Books.zip",      "Kobo Metadata": "/home/cjennings/.config/calibre/plugins/Kobo Metadata.zip",      "Kobo Utilities": "/home/cjennings/.config/calibre/plugins/Kobo Utilities.zip", -    "KoboTouchExtended": "/home/cjennings/.config/calibre/plugins/KoboTouchExtended.zip", -    "Reading List": "/home/cjennings/.config/calibre/plugins/Reading List.zip", -    "Wikidata": "/home/cjennings/.config/calibre/plugins/Wikidata.zip" +    "Reading List": "/home/cjennings/.config/calibre/plugins/Reading List.zip"    }  }
\ No newline at end of file diff --git a/dotfiles/system/.config/calibre/device_drivers_KOBOTOUCH.py.json b/dotfiles/system/.config/calibre/device_drivers_KOBOTOUCH.py.json index 5bdd9c4..d0414f5 100644 --- a/dotfiles/system/.config/calibre/device_drivers_KOBOTOUCH.py.json +++ b/dotfiles/system/.config/calibre/device_drivers_KOBOTOUCH.py.json @@ -1,4 +1,5 @@  { +  "affect_hyphenation": false,    "bookstats_pagecount_template": "",    "bookstats_timetoread_lower_template": "",    "bookstats_timetoread_upper_template": "", @@ -8,30 +9,41 @@    "create_collections": false,    "debugging_title": "",    "delete_empty_collections": false, +  "disable_hyphenation": false,    "dithered_covers": false, -  "driver_version": "2.5.1", +  "driver_version": "2.6.0",    "extra_customization": [], +  "force_series_id": false,    "format_map": [      "kepub",      "epub",      "cbz", -    "cbr" +    "cbr", +    "pdf"    ], +  "hyphenation_limit_lines": 2, +  "hyphenation_min_chars": 6, +  "hyphenation_min_chars_after": 3, +  "hyphenation_min_chars_before": 3,    "ignore_collections_names": "",    "keep_cover_aspect": false, +  "kepubify": true,    "letterbox_fs_covers": false,    "letterbox_fs_covers_color": "#000000",    "manage_collections": true,    "modify_css": false,    "override_kobo_replace_existing": true, +  "per_device_css": "{}",    "png_covers": false,    "read_metadata": true,    "save_template": "{author_sort}/{title} - {authors}", +  "series_index_template": "",    "show_archived_books": false,    "show_previews": false,    "show_recommendations": false,    "subtitle_template": "",    "support_newer_firmware": false, +  "template_for_kepubify": "",    "update_bookstats": false,    "update_core_metadata": false,    "update_device_metadata": true, @@ -43,5 +55,6 @@    "use_author_sort": false,    "use_collections_columns": true,    "use_collections_template": false, +  "use_series_index_template": false,    "use_subdirs": true  }
\ No newline at end of file diff --git a/dotfiles/system/.config/calibre/fonts/scanner_cache.json b/dotfiles/system/.config/calibre/fonts/scanner_cache.json index a4a83d6..39d3005 100644 --- a/dotfiles/system/.config/calibre/fonts/scanner_cache.json +++ b/dotfiles/system/.config/calibre/fonts/scanner_cache.json @@ -1,3677 +1,5 @@  {    "fonts": { -    "/home/cjennings/.local/share/fonts/AppleColorEmoji.ttf||42722048:1748870335.29062": { -      "family_name": "Apple Color Emoji", -      "font-family": "Apple Color Emoji", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Apple Color Emoji", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        6, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/AppleColorEmoji.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/BerkeleyMono-Bold.otf||74596:1748870335.2926202": { -      "family_name": "Berkeley Mono", -      "font-family": "Berkeley Mono", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Berkeley Mono Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/BerkeleyMono-Bold.otf", -      "preferred_family_name": "Berkeley Mono", -      "preferred_subfamily_name": "Bold", -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/BerkeleyMono-Bold.ttf||114872:1748870335.29362": { -      "family_name": "Berkeley Mono", -      "font-family": "Berkeley Mono", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Berkeley Mono Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/BerkeleyMono-Bold.ttf", -      "preferred_family_name": "Berkeley Mono", -      "preferred_subfamily_name": "Bold", -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/BerkeleyMono-BoldItalic.otf||75756:1748870335.29462": { -      "family_name": "Berkeley Mono", -      "font-family": "Berkeley Mono", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Berkeley Mono Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/BerkeleyMono-BoldItalic.otf", -      "preferred_family_name": "Berkeley Mono", -      "preferred_subfamily_name": "Bold Italic", -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/BerkeleyMono-BoldItalic.ttf||116664:1748870335.2966201": { -      "family_name": "Berkeley Mono", -      "font-family": "Berkeley Mono", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Berkeley Mono Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/BerkeleyMono-BoldItalic.ttf", -      "preferred_family_name": "Berkeley Mono", -      "preferred_subfamily_name": "Bold Italic", -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/BerkeleyMono-Italic.otf||75172:1748870335.29762": { -      "family_name": "Berkeley Mono", -      "font-family": "Berkeley Mono", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Berkeley Mono Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/BerkeleyMono-Italic.otf", -      "preferred_family_name": "Berkeley Mono", -      "preferred_subfamily_name": "Italic", -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/BerkeleyMono-Italic.ttf||115636:1748870335.2986202": { -      "family_name": "Berkeley Mono", -      "font-family": "Berkeley Mono", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Berkeley Mono Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/BerkeleyMono-Italic.ttf", -      "preferred_family_name": "Berkeley Mono", -      "preferred_subfamily_name": "Italic", -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/BerkeleyMono-Regular.otf||72688:1748870335.3006203": { -      "family_name": "Berkeley Mono", -      "font-family": "Berkeley Mono", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Berkeley Mono Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/BerkeleyMono-Regular.otf", -      "preferred_family_name": "Berkeley Mono", -      "preferred_subfamily_name": "Regular", -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/BerkeleyMono-Regular.ttf||114656:1748870335.3006203": { -      "family_name": "Berkeley Mono", -      "font-family": "Berkeley Mono", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Berkeley Mono Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/BerkeleyMono-Regular.ttf", -      "preferred_family_name": "Berkeley Mono", -      "preferred_subfamily_name": "Regular", -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/BerkeleyMonoVariable-Italic.ttf||103704:1748870335.3016202": { -      "family_name": "Berkeley Mono Variable Italic", -      "font-family": "Berkeley Mono Variable Italic", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Berkeley Mono Variable Italic", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/BerkeleyMonoVariable-Italic.ttf", -      "preferred_family_name": "Berkeley Mono Variable", -      "preferred_subfamily_name": "Italic", -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/BerkeleyMonoVariable-Regular.ttf||101012:1748870335.3036203": { -      "family_name": "Berkeley Mono Variable", -      "font-family": "Berkeley Mono Variable", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Berkeley Mono Variable Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/BerkeleyMonoVariable-Regular.ttf", -      "preferred_family_name": "Berkeley Mono Variable", -      "preferred_subfamily_name": "Regular", -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-Bold.otf||112176:1748870335.3056202": { -      "family_name": "Cartograph CF Bold", -      "font-family": "Cartograph CF Bold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "Cartograph CF Bold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-Bold.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Bold", -      "subfamily_name": "Regular", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-BoldItalic.otf||120580:1748870335.3066204": { -      "family_name": "Cartograph CF Bold", -      "font-family": "Cartograph CF Bold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "Cartograph CF Bold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-BoldItalic.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Bold Italic", -      "subfamily_name": "Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-DemiBold.otf||110188:1748870335.3076203": { -      "family_name": "Cartograph CF Demi Bold", -      "font-family": "Cartograph CF Demi Bold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 8, -      "full_name": "Cartograph CF Demi Bold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        7, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-DemiBold.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Demi Bold", -      "subfamily_name": "Regular", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-DemiBoldItalic.otf||121136:1748870335.3096204": { -      "family_name": "Cartograph CF Demi Bold", -      "font-family": "Cartograph CF Demi Bold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 8, -      "full_name": "Cartograph CF Demi Bold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        7, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-DemiBoldItalic.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Demi Bold Italic", -      "subfamily_name": "Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-ExtraBold.otf||111532:1748870335.3106203": { -      "family_name": "Cartograph CF Extra Bold", -      "font-family": "Cartograph CF Extra Bold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "800", -      "fs_type": 8, -      "full_name": "Cartograph CF Extra Bold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        9, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-ExtraBold.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Extra Bold", -      "subfamily_name": "Regular", -      "weight": 800, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-ExtraBoldItalic.otf||120080:1748870335.3116205": { -      "family_name": "Cartograph CF Extra Bold", -      "font-family": "Cartograph CF Extra Bold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "800", -      "fs_type": 8, -      "full_name": "Cartograph CF Extra Bold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        9, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-ExtraBoldItalic.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Extra Bold Italic", -      "subfamily_name": "Italic", -      "weight": 800, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-ExtraLight.otf||108024:1748870335.3126204": { -      "family_name": "Cartograph CF Extra Light", -      "font-family": "Cartograph CF Extra Light", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "200", -      "fs_type": 8, -      "full_name": "Cartograph CF Extra Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        3, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-ExtraLight.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Extra Light", -      "subfamily_name": "Regular", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-ExtraLightItalic.otf||118496:1748870335.3136203": { -      "family_name": "Cartograph CF Extra Light", -      "font-family": "Cartograph CF Extra Light", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "200", -      "fs_type": 8, -      "full_name": "Cartograph CF Extra Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        3, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-ExtraLightItalic.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Extra Light Italic", -      "subfamily_name": "Italic", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-Heavy.otf||113700:1748870335.3156204": { -      "family_name": "Cartograph CF Heavy", -      "font-family": "Cartograph CF Heavy", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "900", -      "fs_type": 8, -      "full_name": "Cartograph CF Heavy", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        10, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-Heavy.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Heavy", -      "subfamily_name": "Regular", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-HeavyItalic.otf||123024:1748870335.3166203": { -      "family_name": "Cartograph CF Heavy", -      "font-family": "Cartograph CF Heavy", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "900", -      "fs_type": 8, -      "full_name": "Cartograph CF Heavy Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        10, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-HeavyItalic.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Heavy Italic", -      "subfamily_name": "Italic", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-Light.otf||108372:1748870335.3176205": { -      "family_name": "Cartograph CF Light", -      "font-family": "Cartograph CF Light", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 8, -      "full_name": "Cartograph CF Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        4, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-Light.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Light", -      "subfamily_name": "Regular", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-LightItalic.otf||118272:1748870335.3186204": { -      "family_name": "Cartograph CF Light", -      "font-family": "Cartograph CF Light", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 8, -      "full_name": "Cartograph CF Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        4, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-LightItalic.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Light Italic", -      "subfamily_name": "Italic", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-Regular.otf||107884:1748870335.3196204": { -      "family_name": "Cartograph CF", -      "font-family": "Cartograph CF", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "Cartograph CF Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-Regular.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-RegularItalic.otf||117452:1748870335.3216205": { -      "family_name": "Cartograph CF", -      "font-family": "Cartograph CF", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "Cartograph CF Regular Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-RegularItalic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": "Regular Italic", -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-Thin.otf||105640:1748870335.3226204": { -      "family_name": "Cartograph CF Thin", -      "font-family": "Cartograph CF Thin", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "100", -      "fs_type": 8, -      "full_name": "Cartograph CF Thin", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        2, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-Thin.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Thin", -      "subfamily_name": "Regular", -      "weight": 100, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CartographCF-ThinItalic.otf||116284:1748870335.3236206": { -      "family_name": "Cartograph CF Thin", -      "font-family": "Cartograph CF Thin", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "100", -      "fs_type": 8, -      "full_name": "Cartograph CF Thin Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        2, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CartographCF-ThinItalic.otf", -      "preferred_family_name": "Cartograph CF", -      "preferred_subfamily_name": "Thin Italic", -      "subfamily_name": "Italic", -      "weight": 100, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CodeliaLigatures-Bold.otf||118064:1748870335.3246205": { -      "family_name": "Codelia Ligatures", -      "font-family": "Codelia Ligatures", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "Codelia Ligatures Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CodeliaLigatures-Bold.otf", -      "preferred_family_name": "Codelia Ligatures", -      "preferred_subfamily_name": "Bold", -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CodeliaLigatures-BoldItalic.otf||117860:1748870335.3266206": { -      "family_name": "Codelia Ligatures", -      "font-family": "Codelia Ligatures", -      "font-stretch": "normal", -      "font-style": "oblique", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "Codelia Ligatures Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": true, -      "is_otf": true, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CodeliaLigatures-BoldItalic.otf", -      "preferred_family_name": "Codelia Ligatures", -      "preferred_subfamily_name": "Bold Italic", -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CodeliaLigatures-Italic.otf||114384:1748870335.3276205": { -      "family_name": "Codelia Ligatures", -      "font-family": "Codelia Ligatures", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "Codelia Ligatures Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CodeliaLigatures-Italic.otf", -      "preferred_family_name": "Codelia Ligatures", -      "preferred_subfamily_name": "Italic", -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/CodeliaLigatures-Regular.otf||116684:1748870335.3286207": { -      "family_name": "Codelia Ligatures", -      "font-family": "Codelia Ligatures", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "Codelia Ligatures Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/CodeliaLigatures-Regular.otf", -      "preferred_family_name": "Codelia Ligatures", -      "preferred_subfamily_name": "Regular", -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Bold.otf||92016:1748870335.3296206": { -      "family_name": "Comic Code Ligatures", -      "font-family": "Comic Code Ligatures", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        12, -        5, -        9, -        2, -        2, -        4, -        6, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Bold.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Bold", -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-BoldItalic.otf||92188:1748870335.3306205": { -      "family_name": "Comic Code Ligatures", -      "font-family": "Comic Code Ligatures", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        1, -        5, -        9, -        2, -        2, -        0, -        6, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-BoldItalic.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Bold Italic", -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Italic.otf||95120:1748870335.3316207": { -      "family_name": "Comic Code Ligatures", -      "font-family": "Comic Code Ligatures", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        1, -        5, -        9, -        2, -        2, -        0, -        6, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Italic.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Italic", -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Light.otf||95088:1748870335.3326206": { -      "family_name": "Comic Code Ligatures Light", -      "font-family": "Comic Code Ligatures Light", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        12, -        5, -        9, -        2, -        2, -        4, -        6, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Light.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Light", -      "subfamily_name": "Regular", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-LightItalic.otf||96648:1748870335.3336205": { -      "family_name": "Comic Code Ligatures Light", -      "font-family": "Comic Code Ligatures Light", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        1, -        5, -        9, -        2, -        2, -        0, -        6, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-LightItalic.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Light Italic", -      "subfamily_name": "Italic", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Medium.otf||94056:1748870335.3346207": { -      "family_name": "Comic Code Ligatures Medium", -      "font-family": "Comic Code Ligatures Medium", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "500", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Medium", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        12, -        5, -        9, -        2, -        2, -        4, -        6, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Medium.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Medium", -      "subfamily_name": "Regular", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-MediumItalic.otf||95256:1748870335.3356206": { -      "family_name": "Comic Code Ligatures Medium", -      "font-family": "Comic Code Ligatures Medium", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "500", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Medium Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        1, -        5, -        9, -        2, -        2, -        0, -        6, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-MediumItalic.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Medium Italic", -      "subfamily_name": "Italic", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Regular.otf||93004:1748870335.3366208": { -      "family_name": "Comic Code Ligatures", -      "font-family": "Comic Code Ligatures", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        12, -        5, -        9, -        2, -        2, -        4, -        6, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Regular.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Regular", -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-SBIta.otf||96380:1748870335.3376207": { -      "family_name": "Comic Code Ligatures Semibold", -      "font-family": "Comic Code Ligatures Semibold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Semibold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        1, -        5, -        9, -        2, -        2, -        0, -        6, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-SBIta.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Semibold Italic", -      "subfamily_name": "Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Semibold.otf||94744:1748870335.3386207": { -      "family_name": "Comic Code Ligatures Semibold", -      "font-family": "Comic Code Ligatures Semibold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Semibold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        12, -        5, -        9, -        2, -        2, -        4, -        6, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Semibold.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Semibold", -      "subfamily_name": "Regular", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Thin.otf||89560:1748870335.3396208": { -      "family_name": "Comic Code Ligatures Thin", -      "font-family": "Comic Code Ligatures Thin", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "100", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Thin", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        12, -        5, -        9, -        2, -        2, -        4, -        6, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-Thin.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Thin", -      "subfamily_name": "Regular", -      "weight": 100, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-ThinItalic.otf||90044:1748870335.3406208": { -      "family_name": "Comic Code Ligatures Thin", -      "font-family": "Comic Code Ligatures Thin", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "100", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures Thin Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        1, -        5, -        9, -        2, -        2, -        0, -        6, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-ThinItalic.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "Thin Italic", -      "subfamily_name": "Italic", -      "weight": 100, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-ULIta.otf||97832:1748870335.3416207": { -      "family_name": "Comic Code Ligatures UltraLight", -      "font-family": "Comic Code Ligatures UltraLight", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "200", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures UltraLight Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        1, -        5, -        9, -        2, -        2, -        0, -        6, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-ULIta.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "UltraLight Italic", -      "subfamily_name": "Italic", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/ComicCodeLigatures-UltraLight.otf||95728:1748870335.3426208": { -      "family_name": "Comic Code Ligatures UltraLight", -      "font-family": "Comic Code Ligatures UltraLight", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "200", -      "fs_type": 8, -      "full_name": "Comic Code Ligatures UltraLight", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        12, -        5, -        9, -        2, -        2, -        4, -        6, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/ComicCodeLigatures-UltraLight.otf", -      "preferred_family_name": "Comic Code Ligatures", -      "preferred_subfamily_name": "UltraLight", -      "subfamily_name": "Regular", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/Courier 10 Pitch Regular.otf||35196:1748870335.3426208": { -      "family_name": "Courier 10 Pitch", -      "font-family": "Courier 10 Pitch", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "500", -      "fs_type": 0, -      "full_name": "Courier10PitchBT-Roman", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 2, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/Courier 10 Pitch Regular.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/MERIFONT.TTF||49504:1748870335.3436208": { -      "family_name": "Chess Merida", -      "font-family": "Chess Merida", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Chess Merida", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 0, -      "panose": [ -        2, -        11, -        6, -        3, -        5, -        3, -        2, -        2, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/MERIFONT.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/Merriweather-Black.ttf||141700:1748870335.3446207": { -      "family_name": "Merriweather Black", -      "font-family": "Merriweather Black", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "Merriweather Black", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        10, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/Merriweather-Black.ttf", -      "preferred_family_name": "Merriweather", -      "preferred_subfamily_name": "Black", -      "subfamily_name": "Regular", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/Merriweather-BlackItalic.ttf||142620:1748870335.3466208": { -      "family_name": "Merriweather Black", -      "font-family": "Merriweather Black", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "Merriweather Black Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        10, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/Merriweather-BlackItalic.ttf", -      "preferred_family_name": "Merriweather", -      "preferred_subfamily_name": "Black Italic", -      "subfamily_name": "Italic", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/Merriweather-Bold.ttf||142040:1748870335.348621": { -      "family_name": "Merriweather", -      "font-family": "Merriweather", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Merriweather Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        8, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/Merriweather-Bold.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/Merriweather-BoldItalic.ttf||143832:1748870335.3496208": { -      "family_name": "Merriweather", -      "font-family": "Merriweather", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Merriweather Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        8, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/Merriweather-BoldItalic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/Merriweather-Italic.ttf||142648:1748870335.351621": { -      "family_name": "Merriweather", -      "font-family": "Merriweather", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Merriweather Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/Merriweather-Italic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/Merriweather-Light.ttf||148124:1748870335.3526208": { -      "family_name": "Merriweather Light", -      "font-family": "Merriweather Light", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "Merriweather Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        4, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/Merriweather-Light.ttf", -      "preferred_family_name": "Merriweather", -      "preferred_subfamily_name": "Light", -      "subfamily_name": "Regular", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/Merriweather-LightItalic.ttf||142056:1748870335.354621": { -      "family_name": "Merriweather Light", -      "font-family": "Merriweather Light", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "Merriweather Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        4, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/Merriweather-LightItalic.ttf", -      "preferred_family_name": "Merriweather", -      "preferred_subfamily_name": "Light Italic", -      "subfamily_name": "Italic", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/Merriweather-Regular.ttf||149120:1748870335.3556209": { -      "family_name": "Merriweather", -      "font-family": "Merriweather", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Merriweather Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/Merriweather-Regular.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/MonoLisa-Bold.otf||118736:1748870335.356621": { -      "family_name": "MonoLisa", -      "font-family": "MonoLisa", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "MonoLisa Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        8, -        9, -        3, -        2, -        4, -        6, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/MonoLisa-Bold.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/MonoLisa-BoldItalic.otf||119056:1748870335.357621": { -      "family_name": "MonoLisa", -      "font-family": "MonoLisa", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "MonoLisa Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        8, -        9, -        3, -        2, -        4, -        13, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/MonoLisa-BoldItalic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/MonoLisa-Regular.otf||116244:1748870335.358621": { -      "family_name": "MonoLisa", -      "font-family": "MonoLisa", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "MonoLisa Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        2, -        4, -        6, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/MonoLisa-Regular.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/MonoLisa-RegularItalic.otf||116892:1748870335.359621": { -      "family_name": "MonoLisa", -      "font-family": "MonoLisa", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "MonoLisa Regular Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        2, -        4, -        13, -        2, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/MonoLisa-RegularItalic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-Bold.otf||64528:1748870335.360621": { -      "family_name": "Novaletra Serif CF Medium", -      "font-family": "Novaletra Serif CF Medium", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-Bold.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Bold", -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-BoldItalic.otf||66320:1748870335.3616211": { -      "family_name": "Novaletra Serif CF Medium", -      "font-family": "Novaletra Serif CF Medium", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-BoldItalic.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Bold Italic", -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-DemiBold.otf||64428:1748870335.3616211": { -      "family_name": "Novaletra Serif CF Demi Bold", -      "font-family": "Novaletra Serif CF Demi Bold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Demi Bold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-DemiBold.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Demi Bold", -      "subfamily_name": "Regular", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-DemiBoldItalic.otf||65864:1748870335.362621": { -      "family_name": "Novaletra Serif CF Demi Bold", -      "font-family": "Novaletra Serif CF Demi Bold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Demi Bold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-DemiBoldItalic.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Demi Bold Italic", -      "subfamily_name": "Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-ExtBold.otf||63976:1748870335.363621": { -      "family_name": "Novaletra Serif CF Ext Bold", -      "font-family": "Novaletra Serif CF Ext Bold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "800", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Ext Bold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-ExtBold.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Ext Bold", -      "subfamily_name": "Regular", -      "weight": 800, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-ExtBoldItalic.otf||65820:1748870335.363621": { -      "family_name": "Novaletra Serif CF Ext Bold", -      "font-family": "Novaletra Serif CF Ext Bold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "800", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Ext Bold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-ExtBoldItalic.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Ext Bold Italic", -      "subfamily_name": "Italic", -      "weight": 800, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-Heavy.otf||63884:1748870335.3646212": { -      "family_name": "Novaletra Serif CF Heavy", -      "font-family": "Novaletra Serif CF Heavy", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "900", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Heavy", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-Heavy.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Heavy", -      "subfamily_name": "Regular", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-HeavyItalic.otf||66264:1748870335.3646212": { -      "family_name": "Novaletra Serif CF Heavy", -      "font-family": "Novaletra Serif CF Heavy", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "900", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Heavy Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-HeavyItalic.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Heavy Italic", -      "subfamily_name": "Italic", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-Light.otf||64140:1748870335.365621": { -      "family_name": "Novaletra Serif CF Light", -      "font-family": "Novaletra Serif CF Light", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-Light.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Light", -      "subfamily_name": "Regular", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-LightItalic.otf||66732:1748870335.366621": { -      "family_name": "Novaletra Serif CF Light", -      "font-family": "Novaletra Serif CF Light", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-LightItalic.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Light Italic", -      "subfamily_name": "Italic", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-Medium.otf||64364:1748870335.366621": { -      "family_name": "Novaletra Serif CF Medium", -      "font-family": "Novaletra Serif CF Medium", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "500", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Medium", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-Medium.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Medium", -      "subfamily_name": "Regular", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-MediumItalic.otf||66216:1748870335.3676212": { -      "family_name": "Novaletra Serif CF Medium", -      "font-family": "Novaletra Serif CF Medium", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "500", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Medium Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-MediumItalic.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Medium Italic", -      "subfamily_name": "Italic", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-Regular.otf||64392:1748870335.368621": { -      "family_name": "Novaletra Serif CF", -      "font-family": "Novaletra Serif CF", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-Regular.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Regular", -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/NovaletraSerifCF-RegularItalic.otf||66240:1748870335.368621": { -      "family_name": "Novaletra Serif CF", -      "font-family": "Novaletra Serif CF", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "Novaletra Serif CF Regular Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/NovaletraSerifCF-RegularItalic.otf", -      "preferred_family_name": "Novaletra Serif CF", -      "preferred_subfamily_name": "Regular Italic", -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataProB_09.ttf||4607292:1748870335.4176219": { -      "family_name": "PragmataPro", -      "font-family": "PragmataPro", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 264, -      "full_name": "PragmataPro Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        0, -        3, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataProB_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataProB_liga_09.ttf||4624112:1748870335.431622": { -      "family_name": "PragmataPro Liga", -      "font-family": "PragmataPro Liga", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 264, -      "full_name": "PragmataPro Liga Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        0, -        3, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataProB_liga_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataProI_09.ttf||4490692:1748870335.4696226": { -      "family_name": "PragmataPro", -      "font-family": "PragmataPro", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 264, -      "full_name": "PragmataPro Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        0, -        3, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataProI_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataProI_liga_09.ttf||4507588:1748870335.5396235": { -      "family_name": "PragmataPro Liga", -      "font-family": "PragmataPro Liga", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 264, -      "full_name": "PragmataPro Liga Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        0, -        3, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataProI_liga_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataProR_09.ttf||5051440:1748870335.5506237": { -      "family_name": "PragmataPro", -      "font-family": "PragmataPro", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 264, -      "full_name": "PragmataPro Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataProR_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataProR_liga_09.ttf||5068260:1748870335.5636237": { -      "family_name": "PragmataPro Liga", -      "font-family": "PragmataPro Liga", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 264, -      "full_name": "PragmataPro Liga Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataProR_liga_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataProZ_09.ttf||4439452:1748870335.582624": { -      "family_name": "PragmataPro", -      "font-family": "PragmataPro", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 258, -      "full_name": "PragmataPro Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        0, -        8, -        0, -        8, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataProZ_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataProZ_liga_09.ttf||4456268:1748870335.593624": { -      "family_name": "PragmataPro Liga", -      "font-family": "PragmataPro Liga", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 258, -      "full_name": "PragmataPro Liga Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        0, -        8, -        0, -        8, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataProZ_liga_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataPro_Mono_B_09.ttf||3900260:1748870335.6436248": { -      "family_name": "PragmataPro Mono", -      "font-family": "PragmataPro Mono", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 264, -      "full_name": "PragmataPro Mono Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        9, -        3, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataPro_Mono_B_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataPro_Mono_B_liga_09.ttf||3917076:1748870335.6706252": { -      "family_name": "PragmataPro Mono Liga", -      "font-family": "PragmataPro Mono Liga", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 264, -      "full_name": "PragmataPro Mono Liga Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        9, -        3, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataPro_Mono_B_liga_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataPro_Mono_I_09.ttf||3822344:1748870335.6876254": { -      "family_name": "PragmataPro Mono", -      "font-family": "PragmataPro Mono", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 264, -      "full_name": "PragmataPro Mono Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        9, -        3, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataPro_Mono_I_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataPro_Mono_I_liga_09.ttf||3839140:1748870335.7626264": { -      "family_name": "PragmataPro Mono Liga", -      "font-family": "PragmataPro Mono Liga", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 264, -      "full_name": "PragmataPro Mono Liga Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        9, -        3, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataPro_Mono_I_liga_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataPro_Mono_R_09.ttf||4233500:1748870335.797627": { -      "family_name": "PragmataPro Mono", -      "font-family": "PragmataPro Mono", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 264, -      "full_name": "PragmataPro Mono Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        9, -        4, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataPro_Mono_R_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataPro_Mono_R_liga_09.ttf||4250320:1748870335.8376274": { -      "family_name": "PragmataPro Mono Liga", -      "font-family": "PragmataPro Mono Liga", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 264, -      "full_name": "PragmataPro Mono Liga Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        9, -        4, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataPro_Mono_R_liga_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataPro_Mono_Z_09.ttf||3752628:1748870335.8526278": { -      "family_name": "PragmataPro Mono", -      "font-family": "PragmataPro Mono", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 264, -      "full_name": "PragmataPro Mono Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        9, -        3, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataPro_Mono_Z_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/PragmataPro_Mono_Z_liga_09.ttf||3769444:1748870335.8616278": { -      "family_name": "PragmataPro Mono Liga", -      "font-family": "PragmataPro Mono Liga", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 264, -      "full_name": "PragmataPro Mono Liga Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        9, -        3, -        0, -        0, -        2, -        0, -        4 -      ], -      "path": "/home/cjennings/.local/share/fonts/PragmataPro_Mono_Z_liga_09.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/home/cjennings/.local/share/fonts/all-the-icons.ttf||44732:1748870335.8616278": { -      "family_name": "all-the-icons", -      "font-family": "all-the-icons", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "all-the-icons", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/home/cjennings/.local/share/fonts/all-the-icons.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationMono-Bold.ttf||304804:1614308916.5788176": { -      "family_name": "Liberation Mono", -      "font-family": "Liberation Mono", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Liberation Mono Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        7, -        7, -        9, -        2, -        2, -        5, -        2, -        4, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationMono-Bold.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationMono-BoldItalic.ttf||281156:1614308916.5788176": { -      "family_name": "Liberation Mono", -      "font-family": "Liberation Mono", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Liberation Mono Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        7, -        7, -        9, -        2, -        2, -        5, -        9, -        4, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationMono-BoldItalic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationMono-Italic.ttf||278664:1614308916.5788176": { -      "family_name": "Liberation Mono", -      "font-family": "Liberation Mono", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Liberation Mono Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        7, -        4, -        9, -        2, -        2, -        5, -        9, -        4, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationMono-Italic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationMono-Regular.ttf||316264:1614308916.5788176": { -      "family_name": "Liberation Mono", -      "font-family": "Liberation Mono", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Liberation Mono", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        7, -        4, -        9, -        2, -        2, -        5, -        2, -        4, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationMono-Regular.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationSans-Bold.ttf||412436:1614308916.5788176": { -      "family_name": "Liberation Sans", -      "font-family": "Liberation Sans", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Liberation Sans Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        7, -        4, -        2, -        2, -        2, -        2, -        2, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationSans-Bold.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationSans-BoldItalic.ttf||407300:1614308916.5788176": { -      "family_name": "Liberation Sans", -      "font-family": "Liberation Sans", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Liberation Sans Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        7, -        4, -        2, -        2, -        2, -        9, -        2, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationSans-BoldItalic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationSans-Italic.ttf||413824:1614308916.5788176": { -      "family_name": "Liberation Sans", -      "font-family": "Liberation Sans", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Liberation Sans Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        6, -        4, -        2, -        2, -        2, -        9, -        2, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationSans-Italic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationSans-Regular.ttf||408988:1614308916.5788176": { -      "family_name": "Liberation Sans", -      "font-family": "Liberation Sans", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Liberation Sans", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        6, -        4, -        2, -        2, -        2, -        2, -        2, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationSans-Regular.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationSerif-Bold.ttf||367896:1614308916.582151": { -      "family_name": "Liberation Serif", -      "font-family": "Liberation Serif", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Liberation Serif Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        2, -        8, -        3, -        7, -        5, -        5, -        2, -        3, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationSerif-Bold.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationSerif-BoldItalic.ttf||374468:1614308916.582151": { -      "family_name": "Liberation Serif", -      "font-family": "Liberation Serif", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Liberation Serif Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        2, -        7, -        3, -        6, -        5, -        5, -        9, -        3, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationSerif-BoldItalic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationSerif-Italic.ttf||373340:1614308916.582151": { -      "family_name": "Liberation Serif", -      "font-family": "Liberation Serif", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Liberation Serif Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        2, -        5, -        3, -        5, -        4, -        5, -        9, -        3, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationSerif-Italic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/opt/calibre/resources/fonts/liberation/LiberationSerif-Regular.ttf||391024:1614308916.5788176": { -      "family_name": "Liberation Serif", -      "font-family": "Liberation Serif", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Liberation Serif", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        2, -        6, -        3, -        5, -        4, -        5, -        2, -        3, -        4 -      ], -      "path": "/opt/calibre/resources/fonts/liberation/LiberationSerif-Regular.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    },      "/usr/share/fonts/Adwaita/AdwaitaMono-Bold.ttf||1413212:1751188544.0": {        "family_name": "Adwaita Mono",        "font-family": "Adwaita Mono", @@ -3888,942 +216,6 @@        "wws_family_name": null,        "wws_subfamily_name": null      }, -    "/usr/share/fonts/TTF/AndaleMo.TTF||105468:1701284278.0": { -      "family_name": "Andale Mono", -      "font-family": "Andale Mono", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "Andale Mono", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        5, -        9, -        0, -        0, -        0, -        0, -        0, -        4 -      ], -      "path": "/usr/share/fonts/TTF/AndaleMo.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/AriBlk.TTF||117028:1701284278.0": { -      "family_name": "Arial Black", -      "font-family": "Arial Black", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Arial Black", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        10, -        4, -        2, -        1, -        2, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/AriBlk.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Arial.TTF||275572:1701284278.0": { -      "family_name": "Arial", -      "font-family": "Arial", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Arial", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        6, -        4, -        2, -        2, -        2, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Arial.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Arialbd.TTF||286620:1701284278.0": { -      "family_name": "Arial", -      "font-family": "Arial", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Arial Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        7, -        4, -        2, -        2, -        2, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Arialbd.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Arialbi.TTF||224692:1701284278.0": { -      "family_name": "Arial", -      "font-family": "Arial", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Arial Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        7, -        4, -        2, -        2, -        2, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Arialbi.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Ariali.TTF||206132:1701284278.0": { -      "family_name": "Arial", -      "font-family": "Arial", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Arial Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        6, -        4, -        2, -        2, -        2, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Ariali.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Comic.TTF||126364:1701284278.0": { -      "family_name": "Comic Sans MS", -      "font-family": "Comic Sans MS", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Comic Sans MS", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        3, -        15, -        7, -        2, -        3, -        3, -        2, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Comic.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Comicbd.TTF||111476:1701284278.0": { -      "family_name": "Comic Sans MS", -      "font-family": "Comic Sans MS", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Comic Sans MS Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        3, -        15, -        9, -        2, -        3, -        3, -        2, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Comicbd.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-Black.ttf||138276:1709288021.0": { -      "family_name": "Crimson Pro Black", -      "font-family": "Crimson Pro Black", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "Crimson Pro Black", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-Black.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "Black", -      "subfamily_name": "Regular", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-BlackItalic.ttf||140928:1709288021.0": { -      "family_name": "Crimson Pro Black", -      "font-family": "Crimson Pro Black", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "Crimson Pro Black Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-BlackItalic.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "Black Italic", -      "subfamily_name": "Italic", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-Bold.ttf||137612:1709288021.0": { -      "family_name": "Crimson Pro", -      "font-family": "Crimson Pro", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Crimson Pro Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-Bold.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-BoldItalic.ttf||140500:1709288021.0": { -      "family_name": "Crimson Pro", -      "font-family": "Crimson Pro", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Crimson Pro Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-BoldItalic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-ExtraBold.ttf||137180:1709288021.0": { -      "family_name": "Crimson Pro ExtraBold", -      "font-family": "Crimson Pro ExtraBold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "800", -      "fs_type": 0, -      "full_name": "Crimson Pro ExtraBold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-ExtraBold.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "ExtraBold", -      "subfamily_name": "Regular", -      "weight": 800, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-ExtraBoldItalic.ttf||140660:1709288021.0": { -      "family_name": "Crimson Pro ExtraBold", -      "font-family": "Crimson Pro ExtraBold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "800", -      "fs_type": 0, -      "full_name": "Crimson Pro ExtraBold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-ExtraBoldItalic.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "ExtraBold Italic", -      "subfamily_name": "Italic", -      "weight": 800, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-ExtraLight.ttf||138224:1709288021.0": { -      "family_name": "Crimson Pro ExtraLight", -      "font-family": "Crimson Pro ExtraLight", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "Crimson Pro ExtraLight", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-ExtraLight.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "ExtraLight", -      "subfamily_name": "Regular", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-ExtraLightItalic.ttf||139176:1709288021.0": { -      "family_name": "Crimson Pro ExtraLight", -      "font-family": "Crimson Pro ExtraLight", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "Crimson Pro ExtraLight Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-ExtraLightItalic.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "ExtraLight Italic", -      "subfamily_name": "Italic", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-Italic.ttf||140608:1709288021.0": { -      "family_name": "Crimson Pro", -      "font-family": "Crimson Pro", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Crimson Pro Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-Italic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-Italic[wght].ttf||253176:1709288021.0": { -      "family_name": "Crimson Pro", -      "font-family": "Crimson Pro", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Crimson Pro Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-Italic[wght].ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-Light.ttf||137440:1709288021.0": { -      "family_name": "Crimson Pro Light", -      "font-family": "Crimson Pro Light", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "Crimson Pro Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-Light.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "Light", -      "subfamily_name": "Regular", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-LightItalic.ttf||140536:1709288021.0": { -      "family_name": "Crimson Pro Light", -      "font-family": "Crimson Pro Light", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "Crimson Pro Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-LightItalic.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "Light Italic", -      "subfamily_name": "Italic", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-Medium.ttf||137124:1709288021.0": { -      "family_name": "Crimson Pro Medium", -      "font-family": "Crimson Pro Medium", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "500", -      "fs_type": 0, -      "full_name": "Crimson Pro Medium", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-Medium.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "Medium", -      "subfamily_name": "Regular", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-MediumItalic.ttf||139952:1709288021.0": { -      "family_name": "Crimson Pro Medium", -      "font-family": "Crimson Pro Medium", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "500", -      "fs_type": 0, -      "full_name": "Crimson Pro Medium Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-MediumItalic.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "Medium Italic", -      "subfamily_name": "Italic", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-Regular.ttf||137000:1709288021.0": { -      "family_name": "Crimson Pro", -      "font-family": "Crimson Pro", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Crimson Pro Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-Regular.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-SemiBold.ttf||137452:1709288021.0": { -      "family_name": "Crimson Pro SemiBold", -      "font-family": "Crimson Pro SemiBold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "Crimson Pro SemiBold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-SemiBold.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "SemiBold", -      "subfamily_name": "Regular", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro-SemiBoldItalic.ttf||140536:1709288021.0": { -      "family_name": "Crimson Pro SemiBold", -      "font-family": "Crimson Pro SemiBold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "Crimson Pro SemiBold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro-SemiBoldItalic.ttf", -      "preferred_family_name": "Crimson Pro", -      "preferred_subfamily_name": "SemiBold Italic", -      "subfamily_name": "Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/CrimsonPro[wght].ttf||246324:1709288021.0": { -      "family_name": "Crimson Pro", -      "font-family": "Crimson Pro", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Crimson Pro Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/CrimsonPro[wght].ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    },      "/usr/share/fonts/TTF/FiraCodeNerdFont-Bold.ttf||2672432:1745519248.0": {        "family_name": "FiraCode Nerd Font",        "font-family": "FiraCode Nerd Font", @@ -5472,582 +864,6 @@        "wws_family_name": null,        "wws_subfamily_name": null      }, -    "/usr/share/fonts/TTF/Georgia.TTF||142964:1701284278.0": { -      "family_name": "Georgia", -      "font-family": "Georgia", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Georgia", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        4, -        5, -        2, -        5, -        4, -        5, -        2, -        3, -        3 -      ], -      "path": "/usr/share/fonts/TTF/Georgia.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Georgiab.TTF||139584:1701284278.0": { -      "family_name": "Georgia", -      "font-family": "Georgia", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Georgia Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        4, -        8, -        2, -        5, -        4, -        5, -        2, -        2, -        3 -      ], -      "path": "/usr/share/fonts/TTF/Georgiab.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Georgiai.TTF||156668:1701284278.0": { -      "family_name": "Georgia", -      "font-family": "Georgia", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Georgia Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        4, -        5, -        2, -        5, -        4, -        5, -        9, -        3, -        3 -      ], -      "path": "/usr/share/fonts/TTF/Georgiai.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Georgiaz.TTF||158796:1701284278.0": { -      "family_name": "Georgia", -      "font-family": "Georgia", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Georgia Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        4, -        8, -        2, -        5, -        4, -        5, -        9, -        2, -        3 -      ], -      "path": "/usr/share/fonts/TTF/Georgiaz.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFont-Bold.ttf||2537844:1745519248.0": { -      "family_name": "GoMono Nerd Font", -      "font-family": "GoMono Nerd Font", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        7, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFont-Bold.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFont-BoldItalic.ttf||2547048:1745519248.0": { -      "family_name": "GoMono Nerd Font", -      "font-family": "GoMono Nerd Font", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        7, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFont-BoldItalic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFont-Italic.ttf||2541908:1745519248.0": { -      "family_name": "GoMono Nerd Font", -      "font-family": "GoMono Nerd Font", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        6, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFont-Italic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFont-Regular.ttf||2532128:1745519248.0": { -      "family_name": "GoMono Nerd Font", -      "font-family": "GoMono Nerd Font", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        6, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFont-Regular.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFontMono-Bold.ttf||2543376:1745519248.0": { -      "family_name": "GoMono Nerd Font Mono", -      "font-family": "GoMono Nerd Font Mono", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font Mono Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        7, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFontMono-Bold.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFontMono-BoldItalic.ttf||2552580:1745519248.0": { -      "family_name": "GoMono Nerd Font Mono", -      "font-family": "GoMono Nerd Font Mono", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font Mono Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        7, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFontMono-BoldItalic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFontMono-Italic.ttf||2547440:1745519248.0": { -      "family_name": "GoMono Nerd Font Mono", -      "font-family": "GoMono Nerd Font Mono", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font Mono Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        6, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFontMono-Italic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFontMono-Regular.ttf||2537660:1745519248.0": { -      "family_name": "GoMono Nerd Font Mono", -      "font-family": "GoMono Nerd Font Mono", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font Mono", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        6, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFontMono-Regular.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFontPropo-Bold.ttf||2560132:1745519248.0": { -      "family_name": "GoMono Nerd Font Propo", -      "font-family": "GoMono Nerd Font Propo", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font Propo Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        7, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFontPropo-Bold.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFontPropo-BoldItalic.ttf||2569332:1745519248.0": { -      "family_name": "GoMono Nerd Font Propo", -      "font-family": "GoMono Nerd Font Propo", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font Propo Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        7, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFontPropo-BoldItalic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFontPropo-Italic.ttf||2564196:1745519248.0": { -      "family_name": "GoMono Nerd Font Propo", -      "font-family": "GoMono Nerd Font Propo", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font Propo Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        6, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFontPropo-Italic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/GoMonoNerdFontPropo-Regular.ttf||2554416:1745519248.0": { -      "family_name": "GoMono Nerd Font Propo", -      "font-family": "GoMono Nerd Font Propo", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "GoMono Nerd Font Propo", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        6, -        6, -        9, -        5, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/GoMonoNerdFontPropo-Regular.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    },      "/usr/share/fonts/TTF/HackNerdFont-Bold.ttf||2694312:1745519248.0": {        "family_name": "Hack Nerd Font",        "font-family": "Hack Nerd Font", @@ -6480,42 +1296,6 @@        "wws_family_name": null,        "wws_subfamily_name": null      }, -    "/usr/share/fonts/TTF/Impact.TTF||136076:1701284278.0": { -      "family_name": "Impact", -      "font-family": "Impact", -      "font-stretch": "condensed", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Impact", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        8, -        6, -        3, -        9, -        2, -        5, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Impact.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 3, -      "wws_family_name": null, -      "wws_subfamily_name": null -    },      "/usr/share/fonts/TTF/JetBrainsMonoNLNerdFont-Bold.ttf||2389020:1745519248.0": {        "family_name": "JetBrainsMonoNL NF",        "font-family": "JetBrainsMonoNL NF", @@ -13212,3714 +7992,6 @@        "wws_family_name": null,        "wws_subfamily_name": null      }, -    "/usr/share/fonts/TTF/OpenSans-Bold.ttf||147264:1737204794.0": { -      "family_name": "Open Sans", -      "font-family": "Open Sans", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Open Sans Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-Bold.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-BoldItalic.ttf||153308:1737204794.0": { -      "family_name": "Open Sans", -      "font-family": "Open Sans", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Open Sans Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-BoldItalic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-CondensedBold.ttf||144736:1737204794.0": { -      "family_name": "Open Sans Condensed", -      "font-family": "Open Sans Condensed", -      "font-stretch": "condensed", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Open Sans Condensed Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-CondensedBold.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Condensed Bold", -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 3, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-CondensedBoldItalic.ttf||153496:1737204794.0": { -      "family_name": "Open Sans Condensed", -      "font-family": "Open Sans Condensed", -      "font-stretch": "condensed", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Open Sans Condensed Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-CondensedBoldItalic.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Condensed Bold Italic", -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 3, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-CondensedExtraBold.ttf||148976:1737204794.0": { -      "family_name": "Open Sans Condensed ExtraBold", -      "font-family": "Open Sans Condensed ExtraBold", -      "font-stretch": "condensed", -      "font-style": "normal", -      "font-weight": "800", -      "fs_type": 0, -      "full_name": "Open Sans Condensed ExtraBold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-CondensedExtraBold.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Condensed ExtraBold", -      "subfamily_name": "Regular", -      "weight": 800, -      "width": 3, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-CondensedExtraBoldItalic.ttf||156596:1737204794.0": { -      "family_name": "Open Sans Condensed ExtraBold", -      "font-family": "Open Sans Condensed ExtraBold", -      "font-stretch": "condensed", -      "font-style": "italic", -      "font-weight": "800", -      "fs_type": 0, -      "full_name": "Open Sans Condensed ExtraBold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-CondensedExtraBoldItalic.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Condensed ExtraBold Italic", -      "subfamily_name": "Italic", -      "weight": 800, -      "width": 3, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-CondensedItalic.ttf||153572:1737204794.0": { -      "family_name": "Open Sans Condensed", -      "font-family": "Open Sans Condensed", -      "font-stretch": "condensed", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Open Sans Condensed Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-CondensedItalic.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Condensed Italic", -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 3, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-CondensedLight.ttf||143652:1737204794.0": { -      "family_name": "Open Sans Condensed Light", -      "font-family": "Open Sans Condensed Light", -      "font-stretch": "condensed", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "Open Sans Condensed Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-CondensedLight.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Condensed Light", -      "subfamily_name": "Regular", -      "weight": 300, -      "width": 3, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-CondensedLightItalic.ttf||151568:1737204794.0": { -      "family_name": "Open Sans Condensed Light", -      "font-family": "Open Sans Condensed Light", -      "font-stretch": "condensed", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "Open Sans Condensed Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-CondensedLightItalic.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Condensed Light Italic", -      "subfamily_name": "Italic", -      "weight": 300, -      "width": 3, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-CondensedRegular.ttf||146668:1737204794.0": { -      "family_name": "Open Sans Condensed", -      "font-family": "Open Sans Condensed", -      "font-stretch": "condensed", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Open Sans Condensed Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-CondensedRegular.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Condensed Regular", -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 3, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-CondensedSemiBold.ttf||148564:1737204794.0": { -      "family_name": "Open Sans Condensed SemiBold", -      "font-family": "Open Sans Condensed SemiBold", -      "font-stretch": "condensed", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "Open Sans Condensed SemiBold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-CondensedSemiBold.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Condensed SemiBold", -      "subfamily_name": "Regular", -      "weight": 600, -      "width": 3, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-CondensedSemiBoldItalic.ttf||156304:1737204794.0": { -      "family_name": "Open Sans Condensed SemiBold", -      "font-family": "Open Sans Condensed SemiBold", -      "font-stretch": "condensed", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "Open Sans Condensed SemiBold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-CondensedSemiBoldItalic.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Condensed SemiBold Italic", -      "subfamily_name": "Italic", -      "weight": 600, -      "width": 3, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-ExtraBold.ttf||151700:1737204794.0": { -      "family_name": "Open Sans ExtraBold", -      "font-family": "Open Sans ExtraBold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "800", -      "fs_type": 0, -      "full_name": "Open Sans ExtraBold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-ExtraBold.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "ExtraBold", -      "subfamily_name": "Regular", -      "weight": 800, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-ExtraBoldItalic.ttf||157732:1737204794.0": { -      "family_name": "Open Sans ExtraBold", -      "font-family": "Open Sans ExtraBold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "800", -      "fs_type": 0, -      "full_name": "Open Sans ExtraBold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-ExtraBoldItalic.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "ExtraBold Italic", -      "subfamily_name": "Italic", -      "weight": 800, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-Italic.ttf||153256:1737204794.0": { -      "family_name": "Open Sans", -      "font-family": "Open Sans", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Open Sans Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-Italic.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-Light.ttf||143788:1737204794.0": { -      "family_name": "Open Sans Light", -      "font-family": "Open Sans Light", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "Open Sans Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-Light.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Light", -      "subfamily_name": "Regular", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-LightItalic.ttf||151216:1737204794.0": { -      "family_name": "Open Sans Light", -      "font-family": "Open Sans Light", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "Open Sans Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-LightItalic.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "Light Italic", -      "subfamily_name": "Italic", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-Regular.ttf||147528:1737204794.0": { -      "family_name": "Open Sans", -      "font-family": "Open Sans", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Open Sans Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-Regular.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-SemiBold.ttf||150592:1737204794.0": { -      "family_name": "Open Sans SemiBold", -      "font-family": "Open Sans SemiBold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "Open Sans SemiBold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-SemiBold.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "SemiBold", -      "subfamily_name": "Regular", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/OpenSans-SemiBoldItalic.ttf||157560:1737204794.0": { -      "family_name": "Open Sans SemiBold", -      "font-family": "Open Sans SemiBold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "Open Sans SemiBold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/OpenSans-SemiBoldItalic.ttf", -      "preferred_family_name": "Open Sans", -      "preferred_subfamily_name": "SemiBold Italic", -      "subfamily_name": "Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-Black.ttf||2366924:1745519248.0": { -      "family_name": "SauceCodePro NF Black", -      "font-family": "SauceCodePro NF Black", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "SauceCodePro NF Black", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        8, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-Black.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": "Black", -      "subfamily_name": "Regular", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-BlackItalic.ttf||2333636:1745519248.0": { -      "family_name": "SauceCodePro NF Black", -      "font-family": "SauceCodePro NF Black", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "SauceCodePro NF Black Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        8, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-BlackItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": "Black Italic", -      "subfamily_name": "Italic", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-Bold.ttf||2365848:1745519248.0": { -      "family_name": "SauceCodePro NF", -      "font-family": "SauceCodePro NF", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "SauceCodePro NF Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        7, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-Bold.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-BoldItalic.ttf||2332152:1745519248.0": { -      "family_name": "SauceCodePro NF", -      "font-family": "SauceCodePro NF", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "SauceCodePro NF Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        7, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-BoldItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-ExtraLight.ttf||2358252:1745519248.0": { -      "family_name": "SauceCodePro NF ExtraLight", -      "font-family": "SauceCodePro NF ExtraLight", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "SauceCodePro NF ExtraLight", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        3, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-ExtraLight.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": "ExtraLight", -      "subfamily_name": "Regular", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-ExtraLightItalic.ttf||2327824:1745519248.0": { -      "family_name": "SauceCodePro NF ExtraLight", -      "font-family": "SauceCodePro NF ExtraLight", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "SauceCodePro NF ExtraLight Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        3, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-ExtraLightItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": "ExtraLight Italic", -      "subfamily_name": "Italic", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-Italic.ttf||2329500:1745519248.0": { -      "family_name": "SauceCodePro NF", -      "font-family": "SauceCodePro NF", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "SauceCodePro NF Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-Italic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-Light.ttf||2358244:1745519248.0": { -      "family_name": "SauceCodePro NF Light", -      "font-family": "SauceCodePro NF Light", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "SauceCodePro NF Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        4, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-Light.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": "Light", -      "subfamily_name": "Regular", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-LightItalic.ttf||2328060:1745519248.0": { -      "family_name": "SauceCodePro NF Light", -      "font-family": "SauceCodePro NF Light", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "SauceCodePro NF Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        4, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-LightItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": "Light Italic", -      "subfamily_name": "Italic", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-Medium.ttf||2364804:1745519248.0": { -      "family_name": "SauceCodePro NF Medium", -      "font-family": "SauceCodePro NF Medium", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "500", -      "fs_type": 0, -      "full_name": "SauceCodePro NF Medium", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-Medium.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": "Medium", -      "subfamily_name": "Regular", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-MediumItalic.ttf||2330296:1745519248.0": { -      "family_name": "SauceCodePro NF Medium", -      "font-family": "SauceCodePro NF Medium", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "500", -      "fs_type": 0, -      "full_name": "SauceCodePro NF Medium Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-MediumItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": "Medium Italic", -      "subfamily_name": "Italic", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-Regular.ttf||2369332:1745519248.0": { -      "family_name": "SauceCodePro NF", -      "font-family": "SauceCodePro NF", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "SauceCodePro NF", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-Regular.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-SemiBold.ttf||2365660:1745519248.0": { -      "family_name": "SauceCodePro NF SemiBold", -      "font-family": "SauceCodePro NF SemiBold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "SauceCodePro NF SemiBold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        6, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-SemiBold.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": "SemiBold", -      "subfamily_name": "Regular", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFont-SemiBoldItalic.ttf||2332552:1745519248.0": { -      "family_name": "SauceCodePro NF SemiBold", -      "font-family": "SauceCodePro NF SemiBold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "SauceCodePro NF SemiBold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        6, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFont-SemiBoldItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font", -      "preferred_subfamily_name": "SemiBold Italic", -      "subfamily_name": "Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Black.ttf||2390060:1745519248.0": { -      "family_name": "SauceCodePro NFM Black", -      "font-family": "SauceCodePro NFM Black", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM Black", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        8, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Black.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": "Black", -      "subfamily_name": "Regular", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-BlackItalic.ttf||2356772:1745519248.0": { -      "family_name": "SauceCodePro NFM Black", -      "font-family": "SauceCodePro NFM Black", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM Black Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        8, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-BlackItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": "Black Italic", -      "subfamily_name": "Italic", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Bold.ttf||2388728:1745519248.0": { -      "family_name": "SauceCodePro NFM", -      "font-family": "SauceCodePro NFM", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        7, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Bold.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-BoldItalic.ttf||2355036:1745519248.0": { -      "family_name": "SauceCodePro NFM", -      "font-family": "SauceCodePro NFM", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        7, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-BoldItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-ExtraLight.ttf||2381408:1745519248.0": { -      "family_name": "SauceCodePro NFM ExtraLight", -      "font-family": "SauceCodePro NFM ExtraLight", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM ExtraLight", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        3, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-ExtraLight.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": "ExtraLight", -      "subfamily_name": "Regular", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-ExtraLightItalic.ttf||2350980:1745519248.0": { -      "family_name": "SauceCodePro NFM ExtraLight", -      "font-family": "SauceCodePro NFM ExtraLight", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM ExtraLight Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        3, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-ExtraLightItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": "ExtraLight Italic", -      "subfamily_name": "Italic", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Italic.ttf||2352368:1745519248.0": { -      "family_name": "SauceCodePro NFM", -      "font-family": "SauceCodePro NFM", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Italic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Light.ttf||2381340:1745519248.0": { -      "family_name": "SauceCodePro NFM Light", -      "font-family": "SauceCodePro NFM Light", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        4, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Light.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": "Light", -      "subfamily_name": "Regular", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-LightItalic.ttf||2351156:1745519248.0": { -      "family_name": "SauceCodePro NFM Light", -      "font-family": "SauceCodePro NFM Light", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        4, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-LightItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": "Light Italic", -      "subfamily_name": "Italic", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Medium.ttf||2387764:1745519248.0": { -      "family_name": "SauceCodePro NFM Medium", -      "font-family": "SauceCodePro NFM Medium", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "500", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM Medium", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Medium.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": "Medium", -      "subfamily_name": "Regular", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-MediumItalic.ttf||2353256:1745519248.0": { -      "family_name": "SauceCodePro NFM Medium", -      "font-family": "SauceCodePro NFM Medium", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "500", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM Medium Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-MediumItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": "Medium Italic", -      "subfamily_name": "Italic", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Regular.ttf||2392200:1745519248.0": { -      "family_name": "SauceCodePro NFM", -      "font-family": "SauceCodePro NFM", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-Regular.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-SemiBold.ttf||2388856:1745519248.0": { -      "family_name": "SauceCodePro NFM SemiBold", -      "font-family": "SauceCodePro NFM SemiBold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM SemiBold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        6, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-SemiBold.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": "SemiBold", -      "subfamily_name": "Regular", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-SemiBoldItalic.ttf||2355748:1745519248.0": { -      "family_name": "SauceCodePro NFM SemiBold", -      "font-family": "SauceCodePro NFM SemiBold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "SauceCodePro NFM SemiBold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        6, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontMono-SemiBoldItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Mono", -      "preferred_subfamily_name": "SemiBold Italic", -      "subfamily_name": "Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Black.ttf||2390264:1745519248.0": { -      "family_name": "SauceCodePro NFP Black", -      "font-family": "SauceCodePro NFP Black", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP Black", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        8, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Black.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": "Black", -      "subfamily_name": "Regular", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-BlackItalic.ttf||2356476:1745519248.0": { -      "family_name": "SauceCodePro NFP Black", -      "font-family": "SauceCodePro NFP Black", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP Black Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        8, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-BlackItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": "Black Italic", -      "subfamily_name": "Italic", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Bold.ttf||2389188:1745519248.0": { -      "family_name": "SauceCodePro NFP", -      "font-family": "SauceCodePro NFP", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        7, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Bold.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-BoldItalic.ttf||2354992:1745519248.0": { -      "family_name": "SauceCodePro NFP", -      "font-family": "SauceCodePro NFP", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        7, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-BoldItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-ExtraLight.ttf||2381592:1745519248.0": { -      "family_name": "SauceCodePro NFP ExtraLight", -      "font-family": "SauceCodePro NFP ExtraLight", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP ExtraLight", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        3, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-ExtraLight.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": "ExtraLight", -      "subfamily_name": "Regular", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-ExtraLightItalic.ttf||2350664:1745519248.0": { -      "family_name": "SauceCodePro NFP ExtraLight", -      "font-family": "SauceCodePro NFP ExtraLight", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP ExtraLight Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        3, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-ExtraLightItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": "ExtraLight Italic", -      "subfamily_name": "Italic", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Italic.ttf||2352340:1745519248.0": { -      "family_name": "SauceCodePro NFP", -      "font-family": "SauceCodePro NFP", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Italic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Light.ttf||2381584:1745519248.0": { -      "family_name": "SauceCodePro NFP Light", -      "font-family": "SauceCodePro NFP Light", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        4, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Light.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": "Light", -      "subfamily_name": "Regular", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-LightItalic.ttf||2350900:1745519248.0": { -      "family_name": "SauceCodePro NFP Light", -      "font-family": "SauceCodePro NFP Light", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        4, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-LightItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": "Light Italic", -      "subfamily_name": "Italic", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Medium.ttf||2388144:1745519248.0": { -      "family_name": "SauceCodePro NFP Medium", -      "font-family": "SauceCodePro NFP Medium", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "500", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP Medium", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Medium.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": "Medium", -      "subfamily_name": "Regular", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-MediumItalic.ttf||2353136:1745519248.0": { -      "family_name": "SauceCodePro NFP Medium", -      "font-family": "SauceCodePro NFP Medium", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "500", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP Medium Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-MediumItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": "Medium Italic", -      "subfamily_name": "Italic", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Regular.ttf||2392672:1745519248.0": { -      "family_name": "SauceCodePro NFP", -      "font-family": "SauceCodePro NFP", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-Regular.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-SemiBold.ttf||2389000:1745519248.0": { -      "family_name": "SauceCodePro NFP SemiBold", -      "font-family": "SauceCodePro NFP SemiBold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP SemiBold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        6, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-SemiBold.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": "SemiBold", -      "subfamily_name": "Regular", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-SemiBoldItalic.ttf||2355392:1745519248.0": { -      "family_name": "SauceCodePro NFP SemiBold", -      "font-family": "SauceCodePro NFP SemiBold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "SauceCodePro NFP SemiBold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        6, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/SauceCodeProNerdFontPropo-SemiBoldItalic.ttf", -      "preferred_family_name": "SauceCodePro Nerd Font Propo", -      "preferred_subfamily_name": "SemiBold Italic", -      "subfamily_name": "Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/SymbolsNerdFont-Regular.ttf||2440316:1745602343.0": { -      "family_name": "Symbols Nerd Font", -      "font-family": "Symbols Nerd Font", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Symbols Nerd Font", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": true, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/SymbolsNerdFont-Regular.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Times.TTF||330412:1701284278.0": { -      "family_name": "Times New Roman", -      "font-family": "Times New Roman", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Times New Roman", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        2, -        6, -        3, -        5, -        4, -        5, -        2, -        3, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Times.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Timesbd.TTF||333900:1701284278.0": { -      "family_name": "Times New Roman", -      "font-family": "Times New Roman", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Times New Roman Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        2, -        8, -        3, -        7, -        5, -        5, -        2, -        3, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Timesbd.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Timesbi.TTF||238612:1701284278.0": { -      "family_name": "Times New Roman", -      "font-family": "Times New Roman", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Times New Roman Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        2, -        7, -        3, -        6, -        5, -        5, -        9, -        3, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Timesbi.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Timesi.TTF||247092:1701284278.0": { -      "family_name": "Times New Roman", -      "font-family": "Times New Roman", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Times New Roman Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        2, -        5, -        3, -        5, -        4, -        5, -        9, -        3, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Timesi.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Trebucbd.ttf||123828:1701284278.0": { -      "family_name": "Trebuchet MS", -      "font-family": "Trebuchet MS", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "Trebuchet MS Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        7, -        3, -        2, -        2, -        2, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Trebucbd.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/UBraille.ttf||154412:1701284290.0": { -      "family_name": "Braille", -      "font-family": "Braille", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Braille", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        0, -        6, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/UBraille.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Verdana.TTF||139640:1701284278.0": { -      "family_name": "Verdana", -      "font-family": "Verdana", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Verdana", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        6, -        4, -        3, -        5, -        4, -        4, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Verdana.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Verdanab.TTF||136032:1701284278.0": { -      "family_name": "Verdana", -      "font-family": "Verdana", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Verdana Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        8, -        4, -        3, -        5, -        4, -        4, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Verdanab.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Verdanai.TTF||154264:1701284278.0": { -      "family_name": "Verdana", -      "font-family": "Verdana", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Verdana Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        6, -        4, -        3, -        5, -        4, -        11, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Verdanai.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Verdanaz.TTF||153324:1701284278.0": { -      "family_name": "Verdana", -      "font-family": "Verdana", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Verdana Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        8, -        4, -        3, -        5, -        4, -        11, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/Verdanaz.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/Webdings.TTF||118752:1701284278.0": { -      "family_name": "Webdings", -      "font-family": "Webdings", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Webdings", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        5, -        3, -        1, -        2, -        1, -        5, -        9, -        6, -        7, -        3 -      ], -      "path": "/usr/share/fonts/TTF/Webdings.TTF", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/all-the-icons.ttf||44732:1701284228.0": { -      "family_name": "all-the-icons", -      "font-family": "all-the-icons", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "all-the-icons", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/all-the-icons.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/cour.ttf||302688:1701284278.0": { -      "family_name": "Courier New", -      "font-family": "Courier New", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Courier New", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        7, -        3, -        9, -        2, -        2, -        5, -        2, -        4, -        4 -      ], -      "path": "/usr/share/fonts/TTF/cour.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/courbd.ttf||311508:1701284278.0": { -      "family_name": "Courier New", -      "font-family": "Courier New", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Courier New Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        7, -        6, -        9, -        2, -        2, -        5, -        2, -        4, -        4 -      ], -      "path": "/usr/share/fonts/TTF/courbd.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/courbi.ttf||234788:1701284278.0": { -      "family_name": "Courier New", -      "font-family": "Courier New", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Courier New Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        7, -        6, -        9, -        2, -        2, -        5, -        9, -        4, -        4 -      ], -      "path": "/usr/share/fonts/TTF/courbi.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/couri.ttf||244156:1701284278.0": { -      "family_name": "Courier New", -      "font-family": "Courier New", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Courier New Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        7, -        4, -        9, -        2, -        2, -        5, -        9, -        4, -        4 -      ], -      "path": "/usr/share/fonts/TTF/couri.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/file-icons.ttf||489672:1701284228.0": { -      "family_name": "file-icons", -      "font-family": "file-icons", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "file-icons", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/file-icons.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/fontawesome.ttf||152796:1701284228.0": { -      "family_name": "FontAwesome", -      "font-family": "FontAwesome", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "FontAwesome Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/fontawesome.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/material-design-icons.ttf||128180:1701284228.0": { -      "family_name": "Material Icons", -      "font-family": "Material Icons", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Material Icons", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        0, -        5, -        3, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/material-design-icons.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/octicons.ttf||52544:1701284228.0": { -      "family_name": "github-octicons", -      "font-family": "github-octicons", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "github-octicons", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        0, -        5, -        3, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/octicons.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/trebuc.ttf||126796:1701284278.0": { -      "family_name": "Trebuchet MS", -      "font-family": "Trebuchet MS", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "Trebuchet MS", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        6, -        3, -        2, -        2, -        2, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/trebuc.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/trebucbi.ttf||131188:1701284278.0": { -      "family_name": "Trebuchet MS", -      "font-family": "Trebuchet MS", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 8, -      "full_name": "Trebuchet MS Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        7, -        3, -        2, -        2, -        2, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/trebucbi.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/trebucit.ttf||139288:1701284278.0": { -      "family_name": "Trebuchet MS", -      "font-family": "Trebuchet MS", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 8, -      "full_name": "Trebuchet MS Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 1, -      "panose": [ -        2, -        11, -        6, -        3, -        2, -        2, -        2, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/TTF/trebucit.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/TTF/weathericons.ttf||99564:1701284228.0": { -      "family_name": "Weather Icons", -      "font-family": "Weather Icons", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "Weather Icons Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": false, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/TTF/weathericons.ttf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Black.otf||133768:1720044283.0": { -      "family_name": "Source Code Pro Black", -      "font-family": "Source Code Pro Black", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "Source Code Pro Black", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        8, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Black.otf", -      "preferred_family_name": "Source Code Pro", -      "preferred_subfamily_name": "Black", -      "subfamily_name": "Regular", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-BlackIt.otf||112052:1720044283.0": { -      "family_name": "Source Code Pro Black", -      "font-family": "Source Code Pro Black", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "900", -      "fs_type": 0, -      "full_name": "Source Code Pro Black Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        8, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-BlackIt.otf", -      "preferred_family_name": "Source Code Pro", -      "preferred_subfamily_name": "Black Italic", -      "subfamily_name": "Italic", -      "weight": 900, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Bold.otf||134020:1720044283.0": { -      "family_name": "Source Code Pro", -      "font-family": "Source Code Pro", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Source Code Pro Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        7, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Bold.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-BoldIt.otf||110288:1720044283.0": { -      "family_name": "Source Code Pro", -      "font-family": "Source Code Pro", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 0, -      "full_name": "Source Code Pro Bold Italic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        7, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-BoldIt.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-ExtraLight.otf||125412:1720044283.0": { -      "family_name": "Source Code Pro ExtraLight", -      "font-family": "Source Code Pro ExtraLight", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "Source Code Pro ExtraLight", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        3, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-ExtraLight.otf", -      "preferred_family_name": "Source Code Pro", -      "preferred_subfamily_name": "ExtraLight", -      "subfamily_name": "Regular", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-ExtraLightIt.otf||106204:1720044283.0": { -      "family_name": "Source Code Pro ExtraLight", -      "font-family": "Source Code Pro ExtraLight", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "Source Code Pro ExtraLight Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        3, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-ExtraLightIt.otf", -      "preferred_family_name": "Source Code Pro", -      "preferred_subfamily_name": "ExtraLight Italic", -      "subfamily_name": "Italic", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-It.otf||107752:1720044283.0": { -      "family_name": "Source Code Pro", -      "font-family": "Source Code Pro", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Source Code Pro Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-It.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Light.otf||129956:1720044283.0": { -      "family_name": "Source Code Pro Light", -      "font-family": "Source Code Pro Light", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "Source Code Pro Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        4, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Light.otf", -      "preferred_family_name": "Source Code Pro", -      "preferred_subfamily_name": "Light", -      "subfamily_name": "Regular", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-LightIt.otf||107660:1720044283.0": { -      "family_name": "Source Code Pro Light", -      "font-family": "Source Code Pro Light", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 0, -      "full_name": "Source Code Pro Light Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        4, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-LightIt.otf", -      "preferred_family_name": "Source Code Pro", -      "preferred_subfamily_name": "Light Italic", -      "subfamily_name": "Italic", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Medium.otf||130984:1720044283.0": { -      "family_name": "Source Code Pro Medium", -      "font-family": "Source Code Pro Medium", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "500", -      "fs_type": 0, -      "full_name": "Source Code Pro Medium", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Medium.otf", -      "preferred_family_name": "Source Code Pro", -      "preferred_subfamily_name": "Medium", -      "subfamily_name": "Regular", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-MediumIt.otf||107464:1720044283.0": { -      "family_name": "Source Code Pro Medium", -      "font-family": "Source Code Pro Medium", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "500", -      "fs_type": 0, -      "full_name": "Source Code Pro Medium Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-MediumIt.otf", -      "preferred_family_name": "Source Code Pro", -      "preferred_subfamily_name": "Medium Italic", -      "subfamily_name": "Italic", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Regular.otf||131128:1720044283.0": { -      "family_name": "Source Code Pro", -      "font-family": "Source Code Pro", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "Source Code Pro", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        5, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Regular.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Semibold.otf||131608:1720044283.0": { -      "family_name": "Source Code Pro Semibold", -      "font-family": "Source Code Pro Semibold", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "Source Code Pro Semibold", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        6, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-Semibold.otf", -      "preferred_family_name": "Source Code Pro", -      "preferred_subfamily_name": "Semibold", -      "subfamily_name": "Regular", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-SemiboldIt.otf||108500:1720044283.0": { -      "family_name": "Source Code Pro Semibold", -      "font-family": "Source Code Pro Semibold", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 0, -      "full_name": "Source Code Pro Semibold Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        2, -        11, -        6, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodePro-SemiboldIt.otf", -      "preferred_family_name": "Source Code Pro", -      "preferred_subfamily_name": "Semibold Italic", -      "subfamily_name": "Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodeVF-Italic.otf||122836:1720044283.0": { -      "family_name": "SourceCodeVF", -      "font-family": "SourceCodeVF", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "SourceCodeVF Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        3, -        9, -        3, -        4, -        3, -        9, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodeVF-Italic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/adobe-source-code-pro/SourceCodeVF-Upright.otf||150744:1720044283.0": { -      "family_name": "SourceCodeVF", -      "font-family": "SourceCodeVF", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "200", -      "fs_type": 0, -      "full_name": "SourceCodeVF", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 4, -      "panose": [ -        2, -        11, -        3, -        9, -        3, -        4, -        3, -        2, -        2, -        4 -      ], -      "path": "/usr/share/fonts/adobe-source-code-pro/SourceCodeVF-Upright.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 200, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    },      "/usr/share/fonts/cantarell/Cantarell-VF.otf||170588:1720749076.0": {        "family_name": "Cantarell",        "font-family": "Cantarell", @@ -16956,1266 +8028,6 @@        "wws_family_name": null,        "wws_subfamily_name": null      }, -    "/usr/share/fonts/gsfonts/C059-BdIta.otf||103444:1720802942.0": { -      "family_name": "C059", -      "font-family": "C059", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "C059-BdIta", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/C059-BdIta.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/C059-Bold.otf||100692:1720802942.0": { -      "family_name": "C059", -      "font-family": "C059", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "C059-Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/C059-Bold.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/C059-Italic.otf||101324:1720802942.0": { -      "family_name": "C059", -      "font-family": "C059", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "C059-Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/C059-Italic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/C059-Roman.otf||97476:1720802942.0": { -      "family_name": "C059", -      "font-family": "C059", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "C059-Roman", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/C059-Roman.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Roman", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/D050000L.otf||29832:1720802942.0": { -      "family_name": "D050000L", -      "font-family": "D050000L", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 0, -      "full_name": "D050000L", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        1, -        1, -        6, -        1, -        1, -        1, -        1, -        1, -        1, -        1 -      ], -      "path": "/usr/share/fonts/gsfonts/D050000L.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusMonoPS-Bold.otf||87520:1720802942.0": { -      "family_name": "Nimbus Mono PS", -      "font-family": "Nimbus Mono PS", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "NimbusMonoPS-Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusMonoPS-Bold.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusMonoPS-BoldItalic.otf||89580:1720802942.0": { -      "family_name": "Nimbus Mono PS", -      "font-family": "Nimbus Mono PS", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "NimbusMonoPS-BoldItalic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusMonoPS-BoldItalic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusMonoPS-Italic.otf||82648:1720802942.0": { -      "family_name": "Nimbus Mono PS", -      "font-family": "Nimbus Mono PS", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "NimbusMonoPS-Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusMonoPS-Italic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusMonoPS-Regular.otf||77936:1720802942.0": { -      "family_name": "Nimbus Mono PS", -      "font-family": "Nimbus Mono PS", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "NimbusMonoPS-Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        9, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusMonoPS-Regular.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusRoman-Bold.otf||100984:1720802942.0": { -      "family_name": "Nimbus Roman", -      "font-family": "Nimbus Roman", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "NimbusRoman-Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusRoman-Bold.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusRoman-BoldItalic.otf||104772:1720802942.0": { -      "family_name": "Nimbus Roman", -      "font-family": "Nimbus Roman", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "NimbusRoman-BoldItalic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusRoman-BoldItalic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusRoman-Italic.otf||105684:1720802942.0": { -      "family_name": "Nimbus Roman", -      "font-family": "Nimbus Roman", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "NimbusRoman-Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusRoman-Italic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusRoman-Regular.otf||98200:1720802942.0": { -      "family_name": "Nimbus Roman", -      "font-family": "Nimbus Roman", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "NimbusRoman-Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusRoman-Regular.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusSans-Bold.otf||83264:1720802942.0": { -      "family_name": "Nimbus Sans", -      "font-family": "Nimbus Sans", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "NimbusSans-Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusSans-Bold.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusSans-BoldItalic.otf||95396:1720802942.0": { -      "family_name": "Nimbus Sans", -      "font-family": "Nimbus Sans", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "NimbusSans-BoldItalic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusSans-BoldItalic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusSans-Italic.otf||95244:1720802942.0": { -      "family_name": "Nimbus Sans", -      "font-family": "Nimbus Sans", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "NimbusSans-Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusSans-Italic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusSans-Regular.otf||82264:1720802942.0": { -      "family_name": "Nimbus Sans", -      "font-family": "Nimbus Sans", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "NimbusSans-Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusSans-Regular.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusSansNarrow-Bold.otf||81340:1720802942.0": { -      "family_name": "Nimbus Sans Narrow", -      "font-family": "Nimbus Sans Narrow", -      "font-stretch": "semi-condensed", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "NimbusSansNarrow-Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        6, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusSansNarrow-Bold.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 4, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusSansNarrow-BoldOblique.otf||87956:1720802942.0": { -      "family_name": "Nimbus Sans Narrow", -      "font-family": "Nimbus Sans Narrow", -      "font-stretch": "semi-condensed", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "NimbusSansNarrow-BoldOblique", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        6, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusSansNarrow-BoldOblique.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Oblique", -      "weight": 700, -      "width": 4, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusSansNarrow-Oblique.otf||87068:1720802942.0": { -      "family_name": "Nimbus Sans Narrow", -      "font-family": "Nimbus Sans Narrow", -      "font-stretch": "semi-condensed", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "NimbusSansNarrow-Oblique", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        6, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusSansNarrow-Oblique.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Oblique", -      "weight": 400, -      "width": 4, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/NimbusSansNarrow-Regular.otf||80864:1720802942.0": { -      "family_name": "Nimbus Sans Narrow", -      "font-family": "Nimbus Sans Narrow", -      "font-stretch": "semi-condensed", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "NimbusSansNarrow-Regular", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        6, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/NimbusSansNarrow-Regular.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 4, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/P052-Bold.otf||110980:1720802942.0": { -      "family_name": "P052", -      "font-family": "P052", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "P052-Bold", -      "is_bold": true, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/P052-Bold.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/P052-BoldItalic.otf||110928:1720802942.0": { -      "family_name": "P052", -      "font-family": "P052", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "bold", -      "fs_type": 4, -      "full_name": "P052-BoldItalic", -      "is_bold": true, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        8, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/P052-BoldItalic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Bold Italic", -      "weight": 700, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/P052-Italic.otf||109824:1720802942.0": { -      "family_name": "P052", -      "font-family": "P052", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "P052-Italic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/P052-Italic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Italic", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/P052-Roman.otf||110236:1720802942.0": { -      "family_name": "P052", -      "font-family": "P052", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "P052-Roman", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/P052-Roman.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Roman", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/StandardSymbolsPS.otf||21176:1720802942.0": { -      "family_name": "Standard Symbols PS", -      "font-family": "Standard Symbols PS", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "Standard Symbols PS", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        5, -        5, -        1, -        2, -        1, -        7, -        6, -        2, -        5, -        7 -      ], -      "path": "/usr/share/fonts/gsfonts/StandardSymbolsPS.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Regular", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/URWBookman-Demi.otf||97208:1720802942.0": { -      "family_name": "URW Bookman", -      "font-family": "URW Bookman", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 4, -      "full_name": "URWBookman-Demi", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        7, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/URWBookman-Demi.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Demi", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/URWBookman-DemiItalic.otf||101584:1720802942.0": { -      "family_name": "URW Bookman", -      "font-family": "URW Bookman", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 4, -      "full_name": "URWBookman-DemiItalic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        7, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/URWBookman-DemiItalic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Demi Italic", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/URWBookman-Light.otf||98396:1720802942.0": { -      "family_name": "URW Bookman", -      "font-family": "URW Bookman", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "300", -      "fs_type": 4, -      "full_name": "URWBookman-Light", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        4, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/URWBookman-Light.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Light", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/URWBookman-LightItalic.otf||102764:1720802942.0": { -      "family_name": "URW Bookman", -      "font-family": "URW Bookman", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "300", -      "fs_type": 4, -      "full_name": "URWBookman-LightItalic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        4, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/URWBookman-LightItalic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Light Italic", -      "weight": 300, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/URWGothic-Book.otf||82968:1720802942.0": { -      "family_name": "URW Gothic", -      "font-family": "URW Gothic", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "URWGothic-Book", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": true, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/URWGothic-Book.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Book", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/URWGothic-BookOblique.otf||85336:1720802942.0": { -      "family_name": "URW Gothic", -      "font-family": "URW Gothic", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "normal", -      "fs_type": 4, -      "full_name": "URWGothic-BookOblique", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        5, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/URWGothic-BookOblique.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Book Oblique", -      "weight": 400, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/URWGothic-Demi.otf||83580:1720802942.0": { -      "family_name": "URW Gothic", -      "font-family": "URW Gothic", -      "font-stretch": "normal", -      "font-style": "normal", -      "font-weight": "600", -      "fs_type": 4, -      "full_name": "URWGothic-Demi", -      "is_bold": false, -      "is_italic": false, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        7, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/URWGothic-Demi.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Demi", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/URWGothic-DemiOblique.otf||86232:1720802942.0": { -      "family_name": "URW Gothic", -      "font-family": "URW Gothic", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "600", -      "fs_type": 4, -      "full_name": "URWGothic-DemiOblique", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        7, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/URWGothic-DemiOblique.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Demi Oblique", -      "weight": 600, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    }, -    "/usr/share/fonts/gsfonts/Z003-MediumItalic.otf||114052:1720802942.0": { -      "family_name": "Z003", -      "font-family": "Z003", -      "font-stretch": "normal", -      "font-style": "italic", -      "font-weight": "500", -      "fs_type": 4, -      "full_name": "Z003-MediumItalic", -      "is_bold": false, -      "is_italic": true, -      "is_oblique": false, -      "is_otf": true, -      "is_regular": false, -      "is_wws": false, -      "os2_version": 3, -      "panose": [ -        0, -        0, -        6, -        0, -        0, -        0, -        0, -        0, -        0, -        0 -      ], -      "path": "/usr/share/fonts/gsfonts/Z003-MediumItalic.otf", -      "preferred_family_name": null, -      "preferred_subfamily_name": null, -      "subfamily_name": "Medium Italic", -      "weight": 500, -      "width": 5, -      "wws_family_name": null, -      "wws_subfamily_name": null -    },      "/usr/share/fonts/liberation/LiberationMono-Bold.ttf||308068:1720827900.0": {        "family_name": "Liberation Mono",        "font-family": "Liberation Mono", diff --git a/dotfiles/system/.config/calibre/global.py.json b/dotfiles/system/.config/calibre/global.py.json index 129208f..43f47a0 100644 --- a/dotfiles/system/.config/calibre/global.py.json +++ b/dotfiles/system/.config/calibre/global.py.json @@ -6,8 +6,8 @@    "filename_pattern": "(?P<title>.+) - (?P<author>[^_]+)",    "input_format_order": [      "EPUB", -    "AZW3",      "PDF", +    "AZW3",      "MOBI",      "LIT",      "PRC", @@ -23,38 +23,39 @@      "RTF",      "TXT",      "CB7", -    "DOCM", -    "MARKDOWN", -    "DJV", -    "RB", -    "CBR", -    "POBI", -    "TEXTILE", -    "TEXT",      "SHTM", -    "OPF", -    "HTMLZ", -    "CBZ", +    "SNB", +    "PMLZ", +    "TCR",      "FBZ", +    "DJVU", +    "DOCM",      "LRF", -    "RAR", +    "TXTZ", +    "KEPUB", +    "POBI",      "PDB", -    "PMLZ", -    "CBC", -    "UPDB",      "CHM", -    "TCR", -    "PML", -    "TXTZ", +    "CBR", +    "HTMLZ", +    "RB",      "MD", -    "DOWNLOADED_RECIPE", -    "SNB", +    "CBC", +    "MARKDOWN", +    "TEXT",      "AZW", +    "UPDB",      "RECIPE", -    "DJVU", -    "AZW4" +    "PML", +    "DOWNLOADED_RECIPE", +    "TEXTILE", +    "RAR", +    "CBZ", +    "OPF", +    "AZW4", +    "DJV"    ], -  "installation_uuid": "ddef8582-3acb-46bd-aadf-ec9d121b4811", +  "installation_uuid": "4c998702-215a-4787-a019-abdee4cdf53c",    "isbndb_com_key": "",    "language": "en",    "library_path": "/home/cjennings/sync/books", @@ -63,11 +64,10 @@      "title",      "authors",      "tags", -    "series",      "publisher"    ],    "manage_device_metadata": "manual", -  "mark_new_books": false, +  "mark_new_books": true,    "migrated": false,    "network_timeout": 5,    "new_book_tags": [], diff --git a/dotfiles/system/.config/calibre/gui.py.json b/dotfiles/system/.config/calibre/gui.py.json index 95bdc07..6bfc78c 100644 --- a/dotfiles/system/.config/calibre/gui.py.json +++ b/dotfiles/system/.config/calibre/gui.py.json @@ -20,11 +20,11 @@    "cover_flow_queue_length": 6,    "default_send_to_device_action": "DeviceAction:main::False:False",    "delete_news_from_library_on_upload": false, -  "disable_animations": false, +  "disable_animations": true,    "disable_tray_notification": false,    "enforce_cpu_limit": true,    "get_social_metadata": true, -  "gui_layout": "narrow", +  "gui_layout": "wide",    "highlight_search_matches": false,    "internally_viewed_formats": [      "AZW", @@ -46,55 +46,56 @@    "jobs_search_history": [],    "lrf_viewer_search_history": [],    "main_search_history": [ -<<<<<<< Updated upstream -    "author:Heidegger", -    "author:Heidegger not format:epub", -    "author:Heidegger not format:pdf", -    "author:Heidegger ! format:pdf", -    "author:Heidegger ! format:epub", -    "author:Heidegger !format:epub", -    "lee braver" -======= -    "rebel camus", -    "author:heidegger", -    "phenomenology of intuition author:heidegger", -    "author:hitchens format:epub", -    "author:hitchens", -    "jefferson author:hitchens", -    "author:koestler", -    "author:orwell", -    "hitchens orwell", -    "CARR Yearbook", -    "tag:inbox", -    "inbox", -    "author:machiavelli", -    "machiavelli", -    "authors:\"=Anne Applebaum\"", -    "heidegger",      "format:epub", -    "orwell hitchens", -    "orwell", -    "read", -    "study", -    "author:john rawls", -    "liberty mill", -    "puzzles", -    "author:John Stuart Mill" ->>>>>>> Stashed changes +    "author:orwell", +    "author:zola", +    "sartre humanism", +    "caminos", +    "author:mackie", +    "format:epub mackie", +    "format:epub makie", +    "format:epub metaphysics", +    "not format:epub", +    "dialectic", +    "title:republic", +    "title:republic author:grube", +    "author:markus gabriel", +    "author:gabriel", +    "author:markus gabirel", +    "author:heidegger format:epub", +    "author:marcuse format:epub", +    "format:epub author:horkheimer", +    "author:sheehan", +    "thompson clarke", +    "author:papineau", +    "title:kant", +    "title:kant author:guyer not format:epub", +    "kant guyer"    ],    "main_window_geometry": null,    "match_tags_type": "any",    "new_version_notification": false, -  "oldest_news": 60, +  "oldest_news": 7,    "overwrite_author_title_metadata": true, -  "plugin_search_history": [], -  "save_to_disk_template_history": [], +  "plugin_search_history": [ +    "kobo", +    "covers", +    "cover", +    "rsync" +  ], +  "save_to_disk_template_history": [ +    "{author_sort}/{title}/{title} - {authors}" +  ],    "scheduler_search_history": [],    "search_as_you_type": false,    "send_to_device_template_history": [],    "send_to_storage_card_by_default": false,    "separate_cover_flow": false, -  "shortcuts_search_history": [], +  "shortcuts_search_history": [ +    "quit", +    "quickview", +    "q" +  ],    "show_avg_rating": true,    "sort_tags_by": "name",    "systray_icon": false, @@ -103,8 +104,8 @@      "__value__": []    },    "tweaks_search_history": [], -  "upload_news_to_device": true, -  "use_roman_numerals_for_series_number": true, +  "upload_news_to_device": false, +  "use_roman_numerals_for_series_number": false,    "viewer_search_history": [],    "viewer_toc_search_history": [],    "worker_limit": 6 diff --git a/dotfiles/system/.config/calibre/metadata-sources-cache.json b/dotfiles/system/.config/calibre/metadata-sources-cache.json index 2630da9..d417f23 100644 --- a/dotfiles/system/.config/calibre/metadata-sources-cache.json +++ b/dotfiles/system/.config/calibre/metadata-sources-cache.json @@ -1,14 +1,14 @@  { -  "amazon": "#!/usr/bin/env python\n# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai\n# License: GPLv3 Copyright: 2011, Kovid Goyal <kovid at kovidgoyal.net>\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport re\nimport socket\nimport string\nimport time\nfrom functools import partial\n\ntry:\n    from queue import Empty, Queue\nexcept ImportError:\n    from Queue import Empty, Queue\n\nfrom threading import Thread\n\ntry:\n    from urllib.parse import urlparse\nexcept ImportError:\n    from urlparse import urlparse\n\nfrom mechanize import HTTPError\n\nfrom calibre import as_unicode, browser, random_user_agent, xml_replace_entities\nfrom calibre.ebooks.metadata import check_isbn\nfrom calibre.ebooks.metadata.book.base import Metadata\nfrom calibre.ebooks.metadata.sources.base import Option, Source, fixauthors, fixcase\nfrom calibre.utils.icu import lower as icu_lower\nfrom calibre.utils.localization import canonicalize_lang\nfrom calibre.utils.random_ua import accept_header_for_ua\n\n\ndef sort_matches_preferring_kindle_editions(matches):\n    upos_map = {url:i for i, url in enumerate(matches)}\n\n    def skey(url):\n        opos = upos_map[url]\n        parts = url.split('/')\n        try:\n            idx = parts.index('dp')\n        except Exception:\n            idx = -1\n        if idx < 0 or idx + 1 >= len(parts) or not parts[idx+1].startswith('B'):\n            return 1, opos\n        return 0, opos\n    matches.sort(key=skey)\n    return matches\n\n\ndef iri_quote_plus(url):\n    from calibre.ebooks.oeb.base import urlquote\n    ans = urlquote(url)\n    if isinstance(ans, bytes):\n        ans = ans.decode('utf-8')\n    return ans.replace('%20', '+')\n\n\ndef user_agent_is_ok(ua):\n    return 'Mobile/' not in ua and 'Mobile ' not in ua\n\n\nclass CaptchaError(Exception):\n    pass\n\n\nclass SearchFailed(ValueError):\n    pass\n\n\nclass UrlNotFound(ValueError):\n\n    def __init__(self, url):\n        ValueError.__init__(self, 'The URL {} was not found (HTTP 404)'.format(url))\n\n\nclass UrlTimedOut(ValueError):\n\n    def __init__(self, url):\n        ValueError.__init__(self, 'Timed out fetching {} try again later'.format(url))\n\n\ndef parse_html(raw):\n    try:\n        from html5_parser import parse\n    except ImportError:\n        # Old versions of calibre\n        import html5lib\n        return html5lib.parse(raw, treebuilder='lxml', namespaceHTMLElements=False)\n    else:\n        return parse(raw)\n\n\ndef parse_details_page(url, log, timeout, browser, domain):\n    from lxml.html import tostring\n\n    from calibre.ebooks.chardet import xml_to_unicode\n    from calibre.utils.cleantext import clean_ascii_chars\n    try:\n        from calibre.ebooks.metadata.sources.update import search_engines_module\n        get_data_for_cached_url = search_engines_module().get_data_for_cached_url\n    except Exception:\n        def get_data_for_cached_url(*a):\n            return None\n    raw = get_data_for_cached_url(url)\n    if raw:\n        log('Using cached details for url:', url)\n    else:\n        log('Downloading details from:', url)\n        try:\n            raw = browser.open_novisit(url, timeout=timeout).read().strip()\n        except Exception as e:\n            if callable(getattr(e, 'getcode', None)) and e.getcode() == 404:\n                log.error('URL not found: %r' % url)\n                raise UrlNotFound(url)\n            attr = getattr(e, 'args', [None])\n            attr = attr if attr else [None]\n            if isinstance(attr[0], socket.timeout):\n                msg = 'Details page timed out. Try again later.'\n                log.error(msg)\n                raise UrlTimedOut(url)\n            else:\n                msg = 'Failed to make details query: %r' % url\n                log.exception(msg)\n                raise ValueError('Could not make details query for {}'.format(url))\n\n    oraw = raw\n    if 'amazon.com.br' in url:\n        # amazon.com.br serves utf-8 but has an incorrect latin1 <meta> tag\n        raw = raw.decode('utf-8')\n    raw = xml_to_unicode(raw, strip_encoding_pats=True,\n                         resolve_entities=True)[0]\n    if '<title>404 - ' in raw:\n        raise ValueError('Got a 404 page for: %r' % url)\n    if '>Could not find the requested document in the cache.<' in raw:\n        raise ValueError('No cached entry for %s found' % url)\n\n    try:\n        root = parse_html(clean_ascii_chars(raw))\n    except Exception:\n        msg = 'Failed to parse amazon details page: %r' % url\n        log.exception(msg)\n        raise ValueError(msg)\n    if domain == 'jp':\n        for a in root.xpath('//a[@href]'):\n            if ('black-curtain-redirect.html' in a.get('href')) or ('/black-curtain/save-eligibility/black-curtain' in a.get('href')):\n                url = a.get('href')\n                if url:\n                    if url.startswith('/'):\n                        url = 'https://amazon.co.jp' + a.get('href')\n                    log('Black curtain redirect found, following')\n                    return parse_details_page(url, log, timeout, browser, domain)\n\n    errmsg = root.xpath('//*[@id=\"errorMessage\"]')\n    if errmsg:\n        msg = 'Failed to parse amazon details page: %r' % url\n        msg += tostring(errmsg, method='text', encoding='unicode').strip()\n        log.error(msg)\n        raise ValueError(msg)\n\n    from css_selectors import Select\n    selector = Select(root)\n    return oraw, root, selector\n\n\ndef parse_asin(root, log, url):\n    try:\n        link = root.xpath('//link[@rel=\"canonical\" and @href]')\n        for l in link:\n            return l.get('href').rpartition('/')[-1]\n    except Exception:\n        log.exception('Error parsing ASIN for url: %r' % url)\n\n\nclass Worker(Thread):  # Get details {{{\n\n    '''\n    Get book details from amazons book page in a separate thread\n    '''\n\n    def __init__(self, url, result_queue, browser, log, relevance, domain,\n                 plugin, timeout=20, testing=False, preparsed_root=None,\n                 cover_url_processor=None, filter_result=None):\n        Thread.__init__(self)\n        self.cover_url_processor = cover_url_processor\n        self.preparsed_root = preparsed_root\n        self.daemon = True\n        self.testing = testing\n        self.url, self.result_queue = url, result_queue\n        self.log, self.timeout = log, timeout\n        self.filter_result = filter_result or (lambda x, log: True)\n        self.relevance, self.plugin = relevance, plugin\n        self.browser = browser\n        self.cover_url = self.amazon_id = self.isbn = None\n        self.domain = domain\n        from lxml.html import tostring\n        self.tostring = tostring\n\n        months = {  # {{{\n            'de': {\n                1: ['jän', 'januar'],\n                2: ['februar'],\n                3: ['märz'],\n                5: ['mai'],\n                6: ['juni'],\n                7: ['juli'],\n                10: ['okt', 'oktober'],\n                12: ['dez', 'dezember']\n            },\n            'it': {\n                1: ['gennaio', 'enn'],\n                2: ['febbraio', 'febbr'],\n                3: ['marzo'],\n                4: ['aprile'],\n                5: ['maggio', 'magg'],\n                6: ['giugno'],\n                7: ['luglio'],\n                8: ['agosto', 'ag'],\n                9: ['settembre', 'sett'],\n                10: ['ottobre', 'ott'],\n                11: ['novembre'],\n                12: ['dicembre', 'dic'],\n            },\n            'fr': {\n                1: ['janv'],\n                2: ['févr'],\n                3: ['mars'],\n                4: ['avril'],\n                5: ['mai'],\n                6: ['juin'],\n                7: ['juil'],\n                8: ['août'],\n                9: ['sept'],\n                10: ['oct', 'octobre'],\n                11: ['nov', 'novembre'],\n                12: ['déc', 'décembre'],\n            },\n            'br': {\n                1: ['janeiro'],\n                2: ['fevereiro'],\n                3: ['março'],\n                4: ['abril'],\n                5: ['maio'],\n                6: ['junho'],\n                7: ['julho'],\n                8: ['agosto'],\n                9: ['setembro'],\n                10: ['outubro'],\n                11: ['novembro'],\n                12: ['dezembro'],\n            },\n            'es': {\n                1: ['enero'],\n                2: ['febrero'],\n                3: ['marzo'],\n                4: ['abril'],\n                5: ['mayo'],\n                6: ['junio'],\n                7: ['julio'],\n                8: ['agosto'],\n                9: ['septiembre', 'setiembre'],\n                10: ['octubre'],\n                11: ['noviembre'],\n                12: ['diciembre'],\n            },\n            'se': {\n                1: ['januari'],\n                2: ['februari'],\n                3: ['mars'],\n                4: ['april'],\n                5: ['maj'],\n                6: ['juni'],\n                7: ['juli'],\n                8: ['augusti'],\n                9: ['september'],\n                10: ['oktober'],\n                11: ['november'],\n                12: ['december'],\n            },\n            'jp': {\n                1: ['1月'],\n                2: ['2月'],\n                3: ['3月'],\n                4: ['4月'],\n                5: ['5月'],\n                6: ['6月'],\n                7: ['7月'],\n                8: ['8月'],\n                9: ['9月'],\n                10: ['10月'],\n                11: ['11月'],\n                12: ['12月'],\n            },\n            'nl': {\n                1: ['januari'], 2: ['februari'], 3: ['maart'], 5: ['mei'], 6: ['juni'], 7: ['juli'], 8: ['augustus'], 10: ['oktober'],\n            }\n\n        }  # }}}\n\n        self.english_months = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',\n                               'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']\n        self.months = months.get(self.domain, {})\n\n        self.pd_xpath = '''\n            //h2[text()=\"Product Details\" or \\\n                 text()=\"Produktinformation\" or \\\n                 text()=\"Dettagli prodotto\" or \\\n                 text()=\"Product details\" or \\\n                 text()=\"Détails sur le produit\" or \\\n                 text()=\"Detalles del producto\" or \\\n                 text()=\"Detalhes do produto\" or \\\n                 text()=\"Productgegevens\" or \\\n                 text()=\"基本信息\" or \\\n                 starts-with(text(), \"登録情報\")]/../div[@class=\"content\"]\n            '''\n        # Editor: is for Spanish\n        self.publisher_xpath = '''\n            descendant::*[starts-with(text(), \"Publisher:\") or \\\n                    starts-with(text(), \"Verlag:\") or \\\n                    starts-with(text(), \"Editore:\") or \\\n                    starts-with(text(), \"Editeur\") or \\\n                    starts-with(text(), \"Editor:\") or \\\n                    starts-with(text(), \"Editora:\") or \\\n                    starts-with(text(), \"Uitgever:\") or \\\n                    starts-with(text(), \"Utgivare:\") or \\\n                    starts-with(text(), \"出版社:\")]\n            '''\n        self.pubdate_xpath = '''\n            descendant::*[starts-with(text(), \"Publication Date:\") or \\\n                    starts-with(text(), \"Audible.com Release Date:\")]\n        '''\n        self.publisher_names = {'Publisher', 'Uitgever', 'Verlag', 'Utgivare', 'Herausgeber',\n                                'Editore', 'Editeur', 'Éditeur', 'Editor', 'Editora', '出版社'}\n\n        self.language_xpath = '''\n            descendant::*[\n                starts-with(text(), \"Language:\") \\\n                or text() = \"Language\" \\\n                or text() = \"Sprache:\" \\\n                or text() = \"Lingua:\" \\\n                or text() = \"Idioma:\" \\\n                or starts-with(text(), \"Langue\") \\\n                or starts-with(text(), \"言語\") \\\n                or starts-with(text(), \"Språk\") \\\n                or starts-with(text(), \"语种\")\n                ]\n            '''\n        self.language_names = {'Language', 'Sprache', 'Språk',\n                               'Lingua', 'Idioma', 'Langue', '言語', 'Taal', '语种'}\n\n        self.tags_xpath = '''\n            descendant::h2[\n                text() = \"Look for Similar Items by Category\" or\n                text() = \"Ähnliche Artikel finden\" or\n                text() = \"Buscar productos similares por categoría\" or\n                text() = \"Ricerca articoli simili per categoria\" or\n                text() = \"Rechercher des articles similaires par rubrique\" or\n                text() = \"Procure por items similares por categoria\" or\n                text() = \"関連商品を探す\"\n            ]/../descendant::ul/li\n        '''\n\n        self.ratings_pat = re.compile(\n            r'([0-9.,]+) ?(out of|von|van|su|étoiles sur|つ星のうち|de un máximo de|de|av) '\n            r'([\\d\\.]+)( (stars|Sternen|stelle|estrellas|estrelas|sterren|stjärnor)){0,1}'\n        )\n        self.ratings_pat_cn = re.compile(r'([0-9.]+) 颗星,最多 5 颗星')\n        self.ratings_pat_jp = re.compile(r'\\d+つ星のうち([\\d\\.]+)')\n\n        lm = {\n            'eng': ('English', 'Englisch', 'Engels', 'Engelska'),\n            'fra': ('French', 'Français'),\n            'ita': ('Italian', 'Italiano'),\n            'deu': ('German', 'Deutsch'),\n            'spa': ('Spanish', 'Espa\\xf1ol', 'Espaniol'),\n            'jpn': ('Japanese', '日本語'),\n            'por': ('Portuguese', 'Português'),\n            'nld': ('Dutch', 'Nederlands',),\n            'chs': ('Chinese', '中文', '简体中文'),\n            'swe': ('Swedish', 'Svenska'),\n        }\n        self.lang_map = {}\n        for code, names in lm.items():\n            for name in names:\n                self.lang_map[name] = code\n\n        self.series_pat = re.compile(\n            r'''\n                \\|\\s*              # Prefix\n                (Series)\\s*:\\s*    # Series declaration\n                (?P<series>.+?)\\s+  # The series name\n                \\((Book)\\s*    # Book declaration\n                (?P<index>[0-9.]+) # Series index\n                \\s*\\)\n                ''', re.X)\n\n    def delocalize_datestr(self, raw):\n        if self.domain == 'cn':\n            return raw.replace('年', '-').replace('月', '-').replace('日', '')\n        if not self.months:\n            return raw\n        ans = raw.lower()\n        for i, vals in self.months.items():\n            for x in vals:\n                ans = ans.replace(x, self.english_months[i])\n        ans = ans.replace(' de ', ' ')\n        return ans\n\n    def run(self):\n        try:\n            self.get_details()\n        except:\n            self.log.exception('get_details failed for url: %r' % self.url)\n\n    def get_details(self):\n        if self.preparsed_root is None:\n            raw, root, selector = parse_details_page(\n                self.url, self.log, self.timeout, self.browser, self.domain)\n        else:\n            raw, root, selector = self.preparsed_root\n\n        from css_selectors import Select\n        self.selector = Select(root)\n        self.parse_details(raw, root)\n\n    def parse_details(self, raw, root):\n        asin = parse_asin(root, self.log, self.url)\n        if not asin and root.xpath('//form[@action=\"/errors/validateCaptcha\"]'):\n            raise CaptchaError(\n                'Amazon returned a CAPTCHA page, probably because you downloaded too many books. Wait for some time and try again.')\n        if self.testing:\n            import tempfile\n            import uuid\n            with tempfile.NamedTemporaryFile(prefix=(asin or type('')(uuid.uuid4())) + '_',\n                                             suffix='.html', delete=False) as f:\n                f.write(raw)\n            print('Downloaded HTML for', asin, 'saved in', f.name)\n\n        try:\n            title = self.parse_title(root)\n        except:\n            self.log.exception('Error parsing title for url: %r' % self.url)\n            title = None\n\n        try:\n            authors = self.parse_authors(root)\n        except:\n            self.log.exception('Error parsing authors for url: %r' % self.url)\n            authors = []\n\n        if not title or not authors or not asin:\n            self.log.error(\n                'Could not find title/authors/asin for %r' % self.url)\n            self.log.error('ASIN: %r Title: %r Authors: %r' % (asin, title,\n                                                               authors))\n            return\n\n        mi = Metadata(title, authors)\n        idtype = 'amazon' if self.domain == 'com' else 'amazon_' + self.domain\n        mi.set_identifier(idtype, asin)\n        self.amazon_id = asin\n\n        try:\n            mi.rating = self.parse_rating(root)\n        except:\n            self.log.exception('Error parsing ratings for url: %r' % self.url)\n\n        try:\n            mi.comments = self.parse_comments(root, raw)\n        except:\n            self.log.exception('Error parsing comments for url: %r' % self.url)\n\n        try:\n            series, series_index = self.parse_series(root)\n            if series:\n                mi.series, mi.series_index = series, series_index\n            elif self.testing:\n                mi.series, mi.series_index = 'Dummy series for testing', 1\n        except:\n            self.log.exception('Error parsing series for url: %r' % self.url)\n\n        try:\n            mi.tags = self.parse_tags(root)\n        except:\n            self.log.exception('Error parsing tags for url: %r' % self.url)\n\n        try:\n            self.cover_url = self.parse_cover(root, raw)\n        except:\n            self.log.exception('Error parsing cover for url: %r' % self.url)\n        if self.cover_url_processor is not None and self.cover_url and self.cover_url.startswith('/'):\n            self.cover_url = self.cover_url_processor(self.cover_url)\n        mi.has_cover = bool(self.cover_url)\n\n        detail_bullets = root.xpath('//*[@data-feature-name=\"detailBullets\"]')\n        non_hero = tuple(self.selector(\n            'div#bookDetails_container_div div#nonHeroSection')) or tuple(self.selector(\n                '#productDetails_techSpec_sections'))\n        feature_and_detail_bullets = root.xpath('//*[@data-feature-name=\"featureBulletsAndDetailBullets\"]')\n        if detail_bullets:\n            self.parse_detail_bullets(root, mi, detail_bullets[0])\n        elif non_hero:\n            try:\n                self.parse_new_details(root, mi, non_hero[0])\n            except:\n                self.log.exception(\n                    'Failed to parse new-style book details section')\n        elif feature_and_detail_bullets:\n            self.parse_detail_bullets(root, mi, feature_and_detail_bullets[0], ul_selector='ul')\n\n        else:\n            pd = root.xpath(self.pd_xpath)\n            if pd:\n                pd = pd[0]\n\n                try:\n                    isbn = self.parse_isbn(pd)\n                    if isbn:\n                        self.isbn = mi.isbn = isbn\n                except:\n                    self.log.exception(\n                        'Error parsing ISBN for url: %r' % self.url)\n\n                try:\n                    mi.publisher = self.parse_publisher(pd)\n                except:\n                    self.log.exception(\n                        'Error parsing publisher for url: %r' % self.url)\n\n                try:\n                    mi.pubdate = self.parse_pubdate(pd)\n                except:\n                    self.log.exception(\n                        'Error parsing publish date for url: %r' % self.url)\n\n                try:\n                    lang = self.parse_language(pd)\n                    if lang:\n                        mi.language = lang\n                except:\n                    self.log.exception(\n                        'Error parsing language for url: %r' % self.url)\n\n            else:\n                self.log.warning(\n                    'Failed to find product description for url: %r' % self.url)\n\n        mi.source_relevance = self.relevance\n\n        if self.amazon_id:\n            if self.isbn:\n                self.plugin.cache_isbn_to_identifier(self.isbn, self.amazon_id)\n            if self.cover_url:\n                self.plugin.cache_identifier_to_cover_url(self.amazon_id,\n                                                          self.cover_url)\n\n        self.plugin.clean_downloaded_metadata(mi)\n\n        if self.filter_result(mi, self.log):\n            self.result_queue.put(mi)\n\n    def totext(self, elem, only_printable=False):\n        res = self.tostring(elem, encoding='unicode', method='text')\n        if only_printable:\n            try:\n                filtered_characters = [s for s in res if s.isprintable()]\n            except AttributeError:\n                filtered_characters = [s for s in res if s in string.printable]\n            res = ''.join(filtered_characters)\n        return res.strip()\n\n    def parse_title(self, root):\n\n        def sanitize_title(title):\n            ans = title.strip()\n            if not ans.startswith('['):\n                ans = re.sub(r'[(\\[].*[)\\]]', '', title).strip()\n            return ans\n\n        h1 = root.xpath('//h1[@id=\"title\"]')\n        if h1:\n            h1 = h1[0]\n            for child in h1.xpath('./*[contains(@class, \"a-color-secondary\")]'):\n                h1.remove(child)\n            return sanitize_title(self.totext(h1))\n        # audiobooks\n        elem = root.xpath('//*[@id=\"productTitle\"]')\n        if elem:\n            return sanitize_title(self.totext(elem[0]))\n        tdiv = root.xpath('//h1[contains(@class, \"parseasinTitle\")]')\n        if not tdiv:\n            span = root.xpath('//*[@id=\"ebooksTitle\"]')\n            if span:\n                return sanitize_title(self.totext(span[0]))\n            h1 = root.xpath('//h1[@data-feature-name=\"title\"]')\n            if h1:\n                return sanitize_title(self.totext(h1[0]))\n            raise ValueError('No title block found')\n        tdiv = tdiv[0]\n        actual_title = tdiv.xpath('descendant::*[@id=\"btAsinTitle\"]')\n        if actual_title:\n            title = self.tostring(actual_title[0], encoding='unicode',\n                                  method='text').strip()\n        else:\n            title = self.tostring(tdiv, encoding='unicode',\n                                  method='text').strip()\n        return sanitize_title(title)\n\n    def parse_authors(self, root):\n        for sel in (\n                '#byline .author .contributorNameID',\n                '#byline .author a.a-link-normal',\n                '#bylineInfo .author .contributorNameID',\n                '#bylineInfo .author a.a-link-normal',\n                '#bylineInfo #bylineContributor',\n                '#bylineInfo #contributorLink',\n        ):\n            matches = tuple(self.selector(sel))\n            if matches:\n                authors = [self.totext(x) for x in matches]\n                return [a for a in authors if a]\n\n        x = '//h1[contains(@class, \"parseasinTitle\")]/following-sibling::span/*[(name()=\"a\" and @href) or (name()=\"span\" and @class=\"contributorNameTrigger\")]'\n        aname = root.xpath(x)\n        if not aname:\n            aname = root.xpath('''\n            //h1[contains(@class, \"parseasinTitle\")]/following-sibling::*[(name()=\"a\" and @href) or (name()=\"span\" and @class=\"contributorNameTrigger\")]\n                    ''')\n        for x in aname:\n            x.tail = ''\n        authors = [self.tostring(x, encoding='unicode', method='text').strip() for x\n                   in aname]\n        authors = [a for a in authors if a]\n        return authors\n\n    def parse_rating(self, root):\n        for x in root.xpath('//div[@id=\"cpsims-feature\" or @id=\"purchase-sims-feature\" or @id=\"rhf\"]'):\n            # Remove the similar books section as it can cause spurious\n            # ratings matches\n            x.getparent().remove(x)\n\n        rating_paths = (\n            '//div[@data-feature-name=\"averageCustomerReviews\" or @id=\"averageCustomerReviews\"]',\n            '//div[@class=\"jumpBar\"]/descendant::span[contains(@class,\"asinReviewsSummary\")]',\n            '//div[@class=\"buying\"]/descendant::span[contains(@class,\"asinReviewsSummary\")]',\n            '//span[@class=\"crAvgStars\"]/descendant::span[contains(@class,\"asinReviewsSummary\")]'\n        )\n        ratings = None\n        for p in rating_paths:\n            ratings = root.xpath(p)\n            if ratings:\n                break\n\n        def parse_ratings_text(text):\n            try:\n                m = self.ratings_pat.match(text)\n                return float(m.group(1).replace(',', '.')) / float(m.group(3)) * 5\n            except Exception:\n                pass\n\n        if ratings:\n            ratings = ratings[0]\n            for elem in ratings.xpath('descendant::*[@title]'):\n                t = elem.get('title').strip()\n                if self.domain == 'cn':\n                    m = self.ratings_pat_cn.match(t)\n                    if m is not None:\n                        return float(m.group(1))\n                elif self.domain == 'jp':\n                    m = self.ratings_pat_jp.match(t)\n                    if m is not None:\n                        return float(m.group(1))\n                else:\n                    ans = parse_ratings_text(t)\n                    if ans is not None:\n                        return ans\n            for elem in ratings.xpath('descendant::span[@class=\"a-icon-alt\"]'):\n                t = self.tostring(\n                    elem, encoding='unicode', method='text', with_tail=False).strip()\n                ans = parse_ratings_text(t)\n                if ans is not None:\n                    return ans\n        else:\n            # found in kindle book pages on amazon.com\n            for x in root.xpath('//a[@id=\"acrCustomerReviewLink\"]'):\n                spans = x.xpath('./span')\n                if spans:\n                    txt = self.tostring(spans[0], method='text', encoding='unicode', with_tail=False).strip()\n                    try:\n                        return float(txt.replace(',', '.'))\n                    except Exception:\n                        pass\n\n    def _render_comments(self, desc):\n        from calibre.library.comments import sanitize_comments_html\n\n        for c in desc.xpath('descendant::noscript'):\n            c.getparent().remove(c)\n        for c in desc.xpath('descendant::*[@class=\"seeAll\" or'\n                            ' @class=\"emptyClear\" or @id=\"collapsePS\" or'\n                            ' @id=\"expandPS\"]'):\n            c.getparent().remove(c)\n        for b in desc.xpath('descendant::b[@style]'):\n            # Bing highlights search results\n            s = b.get('style', '')\n            if 'color' in s:\n                b.tag = 'span'\n                del b.attrib['style']\n\n        for a in desc.xpath('descendant::a[@href]'):\n            del a.attrib['href']\n            a.tag = 'span'\n        for a in desc.xpath('descendant::span[@class=\"a-text-italic\"]'):\n            a.tag = 'i'\n        for a in desc.xpath('descendant::span[@class=\"a-text-bold\"]'):\n            a.tag = 'b'\n        desc = self.tostring(desc, method='html', encoding='unicode').strip()\n        desc = xml_replace_entities(desc, 'utf-8')\n\n        # Encoding bug in Amazon data U+fffd (replacement char)\n        # in some examples it is present in place of '\n        desc = desc.replace('\\ufffd', \"'\")\n        # remove all attributes from tags\n        desc = re.sub(r'<([a-zA-Z0-9]+)\\s[^>]+>', r'<\\1>', desc)\n        # Collapse whitespace\n        # desc = re.sub(r'\\n+', '\\n', desc)\n        # desc = re.sub(r' +', ' ', desc)\n        # Remove the notice about text referring to out of print editions\n        desc = re.sub(r'(?s)<em>--This text ref.*?</em>', '', desc)\n        # Remove comments\n        desc = re.sub(r'(?s)<!--.*?-->', '', desc)\n        return sanitize_comments_html(desc)\n\n    def parse_comments(self, root, raw):\n        try:\n            from urllib.parse import unquote\n        except ImportError:\n            from urllib import unquote\n        ans = ''\n        ovr = tuple(self.selector('#drengr_MobileTabbedDescriptionOverviewContent_feature_div')) or tuple(\n            self.selector('#drengr_DesktopTabbedDescriptionOverviewContent_feature_div'))\n        if ovr:\n            ovr = ovr[0]\n            ovr.tag = 'div'\n            ans = self._render_comments(ovr)\n            ovr = tuple(self.selector('#drengr_MobileTabbedDescriptionEditorialsContent_feature_div')) or tuple(\n                self.selector('#drengr_DesktopTabbedDescriptionEditorialsContent_feature_div'))\n            if ovr:\n                ovr = ovr[0]\n                ovr.tag = 'div'\n                ans += self._render_comments(ovr)\n        else:\n            ns = tuple(self.selector('#bookDescription_feature_div noscript'))\n            if ns:\n                ns = ns[0]\n                if len(ns) == 0 and ns.text:\n                    import html5lib\n\n                    # html5lib parsed noscript as CDATA\n                    ns = html5lib.parseFragment(\n                        '<div>%s</div>' % (ns.text), treebuilder='lxml', namespaceHTMLElements=False)[0]\n                else:\n                    ns.tag = 'div'\n                ans = self._render_comments(ns)\n            else:\n                desc = root.xpath('//div[@id=\"ps-content\"]/div[@class=\"content\"]')\n                if desc:\n                    ans = self._render_comments(desc[0])\n                else:\n                    ns = tuple(self.selector('#bookDescription_feature_div .a-expander-content'))\n                    if ns:\n                        ans = self._render_comments(ns[0])\n        # audiobooks\n        if not ans:\n            elem = root.xpath('//*[@id=\"audible_desktopTabbedDescriptionOverviewContent_feature_div\"]')\n            if elem:\n                ans = self._render_comments(elem[0])\n        desc = root.xpath(\n            '//div[@id=\"productDescription\"]/*[@class=\"content\"]')\n        if desc:\n            ans += self._render_comments(desc[0])\n        else:\n            # Idiot chickens from amazon strike again. This data is now stored\n            # in a JS variable inside a script tag URL encoded.\n            m = re.search(br'var\\s+iframeContent\\s*=\\s*\"([^\"]+)\"', raw)\n            if m is not None:\n                try:\n                    text = unquote(m.group(1)).decode('utf-8')\n                    nr = parse_html(text)\n                    desc = nr.xpath(\n                        '//div[@id=\"productDescription\"]/*[@class=\"content\"]')\n                    if desc:\n                        ans += self._render_comments(desc[0])\n                except Exception as e:\n                    self.log.warn(\n                        'Parsing of obfuscated product description failed with error: %s' % as_unicode(e))\n            else:\n                desc = root.xpath('//div[@id=\"productDescription_fullView\"]')\n                if desc:\n                    ans += self._render_comments(desc[0])\n\n        return ans\n\n    def parse_series(self, root):\n        ans = (None, None)\n\n        # This is found on kindle pages for books on amazon.com\n        series = root.xpath('//*[@id=\"rpi-attribute-book_details-series\"]')\n        if series:\n            spans = series[0].xpath('descendant::span')\n            if spans:\n                texts = [self.tostring(x, encoding='unicode', method='text', with_tail=False).strip() for x in spans]\n                texts = list(filter(None, texts))\n                if len(texts) == 2:\n                    idxinfo, series = texts\n                    m = re.search(r'[0-9.]+', idxinfo.strip())\n                    if m is not None:\n                        ans = series, float(m.group())\n                        return ans\n\n        # This is found on the paperback/hardback pages for books on amazon.com\n        series = root.xpath('//div[@data-feature-name=\"seriesTitle\"]')\n        if series:\n            series = series[0]\n            spans = series.xpath('./span')\n            if spans:\n                raw = self.tostring(\n                    spans[0], encoding='unicode', method='text', with_tail=False).strip()\n                m = re.search(r'\\s+([0-9.]+)$', raw.strip())\n                if m is not None:\n                    series_index = float(m.group(1))\n                    s = series.xpath('./a[@id=\"series-page-link\"]')\n                    if s:\n                        series = self.tostring(\n                            s[0], encoding='unicode', method='text', with_tail=False).strip()\n                        if series:\n                            ans = (series, series_index)\n        else:\n            series = root.xpath('//div[@id=\"seriesBulletWidget_feature_div\"]')\n            if series:\n                a = series[0].xpath('descendant::a')\n                if a:\n                    raw = self.tostring(a[0], encoding='unicode', method='text', with_tail=False)\n                    if self.domain == 'jp':\n                        m = re.search(r'(?P<index>[0-9.]+)\\s*(?:巻|冊)\\s*\\(全\\s*([0-9.]+)\\s*(?:巻|冊)\\):\\s*(?P<series>.+)', raw.strip())\n                    else:\n                        m = re.search(r'(?:Book|Libro|Buch)\\s+(?P<index>[0-9.]+)\\s+(?:of|de|von)\\s+([0-9.]+)\\s*:\\s*(?P<series>.+)', raw.strip())\n                    if m is not None:\n                        ans = (m.group('series').strip(), float(m.group('index')))\n\n        # This is found on Kindle edition pages on amazon.com\n        if ans == (None, None):\n            for span in root.xpath('//div[@id=\"aboutEbooksSection\"]//li/span'):\n                text = (span.text or '').strip()\n                m = re.match(r'Book\\s+([0-9.]+)', text)\n                if m is not None:\n                    series_index = float(m.group(1))\n                    a = span.xpath('./a[@href]')\n                    if a:\n                        series = self.tostring(\n                            a[0], encoding='unicode', method='text', with_tail=False).strip()\n                        if series:\n                            ans = (series, series_index)\n        # This is found on newer Kindle edition pages on amazon.com\n        if ans == (None, None):\n            for b in root.xpath('//div[@id=\"reviewFeatureGroup\"]/span/b'):\n                text = (b.text or '').strip()\n                m = re.match(r'Book\\s+([0-9.]+)', text)\n                if m is not None:\n                    series_index = float(m.group(1))\n                    a = b.getparent().xpath('./a[@href]')\n                    if a:\n                        series = self.tostring(\n                            a[0], encoding='unicode', method='text', with_tail=False).partition('(')[0].strip()\n                        if series:\n                            ans = series, series_index\n\n        if ans == (None, None):\n            desc = root.xpath('//div[@id=\"ps-content\"]/div[@class=\"buying\"]')\n            if desc:\n                raw = self.tostring(desc[0], method='text', encoding='unicode')\n                raw = re.sub(r'\\s+', ' ', raw)\n                match = self.series_pat.search(raw)\n                if match is not None:\n                    s, i = match.group('series'), float(match.group('index'))\n                    if s:\n                        ans = (s, i)\n        if ans[0]:\n            ans = (re.sub(r'\\s+Series$', '', ans[0]).strip(), ans[1])\n            ans = (re.sub(r'\\(.+?\\s+Series\\)$', '', ans[0]).strip(), ans[1])\n        return ans\n\n    def parse_tags(self, root):\n        ans = []\n        exclude_tokens = {'kindle', 'a-z'}\n        exclude = {'special features', 'by authors',\n                   'authors & illustrators', 'books', 'new; used & rental textbooks'}\n        seen = set()\n        for li in root.xpath(self.tags_xpath):\n            for i, a in enumerate(li.iterdescendants('a')):\n                if i > 0:\n                    # we ignore the first category since it is almost always\n                    # too broad\n                    raw = (a.text or '').strip().replace(',', ';')\n                    lraw = icu_lower(raw)\n                    tokens = frozenset(lraw.split())\n                    if raw and lraw not in exclude and not tokens.intersection(exclude_tokens) and lraw not in seen:\n                        ans.append(raw)\n                        seen.add(lraw)\n        return ans\n\n    def parse_cover(self, root, raw=b''):\n        # Look for the image URL in javascript, using the first image in the\n        # image gallery as the cover\n        import json\n        imgpat = re.compile(r'\"hiRes\":\"(.+?)\",\"thumb\"')\n        for script in root.xpath('//script'):\n            m = imgpat.search(script.text or '')\n            if m is not None:\n                return m.group(1)\n        imgpat = re.compile(r''''imageGalleryData'\\s*:\\s*(\\[\\s*{.+])''')\n        for script in root.xpath('//script'):\n            m = imgpat.search(script.text or '')\n            if m is not None:\n                try:\n                    return json.loads(m.group(1))[0]['mainUrl']\n                except Exception:\n                    continue\n\n        def clean_img_src(src):\n            parts = src.split('/')\n            if len(parts) > 3:\n                bn = parts[-1]\n                sparts = bn.split('_')\n                if len(sparts) > 2:\n                    bn = re.sub(r'\\.\\.jpg$', '.jpg', (sparts[0] + sparts[-1]))\n                    return ('/'.join(parts[:-1])) + '/' + bn\n\n        imgpat2 = re.compile(r'var imageSrc = \"([^\"]+)\"')\n        for script in root.xpath('//script'):\n            m = imgpat2.search(script.text or '')\n            if m is not None:\n                src = m.group(1)\n                url = clean_img_src(src)\n                if url:\n                    return url\n\n        imgs = root.xpath(\n            '//img[(@id=\"prodImage\" or @id=\"original-main-image\" or @id=\"main-image\" or @id=\"main-image-nonjs\") and @src]')\n        if not imgs:\n            imgs = (\n                root.xpath('//div[@class=\"main-image-inner-wrapper\"]/img[@src]') or\n                root.xpath('//div[@id=\"main-image-container\" or @id=\"ebooks-main-image-container\"]//img[@src]') or\n                root.xpath(\n                    '//div[@id=\"mainImageContainer\"]//img[@data-a-dynamic-image]')\n            )\n            for img in imgs:\n                try:\n                    idata = json.loads(img.get('data-a-dynamic-image'))\n                except Exception:\n                    imgs = ()\n                else:\n                    mwidth = 0\n                    try:\n                        url = None\n                        for iurl, (width, height) in idata.items():\n                            if width > mwidth:\n                                mwidth = width\n                                url = iurl\n\n                        return url\n                    except Exception:\n                        pass\n\n        for img in imgs:\n            src = img.get('src')\n            if 'data:' in src:\n                continue\n            if 'loading-' in src:\n                js_img = re.search(br'\"largeImage\":\"(https?://[^\"]+)\",', raw)\n                if js_img:\n                    src = js_img.group(1).decode('utf-8')\n            if ('/no-image-avail' not in src and 'loading-' not in src and '/no-img-sm' not in src):\n                self.log('Found image: %s' % src)\n                url = clean_img_src(src)\n                if url:\n                    return url\n\n    def parse_detail_bullets(self, root, mi, container, ul_selector='.detail-bullet-list'):\n        try:\n            ul = next(self.selector(ul_selector, root=container))\n        except StopIteration:\n            return\n        for span in self.selector('.a-list-item', root=ul):\n            cells = span.xpath('./span')\n            if len(cells) >= 2:\n                self.parse_detail_cells(mi, cells[0], cells[1])\n\n    def parse_new_details(self, root, mi, non_hero):\n        table = non_hero.xpath('descendant::table')[0]\n        for tr in table.xpath('descendant::tr'):\n            cells = tr.xpath('descendant::*[local-name()=\"td\" or local-name()=\"th\"]')\n            if len(cells) == 2:\n                self.parse_detail_cells(mi, cells[0], cells[1])\n\n    def parse_detail_cells(self, mi, c1, c2):\n        name = self.totext(c1, only_printable=True).strip().strip(':').strip()\n        val = self.totext(c2)\n        val = val.replace('\\u200e', '').replace('\\u200f', '')\n        if not val:\n            return\n        if name in self.language_names:\n            ans = self.lang_map.get(val)\n            if not ans:\n                ans = canonicalize_lang(val)\n            if ans:\n                mi.language = ans\n        elif name in self.publisher_names:\n            pub = val.partition(';')[0].partition('(')[0].strip()\n            if pub:\n                mi.publisher = pub\n            date = val.rpartition('(')[-1].replace(')', '').strip()\n            try:\n                from calibre.utils.date import parse_only_date\n                date = self.delocalize_datestr(date)\n                mi.pubdate = parse_only_date(date, assume_utc=True)\n            except:\n                self.log.exception('Failed to parse pubdate: %s' % val)\n        elif name in {'ISBN', 'ISBN-10', 'ISBN-13'}:\n            ans = check_isbn(val)\n            if ans:\n                self.isbn = mi.isbn = ans\n        elif name in {'Publication date'}:\n            from calibre.utils.date import parse_only_date\n            date = self.delocalize_datestr(val)\n            mi.pubdate = parse_only_date(date, assume_utc=True)\n\n    def parse_isbn(self, pd):\n        items = pd.xpath(\n            'descendant::*[starts-with(text(), \"ISBN\")]')\n        if not items:\n            items = pd.xpath(\n                'descendant::b[contains(text(), \"ISBN:\")]')\n        for x in reversed(items):\n            if x.tail:\n                ans = check_isbn(x.tail.strip())\n                if ans:\n                    return ans\n\n    def parse_publisher(self, pd):\n        for x in reversed(pd.xpath(self.publisher_xpath)):\n            if x.tail:\n                ans = x.tail.partition(';')[0]\n                return ans.partition('(')[0].strip()\n\n    def parse_pubdate(self, pd):\n        from calibre.utils.date import parse_only_date\n        for x in reversed(pd.xpath(self.pubdate_xpath)):\n            if x.tail:\n                date = x.tail.strip()\n                date = self.delocalize_datestr(date)\n                try:\n                    return parse_only_date(date, assume_utc=True)\n                except Exception:\n                    pass\n        for x in reversed(pd.xpath(self.publisher_xpath)):\n            if x.tail:\n                ans = x.tail\n                date = ans.rpartition('(')[-1].replace(')', '').strip()\n                date = self.delocalize_datestr(date)\n                try:\n                    return parse_only_date(date, assume_utc=True)\n                except Exception:\n                    pass\n\n    def parse_language(self, pd):\n        for x in reversed(pd.xpath(self.language_xpath)):\n            if x.tail:\n                raw = x.tail.strip().partition(',')[0].strip()\n                ans = self.lang_map.get(raw, None)\n                if ans:\n                    return ans\n                ans = canonicalize_lang(ans)\n                if ans:\n                    return ans\n# }}}\n\n\nclass Amazon(Source):\n\n    name = 'Amazon.com'\n    version = (1, 3, 13)\n    minimum_calibre_version = (2, 82, 0)\n    description = _('Downloads metadata and covers from Amazon')\n\n    capabilities = frozenset(('identify', 'cover'))\n    touched_fields = frozenset(('title', 'authors', 'identifier:amazon',\n        'rating', 'comments', 'publisher', 'pubdate',\n        'languages', 'series', 'tags'))\n    has_html_comments = True\n    supports_gzip_transfer_encoding = True\n    prefer_results_with_isbn = False\n\n    AMAZON_DOMAINS = {\n        'com': _('US'),\n        'fr': _('France'),\n        'de': _('Germany'),\n        'uk': _('UK'),\n        'au': _('Australia'),\n        'it': _('Italy'),\n        'jp': _('Japan'),\n        'es': _('Spain'),\n        'br': _('Brazil'),\n        'in': _('India'),\n        'nl': _('Netherlands'),\n        'cn': _('China'),\n        'ca': _('Canada'),\n        'se': _('Sweden'),\n    }\n\n    SERVERS = {\n        'auto': _('Choose server automatically'),\n        'amazon': _('Amazon servers'),\n        'bing': _('Bing search cache'),\n        'google': _('Google search cache'),\n        'wayback': _('Wayback machine cache (slow)'),\n        'ddg': _('DuckDuckGo search and Google cache'),\n    }\n\n    options = (\n        Option('domain', 'choices', 'com', _('Amazon country website to use:'),\n               _('Metadata from Amazon will be fetched using this '\n                 \"country's Amazon website.\"), choices=AMAZON_DOMAINS),\n        Option('server', 'choices', 'auto', _('Server to get data from:'),\n               _(\n                   'Amazon has started blocking attempts to download'\n                   ' metadata from its servers. To get around this problem,'\n                   ' calibre can fetch the Amazon data from many different'\n                   ' places where it is cached. Choose the source you prefer.'\n               ), choices=SERVERS),\n        Option('use_mobi_asin', 'bool', False, _('Use the MOBI-ASIN for metadata search'),\n               _(\n                   'Enable this option to search for metadata with an'\n                   ' ASIN identifier from the MOBI file at the current country website,'\n                   ' unless any other amazon id is available. Note that if the'\n                   ' MOBI file came from a different Amazon country store, you could get'\n                   ' incorrect results.'\n               )),\n        Option('prefer_kindle_edition', 'bool', False, _('Prefer the Kindle edition, when available'),\n               _(\n                   'When searching for a book and the search engine returns both paper and Kindle editions,'\n                   ' always prefer the Kindle edition, instead of whatever the search engine returns at the'\n                   ' top.')\n        ),\n    )\n\n    def __init__(self, *args, **kwargs):\n        Source.__init__(self, *args, **kwargs)\n        self.set_amazon_id_touched_fields()\n\n    def id_from_url(self, url):\n        from polyglot.urllib import urlparse\n        purl = urlparse(url)\n        if purl.netloc and purl.path and '/dp/' in purl.path:\n            host_parts = tuple(x.lower() for x in purl.netloc.split('.'))\n            if 'amazon' in host_parts:\n                domain = host_parts[-1]\n            parts = purl.path.split('/')\n            idx = parts.index('dp')\n            try:\n                val = parts[idx+1]\n            except IndexError:\n                return\n            aid = 'amazon' if domain == 'com' else ('amazon_' + domain)\n            return aid, val\n\n    def test_fields(self, mi):\n        '''\n        Return the first field from self.touched_fields that is null on the\n        mi object\n        '''\n        for key in self.touched_fields:\n            if key.startswith('identifier:'):\n                key = key.partition(':')[-1]\n                if key == 'amazon':\n                    if self.domain != 'com':\n                        key += '_' + self.domain\n                if not mi.has_identifier(key):\n                    return 'identifier: ' + key\n            elif mi.is_null(key):\n                return key\n\n    @property\n    def browser(self):\n        br = self._browser\n        if br is None:\n            ua = 'Mobile '\n            while not user_agent_is_ok(ua):\n                ua = random_user_agent(allow_ie=False)\n            # ua = 'Mozilla/5.0 (Linux; Android 8.0.0; VTR-L29; rv:63.0) Gecko/20100101 Firefox/63.0'\n            self._browser = br = browser(user_agent=ua)\n            br.set_handle_gzip(True)\n            if self.use_search_engine:\n                br.addheaders += [\n                    ('Accept', accept_header_for_ua(ua)),\n                    ('Upgrade-insecure-requests', '1'),\n                ]\n            else:\n                br.addheaders += [\n                    ('Accept', accept_header_for_ua(ua)),\n                    ('Upgrade-insecure-requests', '1'),\n                    ('Referer', self.referrer_for_domain()),\n                ]\n        return br\n\n    def save_settings(self, *args, **kwargs):\n        Source.save_settings(self, *args, **kwargs)\n        self.set_amazon_id_touched_fields()\n\n    def set_amazon_id_touched_fields(self):\n        ident_name = 'identifier:amazon'\n        if self.domain != 'com':\n            ident_name += '_' + self.domain\n        tf = [x for x in self.touched_fields if not\n              x.startswith('identifier:amazon')] + [ident_name]\n        self.touched_fields = frozenset(tf)\n\n    def get_domain_and_asin(self, identifiers, extra_domains=()):\n        identifiers = {k.lower(): v for k, v in identifiers.items()}\n        for key, val in identifiers.items():\n            if key in ('amazon', 'asin'):\n                return 'com', val\n            if key.startswith('amazon_'):\n                domain = key.partition('_')[-1]\n                if domain and (domain in self.AMAZON_DOMAINS or domain in extra_domains):\n                    return domain, val\n        if self.prefs['use_mobi_asin']:\n            val = identifiers.get('mobi-asin')\n            if val is not None:\n                return self.domain, val\n        return None, None\n\n    def referrer_for_domain(self, domain=None):\n        domain = domain or self.domain\n        return {\n            'uk':  'https://www.amazon.co.uk/',\n            'au':  'https://www.amazon.com.au/',\n            'br':  'https://www.amazon.com.br/',\n            'jp':  'https://www.amazon.co.jp/',\n            'mx':  'https://www.amazon.com.mx/',\n        }.get(domain, 'https://www.amazon.%s/' % domain)\n\n    def _get_book_url(self, identifiers):  # {{{\n        domain, asin = self.get_domain_and_asin(\n            identifiers, extra_domains=('au', 'ca'))\n        if domain and asin:\n            url = None\n            r = self.referrer_for_domain(domain)\n            if r is not None:\n                url = r + 'dp/' + asin\n            if url:\n                idtype = 'amazon' if domain == 'com' else 'amazon_' + domain\n                return domain, idtype, asin, url\n\n    def get_book_url(self, identifiers):\n        ans = self._get_book_url(identifiers)\n        if ans is not None:\n            return ans[1:]\n\n    def get_book_url_name(self, idtype, idval, url):\n        if idtype == 'amazon':\n            return self.name\n        return 'A' + idtype.replace('_', '.')[1:]\n    # }}}\n\n    @property\n    def domain(self):\n        x = getattr(self, 'testing_domain', None)\n        if x is not None:\n            return x\n        domain = self.prefs['domain']\n        if domain not in self.AMAZON_DOMAINS:\n            domain = 'com'\n\n        return domain\n\n    @property\n    def server(self):\n        x = getattr(self, 'testing_server', None)\n        if x is not None:\n            return x\n        server = self.prefs['server']\n        if server not in self.SERVERS:\n            server = 'auto'\n        return server\n\n    @property\n    def use_search_engine(self):\n        return self.server != 'amazon'\n\n    def clean_downloaded_metadata(self, mi):\n        docase = (\n            mi.language == 'eng' or\n            (mi.is_null('language') and self.domain in {'com', 'uk', 'au'})\n        )\n        if mi.title and docase:\n            # Remove series information from title\n            m = re.search(r'\\S+\\s+(\\(.+?\\s+Book\\s+\\d+\\))$', mi.title)\n            if m is not None:\n                mi.title = mi.title.replace(m.group(1), '').strip()\n            mi.title = fixcase(mi.title)\n        mi.authors = fixauthors(mi.authors)\n        if mi.tags and docase:\n            mi.tags = list(map(fixcase, mi.tags))\n        mi.isbn = check_isbn(mi.isbn)\n        if mi.series and docase:\n            mi.series = fixcase(mi.series)\n        if mi.title and mi.series:\n            for pat in (r':\\s*Book\\s+\\d+\\s+of\\s+%s$', r'\\(%s\\)$', r':\\s*%s\\s+Book\\s+\\d+$'):\n                pat = pat % re.escape(mi.series)\n                q = re.sub(pat, '', mi.title, flags=re.I).strip()\n                if q and q != mi.title:\n                    mi.title = q\n                    break\n\n    def get_website_domain(self, domain):\n        return {'uk': 'co.uk', 'jp': 'co.jp', 'br': 'com.br', 'au': 'com.au'}.get(domain, domain)\n\n    def create_query(self, log, title=None, authors=None, identifiers={},  # {{{\n                     domain=None, for_amazon=True):\n        try:\n            from urllib.parse import unquote_plus, urlencode\n        except ImportError:\n            from urllib import unquote_plus, urlencode\n        if domain is None:\n            domain = self.domain\n\n        idomain, asin = self.get_domain_and_asin(identifiers)\n        if idomain is not None:\n            domain = idomain\n\n        # See the amazon detailed search page to get all options\n        terms = []\n        q = {'search-alias': 'aps',\n             'unfiltered': '1',\n        }\n\n        if domain == 'com':\n            q['sort'] = 'relevanceexprank'\n        else:\n            q['sort'] = 'relevancerank'\n\n        isbn = check_isbn(identifiers.get('isbn', None))\n\n        if asin is not None:\n            q['field-keywords'] = asin\n            terms.append(asin)\n        elif isbn is not None:\n            q['field-isbn'] = isbn\n            if len(isbn) == 13:\n                terms.extend('({} OR {}-{})'.format(isbn, isbn[:3], isbn[3:]).split())\n            else:\n                terms.append(isbn)\n        else:\n            # Only return book results\n            q['search-alias'] = {'br': 'digital-text',\n                                 'nl': 'aps'}.get(domain, 'stripbooks')\n            if title:\n                title_tokens = list(self.get_title_tokens(title))\n                if title_tokens:\n                    q['field-title'] = ' '.join(title_tokens)\n                    terms.extend(title_tokens)\n            if authors:\n                author_tokens = list(self.get_author_tokens(authors,\n                                                       only_first_author=True))\n                if author_tokens:\n                    q['field-author'] = ' '.join(author_tokens)\n                    terms.extend(author_tokens)\n\n        if not ('field-keywords' in q or 'field-isbn' in q or\n                ('field-title' in q)):\n            # Insufficient metadata to make an identify query\n            log.error('Insufficient metadata to construct query, none of title, ISBN or ASIN supplied')\n            raise SearchFailed()\n\n        if not for_amazon:\n            return terms, domain\n\n        if domain == 'nl':\n            q['__mk_nl_NL'] = 'ÅMÅŽÕÑ'\n            if 'field-keywords' not in q:\n                q['field-keywords'] = ''\n            for f in 'field-isbn field-title field-author'.split():\n                q['field-keywords'] += ' ' + q.pop(f, '')\n            q['field-keywords'] = q['field-keywords'].strip()\n\n        encoded_q = {x.encode('utf-8', 'ignore'): y.encode('utf-8', 'ignore') for x, y in q.items()}\n        url_query = urlencode(encoded_q)\n        # amazon's servers want IRIs with unicode characters not percent esaped\n        parts = []\n        for x in url_query.split(b'&' if isinstance(url_query, bytes) else '&'):\n            k, v = x.split(b'=' if isinstance(x, bytes) else '=', 1)\n            parts.append('{}={}'.format(iri_quote_plus(unquote_plus(k)), iri_quote_plus(unquote_plus(v))))\n        url_query = '&'.join(parts)\n        url = 'https://www.amazon.%s/s/?' % self.get_website_domain(\n            domain) + url_query\n        return url, domain\n\n    # }}}\n\n    def get_cached_cover_url(self, identifiers):  # {{{\n        url = None\n        domain, asin = self.get_domain_and_asin(identifiers)\n        if asin is None:\n            isbn = identifiers.get('isbn', None)\n            if isbn is not None:\n                asin = self.cached_isbn_to_identifier(isbn)\n        if asin is not None:\n            url = self.cached_identifier_to_cover_url(asin)\n\n        return url\n    # }}}\n\n    def parse_results_page(self, root, domain):  # {{{\n        from lxml.html import tostring\n\n        matches = []\n\n        def title_ok(title):\n            title = title.lower()\n            bad = ['bulk pack', '[audiobook]', '[audio cd]',\n                   '(a book companion)', '( slipcase with door )', ': free sampler']\n            if self.domain == 'com':\n                bad.extend(['(%s edition)' % x for x in ('spanish', 'german')])\n            for x in bad:\n                if x in title:\n                    return False\n            if title and title[0] in '[{' and re.search(r'\\(\\s*author\\s*\\)', title) is not None:\n                # Bad entries in the catalog\n                return False\n            return True\n\n        for query in (\n            '//div[contains(@class, \"s-result-list\")]//h2/a[@href]',\n            '//div[contains(@class, \"s-result-list\")]//div[@data-index]//h5//a[@href]',\n            r'//li[starts-with(@id, \"result_\")]//a[@href and contains(@class, \"s-access-detail-page\")]',\n            '//div[@data-cy=\"title-recipe\"]/a[@href]',\n        ):\n            result_links = root.xpath(query)\n            if result_links:\n                break\n        for a in result_links:\n            title = tostring(a, method='text', encoding='unicode')\n            if title_ok(title):\n                url = a.get('href')\n                if url.startswith('/'):\n                    url = 'https://www.amazon.%s%s' % (\n                        self.get_website_domain(domain), url)\n                matches.append(url)\n\n        if not matches:\n            # Previous generation of results page markup\n            for div in root.xpath(r'//div[starts-with(@id, \"result_\")]'):\n                links = div.xpath(r'descendant::a[@class=\"title\" and @href]')\n                if not links:\n                    # New amazon markup\n                    links = div.xpath('descendant::h3/a[@href]')\n                for a in links:\n                    title = tostring(a, method='text', encoding='unicode')\n                    if title_ok(title):\n                        url = a.get('href')\n                        if url.startswith('/'):\n                            url = 'https://www.amazon.%s%s' % (\n                                self.get_website_domain(domain), url)\n                        matches.append(url)\n                    break\n\n        if not matches:\n            # This can happen for some user agents that Amazon thinks are\n            # mobile/less capable\n            for td in root.xpath(\n                    r'//div[@id=\"Results\"]/descendant::td[starts-with(@id, \"search:Td:\")]'):\n                for a in td.xpath(r'descendant::td[@class=\"dataColumn\"]/descendant::a[@href]/span[@class=\"srTitle\"]/..'):\n                    title = tostring(a, method='text', encoding='unicode')\n                    if title_ok(title):\n                        url = a.get('href')\n                        if url.startswith('/'):\n                            url = 'https://www.amazon.%s%s' % (\n                                self.get_website_domain(domain), url)\n                        matches.append(url)\n                    break\n        if not matches and root.xpath('//form[@action=\"/errors/validateCaptcha\"]'):\n            raise CaptchaError('Amazon returned a CAPTCHA page. Recently Amazon has begun using statistical'\n                               ' profiling to block access to its website. As such this metadata plugin is'\n                               ' unlikely to ever work reliably.')\n\n        # Keep only the top 3 matches as the matches are sorted by relevance by\n        # Amazon so lower matches are not likely to be very relevant\n        return matches[:3]\n    # }}}\n\n    def search_amazon(self, br, testing, log, abort, title, authors, identifiers, timeout):  # {{{\n        from calibre.ebooks.chardet import xml_to_unicode\n        from calibre.utils.cleantext import clean_ascii_chars\n        matches = []\n        query, domain = self.create_query(log, title=title, authors=authors,\n                                          identifiers=identifiers)\n        time.sleep(1)\n        try:\n            raw = br.open_novisit(query, timeout=timeout).read().strip()\n        except Exception as e:\n            if callable(getattr(e, 'getcode', None)) and \\\n                    e.getcode() == 404:\n                log.error('Query malformed: %r' % query)\n                raise SearchFailed()\n            attr = getattr(e, 'args', [None])\n            attr = attr if attr else [None]\n            if isinstance(attr[0], socket.timeout):\n                msg = _('Amazon timed out. Try again later.')\n                log.error(msg)\n            else:\n                msg = 'Failed to make identify query: %r' % query\n                log.exception(msg)\n            raise SearchFailed()\n\n        raw = clean_ascii_chars(xml_to_unicode(raw,\n                                               strip_encoding_pats=True, resolve_entities=True)[0])\n\n        if testing:\n            import tempfile\n            with tempfile.NamedTemporaryFile(prefix='amazon_results_',\n                                             suffix='.html', delete=False) as f:\n                f.write(raw.encode('utf-8'))\n            print('Downloaded html for results page saved in', f.name)\n\n        matches = []\n        found = '<title>404 - ' not in raw\n\n        if found:\n            try:\n                root = parse_html(raw)\n            except Exception:\n                msg = 'Failed to parse amazon page for query: %r' % query\n                log.exception(msg)\n                raise SearchFailed()\n\n        matches = self.parse_results_page(root, domain)\n\n        return matches, query, domain, None\n    # }}}\n\n    def search_search_engine(self, br, testing, log, abort, title, authors, identifiers, timeout, override_server=None):  # {{{\n        from calibre.ebooks.metadata.sources.update import search_engines_module\n        se = search_engines_module()\n        terms, domain = self.create_query(log, title=title, authors=authors,\n                                          identifiers=identifiers, for_amazon=False)\n        site = self.referrer_for_domain(\n            domain)[len('https://'):].partition('/')[0]\n        matches = []\n        server = override_server or self.server\n        if server == 'bing':\n            urlproc, sfunc = se.bing_url_processor, se.bing_search\n        elif server == 'wayback':\n            urlproc, sfunc = se.wayback_url_processor, se.ddg_search\n        elif server == 'ddg':\n            urlproc, sfunc = se.ddg_url_processor, se.ddg_search\n        elif server == 'google':\n            urlproc, sfunc = se.google_url_processor, se.google_search\n        else:  # auto or unknown\n            urlproc, sfunc = se.google_url_processor, se.google_search\n            # urlproc, sfunc = se.bing_url_processor, se.bing_search\n        try:\n            results, qurl = sfunc(terms, site, log=log, br=br, timeout=timeout)\n        except HTTPError as err:\n            if err.code == 429 and sfunc is se.google_search:\n                log('Got too many requests error from Google, trying via DuckDuckGo')\n                urlproc, sfunc = se.ddg_url_processor, se.ddg_search\n                results, qurl = sfunc(terms, site, log=log, br=br, timeout=timeout)\n            else:\n                raise\n\n        br.set_current_header('Referer', qurl)\n        for result in results:\n            if abort.is_set():\n                return matches, terms, domain, None\n\n            purl = urlparse(result.url)\n            if '/dp/' in purl.path and site in purl.netloc:\n                # We cannot use cached URL as wayback machine no longer caches\n                # amazon and Google and Bing web caches are no longer\n                # accessible.\n                url = result.url\n                if url not in matches:\n                    matches.append(url)\n                if len(matches) >= 3:\n                    break\n            else:\n                log('Skipping non-book result:', result)\n        if not matches:\n            log('No search engine results for terms:', ' '.join(terms))\n            if urlproc is se.google_url_processor:\n                # Google does not cache adult titles\n                log('Trying the bing search engine instead')\n                return self.search_search_engine(br, testing, log, abort, title, authors, identifiers, timeout, 'bing')\n        return matches, terms, domain, urlproc\n    # }}}\n\n    def identify(self, log, result_queue, abort, title=None, authors=None,  # {{{\n                 identifiers={}, timeout=60):\n        '''\n        Note this method will retry without identifiers automatically if no\n        match is found with identifiers.\n        '''\n\n        testing = getattr(self, 'running_a_test', False)\n\n        udata = self._get_book_url(identifiers)\n        br = self.browser\n        log('User-agent:', br.current_user_agent())\n        log('Server:', self.server)\n        if testing:\n            print('User-agent:', br.current_user_agent())\n        if udata is not None and not self.use_search_engine:\n            # Try to directly get details page instead of running a search\n            # Cannot use search engine as the directly constructed URL is\n            # usually redirected to a full URL by amazon, and is therefore\n            # not cached\n            domain, idtype, asin, durl = udata\n            if durl is not None:\n                preparsed_root = parse_details_page(\n                    durl, log, timeout, br, domain)\n                if preparsed_root is not None:\n                    qasin = parse_asin(preparsed_root[1], log, durl)\n                    if qasin == asin:\n                        w = Worker(durl, result_queue, br, log, 0, domain,\n                                   self, testing=testing, preparsed_root=preparsed_root, timeout=timeout)\n                        try:\n                            w.get_details()\n                            return\n                        except Exception:\n                            log.exception(\n                                'get_details failed for url: %r' % durl)\n        func = self.search_search_engine if self.use_search_engine else self.search_amazon\n        try:\n            matches, query, domain, cover_url_processor = func(\n                br, testing, log, abort, title, authors, identifiers, timeout)\n        except SearchFailed:\n            return\n\n        if abort.is_set():\n            return\n\n        if not matches:\n            if identifiers and title and authors:\n                log('No matches found with identifiers, retrying using only'\n                    ' title and authors. Query: %r' % query)\n                time.sleep(1)\n                return self.identify(log, result_queue, abort, title=title,\n                                     authors=authors, timeout=timeout)\n            log.error('No matches found with query: %r' % query)\n            return\n\n        if self.prefs['prefer_kindle_edition']:\n            matches = sort_matches_preferring_kindle_editions(matches)\n\n        workers = [Worker(\n            url, result_queue, br, log, i, domain, self, testing=testing, timeout=timeout,\n            cover_url_processor=cover_url_processor, filter_result=partial(\n                self.filter_result, title, authors, identifiers)) for i, url in enumerate(matches)]\n\n        for w in workers:\n            # Don't send all requests at the same time\n            time.sleep(1)\n            w.start()\n            if abort.is_set():\n                return\n\n        while not abort.is_set():\n            a_worker_is_alive = False\n            for w in workers:\n                w.join(0.2)\n                if abort.is_set():\n                    break\n                if w.is_alive():\n                    a_worker_is_alive = True\n            if not a_worker_is_alive:\n                break\n\n        return None\n    # }}}\n\n    def filter_result(self, title, authors, identifiers, mi, log):  # {{{\n        if not self.use_search_engine:\n            return True\n        if title is not None:\n            import regex\n            only_punctuation_pat = regex.compile(r'^\\p{P}+$')\n\n            def tokenize_title(x):\n                ans = icu_lower(x).replace(\"'\", '').replace('\"', '').rstrip(':')\n                if only_punctuation_pat.match(ans) is not None:\n                    ans = ''\n                return ans\n\n            tokens = {tokenize_title(x) for x in title.split() if len(x) > 3}\n            tokens.discard('')\n            if tokens:\n                result_tokens = {tokenize_title(x) for x in mi.title.split()}\n                result_tokens.discard('')\n                if not tokens.intersection(result_tokens):\n                    log('Ignoring result:', mi.title, 'as its title does not match')\n                    return False\n        if authors:\n            author_tokens = set()\n            for author in authors:\n                author_tokens |= {icu_lower(x) for x in author.split() if len(x) > 2}\n            result_tokens = set()\n            for author in mi.authors:\n                result_tokens |= {icu_lower(x) for x in author.split() if len(x) > 2}\n            if author_tokens and not author_tokens.intersection(result_tokens):\n                log('Ignoring result:', mi.title, 'by', ' & '.join(mi.authors), 'as its author does not match')\n                return False\n        return True\n    # }}}\n\n    def download_cover(self, log, result_queue, abort,  # {{{\n                       title=None, authors=None, identifiers={}, timeout=60, get_best_cover=False):\n        cached_url = self.get_cached_cover_url(identifiers)\n        if cached_url is None:\n            log.info('No cached cover found, running identify')\n            rq = Queue()\n            self.identify(log, rq, abort, title=title, authors=authors,\n                          identifiers=identifiers)\n            if abort.is_set():\n                return\n            results = []\n            while True:\n                try:\n                    results.append(rq.get_nowait())\n                except Empty:\n                    break\n            results.sort(key=self.identify_results_keygen(\n                title=title, authors=authors, identifiers=identifiers))\n            for mi in results:\n                cached_url = self.get_cached_cover_url(mi.identifiers)\n                if cached_url is not None:\n                    break\n        if cached_url is None:\n            log.info('No cover found')\n            return\n\n        if abort.is_set():\n            return\n        log('Downloading cover from:', cached_url)\n        br = self.browser\n        if self.use_search_engine:\n            br = br.clone_browser()\n            br.set_current_header('Referer', self.referrer_for_domain(self.domain))\n        try:\n            time.sleep(1)\n            cdata = br.open_novisit(\n                cached_url, timeout=timeout).read()\n            result_queue.put((self, cdata))\n        except:\n            log.exception('Failed to download cover from:', cached_url)\n    # }}}\n\n\ndef manual_tests(domain, **kw):  # {{{\n    # To run these test use:\n    # calibre-debug -c \"from calibre.ebooks.metadata.sources.amazon import *; manual_tests('com')\"\n    from calibre.ebooks.metadata.sources.test import authors_test, comments_test, isbn_test, series_test, test_identify_plugin, title_test\n    all_tests = {}\n    all_tests['com'] = [  # {{{\n        (  # in title\n            {'title': 'Expert C# 2008 Business Objects',\n             'authors': ['Lhotka']},\n            [title_test('Expert C#'),\n             authors_test(['Rockford Lhotka'])\n             ]\n        ),\n\n        (   # Paperback with series\n            {'identifiers': {'amazon': '1423146786'}},\n            [title_test('Heroes of Olympus', exact=False), series_test('The Heroes of Olympus', 5)]\n        ),\n\n        (   # Kindle edition with series\n            {'identifiers': {'amazon': 'B0085UEQDO'}},\n            [title_test('Three Parts Dead', exact=True),\n             series_test('Craft Sequence', 1)]\n        ),\n\n        (  # + in title and uses id=\"main-image\" for cover\n            {'identifiers': {'amazon': '1933988770'}},\n            [title_test(\n                'C++ Concurrency in Action: Practical Multithreading', exact=True)]\n        ),\n\n\n        (  # Different comments markup, using Book Description section\n            {'identifiers': {'amazon': '0982514506'}},\n            [title_test(\n                \"Griffin's Destiny\",\n                exact=True),\n             comments_test('Jelena'), comments_test('Ashinji'),\n             ]\n        ),\n\n        (   # New search results page markup (Dec 2024)\n            {'title': 'Come si scrive un articolo medico-scientifico'},\n            [title_test('Come si scrive un articolo medico-scientifico', exact=True)]\n        ),\n\n        (  # No specific problems\n            {'identifiers': {'isbn': '0743273567'}},\n            [title_test('the great gatsby'),\n             authors_test(['f. Scott Fitzgerald'])]\n        ),\n\n    ]\n\n    # }}}\n\n    all_tests['de'] = [  # {{{\n        # series\n        (\n            {'identifiers': {'isbn': '3499275120'}},\n            [title_test('Vespasian: Das Schwert des Tribuns: Historischer Roman',\n                        exact=False), authors_test(['Robert Fabbri']), series_test('Die Vespasian-Reihe', 1)\n             ]\n\n        ),\n\n        (  # umlaut in title/authors\n            {'title': 'Flüsternde Wälder',\n             'authors': ['Nicola Förg']},\n            [title_test('Flüsternde Wälder'),\n             authors_test(['Nicola Förg'], subset=True)\n             ]\n        ),\n\n        (\n            {'identifiers': {'isbn': '9783453314979'}},\n            [title_test('Die letzten Wächter: Roman',\n                        exact=False), authors_test(['Sergej Lukianenko'])\n             ]\n\n        ),\n\n        (\n            {'identifiers': {'isbn': '3548283519'}},\n            [title_test('Wer Wind Sät: Der Fünfte Fall Für Bodenstein Und Kirchhoff',\n                        exact=False), authors_test(['Nele Neuhaus'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['it'] = [  # {{{\n        (\n            {'identifiers': {'isbn': '8838922195'}},\n            [title_test('La briscola in cinque',\n                        exact=True), authors_test(['Marco Malvaldi'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['fr'] = [  # {{{\n        (\n            {'identifiers': {'amazon_fr': 'B07L7ST4RS'}},\n            [title_test('Le secret de Lola', exact=True),\n                authors_test(['Amélie BRIZIO'])\n            ]\n        ),\n        (\n            {'identifiers': {'isbn': '2221116798'}},\n            [title_test(\"L'étrange voyage de Monsieur Daldry\",\n                        exact=True), authors_test(['Marc Levy'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['es'] = [  # {{{\n        (\n            {'identifiers': {'isbn': '8483460831'}},\n            [title_test('Tiempos Interesantes',\n                        exact=False), authors_test(['Terry Pratchett'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['se'] = [  # {{{\n        (\n            {'identifiers': {'isbn': '9780552140287'}},\n            [title_test('Men At Arms: A Discworld Novel: 14',\n                        exact=False), authors_test(['Terry Pratchett'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['jp'] = [  # {{{\n        (  # Adult filtering test\n            {'identifiers': {'isbn': '4799500066'}},\n            [title_test('Bitch Trap'), ]\n        ),\n\n        (  # isbn -> title, authors\n            {'identifiers': {'isbn': '9784101302720'}},\n            [title_test('精霊の守り人',\n                        exact=True), authors_test(['上橋 菜穂子'])\n             ]\n        ),\n        (  # title, authors -> isbn (will use Shift_JIS encoding in query.)\n            {'title': '考えない練習',\n             'authors': ['小池 龍之介']},\n            [isbn_test('9784093881067'), ]\n        ),\n    ]  # }}}\n\n    all_tests['br'] = [  # {{{\n        (\n            {'title': 'A Ascensão da Sombra'},\n            [title_test('A Ascensão da Sombra'), authors_test(['Robert Jordan'])]\n        ),\n\n        (\n            {'title': 'Guerra dos Tronos'},\n            [title_test('A Guerra dos Tronos. As Crônicas de Gelo e Fogo - Livro 1'), authors_test(['George R. R. Martin'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['nl'] = [  # {{{\n        (\n            {'title': 'Freakonomics'},\n            [title_test('Freakonomics',\n                        exact=True), authors_test(['Steven Levitt & Stephen Dubner & R. Kuitenbrouwer & O. Brenninkmeijer & A. van Den Berg'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['cn'] = [  # {{{\n        (\n            {'identifiers': {'isbn': '9787115369512'}},\n            [title_test('若为自由故 自由软件之父理查德斯托曼传', exact=True),\n             authors_test(['[美]sam Williams', '邓楠,李凡希'])]\n        ),\n        (\n            {'title': '爱上Raspberry Pi'},\n            [title_test('爱上Raspberry Pi',\n                        exact=True), authors_test(['Matt Richardson', 'Shawn Wallace', '李凡希'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['ca'] = [  # {{{\n        (   # Paperback with series\n            {'identifiers': {'isbn': '9781623808747'}},\n            [title_test('Parting Shot', exact=True),\n             authors_test(['Mary Calmes'])]\n        ),\n        (  # in title\n            {'title': 'Expert C# 2008 Business Objects',\n             'authors': ['Lhotka']},\n            [title_test('Expert C# 2008 Business Objects'),\n             authors_test(['Rockford Lhotka'])]\n        ),\n        (  # noscript description\n            {'identifiers': {'amazon_ca': '162380874X'}},\n            [title_test('Parting Shot', exact=True), authors_test(['Mary Calmes'])\n             ]\n        ),\n    ]  # }}}\n\n    all_tests['in'] = [  # {{{\n        (   # Paperback with series\n            {'identifiers': {'amazon_in': '1423146786'}},\n            [title_test('The Heroes of Olympus, Book Five The Blood of Olympus', exact=True)]\n        ),\n    ]  # }}}\n\n    def do_test(domain, start=0, stop=None, server='auto'):\n        tests = all_tests[domain]\n        if stop is None:\n            stop = len(tests)\n        tests = tests[start:stop]\n        test_identify_plugin(Amazon.name, tests, modify_plugin=lambda p: (\n            setattr(p, 'testing_domain', domain),\n            setattr(p, 'touched_fields', p.touched_fields - {'tags'}),\n            setattr(p, 'testing_server', server),\n        ))\n\n    do_test(domain, **kw)\n# }}}\n", +  "amazon": "#!/usr/bin/env python\n# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai\n# License: GPLv3 Copyright: 2011, Kovid Goyal <kovid at kovidgoyal.net>\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport re\nimport socket\nimport string\nimport time\nfrom functools import partial\n\ntry:\n    from queue import Empty, Queue\nexcept ImportError:\n    from Queue import Empty, Queue\n\nfrom threading import Thread\n\ntry:\n    from urllib.parse import urlparse\nexcept ImportError:\n    from urlparse import urlparse\n\nfrom mechanize import HTTPError\n\nfrom calibre import as_unicode, browser, random_user_agent, xml_replace_entities\nfrom calibre.ebooks.metadata import check_isbn\nfrom calibre.ebooks.metadata.book.base import Metadata\nfrom calibre.ebooks.metadata.sources.base import Option, Source, fixauthors, fixcase\nfrom calibre.utils.icu import lower as icu_lower\nfrom calibre.utils.localization import canonicalize_lang\nfrom calibre.utils.random_ua import accept_header_for_ua\n\n\ndef sort_matches_preferring_kindle_editions(matches):\n    upos_map = {url:i for i, url in enumerate(matches)}\n\n    def skey(url):\n        opos = upos_map[url]\n        parts = url.split('/')\n        try:\n            idx = parts.index('dp')\n        except Exception:\n            idx = -1\n        if idx < 0 or idx + 1 >= len(parts) or not parts[idx+1].startswith('B'):\n            return 1, opos\n        return 0, opos\n    matches.sort(key=skey)\n    return matches\n\n\ndef iri_quote_plus(url):\n    from calibre.ebooks.oeb.base import urlquote\n    ans = urlquote(url)\n    if isinstance(ans, bytes):\n        ans = ans.decode('utf-8')\n    return ans.replace('%20', '+')\n\n\ndef user_agent_is_ok(ua):\n    return 'Mobile/' not in ua and 'Mobile ' not in ua\n\n\nclass CaptchaError(Exception):\n    pass\n\n\nclass SearchFailed(ValueError):\n    pass\n\n\nclass UrlNotFound(ValueError):\n\n    def __init__(self, url):\n        ValueError.__init__(self, 'The URL {} was not found (HTTP 404)'.format(url))\n\n\nclass UrlTimedOut(ValueError):\n\n    def __init__(self, url):\n        ValueError.__init__(self, 'Timed out fetching {} try again later'.format(url))\n\n\ndef parse_html(raw):\n    try:\n        from html5_parser import parse\n    except ImportError:\n        # Old versions of calibre\n        import html5lib\n        return html5lib.parse(raw, treebuilder='lxml', namespaceHTMLElements=False)\n    else:\n        return parse(raw)\n\n\ndef parse_details_page(url, log, timeout, browser, domain):\n    from lxml.html import tostring\n\n    from calibre.ebooks.chardet import xml_to_unicode\n    from calibre.utils.cleantext import clean_ascii_chars\n    try:\n        from calibre.ebooks.metadata.sources.update import search_engines_module\n        get_data_for_cached_url = search_engines_module().get_data_for_cached_url\n    except Exception:\n        def get_data_for_cached_url(*a):\n            return None\n    raw = get_data_for_cached_url(url)\n    if raw:\n        log('Using cached details for url:', url)\n    else:\n        log('Downloading details from:', url)\n        try:\n            raw = browser.open_novisit(url, timeout=timeout).read().strip()\n        except Exception as e:\n            if callable(getattr(e, 'getcode', None)) and e.getcode() == 404:\n                log.error('URL not found: %r' % url)\n                raise UrlNotFound(url)\n            attr = getattr(e, 'args', [None])\n            attr = attr if attr else [None]\n            if isinstance(attr[0], socket.timeout):\n                msg = 'Details page timed out. Try again later.'\n                log.error(msg)\n                raise UrlTimedOut(url)\n            else:\n                msg = 'Failed to make details query: %r' % url\n                log.exception(msg)\n                raise ValueError('Could not make details query for {}'.format(url))\n\n    oraw = raw\n    if 'amazon.com.br' in url:\n        # amazon.com.br serves utf-8 but has an incorrect latin1 <meta> tag\n        raw = raw.decode('utf-8')\n    raw = xml_to_unicode(raw, strip_encoding_pats=True,\n                         resolve_entities=True)[0]\n    if '<title>404 - ' in raw:\n        raise ValueError('Got a 404 page for: %r' % url)\n    if '>Could not find the requested document in the cache.<' in raw:\n        raise ValueError('No cached entry for %s found' % url)\n\n    try:\n        root = parse_html(clean_ascii_chars(raw))\n    except Exception:\n        msg = 'Failed to parse amazon details page: %r' % url\n        log.exception(msg)\n        raise ValueError(msg)\n    if domain == 'jp':\n        for a in root.xpath('//a[@href]'):\n            if ('black-curtain-redirect.html' in a.get('href')) or ('/black-curtain/save-eligibility/black-curtain' in a.get('href')):\n                url = a.get('href')\n                if url:\n                    if url.startswith('/'):\n                        url = 'https://amazon.co.jp' + a.get('href')\n                    log('Black curtain redirect found, following')\n                    return parse_details_page(url, log, timeout, browser, domain)\n\n    errmsg = root.xpath('//*[@id=\"errorMessage\"]')\n    if errmsg:\n        msg = 'Failed to parse amazon details page: %r' % url\n        msg += tostring(errmsg, method='text', encoding='unicode').strip()\n        log.error(msg)\n        raise ValueError(msg)\n\n    from css_selectors import Select\n    selector = Select(root)\n    return oraw, root, selector\n\n\ndef parse_asin(root, log, url):\n    try:\n        link = root.xpath('//link[@rel=\"canonical\" and @href]')\n        for l in link:\n            return l.get('href').rpartition('/')[-1]\n    except Exception:\n        log.exception('Error parsing ASIN for url: %r' % url)\n\n\nclass Worker(Thread):  # Get details {{{\n\n    '''\n    Get book details from amazons book page in a separate thread\n    '''\n\n    def __init__(self, url, result_queue, browser, log, relevance, domain,\n                 plugin, timeout=20, testing=False, preparsed_root=None,\n                 cover_url_processor=None, filter_result=None):\n        Thread.__init__(self)\n        self.cover_url_processor = cover_url_processor\n        self.preparsed_root = preparsed_root\n        self.daemon = True\n        self.testing = testing\n        self.url, self.result_queue = url, result_queue\n        self.log, self.timeout = log, timeout\n        self.filter_result = filter_result or (lambda x, log: True)\n        self.relevance, self.plugin = relevance, plugin\n        self.browser = browser\n        self.cover_url = self.amazon_id = self.isbn = None\n        self.domain = domain\n        from lxml.html import tostring\n        self.tostring = tostring\n\n        months = {  # {{{\n            'de': {\n                1: ['jän', 'januar'],\n                2: ['februar'],\n                3: ['märz'],\n                5: ['mai'],\n                6: ['juni'],\n                7: ['juli'],\n                10: ['okt', 'oktober'],\n                12: ['dez', 'dezember']\n            },\n            'it': {\n                1: ['gennaio', 'enn'],\n                2: ['febbraio', 'febbr'],\n                3: ['marzo'],\n                4: ['aprile'],\n                5: ['maggio', 'magg'],\n                6: ['giugno'],\n                7: ['luglio'],\n                8: ['agosto', 'ag'],\n                9: ['settembre', 'sett'],\n                10: ['ottobre', 'ott'],\n                11: ['novembre'],\n                12: ['dicembre', 'dic'],\n            },\n            'fr': {\n                1: ['janv'],\n                2: ['févr'],\n                3: ['mars'],\n                4: ['avril'],\n                5: ['mai'],\n                6: ['juin'],\n                7: ['juil'],\n                8: ['août'],\n                9: ['sept'],\n                10: ['oct', 'octobre'],\n                11: ['nov', 'novembre'],\n                12: ['déc', 'décembre'],\n            },\n            'br': {\n                1: ['janeiro'],\n                2: ['fevereiro'],\n                3: ['março'],\n                4: ['abril'],\n                5: ['maio'],\n                6: ['junho'],\n                7: ['julho'],\n                8: ['agosto'],\n                9: ['setembro'],\n                10: ['outubro'],\n                11: ['novembro'],\n                12: ['dezembro'],\n            },\n            'es': {\n                1: ['enero'],\n                2: ['febrero'],\n                3: ['marzo'],\n                4: ['abril'],\n                5: ['mayo'],\n                6: ['junio'],\n                7: ['julio'],\n                8: ['agosto'],\n                9: ['septiembre', 'setiembre'],\n                10: ['octubre'],\n                11: ['noviembre'],\n                12: ['diciembre'],\n            },\n            'se': {\n                1: ['januari'],\n                2: ['februari'],\n                3: ['mars'],\n                4: ['april'],\n                5: ['maj'],\n                6: ['juni'],\n                7: ['juli'],\n                8: ['augusti'],\n                9: ['september'],\n                10: ['oktober'],\n                11: ['november'],\n                12: ['december'],\n            },\n            'jp': {\n                1: ['1月'],\n                2: ['2月'],\n                3: ['3月'],\n                4: ['4月'],\n                5: ['5月'],\n                6: ['6月'],\n                7: ['7月'],\n                8: ['8月'],\n                9: ['9月'],\n                10: ['10月'],\n                11: ['11月'],\n                12: ['12月'],\n            },\n            'nl': {\n                1: ['januari'], 2: ['februari'], 3: ['maart'], 5: ['mei'], 6: ['juni'], 7: ['juli'], 8: ['augustus'], 10: ['oktober'],\n            }\n\n        }  # }}}\n\n        self.english_months = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',\n                               'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']\n        self.months = months.get(self.domain, {})\n\n        self.pd_xpath = '''\n            //h2[text()=\"Product Details\" or \\\n                 text()=\"Produktinformation\" or \\\n                 text()=\"Dettagli prodotto\" or \\\n                 text()=\"Product details\" or \\\n                 text()=\"Détails sur le produit\" or \\\n                 text()=\"Detalles del producto\" or \\\n                 text()=\"Detalhes do produto\" or \\\n                 text()=\"Productgegevens\" or \\\n                 text()=\"基本信息\" or \\\n                 starts-with(text(), \"登録情報\")]/../div[@class=\"content\"]\n            '''\n        # Editor: is for Spanish\n        self.publisher_xpath = '''\n            descendant::*[starts-with(text(), \"Publisher:\") or \\\n                    starts-with(text(), \"Verlag:\") or \\\n                    starts-with(text(), \"Editore:\") or \\\n                    starts-with(text(), \"Editeur\") or \\\n                    starts-with(text(), \"Editor:\") or \\\n                    starts-with(text(), \"Editora:\") or \\\n                    starts-with(text(), \"Uitgever:\") or \\\n                    starts-with(text(), \"Utgivare:\") or \\\n                    starts-with(text(), \"出版社:\")]\n            '''\n        self.pubdate_xpath = '''\n            descendant::*[starts-with(text(), \"Publication Date:\") or \\\n                    starts-with(text(), \"Audible.com Release Date:\")]\n        '''\n        self.publisher_names = {'Publisher', 'Uitgever', 'Verlag', 'Utgivare', 'Herausgeber',\n                                'Editore', 'Editeur', 'Éditeur', 'Editor', 'Editora', '出版社'}\n\n        self.language_xpath = '''\n            descendant::*[\n                starts-with(text(), \"Language:\") \\\n                or text() = \"Language\" \\\n                or text() = \"Sprache:\" \\\n                or text() = \"Lingua:\" \\\n                or text() = \"Idioma:\" \\\n                or starts-with(text(), \"Langue\") \\\n                or starts-with(text(), \"言語\") \\\n                or starts-with(text(), \"Språk\") \\\n                or starts-with(text(), \"语种\")\n                ]\n            '''\n        self.language_names = {'Language', 'Sprache', 'Språk',\n                               'Lingua', 'Idioma', 'Langue', '言語', 'Taal', '语种'}\n\n        self.tags_xpath = '''\n            descendant::h2[\n                text() = \"Look for Similar Items by Category\" or\n                text() = \"Ähnliche Artikel finden\" or\n                text() = \"Buscar productos similares por categoría\" or\n                text() = \"Ricerca articoli simili per categoria\" or\n                text() = \"Rechercher des articles similaires par rubrique\" or\n                text() = \"Procure por items similares por categoria\" or\n                text() = \"関連商品を探す\"\n            ]/../descendant::ul/li\n        '''\n\n        self.ratings_pat = re.compile(\n            r'([0-9.,]+) ?(out of|von|van|su|étoiles sur|つ星のうち|de un máximo de|de|av) '\n            r'([\\d\\.]+)( (stars|Sternen|stelle|estrellas|estrelas|sterren|stjärnor)){0,1}'\n        )\n        self.ratings_pat_cn = re.compile(r'([0-9.]+) 颗星,最多 5 颗星')\n        self.ratings_pat_jp = re.compile(r'\\d+つ星のうち([\\d\\.]+)')\n\n        lm = {\n            'eng': ('English', 'Englisch', 'Engels', 'Engelska'),\n            'fra': ('French', 'Français'),\n            'ita': ('Italian', 'Italiano'),\n            'deu': ('German', 'Deutsch'),\n            'spa': ('Spanish', 'Espa\\xf1ol', 'Espaniol'),\n            'jpn': ('Japanese', '日本語'),\n            'por': ('Portuguese', 'Português'),\n            'nld': ('Dutch', 'Nederlands',),\n            'chs': ('Chinese', '中文', '简体中文'),\n            'swe': ('Swedish', 'Svenska'),\n        }\n        self.lang_map = {}\n        for code, names in lm.items():\n            for name in names:\n                self.lang_map[name] = code\n\n        self.series_pat = re.compile(\n            r'''\n                \\|\\s*              # Prefix\n                (Series)\\s*:\\s*    # Series declaration\n                (?P<series>.+?)\\s+  # The series name\n                \\((Book)\\s*    # Book declaration\n                (?P<index>[0-9.]+) # Series index\n                \\s*\\)\n                ''', re.X)\n\n    def delocalize_datestr(self, raw):\n        if self.domain == 'cn':\n            return raw.replace('年', '-').replace('月', '-').replace('日', '')\n        if not self.months:\n            return raw\n        ans = raw.lower()\n        for i, vals in self.months.items():\n            for x in vals:\n                ans = ans.replace(x, self.english_months[i])\n        ans = ans.replace(' de ', ' ')\n        return ans\n\n    def run(self):\n        try:\n            self.get_details()\n        except Exception:\n            self.log.exception('get_details failed for url: %r' % self.url)\n\n    def get_details(self):\n        if self.preparsed_root is None:\n            raw, root, selector = parse_details_page(\n                self.url, self.log, self.timeout, self.browser, self.domain)\n        else:\n            raw, root, selector = self.preparsed_root\n\n        from css_selectors import Select\n        self.selector = Select(root)\n        self.parse_details(raw, root)\n\n    def parse_details(self, raw, root):\n        asin = parse_asin(root, self.log, self.url)\n        if not asin and root.xpath('//form[@action=\"/errors/validateCaptcha\"]'):\n            raise CaptchaError(\n                'Amazon returned a CAPTCHA page, probably because you downloaded too many books. Wait for some time and try again.')\n        if self.testing:\n            import tempfile\n            import uuid\n            with tempfile.NamedTemporaryFile(prefix=(asin or type('')(uuid.uuid4())) + '_',\n                                             suffix='.html', delete=False) as f:\n                f.write(raw)\n            print('Downloaded HTML for', asin, 'saved in', f.name)\n\n        try:\n            title = self.parse_title(root)\n        except Exception:\n            self.log.exception('Error parsing title for url: %r' % self.url)\n            title = None\n\n        try:\n            authors = self.parse_authors(root)\n        except Exception:\n            self.log.exception('Error parsing authors for url: %r' % self.url)\n            authors = []\n\n        if not title or not authors or not asin:\n            self.log.error(\n                'Could not find title/authors/asin for %r' % self.url)\n            self.log.error('ASIN: %r Title: %r Authors: %r' % (asin, title,\n                                                               authors))\n            return\n\n        mi = Metadata(title, authors)\n        idtype = 'amazon' if self.domain == 'com' else 'amazon_' + self.domain\n        mi.set_identifier(idtype, asin)\n        self.amazon_id = asin\n\n        try:\n            mi.rating = self.parse_rating(root)\n        except Exception:\n            self.log.exception('Error parsing ratings for url: %r' % self.url)\n\n        try:\n            mi.comments = self.parse_comments(root, raw)\n        except Exception:\n            self.log.exception('Error parsing comments for url: %r' % self.url)\n\n        try:\n            series, series_index = self.parse_series(root)\n            if series:\n                mi.series, mi.series_index = series, series_index\n            elif self.testing:\n                mi.series, mi.series_index = 'Dummy series for testing', 1\n        except Exception:\n            self.log.exception('Error parsing series for url: %r' % self.url)\n\n        try:\n            mi.tags = self.parse_tags(root)\n        except Exception:\n            self.log.exception('Error parsing tags for url: %r' % self.url)\n\n        try:\n            self.cover_url = self.parse_cover(root, raw)\n        except Exception:\n            self.log.exception('Error parsing cover for url: %r' % self.url)\n        if self.cover_url_processor is not None and self.cover_url and self.cover_url.startswith('/'):\n            self.cover_url = self.cover_url_processor(self.cover_url)\n        mi.has_cover = bool(self.cover_url)\n\n        detail_bullets = root.xpath('//*[@data-feature-name=\"detailBullets\"]')\n        non_hero = tuple(self.selector(\n            'div#bookDetails_container_div div#nonHeroSection')) or tuple(self.selector(\n                '#productDetails_techSpec_sections'))\n        feature_and_detail_bullets = root.xpath('//*[@data-feature-name=\"featureBulletsAndDetailBullets\"]')\n        if detail_bullets:\n            self.parse_detail_bullets(root, mi, detail_bullets[0])\n        elif non_hero:\n            try:\n                self.parse_new_details(root, mi, non_hero[0])\n            except Exception:\n                self.log.exception(\n                    'Failed to parse new-style book details section')\n        elif feature_and_detail_bullets:\n            self.parse_detail_bullets(root, mi, feature_and_detail_bullets[0], ul_selector='ul')\n\n        else:\n            pd = root.xpath(self.pd_xpath)\n            if pd:\n                pd = pd[0]\n\n                try:\n                    isbn = self.parse_isbn(pd)\n                    if isbn:\n                        self.isbn = mi.isbn = isbn\n                except Exception:\n                    self.log.exception(\n                        'Error parsing ISBN for url: %r' % self.url)\n\n                try:\n                    mi.publisher = self.parse_publisher(pd)\n                except Exception:\n                    self.log.exception(\n                        'Error parsing publisher for url: %r' % self.url)\n\n                try:\n                    mi.pubdate = self.parse_pubdate(pd)\n                except Exception:\n                    self.log.exception(\n                        'Error parsing publish date for url: %r' % self.url)\n\n                try:\n                    lang = self.parse_language(pd)\n                    if lang:\n                        mi.language = lang\n                except Exception:\n                    self.log.exception(\n                        'Error parsing language for url: %r' % self.url)\n\n            else:\n                self.log.warning(\n                    'Failed to find product description for url: %r' % self.url)\n\n        mi.source_relevance = self.relevance\n\n        if self.amazon_id:\n            if self.isbn:\n                self.plugin.cache_isbn_to_identifier(self.isbn, self.amazon_id)\n            if self.cover_url:\n                self.plugin.cache_identifier_to_cover_url(self.amazon_id,\n                                                          self.cover_url)\n\n        self.plugin.clean_downloaded_metadata(mi)\n\n        if self.filter_result(mi, self.log):\n            self.result_queue.put(mi)\n\n    def totext(self, elem, only_printable=False):\n        res = self.tostring(elem, encoding='unicode', method='text')\n        if only_printable:\n            try:\n                filtered_characters = [s for s in res if s.isprintable()]\n            except AttributeError:\n                filtered_characters = [s for s in res if s in string.printable]\n            res = ''.join(filtered_characters)\n        return res.strip()\n\n    def parse_title(self, root):\n\n        def sanitize_title(title):\n            ans = title.strip()\n            if not ans.startswith('['):\n                ans = re.sub(r'[(\\[].*[)\\]]', '', title).strip()\n            return ans\n\n        h1 = root.xpath('//h1[@id=\"title\"]')\n        if h1:\n            h1 = h1[0]\n            for child in h1.xpath('./*[contains(@class, \"a-color-secondary\")]'):\n                h1.remove(child)\n            return sanitize_title(self.totext(h1))\n        # audiobooks\n        elem = root.xpath('//*[@id=\"productTitle\"]')\n        if elem:\n            return sanitize_title(self.totext(elem[0]))\n        tdiv = root.xpath('//h1[contains(@class, \"parseasinTitle\")]')\n        if not tdiv:\n            span = root.xpath('//*[@id=\"ebooksTitle\"]')\n            if span:\n                return sanitize_title(self.totext(span[0]))\n            h1 = root.xpath('//h1[@data-feature-name=\"title\"]')\n            if h1:\n                return sanitize_title(self.totext(h1[0]))\n            raise ValueError('No title block found')\n        tdiv = tdiv[0]\n        actual_title = tdiv.xpath('descendant::*[@id=\"btAsinTitle\"]')\n        if actual_title:\n            title = self.tostring(actual_title[0], encoding='unicode',\n                                  method='text').strip()\n        else:\n            title = self.tostring(tdiv, encoding='unicode',\n                                  method='text').strip()\n        return sanitize_title(title)\n\n    def parse_authors(self, root):\n        for sel in (\n                '#byline .author .contributorNameID',\n                '#byline .author a.a-link-normal',\n                '#bylineInfo .author .contributorNameID',\n                '#bylineInfo .author a.a-link-normal',\n                '#bylineInfo #bylineContributor',\n                '#bylineInfo #contributorLink',\n        ):\n            matches = tuple(self.selector(sel))\n            if matches:\n                authors = [self.totext(x) for x in matches]\n                return [a for a in authors if a]\n\n        x = '//h1[contains(@class, \"parseasinTitle\")]/following-sibling::span/*[(name()=\"a\" and @href) or (name()=\"span\" and @class=\"contributorNameTrigger\")]'\n        aname = root.xpath(x)\n        if not aname:\n            aname = root.xpath('''\n            //h1[contains(@class, \"parseasinTitle\")]/following-sibling::*[(name()=\"a\" and @href) or (name()=\"span\" and @class=\"contributorNameTrigger\")]\n                    ''')\n        for x in aname:\n            x.tail = ''\n        authors = [self.tostring(x, encoding='unicode', method='text').strip() for x\n                   in aname]\n        authors = [a for a in authors if a]\n        return authors\n\n    def parse_rating(self, root):\n        for x in root.xpath('//div[@id=\"cpsims-feature\" or @id=\"purchase-sims-feature\" or @id=\"rhf\"]'):\n            # Remove the similar books section as it can cause spurious\n            # ratings matches\n            x.getparent().remove(x)\n\n        rating_paths = (\n            '//div[@data-feature-name=\"averageCustomerReviews\" or @id=\"averageCustomerReviews\"]',\n            '//div[@class=\"jumpBar\"]/descendant::span[contains(@class,\"asinReviewsSummary\")]',\n            '//div[@class=\"buying\"]/descendant::span[contains(@class,\"asinReviewsSummary\")]',\n            '//span[@class=\"crAvgStars\"]/descendant::span[contains(@class,\"asinReviewsSummary\")]'\n        )\n        ratings = None\n        for p in rating_paths:\n            ratings = root.xpath(p)\n            if ratings:\n                break\n\n        def parse_ratings_text(text):\n            try:\n                m = self.ratings_pat.match(text)\n                return float(m.group(1).replace(',', '.')) / float(m.group(3)) * 5\n            except Exception:\n                pass\n\n        if ratings:\n            ratings = ratings[0]\n            for elem in ratings.xpath('descendant::*[@title]'):\n                t = elem.get('title').strip()\n                if self.domain == 'cn':\n                    m = self.ratings_pat_cn.match(t)\n                    if m is not None:\n                        return float(m.group(1))\n                elif self.domain == 'jp':\n                    m = self.ratings_pat_jp.match(t)\n                    if m is not None:\n                        return float(m.group(1))\n                else:\n                    ans = parse_ratings_text(t)\n                    if ans is not None:\n                        return ans\n            for elem in ratings.xpath('descendant::span[@class=\"a-icon-alt\"]'):\n                t = self.tostring(\n                    elem, encoding='unicode', method='text', with_tail=False).strip()\n                ans = parse_ratings_text(t)\n                if ans is not None:\n                    return ans\n        else:\n            # found in kindle book pages on amazon.com\n            for x in root.xpath('//a[@id=\"acrCustomerReviewLink\"]'):\n                spans = x.xpath('./span')\n                if spans:\n                    txt = self.tostring(spans[0], method='text', encoding='unicode', with_tail=False).strip()\n                    try:\n                        return float(txt.replace(',', '.'))\n                    except Exception:\n                        pass\n\n    def _render_comments(self, desc):\n        from calibre.library.comments import sanitize_comments_html\n\n        for c in desc.xpath('descendant::noscript'):\n            c.getparent().remove(c)\n        for c in desc.xpath('descendant::*[@class=\"seeAll\" or'\n                            ' @class=\"emptyClear\" or @id=\"collapsePS\" or'\n                            ' @id=\"expandPS\"]'):\n            c.getparent().remove(c)\n        for b in desc.xpath('descendant::b[@style]'):\n            # Bing highlights search results\n            s = b.get('style', '')\n            if 'color' in s:\n                b.tag = 'span'\n                del b.attrib['style']\n\n        for a in desc.xpath('descendant::a[@href]'):\n            del a.attrib['href']\n            a.tag = 'span'\n        for a in desc.xpath('descendant::span[@class=\"a-text-italic\"]'):\n            a.tag = 'i'\n        for a in desc.xpath('descendant::span[@class=\"a-text-bold\"]'):\n            a.tag = 'b'\n        desc = self.tostring(desc, method='html', encoding='unicode').strip()\n        desc = xml_replace_entities(desc, 'utf-8')\n\n        # Encoding bug in Amazon data U+fffd (replacement char)\n        # in some examples it is present in place of '\n        desc = desc.replace('\\ufffd', \"'\")\n        # remove all attributes from tags\n        desc = re.sub(r'<([a-zA-Z0-9]+)\\s[^>]+>', r'<\\1>', desc)\n        # Collapse whitespace\n        # desc = re.sub(r'\\n+', '\\n', desc)\n        # desc = re.sub(r' +', ' ', desc)\n        # Remove the notice about text referring to out of print editions\n        desc = re.sub(r'(?s)<em>--This text ref.*?</em>', '', desc)\n        # Remove comments\n        desc = re.sub(r'(?s)<!--.*?-->', '', desc)\n        return sanitize_comments_html(desc)\n\n    def parse_comments(self, root, raw):\n        try:\n            from urllib.parse import unquote\n        except ImportError:\n            from urllib import unquote\n        ans = ''\n        ovr = tuple(self.selector('#drengr_MobileTabbedDescriptionOverviewContent_feature_div')) or tuple(\n            self.selector('#drengr_DesktopTabbedDescriptionOverviewContent_feature_div'))\n        if ovr:\n            ovr = ovr[0]\n            ovr.tag = 'div'\n            ans = self._render_comments(ovr)\n            ovr = tuple(self.selector('#drengr_MobileTabbedDescriptionEditorialsContent_feature_div')) or tuple(\n                self.selector('#drengr_DesktopTabbedDescriptionEditorialsContent_feature_div'))\n            if ovr:\n                ovr = ovr[0]\n                ovr.tag = 'div'\n                ans += self._render_comments(ovr)\n        else:\n            ns = tuple(self.selector('#bookDescription_feature_div noscript'))\n            if ns:\n                ns = ns[0]\n                if len(ns) == 0 and ns.text:\n                    import html5lib\n\n                    # html5lib parsed noscript as CDATA\n                    ns = html5lib.parseFragment(\n                        '<div>%s</div>' % (ns.text), treebuilder='lxml', namespaceHTMLElements=False)[0]\n                else:\n                    ns.tag = 'div'\n                ans = self._render_comments(ns)\n            else:\n                desc = root.xpath('//div[@id=\"ps-content\"]/div[@class=\"content\"]')\n                if desc:\n                    ans = self._render_comments(desc[0])\n                else:\n                    ns = tuple(self.selector('#bookDescription_feature_div .a-expander-content'))\n                    if ns:\n                        ans = self._render_comments(ns[0])\n        # audiobooks\n        if not ans:\n            elem = root.xpath('//*[@id=\"audible_desktopTabbedDescriptionOverviewContent_feature_div\"]')\n            if elem:\n                ans = self._render_comments(elem[0])\n        desc = root.xpath(\n            '//div[@id=\"productDescription\"]/*[@class=\"content\"]')\n        if desc:\n            ans += self._render_comments(desc[0])\n        else:\n            # Idiot chickens from amazon strike again. This data is now stored\n            # in a JS variable inside a script tag URL encoded.\n            m = re.search(br'var\\s+iframeContent\\s*=\\s*\"([^\"]+)\"', raw)\n            if m is not None:\n                try:\n                    text = unquote(m.group(1)).decode('utf-8')\n                    nr = parse_html(text)\n                    desc = nr.xpath(\n                        '//div[@id=\"productDescription\"]/*[@class=\"content\"]')\n                    if desc:\n                        ans += self._render_comments(desc[0])\n                except Exception as e:\n                    self.log.warn(\n                        'Parsing of obfuscated product description failed with error: %s' % as_unicode(e))\n            else:\n                desc = root.xpath('//div[@id=\"productDescription_fullView\"]')\n                if desc:\n                    ans += self._render_comments(desc[0])\n\n        return ans\n\n    def parse_series(self, root):\n        ans = (None, None)\n\n        # This is found on kindle pages for books on amazon.com\n        series = root.xpath('//*[@id=\"rpi-attribute-book_details-series\"]')\n        if series:\n            spans = series[0].xpath('descendant::span')\n            if spans:\n                texts = [self.tostring(x, encoding='unicode', method='text', with_tail=False).strip() for x in spans]\n                texts = list(filter(None, texts))\n                if len(texts) == 2:\n                    idxinfo, series = texts\n                    m = re.search(r'[0-9.]+', idxinfo.strip())\n                    if m is not None:\n                        ans = series, float(m.group())\n                        return ans\n\n        # This is found on the paperback/hardback pages for books on amazon.com\n        series = root.xpath('//div[@data-feature-name=\"seriesTitle\"]')\n        if series:\n            series = series[0]\n            spans = series.xpath('./span')\n            if spans:\n                raw = self.tostring(\n                    spans[0], encoding='unicode', method='text', with_tail=False).strip()\n                m = re.search(r'\\s+([0-9.]+)$', raw.strip())\n                if m is not None:\n                    series_index = float(m.group(1))\n                    s = series.xpath('./a[@id=\"series-page-link\"]')\n                    if s:\n                        series = self.tostring(\n                            s[0], encoding='unicode', method='text', with_tail=False).strip()\n                        if series:\n                            ans = (series, series_index)\n        else:\n            series = root.xpath('//div[@id=\"seriesBulletWidget_feature_div\"]')\n            if series:\n                a = series[0].xpath('descendant::a')\n                if a:\n                    raw = self.tostring(a[0], encoding='unicode', method='text', with_tail=False)\n                    if self.domain == 'jp':\n                        m = re.search(r'(?P<index>[0-9.]+)\\s*(?:巻|冊)\\s*\\(全\\s*([0-9.]+)\\s*(?:巻|冊)\\):\\s*(?P<series>.+)', raw.strip())\n                    else:\n                        m = re.search(r'(?:Book|Libro|Buch)\\s+(?P<index>[0-9.]+)\\s+(?:of|de|von)\\s+([0-9.]+)\\s*:\\s*(?P<series>.+)', raw.strip())\n                    if m is not None:\n                        ans = (m.group('series').strip(), float(m.group('index')))\n\n        # This is found on Kindle edition pages on amazon.com\n        if ans == (None, None):\n            for span in root.xpath('//div[@id=\"aboutEbooksSection\"]//li/span'):\n                text = (span.text or '').strip()\n                m = re.match(r'Book\\s+([0-9.]+)', text)\n                if m is not None:\n                    series_index = float(m.group(1))\n                    a = span.xpath('./a[@href]')\n                    if a:\n                        series = self.tostring(\n                            a[0], encoding='unicode', method='text', with_tail=False).strip()\n                        if series:\n                            ans = (series, series_index)\n        # This is found on newer Kindle edition pages on amazon.com\n        if ans == (None, None):\n            for b in root.xpath('//div[@id=\"reviewFeatureGroup\"]/span/b'):\n                text = (b.text or '').strip()\n                m = re.match(r'Book\\s+([0-9.]+)', text)\n                if m is not None:\n                    series_index = float(m.group(1))\n                    a = b.getparent().xpath('./a[@href]')\n                    if a:\n                        series = self.tostring(\n                            a[0], encoding='unicode', method='text', with_tail=False).partition('(')[0].strip()\n                        if series:\n                            ans = series, series_index\n\n        if ans == (None, None):\n            desc = root.xpath('//div[@id=\"ps-content\"]/div[@class=\"buying\"]')\n            if desc:\n                raw = self.tostring(desc[0], method='text', encoding='unicode')\n                raw = re.sub(r'\\s+', ' ', raw)\n                match = self.series_pat.search(raw)\n                if match is not None:\n                    s, i = match.group('series'), float(match.group('index'))\n                    if s:\n                        ans = (s, i)\n        if ans[0]:\n            ans = (re.sub(r'\\s+Series$', '', ans[0]).strip(), ans[1])\n            ans = (re.sub(r'\\(.+?\\s+Series\\)$', '', ans[0]).strip(), ans[1])\n        return ans\n\n    def parse_tags(self, root):\n        ans = []\n        exclude_tokens = {'kindle', 'a-z'}\n        exclude = {'special features', 'by authors',\n                   'authors & illustrators', 'books', 'new; used & rental textbooks'}\n        seen = set()\n        for li in root.xpath(self.tags_xpath):\n            for i, a in enumerate(li.iterdescendants('a')):\n                if i > 0:\n                    # we ignore the first category since it is almost always\n                    # too broad\n                    raw = (a.text or '').strip().replace(',', ';')\n                    lraw = icu_lower(raw)\n                    tokens = frozenset(lraw.split())\n                    if raw and lraw not in exclude and not tokens.intersection(exclude_tokens) and lraw not in seen:\n                        ans.append(raw)\n                        seen.add(lraw)\n        return ans\n\n    def parse_cover(self, root, raw=b''):\n        # Look for the image URL in javascript, using the first image in the\n        # image gallery as the cover\n        import json\n        imgpat = re.compile(r'\"hiRes\":\"(.+?)\",\"thumb\"')\n        for script in root.xpath('//script'):\n            m = imgpat.search(script.text or '')\n            if m is not None:\n                return m.group(1)\n        imgpat = re.compile(r''''imageGalleryData'\\s*:\\s*(\\[\\s*{.+])''')\n        for script in root.xpath('//script'):\n            m = imgpat.search(script.text or '')\n            if m is not None:\n                try:\n                    return json.loads(m.group(1))[0]['mainUrl']\n                except Exception:\n                    continue\n\n        def clean_img_src(src):\n            parts = src.split('/')\n            if len(parts) > 3:\n                bn = parts[-1]\n                sparts = bn.split('_')\n                if len(sparts) > 2:\n                    bn = re.sub(r'\\.\\.jpg$', '.jpg', (sparts[0] + sparts[-1]))\n                    return ('/'.join(parts[:-1])) + '/' + bn\n\n        imgpat2 = re.compile(r'var imageSrc = \"([^\"]+)\"')\n        for script in root.xpath('//script'):\n            m = imgpat2.search(script.text or '')\n            if m is not None:\n                src = m.group(1)\n                url = clean_img_src(src)\n                if url:\n                    return url\n\n        imgs = root.xpath(\n            '//img[(@id=\"prodImage\" or @id=\"original-main-image\" or @id=\"main-image\" or @id=\"main-image-nonjs\") and @src]')\n        if not imgs:\n            imgs = (\n                root.xpath('//div[@class=\"main-image-inner-wrapper\"]/img[@src]') or\n                root.xpath('//div[@id=\"main-image-container\" or @id=\"ebooks-main-image-container\"]//img[@src]') or\n                root.xpath(\n                    '//div[@id=\"mainImageContainer\"]//img[@data-a-dynamic-image]')\n            )\n            for img in imgs:\n                try:\n                    idata = json.loads(img.get('data-a-dynamic-image'))\n                except Exception:\n                    imgs = ()\n                else:\n                    mwidth = 0\n                    try:\n                        url = None\n                        for iurl, (width, height) in idata.items():\n                            if width > mwidth:\n                                mwidth = width\n                                url = iurl\n\n                        return url\n                    except Exception:\n                        pass\n\n        for img in imgs:\n            src = img.get('src')\n            if 'data:' in src:\n                continue\n            if 'loading-' in src:\n                js_img = re.search(br'\"largeImage\":\"(https?://[^\"]+)\",', raw)\n                if js_img:\n                    src = js_img.group(1).decode('utf-8')\n            if ('/no-image-avail' not in src and 'loading-' not in src and '/no-img-sm' not in src):\n                self.log('Found image: %s' % src)\n                url = clean_img_src(src)\n                if url:\n                    return url\n\n    def parse_detail_bullets(self, root, mi, container, ul_selector='.detail-bullet-list'):\n        try:\n            ul = next(self.selector(ul_selector, root=container))\n        except StopIteration:\n            return\n        for span in self.selector('.a-list-item', root=ul):\n            cells = span.xpath('./span')\n            if len(cells) >= 2:\n                self.parse_detail_cells(mi, cells[0], cells[1])\n\n    def parse_new_details(self, root, mi, non_hero):\n        table = non_hero.xpath('descendant::table')[0]\n        for tr in table.xpath('descendant::tr'):\n            cells = tr.xpath('descendant::*[local-name()=\"td\" or local-name()=\"th\"]')\n            if len(cells) == 2:\n                self.parse_detail_cells(mi, cells[0], cells[1])\n\n    def parse_detail_cells(self, mi, c1, c2):\n        name = self.totext(c1, only_printable=True).strip().strip(':').strip()\n        val = self.totext(c2)\n        val = val.replace('\\u200e', '').replace('\\u200f', '')\n        if not val:\n            return\n        if name in self.language_names:\n            ans = self.lang_map.get(val)\n            if not ans:\n                ans = canonicalize_lang(val)\n            if ans:\n                mi.language = ans\n        elif name in self.publisher_names:\n            pub = val.partition(';')[0].partition('(')[0].strip()\n            if pub:\n                mi.publisher = pub\n            date = val.rpartition('(')[-1].replace(')', '').strip()\n            try:\n                from calibre.utils.date import parse_only_date\n                date = self.delocalize_datestr(date)\n                mi.pubdate = parse_only_date(date, assume_utc=True)\n            except Exception:\n                self.log.exception('Failed to parse pubdate: %s' % val)\n        elif name in {'ISBN', 'ISBN-10', 'ISBN-13'}:\n            ans = check_isbn(val)\n            if ans:\n                self.isbn = mi.isbn = ans\n        elif name in {'Publication date'}:\n            from calibre.utils.date import parse_only_date\n            date = self.delocalize_datestr(val)\n            mi.pubdate = parse_only_date(date, assume_utc=True)\n\n    def parse_isbn(self, pd):\n        items = pd.xpath(\n            'descendant::*[starts-with(text(), \"ISBN\")]')\n        if not items:\n            items = pd.xpath(\n                'descendant::b[contains(text(), \"ISBN:\")]')\n        for x in reversed(items):\n            if x.tail:\n                ans = check_isbn(x.tail.strip())\n                if ans:\n                    return ans\n\n    def parse_publisher(self, pd):\n        for x in reversed(pd.xpath(self.publisher_xpath)):\n            if x.tail:\n                ans = x.tail.partition(';')[0]\n                return ans.partition('(')[0].strip()\n\n    def parse_pubdate(self, pd):\n        from calibre.utils.date import parse_only_date\n        for x in reversed(pd.xpath(self.pubdate_xpath)):\n            if x.tail:\n                date = x.tail.strip()\n                date = self.delocalize_datestr(date)\n                try:\n                    return parse_only_date(date, assume_utc=True)\n                except Exception:\n                    pass\n        for x in reversed(pd.xpath(self.publisher_xpath)):\n            if x.tail:\n                ans = x.tail\n                date = ans.rpartition('(')[-1].replace(')', '').strip()\n                date = self.delocalize_datestr(date)\n                try:\n                    return parse_only_date(date, assume_utc=True)\n                except Exception:\n                    pass\n\n    def parse_language(self, pd):\n        for x in reversed(pd.xpath(self.language_xpath)):\n            if x.tail:\n                raw = x.tail.strip().partition(',')[0].strip()\n                ans = self.lang_map.get(raw, None)\n                if ans:\n                    return ans\n                ans = canonicalize_lang(ans)\n                if ans:\n                    return ans\n# }}}\n\n\nclass Amazon(Source):\n\n    name = 'Amazon.com'\n    version = (1, 3, 13)\n    minimum_calibre_version = (2, 82, 0)\n    description = _('Downloads metadata and covers from Amazon')\n\n    capabilities = frozenset(('identify', 'cover'))\n    touched_fields = frozenset(('title', 'authors', 'identifier:amazon',\n        'rating', 'comments', 'publisher', 'pubdate',\n        'languages', 'series', 'tags'))\n    has_html_comments = True\n    supports_gzip_transfer_encoding = True\n    prefer_results_with_isbn = False\n\n    AMAZON_DOMAINS = {\n        'com': _('US'),\n        'fr': _('France'),\n        'de': _('Germany'),\n        'uk': _('UK'),\n        'au': _('Australia'),\n        'it': _('Italy'),\n        'jp': _('Japan'),\n        'es': _('Spain'),\n        'br': _('Brazil'),\n        'in': _('India'),\n        'nl': _('Netherlands'),\n        'cn': _('China'),\n        'ca': _('Canada'),\n        'se': _('Sweden'),\n    }\n\n    SERVERS = {\n        'auto': _('Choose server automatically'),\n        'amazon': _('Amazon servers'),\n        'bing': _('Bing search cache'),\n        'google': _('Google search cache'),\n        'wayback': _('Wayback machine cache (slow)'),\n        'ddg': _('DuckDuckGo search and Google cache'),\n    }\n\n    options = (\n        Option('domain', 'choices', 'com', _('Amazon country website to use:'),\n               _('Metadata from Amazon will be fetched using this '\n                 \"country's Amazon website.\"), choices=AMAZON_DOMAINS),\n        Option('server', 'choices', 'auto', _('Server to get data from:'),\n               _(\n                   'Amazon has started blocking attempts to download'\n                   ' metadata from its servers. To get around this problem,'\n                   ' calibre can fetch the Amazon data from many different'\n                   ' places where it is cached. Choose the source you prefer.'\n               ), choices=SERVERS),\n        Option('use_mobi_asin', 'bool', False, _('Use the MOBI-ASIN for metadata search'),\n               _(\n                   'Enable this option to search for metadata with an'\n                   ' ASIN identifier from the MOBI file at the current country website,'\n                   ' unless any other amazon id is available. Note that if the'\n                   ' MOBI file came from a different Amazon country store, you could get'\n                   ' incorrect results.'\n               )),\n        Option('prefer_kindle_edition', 'bool', False, _('Prefer the Kindle edition, when available'),\n               _(\n                   'When searching for a book and the search engine returns both paper and Kindle editions,'\n                   ' always prefer the Kindle edition, instead of whatever the search engine returns at the'\n                   ' top.')\n        ),\n    )\n\n    def __init__(self, *args, **kwargs):\n        Source.__init__(self, *args, **kwargs)\n        self.set_amazon_id_touched_fields()\n\n    def id_from_url(self, url):\n        from polyglot.urllib import urlparse\n        purl = urlparse(url)\n        if purl.netloc and purl.path and '/dp/' in purl.path:\n            host_parts = tuple(x.lower() for x in purl.netloc.split('.'))\n            if 'amazon' in host_parts:\n                domain = host_parts[-1]\n            parts = purl.path.split('/')\n            idx = parts.index('dp')\n            try:\n                val = parts[idx+1]\n            except IndexError:\n                return\n            aid = 'amazon' if domain == 'com' else ('amazon_' + domain)\n            return aid, val\n\n    def test_fields(self, mi):\n        '''\n        Return the first field from self.touched_fields that is null on the\n        mi object\n        '''\n        for key in self.touched_fields:\n            if key.startswith('identifier:'):\n                key = key.partition(':')[-1]\n                if key == 'amazon':\n                    if self.domain != 'com':\n                        key += '_' + self.domain\n                if not mi.has_identifier(key):\n                    return 'identifier: ' + key\n            elif mi.is_null(key):\n                return key\n\n    @property\n    def browser(self):\n        br = self._browser\n        if br is None:\n            ua = 'Mobile '\n            while not user_agent_is_ok(ua):\n                ua = random_user_agent(allow_ie=False)\n            # ua = 'Mozilla/5.0 (Linux; Android 8.0.0; VTR-L29; rv:63.0) Gecko/20100101 Firefox/63.0'\n            self._browser = br = browser(user_agent=ua)\n            br.set_handle_gzip(True)\n            if self.use_search_engine:\n                br.addheaders += [\n                    ('Accept', accept_header_for_ua(ua)),\n                    ('Upgrade-insecure-requests', '1'),\n                ]\n            else:\n                br.addheaders += [\n                    ('Accept', accept_header_for_ua(ua)),\n                    ('Upgrade-insecure-requests', '1'),\n                    ('Referer', self.referrer_for_domain()),\n                ]\n        return br\n\n    def save_settings(self, *args, **kwargs):\n        Source.save_settings(self, *args, **kwargs)\n        self.set_amazon_id_touched_fields()\n\n    def set_amazon_id_touched_fields(self):\n        ident_name = 'identifier:amazon'\n        if self.domain != 'com':\n            ident_name += '_' + self.domain\n        tf = [x for x in self.touched_fields if not\n              x.startswith('identifier:amazon')] + [ident_name]\n        self.touched_fields = frozenset(tf)\n\n    def get_domain_and_asin(self, identifiers, extra_domains=()):\n        identifiers = {k.lower(): v for k, v in identifiers.items()}\n        for key, val in identifiers.items():\n            if key in ('amazon', 'asin'):\n                return 'com', val\n            if key.startswith('amazon_'):\n                domain = key.partition('_')[-1]\n                if domain and (domain in self.AMAZON_DOMAINS or domain in extra_domains):\n                    return domain, val\n        if self.prefs['use_mobi_asin']:\n            val = identifiers.get('mobi-asin')\n            if val is not None:\n                return self.domain, val\n        return None, None\n\n    def referrer_for_domain(self, domain=None):\n        domain = domain or self.domain\n        return {\n            'uk':  'https://www.amazon.co.uk/',\n            'au':  'https://www.amazon.com.au/',\n            'br':  'https://www.amazon.com.br/',\n            'jp':  'https://www.amazon.co.jp/',\n            'mx':  'https://www.amazon.com.mx/',\n        }.get(domain, 'https://www.amazon.%s/' % domain)\n\n    def _get_book_url(self, identifiers):  # {{{\n        domain, asin = self.get_domain_and_asin(\n            identifiers, extra_domains=('au', 'ca'))\n        if domain and asin:\n            url = None\n            r = self.referrer_for_domain(domain)\n            if r is not None:\n                url = r + 'dp/' + asin\n            if url:\n                idtype = 'amazon' if domain == 'com' else 'amazon_' + domain\n                return domain, idtype, asin, url\n\n    def get_book_url(self, identifiers):\n        ans = self._get_book_url(identifiers)\n        if ans is not None:\n            return ans[1:]\n\n    def get_book_url_name(self, idtype, idval, url):\n        if idtype == 'amazon':\n            return self.name\n        return 'A' + idtype.replace('_', '.')[1:]\n    # }}}\n\n    @property\n    def domain(self):\n        x = getattr(self, 'testing_domain', None)\n        if x is not None:\n            return x\n        domain = self.prefs['domain']\n        if domain not in self.AMAZON_DOMAINS:\n            domain = 'com'\n\n        return domain\n\n    @property\n    def server(self):\n        x = getattr(self, 'testing_server', None)\n        if x is not None:\n            return x\n        server = self.prefs['server']\n        if server not in self.SERVERS:\n            server = 'auto'\n        return server\n\n    @property\n    def use_search_engine(self):\n        return self.server != 'amazon'\n\n    def clean_downloaded_metadata(self, mi):\n        docase = (\n            mi.language == 'eng' or\n            (mi.is_null('language') and self.domain in {'com', 'uk', 'au'})\n        )\n        if mi.title and docase:\n            # Remove series information from title\n            m = re.search(r'\\S+\\s+(\\(.+?\\s+Book\\s+\\d+\\))$', mi.title)\n            if m is not None:\n                mi.title = mi.title.replace(m.group(1), '').strip()\n            mi.title = fixcase(mi.title)\n        mi.authors = fixauthors(mi.authors)\n        if mi.tags and docase:\n            mi.tags = list(map(fixcase, mi.tags))\n        mi.isbn = check_isbn(mi.isbn)\n        if mi.series and docase:\n            mi.series = fixcase(mi.series)\n        if mi.title and mi.series:\n            for pat in (r':\\s*Book\\s+\\d+\\s+of\\s+%s$', r'\\(%s\\)$', r':\\s*%s\\s+Book\\s+\\d+$'):\n                pat = pat % re.escape(mi.series)\n                q = re.sub(pat, '', mi.title, flags=re.I).strip()\n                if q and q != mi.title:\n                    mi.title = q\n                    break\n\n    def get_website_domain(self, domain):\n        return {'uk': 'co.uk', 'jp': 'co.jp', 'br': 'com.br', 'au': 'com.au'}.get(domain, domain)\n\n    def create_query(self, log, title=None, authors=None, identifiers={},  # {{{\n                     domain=None, for_amazon=True):\n        try:\n            from urllib.parse import unquote_plus, urlencode\n        except ImportError:\n            from urllib import unquote_plus, urlencode\n        if domain is None:\n            domain = self.domain\n\n        idomain, asin = self.get_domain_and_asin(identifiers)\n        if idomain is not None:\n            domain = idomain\n\n        # See the amazon detailed search page to get all options\n        terms = []\n        q = {'search-alias': 'aps',\n             'unfiltered': '1',\n        }\n\n        if domain == 'com':\n            q['sort'] = 'relevanceexprank'\n        else:\n            q['sort'] = 'relevancerank'\n\n        isbn = check_isbn(identifiers.get('isbn', None))\n\n        if asin is not None:\n            q['field-keywords'] = asin\n            terms.append(asin)\n        elif isbn is not None:\n            q['field-isbn'] = isbn\n            if len(isbn) == 13:\n                terms.extend('({} OR {}-{})'.format(isbn, isbn[:3], isbn[3:]).split())\n            else:\n                terms.append(isbn)\n        else:\n            # Only return book results\n            q['search-alias'] = {'br': 'digital-text',\n                                 'nl': 'aps'}.get(domain, 'stripbooks')\n            if title:\n                title_tokens = list(self.get_title_tokens(title))\n                if title_tokens:\n                    q['field-title'] = ' '.join(title_tokens)\n                    terms.extend(title_tokens)\n            if authors:\n                author_tokens = list(self.get_author_tokens(authors,\n                                                       only_first_author=True))\n                if author_tokens:\n                    q['field-author'] = ' '.join(author_tokens)\n                    terms.extend(author_tokens)\n\n        if not ('field-keywords' in q or 'field-isbn' in q or\n                ('field-title' in q)):\n            # Insufficient metadata to make an identify query\n            log.error('Insufficient metadata to construct query, none of title, ISBN or ASIN supplied')\n            raise SearchFailed()\n\n        if not for_amazon:\n            return terms, domain\n\n        if domain == 'nl':\n            q['__mk_nl_NL'] = 'ÅMÅŽÕÑ'\n            if 'field-keywords' not in q:\n                q['field-keywords'] = ''\n            for f in 'field-isbn field-title field-author'.split():\n                q['field-keywords'] += ' ' + q.pop(f, '')\n            q['field-keywords'] = q['field-keywords'].strip()\n\n        encoded_q = {x.encode('utf-8', 'ignore'): y.encode('utf-8', 'ignore') for x, y in q.items()}\n        url_query = urlencode(encoded_q)\n        # amazon's servers want IRIs with unicode characters not percent esaped\n        parts = []\n        for x in url_query.split(b'&' if isinstance(url_query, bytes) else '&'):\n            k, v = x.split(b'=' if isinstance(x, bytes) else '=', 1)\n            parts.append('{}={}'.format(iri_quote_plus(unquote_plus(k)), iri_quote_plus(unquote_plus(v))))\n        url_query = '&'.join(parts)\n        url = 'https://www.amazon.%s/s/?' % self.get_website_domain(\n            domain) + url_query\n        return url, domain\n\n    # }}}\n\n    def get_cached_cover_url(self, identifiers):  # {{{\n        url = None\n        domain, asin = self.get_domain_and_asin(identifiers)\n        if asin is None:\n            isbn = identifiers.get('isbn', None)\n            if isbn is not None:\n                asin = self.cached_isbn_to_identifier(isbn)\n        if asin is not None:\n            url = self.cached_identifier_to_cover_url(asin)\n\n        return url\n    # }}}\n\n    def parse_results_page(self, root, domain):  # {{{\n        from lxml.html import tostring\n\n        matches = []\n\n        def title_ok(title):\n            title = title.lower()\n            bad = ['bulk pack', '[audiobook]', '[audio cd]',\n                   '(a book companion)', '( slipcase with door )', ': free sampler']\n            if self.domain == 'com':\n                bad.extend(['(%s edition)' % x for x in ('spanish', 'german')])\n            for x in bad:\n                if x in title:\n                    return False\n            if title and title[0] in '[{' and re.search(r'\\(\\s*author\\s*\\)', title) is not None:\n                # Bad entries in the catalog\n                return False\n            return True\n\n        for query in (\n            '//div[contains(@class, \"s-result-list\")]//h2/a[@href]',\n            '//div[contains(@class, \"s-result-list\")]//div[@data-index]//h5//a[@href]',\n            r'//li[starts-with(@id, \"result_\")]//a[@href and contains(@class, \"s-access-detail-page\")]',\n            '//div[@data-cy=\"title-recipe\"]/a[@href]',\n        ):\n            result_links = root.xpath(query)\n            if result_links:\n                break\n        for a in result_links:\n            title = tostring(a, method='text', encoding='unicode')\n            if title_ok(title):\n                url = a.get('href')\n                if url.startswith('/'):\n                    url = 'https://www.amazon.%s%s' % (\n                        self.get_website_domain(domain), url)\n                matches.append(url)\n\n        if not matches:\n            # Previous generation of results page markup\n            for div in root.xpath(r'//div[starts-with(@id, \"result_\")]'):\n                links = div.xpath(r'descendant::a[@class=\"title\" and @href]')\n                if not links:\n                    # New amazon markup\n                    links = div.xpath('descendant::h3/a[@href]')\n                for a in links:\n                    title = tostring(a, method='text', encoding='unicode')\n                    if title_ok(title):\n                        url = a.get('href')\n                        if url.startswith('/'):\n                            url = 'https://www.amazon.%s%s' % (\n                                self.get_website_domain(domain), url)\n                        matches.append(url)\n                    break\n\n        if not matches:\n            # This can happen for some user agents that Amazon thinks are\n            # mobile/less capable\n            for td in root.xpath(\n                    r'//div[@id=\"Results\"]/descendant::td[starts-with(@id, \"search:Td:\")]'):\n                for a in td.xpath(r'descendant::td[@class=\"dataColumn\"]/descendant::a[@href]/span[@class=\"srTitle\"]/..'):\n                    title = tostring(a, method='text', encoding='unicode')\n                    if title_ok(title):\n                        url = a.get('href')\n                        if url.startswith('/'):\n                            url = 'https://www.amazon.%s%s' % (\n                                self.get_website_domain(domain), url)\n                        matches.append(url)\n                    break\n        if not matches and root.xpath('//form[@action=\"/errors/validateCaptcha\"]'):\n            raise CaptchaError('Amazon returned a CAPTCHA page. Recently Amazon has begun using statistical'\n                               ' profiling to block access to its website. As such this metadata plugin is'\n                               ' unlikely to ever work reliably.')\n\n        # Keep only the top 3 matches as the matches are sorted by relevance by\n        # Amazon so lower matches are not likely to be very relevant\n        return matches[:3]\n    # }}}\n\n    def search_amazon(self, br, testing, log, abort, title, authors, identifiers, timeout):  # {{{\n        from calibre.ebooks.chardet import xml_to_unicode\n        from calibre.utils.cleantext import clean_ascii_chars\n        matches = []\n        query, domain = self.create_query(log, title=title, authors=authors,\n                                          identifiers=identifiers)\n        time.sleep(1)\n        try:\n            raw = br.open_novisit(query, timeout=timeout).read().strip()\n        except Exception as e:\n            if callable(getattr(e, 'getcode', None)) and \\\n                    e.getcode() == 404:\n                log.error('Query malformed: %r' % query)\n                raise SearchFailed()\n            attr = getattr(e, 'args', [None])\n            attr = attr if attr else [None]\n            if isinstance(attr[0], socket.timeout):\n                msg = _('Amazon timed out. Try again later.')\n                log.error(msg)\n            else:\n                msg = 'Failed to make identify query: %r' % query\n                log.exception(msg)\n            raise SearchFailed()\n\n        raw = clean_ascii_chars(xml_to_unicode(raw,\n                                               strip_encoding_pats=True, resolve_entities=True)[0])\n\n        if testing:\n            import tempfile\n            with tempfile.NamedTemporaryFile(prefix='amazon_results_',\n                                             suffix='.html', delete=False) as f:\n                f.write(raw.encode('utf-8'))\n            print('Downloaded html for results page saved in', f.name)\n\n        matches = []\n        found = '<title>404 - ' not in raw\n\n        if found:\n            try:\n                root = parse_html(raw)\n            except Exception:\n                msg = 'Failed to parse amazon page for query: %r' % query\n                log.exception(msg)\n                raise SearchFailed()\n\n        matches = self.parse_results_page(root, domain)\n\n        return matches, query, domain, None\n    # }}}\n\n    def search_search_engine(self, br, testing, log, abort, title, authors, identifiers, timeout, override_server=None):  # {{{\n        from calibre.ebooks.metadata.sources.update import search_engines_module\n        se = search_engines_module()\n        terms, domain = self.create_query(log, title=title, authors=authors,\n                                          identifiers=identifiers, for_amazon=False)\n        site = self.referrer_for_domain(\n            domain)[len('https://'):].partition('/')[0]\n        matches = []\n        server = override_server or self.server\n        if server == 'bing':\n            urlproc, sfunc = se.bing_url_processor, se.bing_search\n        elif server == 'wayback':\n            urlproc, sfunc = se.wayback_url_processor, se.ddg_search\n        elif server == 'ddg':\n            urlproc, sfunc = se.ddg_url_processor, se.ddg_search\n        elif server == 'google':\n            urlproc, sfunc = se.google_url_processor, se.google_search\n        else:  # auto or unknown\n            urlproc, sfunc = se.google_url_processor, se.google_search\n            # urlproc, sfunc = se.bing_url_processor, se.bing_search\n        try:\n            results, qurl = sfunc(terms, site, log=log, br=br, timeout=timeout)\n        except HTTPError as err:\n            if err.code == 429 and sfunc is se.google_search:\n                log('Got too many requests error from Google, trying via DuckDuckGo')\n                urlproc, sfunc = se.ddg_url_processor, se.ddg_search\n                results, qurl = sfunc(terms, site, log=log, br=br, timeout=timeout)\n            else:\n                raise\n\n        br.set_current_header('Referer', qurl)\n        for result in results:\n            if abort.is_set():\n                return matches, terms, domain, None\n\n            purl = urlparse(result.url)\n            if '/dp/' in purl.path and site in purl.netloc:\n                # We cannot use cached URL as wayback machine no longer caches\n                # amazon and Google and Bing web caches are no longer\n                # accessible.\n                url = result.url\n                if url not in matches:\n                    matches.append(url)\n                if len(matches) >= 3:\n                    break\n            else:\n                log('Skipping non-book result:', result)\n        if not matches:\n            log('No search engine results for terms:', ' '.join(terms))\n            if urlproc is se.google_url_processor:\n                # Google does not cache adult titles\n                log('Trying the bing search engine instead')\n                return self.search_search_engine(br, testing, log, abort, title, authors, identifiers, timeout, 'bing')\n        return matches, terms, domain, urlproc\n    # }}}\n\n    def identify(self, log, result_queue, abort, title=None, authors=None,  # {{{\n                 identifiers={}, timeout=60):\n        '''\n        Note this method will retry without identifiers automatically if no\n        match is found with identifiers.\n        '''\n\n        testing = getattr(self, 'running_a_test', False)\n\n        udata = self._get_book_url(identifiers)\n        br = self.browser\n        log('User-agent:', br.current_user_agent())\n        log('Server:', self.server)\n        if testing:\n            print('User-agent:', br.current_user_agent())\n        if udata is not None and not self.use_search_engine:\n            # Try to directly get details page instead of running a search\n            # Cannot use search engine as the directly constructed URL is\n            # usually redirected to a full URL by amazon, and is therefore\n            # not cached\n            domain, idtype, asin, durl = udata\n            if durl is not None:\n                preparsed_root = parse_details_page(\n                    durl, log, timeout, br, domain)\n                if preparsed_root is not None:\n                    qasin = parse_asin(preparsed_root[1], log, durl)\n                    if qasin == asin:\n                        w = Worker(durl, result_queue, br, log, 0, domain,\n                                   self, testing=testing, preparsed_root=preparsed_root, timeout=timeout)\n                        try:\n                            w.get_details()\n                            return\n                        except Exception:\n                            log.exception(\n                                'get_details failed for url: %r' % durl)\n        func = self.search_search_engine if self.use_search_engine else self.search_amazon\n        try:\n            matches, query, domain, cover_url_processor = func(\n                br, testing, log, abort, title, authors, identifiers, timeout)\n        except SearchFailed:\n            return\n\n        if abort.is_set():\n            return\n\n        if not matches:\n            if identifiers and title and authors:\n                log('No matches found with identifiers, retrying using only'\n                    ' title and authors. Query: %r' % query)\n                time.sleep(1)\n                return self.identify(log, result_queue, abort, title=title,\n                                     authors=authors, timeout=timeout)\n            log.error('No matches found with query: %r' % query)\n            return\n\n        if self.prefs['prefer_kindle_edition']:\n            matches = sort_matches_preferring_kindle_editions(matches)\n\n        workers = [Worker(\n            url, result_queue, br, log, i, domain, self, testing=testing, timeout=timeout,\n            cover_url_processor=cover_url_processor, filter_result=partial(\n                self.filter_result, title, authors, identifiers)) for i, url in enumerate(matches)]\n\n        for w in workers:\n            # Don't send all requests at the same time\n            time.sleep(1)\n            w.start()\n            if abort.is_set():\n                return\n\n        while not abort.is_set():\n            a_worker_is_alive = False\n            for w in workers:\n                w.join(0.2)\n                if abort.is_set():\n                    break\n                if w.is_alive():\n                    a_worker_is_alive = True\n            if not a_worker_is_alive:\n                break\n\n        return None\n    # }}}\n\n    def filter_result(self, title, authors, identifiers, mi, log):  # {{{\n        if not self.use_search_engine:\n            return True\n        if title is not None:\n            import regex\n            only_punctuation_pat = regex.compile(r'^\\p{P}+$')\n\n            def tokenize_title(x):\n                ans = icu_lower(x).replace(\"'\", '').replace('\"', '').rstrip(':')\n                if only_punctuation_pat.match(ans) is not None:\n                    ans = ''\n                return ans\n\n            tokens = {tokenize_title(x) for x in title.split() if len(x) > 3}\n            tokens.discard('')\n            if tokens:\n                result_tokens = {tokenize_title(x) for x in mi.title.split()}\n                result_tokens.discard('')\n                if not tokens.intersection(result_tokens):\n                    log('Ignoring result:', mi.title, 'as its title does not match')\n                    return False\n        if authors:\n            author_tokens = set()\n            for author in authors:\n                author_tokens |= {icu_lower(x) for x in author.split() if len(x) > 2}\n            result_tokens = set()\n            for author in mi.authors:\n                result_tokens |= {icu_lower(x) for x in author.split() if len(x) > 2}\n            if author_tokens and not author_tokens.intersection(result_tokens):\n                log('Ignoring result:', mi.title, 'by', ' & '.join(mi.authors), 'as its author does not match')\n                return False\n        return True\n    # }}}\n\n    def download_cover(self, log, result_queue, abort,  # {{{\n                       title=None, authors=None, identifiers={}, timeout=60, get_best_cover=False):\n        cached_url = self.get_cached_cover_url(identifiers)\n        if cached_url is None:\n            log.info('No cached cover found, running identify')\n            rq = Queue()\n            self.identify(log, rq, abort, title=title, authors=authors,\n                          identifiers=identifiers)\n            if abort.is_set():\n                return\n            results = []\n            while True:\n                try:\n                    results.append(rq.get_nowait())\n                except Empty:\n                    break\n            results.sort(key=self.identify_results_keygen(\n                title=title, authors=authors, identifiers=identifiers))\n            for mi in results:\n                cached_url = self.get_cached_cover_url(mi.identifiers)\n                if cached_url is not None:\n                    break\n        if cached_url is None:\n            log.info('No cover found')\n            return\n\n        if abort.is_set():\n            return\n        log('Downloading cover from:', cached_url)\n        br = self.browser\n        if self.use_search_engine:\n            br = br.clone_browser()\n            br.set_current_header('Referer', self.referrer_for_domain(self.domain))\n        try:\n            time.sleep(1)\n            cdata = br.open_novisit(\n                cached_url, timeout=timeout).read()\n            result_queue.put((self, cdata))\n        except Exception:\n            log.exception('Failed to download cover from:', cached_url)\n    # }}}\n\n\ndef manual_tests(domain, **kw):  # {{{\n    # To run these test use:\n    # calibre-debug -c \"from calibre.ebooks.metadata.sources.amazon import *; manual_tests('com')\"\n    from calibre.ebooks.metadata.sources.test import authors_test, comments_test, isbn_test, series_test, test_identify_plugin, title_test\n    all_tests = {}\n    all_tests['com'] = [  # {{{\n        (  # in title\n            {'title': 'Expert C# 2008 Business Objects',\n             'authors': ['Lhotka']},\n            [title_test('Expert C#'),\n             authors_test(['Rockford Lhotka'])\n             ]\n        ),\n\n        (   # Paperback with series\n            {'identifiers': {'amazon': '1423146786'}},\n            [title_test('Heroes of Olympus', exact=False), series_test('The Heroes of Olympus', 5)]\n        ),\n\n        (   # Kindle edition with series\n            {'identifiers': {'amazon': 'B0085UEQDO'}},\n            [title_test('Three Parts Dead', exact=True),\n             series_test('Craft Sequence', 1)]\n        ),\n\n        (  # + in title and uses id=\"main-image\" for cover\n            {'identifiers': {'amazon': '1933988770'}},\n            [title_test(\n                'C++ Concurrency in Action: Practical Multithreading', exact=True)]\n        ),\n\n\n        (  # Different comments markup, using Book Description section\n            {'identifiers': {'amazon': '0982514506'}},\n            [title_test(\n                \"Griffin's Destiny\",\n                exact=True),\n             comments_test('Jelena'), comments_test('Ashinji'),\n             ]\n        ),\n\n        (   # New search results page markup (Dec 2024)\n            {'title': 'Come si scrive un articolo medico-scientifico'},\n            [title_test('Come si scrive un articolo medico-scientifico', exact=True)]\n        ),\n\n        (  # No specific problems\n            {'identifiers': {'isbn': '0743273567'}},\n            [title_test('the great gatsby'),\n             authors_test(['f. Scott Fitzgerald'])]\n        ),\n\n    ]\n\n    # }}}\n\n    all_tests['de'] = [  # {{{\n        # series\n        (\n            {'identifiers': {'isbn': '3499275120'}},\n            [title_test('Vespasian: Das Schwert des Tribuns: Historischer Roman',\n                        exact=False), authors_test(['Robert Fabbri']), series_test('Die Vespasian-Reihe', 1)\n             ]\n\n        ),\n\n        (  # umlaut in title/authors\n            {'title': 'Flüsternde Wälder',\n             'authors': ['Nicola Förg']},\n            [title_test('Flüsternde Wälder'),\n             authors_test(['Nicola Förg'], subset=True)\n             ]\n        ),\n\n        (\n            {'identifiers': {'isbn': '9783453314979'}},\n            [title_test('Die letzten Wächter: Roman',\n                        exact=False), authors_test(['Sergej Lukianenko'])\n             ]\n\n        ),\n\n        (\n            {'identifiers': {'isbn': '3548283519'}},\n            [title_test('Wer Wind Sät: Der Fünfte Fall Für Bodenstein Und Kirchhoff',\n                        exact=False), authors_test(['Nele Neuhaus'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['it'] = [  # {{{\n        (\n            {'identifiers': {'isbn': '8838922195'}},\n            [title_test('La briscola in cinque',\n                        exact=True), authors_test(['Marco Malvaldi'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['fr'] = [  # {{{\n        (\n            {'identifiers': {'amazon_fr': 'B07L7ST4RS'}},\n            [title_test('Le secret de Lola', exact=True),\n                authors_test(['Amélie BRIZIO'])\n            ]\n        ),\n        (\n            {'identifiers': {'isbn': '2221116798'}},\n            [title_test(\"L'étrange voyage de Monsieur Daldry\",\n                        exact=True), authors_test(['Marc Levy'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['es'] = [  # {{{\n        (\n            {'identifiers': {'isbn': '8483460831'}},\n            [title_test('Tiempos Interesantes',\n                        exact=False), authors_test(['Terry Pratchett'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['se'] = [  # {{{\n        (\n            {'identifiers': {'isbn': '9780552140287'}},\n            [title_test('Men At Arms: A Discworld Novel: 14',\n                        exact=False), authors_test(['Terry Pratchett'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['jp'] = [  # {{{\n        (  # Adult filtering test\n            {'identifiers': {'isbn': '4799500066'}},\n            [title_test('Bitch Trap'), ]\n        ),\n\n        (  # isbn -> title, authors\n            {'identifiers': {'isbn': '9784101302720'}},\n            [title_test('精霊の守り人',\n                        exact=True), authors_test(['上橋 菜穂子'])\n             ]\n        ),\n        (  # title, authors -> isbn (will use Shift_JIS encoding in query.)\n            {'title': '考えない練習',\n             'authors': ['小池 龍之介']},\n            [isbn_test('9784093881067'), ]\n        ),\n    ]  # }}}\n\n    all_tests['br'] = [  # {{{\n        (\n            {'title': 'A Ascensão da Sombra'},\n            [title_test('A Ascensão da Sombra'), authors_test(['Robert Jordan'])]\n        ),\n\n        (\n            {'title': 'Guerra dos Tronos'},\n            [title_test('A Guerra dos Tronos. As Crônicas de Gelo e Fogo - Livro 1'), authors_test(['George R. R. Martin'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['nl'] = [  # {{{\n        (\n            {'title': 'Freakonomics'},\n            [title_test('Freakonomics',\n                        exact=True), authors_test(['Steven Levitt & Stephen Dubner & R. Kuitenbrouwer & O. Brenninkmeijer & A. van Den Berg'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['cn'] = [  # {{{\n        (\n            {'identifiers': {'isbn': '9787115369512'}},\n            [title_test('若为自由故 自由软件之父理查德斯托曼传', exact=True),\n             authors_test(['[美]sam Williams', '邓楠,李凡希'])]\n        ),\n        (\n            {'title': '爱上Raspberry Pi'},\n            [title_test('爱上Raspberry Pi',\n                        exact=True), authors_test(['Matt Richardson', 'Shawn Wallace', '李凡希'])\n             ]\n\n        ),\n    ]  # }}}\n\n    all_tests['ca'] = [  # {{{\n        (   # Paperback with series\n            {'identifiers': {'isbn': '9781623808747'}},\n            [title_test('Parting Shot', exact=True),\n             authors_test(['Mary Calmes'])]\n        ),\n        (  # in title\n            {'title': 'Expert C# 2008 Business Objects',\n             'authors': ['Lhotka']},\n            [title_test('Expert C# 2008 Business Objects'),\n             authors_test(['Rockford Lhotka'])]\n        ),\n        (  # noscript description\n            {'identifiers': {'amazon_ca': '162380874X'}},\n            [title_test('Parting Shot', exact=True), authors_test(['Mary Calmes'])\n             ]\n        ),\n    ]  # }}}\n\n    all_tests['in'] = [  # {{{\n        (   # Paperback with series\n            {'identifiers': {'amazon_in': '1423146786'}},\n            [title_test('The Heroes of Olympus, Book Five The Blood of Olympus', exact=True)]\n        ),\n    ]  # }}}\n\n    def do_test(domain, start=0, stop=None, server='auto'):\n        tests = all_tests[domain]\n        if stop is None:\n            stop = len(tests)\n        tests = tests[start:stop]\n        test_identify_plugin(Amazon.name, tests, modify_plugin=lambda p: (\n            setattr(p, 'testing_domain', domain),\n            setattr(p, 'touched_fields', p.touched_fields - {'tags'}),\n            setattr(p, 'testing_server', server),\n        ))\n\n    do_test(domain, **kw)\n# }}}\n",    "big_book_search": "#!/usr/bin/env python\n# vim:fileencoding=UTF-8\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\n__license__   = 'GPL v3'\n__copyright__ = '2013, Kovid Goyal <kovid@kovidgoyal.net>'\n__docformat__ = 'restructuredtext en'\n\nfrom calibre.ebooks.metadata.sources.base import Option, Source\n\n\ndef get_urls(br, tokens):\n    from urllib.parse import quote_plus\n\n    from html5_parser import parse\n    escaped = (quote_plus(x) for x in tokens if x and x.strip())\n    q = '+'.join(escaped)\n    url = 'https://bigbooksearch.com/please-dont-scrape-my-site-you-will-put-my-api-key-over-the-usage-limit-and-the-site-will-break/books/'+q\n    raw = br.open(url).read()\n    root = parse(raw.decode('utf-8'))\n    urls = [i.get('src') for i in root.xpath('//img[@src]')]\n    return urls\n\n\nclass BigBookSearch(Source):\n\n    name = 'Big Book Search'\n    version = (1, 0, 1)\n    minimum_calibre_version = (2, 80, 0)\n    description = _('Downloads multiple book covers from Amazon. Useful to find alternate covers.')\n    capabilities = frozenset(['cover'])\n    can_get_multiple_covers = True\n    options = (Option('max_covers', 'number', 5, _('Maximum number of covers to get'),\n                      _('The maximum number of covers to process from the search result')),\n    )\n    supports_gzip_transfer_encoding = True\n\n    def download_cover(self, log, result_queue, abort,\n            title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):\n        if not title:\n            return\n        br = self.browser\n        tokens = tuple(self.get_title_tokens(title)) + tuple(self.get_author_tokens(authors))\n        urls = get_urls(br, tokens)\n        self.download_multiple_covers(title, authors, urls, get_best_cover, timeout, result_queue, abort, log)\n\n\ndef test():\n    import pprint\n\n    from calibre import browser\n    br = browser()\n    urls = get_urls(br, ['consider', 'phlebas', 'banks'])\n    pprint.pprint(urls)\n\n\nif __name__ == '__main__':\n    test()\n", -  "edelweiss": "#!/usr/bin/env python\n# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\n__license__   = 'GPL v3'\n__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'\n__docformat__ = 'restructuredtext en'\n\nimport re\nimport time\nfrom threading import Thread\n\ntry:\n    from queue import Empty, Queue\nexcept ImportError:\n    from Queue import Empty, Queue\n\nfrom calibre import as_unicode, random_user_agent\nfrom calibre.ebooks.metadata import check_isbn\nfrom calibre.ebooks.metadata.sources.base import Source\n\n\ndef clean_html(raw):\n    from calibre.ebooks.chardet import xml_to_unicode\n    from calibre.utils.cleantext import clean_ascii_chars\n    return clean_ascii_chars(xml_to_unicode(raw, strip_encoding_pats=True,\n                                resolve_entities=True, assume_utf8=True)[0])\n\n\ndef parse_html(raw):\n    raw = clean_html(raw)\n    from html5_parser import parse\n    return parse(raw)\n\n\ndef astext(node):\n    from lxml import etree\n    return etree.tostring(node, method='text', encoding='unicode',\n                          with_tail=False).strip()\n\n\nclass Worker(Thread):  # {{{\n\n    def __init__(self, basic_data, relevance, result_queue, br, timeout, log, plugin):\n        Thread.__init__(self)\n        self.daemon = True\n        self.basic_data = basic_data\n        self.br, self.log, self.timeout = br, log, timeout\n        self.result_queue, self.plugin, self.sku = result_queue, plugin, self.basic_data['sku']\n        self.relevance = relevance\n\n    def run(self):\n        url = ('https://www.edelweiss.plus/GetTreelineControl.aspx?controlName=/uc/product/two_Enhanced.ascx&'\n        'sku={0}&idPrefix=content_1_{0}&mode=0'.format(self.sku))\n        try:\n            raw = self.br.open_novisit(url, timeout=self.timeout).read()\n        except:\n            self.log.exception('Failed to load comments page: %r'%url)\n            return\n\n        try:\n            mi = self.parse(raw)\n            mi.source_relevance = self.relevance\n            self.plugin.clean_downloaded_metadata(mi)\n            self.result_queue.put(mi)\n        except:\n            self.log.exception('Failed to parse details for sku: %s'%self.sku)\n\n    def parse(self, raw):\n        from calibre.ebooks.metadata.book.base import Metadata\n        from calibre.utils.date import UNDEFINED_DATE\n        root = parse_html(raw)\n        mi = Metadata(self.basic_data['title'], self.basic_data['authors'])\n\n        # Identifiers\n        if self.basic_data['isbns']:\n            mi.isbn = self.basic_data['isbns'][0]\n        mi.set_identifier('edelweiss', self.sku)\n\n        # Tags\n        if self.basic_data['tags']:\n            mi.tags = self.basic_data['tags']\n            mi.tags = [t[1:].strip() if t.startswith('&') else t for t in mi.tags]\n\n        # Publisher\n        mi.publisher = self.basic_data['publisher']\n\n        # Pubdate\n        if self.basic_data['pubdate'] and self.basic_data['pubdate'].year != UNDEFINED_DATE:\n            mi.pubdate = self.basic_data['pubdate']\n\n        # Rating\n        if self.basic_data['rating']:\n            mi.rating = self.basic_data['rating']\n\n        # Comments\n        comments = ''\n        for cid in ('summary', 'contributorbio', 'quotes_reviews'):\n            cid = 'desc_{}{}-content'.format(cid, self.sku)\n            div = root.xpath('//*[@id=\"{}\"]'.format(cid))\n            if div:\n                comments += self.render_comments(div[0])\n        if comments:\n            mi.comments = comments\n\n        mi.has_cover = self.plugin.cached_identifier_to_cover_url(self.sku) is not None\n        return mi\n\n    def render_comments(self, desc):\n        from lxml import etree\n\n        from calibre.library.comments import sanitize_comments_html\n        for c in desc.xpath('descendant::noscript'):\n            c.getparent().remove(c)\n        for a in desc.xpath('descendant::a[@href]'):\n            del a.attrib['href']\n            a.tag = 'span'\n        desc = etree.tostring(desc, method='html', encoding='unicode').strip()\n\n        # remove all attributes from tags\n        desc = re.sub(r'<([a-zA-Z0-9]+)\\s[^>]+>', r'<\\1>', desc)\n        # Collapse whitespace\n        # desc = re.sub(r'\\n+', '\\n', desc)\n        # desc = re.sub(r' +', ' ', desc)\n        # Remove comments\n        desc = re.sub(r'(?s)<!--.*?-->', '', desc)\n        return sanitize_comments_html(desc)\n# }}}\n\n\ndef get_basic_data(browser, log, *skus):\n    from mechanize import Request\n\n    from calibre.utils.date import parse_only_date\n    zeroes = ','.join('0' for sku in skus)\n    data = {\n            'skus': ','.join(skus),\n            'drc': zeroes,\n            'startPosition': '0',\n            'sequence': '1',\n            'selected': zeroes,\n            'itemID': '0',\n            'orderID': '0',\n            'mailingID': '',\n            'tContentWidth': '926',\n            'originalOrder': ','.join(type('')(i) for i in range(len(skus))),\n            'selectedOrderID': '0',\n            'selectedSortColumn': '0',\n            'listType': '1',\n            'resultType': '32',\n            'blockView': '1',\n    }\n    items_data_url = 'https://www.edelweiss.plus/GetTreelineControl.aspx?controlName=/uc/listviews/ListView_Title_Multi.ascx'\n    req = Request(items_data_url, data)\n    response = browser.open_novisit(req)\n    raw = response.read()\n    root = parse_html(raw)\n    for item in root.xpath('//div[@data-priority]'):\n        row = item.getparent().getparent()\n        sku = item.get('id').split('-')[-1]\n        isbns = [x.strip() for x in row.xpath('descendant::*[contains(@class, \"pev_sku\")]/text()')[0].split(',') if check_isbn(x.strip())]\n        isbns.sort(key=len, reverse=True)\n        try:\n            tags = [x.strip() for x in astext(row.xpath('descendant::*[contains(@class, \"pev_categories\")]')[0]).split('/')]\n        except IndexError:\n            tags = []\n        rating = 0\n        for bar in row.xpath('descendant::*[contains(@class, \"bgdColorCommunity\")]/@style'):\n            m = re.search(r'width: (\\d+)px;.*max-width: (\\d+)px', bar)\n            if m is not None:\n                rating = float(m.group(1)) / float(m.group(2))\n                break\n        try:\n            pubdate = parse_only_date(astext(row.xpath('descendant::*[contains(@class, \"pev_shipDate\")]')[0]\n                ).split(':')[-1].split(u'\\xa0')[-1].strip(), assume_utc=True)\n        except Exception:\n            log.exception('Error parsing published date')\n            pubdate = None\n        authors = []\n        for x in [x.strip() for x in row.xpath('descendant::*[contains(@class, \"pev_contributor\")]/@title')]:\n            authors.extend(a.strip() for a in x.split(','))\n        entry = {\n                'sku': sku,\n                'cover': row.xpath('descendant::img/@src')[0].split('?')[0],\n                'publisher': astext(row.xpath('descendant::*[contains(@class, \"headerPublisher\")]')[0]),\n                'title': astext(row.xpath('descendant::*[@id=\"title_{}\"]'.format(sku))[0]),\n                'authors': authors,\n                'isbns': isbns,\n                'tags': tags,\n                'pubdate': pubdate,\n                'format': ' '.join(row.xpath('descendant::*[contains(@class, \"pev_format\")]/text()')).strip(),\n                'rating': rating,\n        }\n        if entry['cover'].startswith('/'):\n            entry['cover'] = None\n        yield entry\n\n\nclass Edelweiss(Source):\n\n    name = 'Edelweiss'\n    version = (2, 0, 1)\n    minimum_calibre_version = (3, 6, 0)\n    description = _('Downloads metadata and covers from Edelweiss - A catalog updated by book publishers')\n\n    capabilities = frozenset(['identify', 'cover'])\n    touched_fields = frozenset([\n        'title', 'authors', 'tags', 'pubdate', 'comments', 'publisher',\n        'identifier:isbn', 'identifier:edelweiss', 'rating'])\n    supports_gzip_transfer_encoding = True\n    has_html_comments = True\n\n    @property\n    def user_agent(self):\n        # Pass in an index to random_user_agent() to test with a particular\n        # user agent\n        return random_user_agent(allow_ie=False)\n\n    def _get_book_url(self, sku):\n        if sku:\n            return 'https://www.edelweiss.plus/#sku={}&page=1'.format(sku)\n\n    def get_book_url(self, identifiers):  # {{{\n        sku = identifiers.get('edelweiss', None)\n        if sku:\n            return 'edelweiss', sku, self._get_book_url(sku)\n\n    # }}}\n\n    def get_cached_cover_url(self, identifiers):  # {{{\n        sku = identifiers.get('edelweiss', None)\n        if not sku:\n            isbn = identifiers.get('isbn', None)\n            if isbn is not None:\n                sku = self.cached_isbn_to_identifier(isbn)\n        return self.cached_identifier_to_cover_url(sku)\n    # }}}\n\n    def create_query(self, log, title=None, authors=None, identifiers={}):\n        try:\n            from urllib.parse import urlencode\n        except ImportError:\n            from urllib import urlencode\n        import time\n        BASE_URL = ('https://www.edelweiss.plus/GetTreelineControl.aspx?'\n        'controlName=/uc/listviews/controls/ListView_data.ascx&itemID=0&resultType=32&dashboardType=8&itemType=1&dataType=products&keywordSearch&')\n        keywords = []\n        isbn = check_isbn(identifiers.get('isbn', None))\n        if isbn is not None:\n            keywords.append(isbn)\n        elif title:\n            title_tokens = list(self.get_title_tokens(title))\n            if title_tokens:\n                keywords.extend(title_tokens)\n            author_tokens = self.get_author_tokens(authors, only_first_author=True)\n            if author_tokens:\n                keywords.extend(author_tokens)\n        if not keywords:\n            return None\n        params = {\n            'q': (' '.join(keywords)).encode('utf-8'),\n            '_': type('')(int(time.time()))\n        }\n        return BASE_URL+urlencode(params)\n\n    # }}}\n\n    def identify(self, log, result_queue, abort, title=None, authors=None,  # {{{\n            identifiers={}, timeout=30):\n        import json\n\n        br = self.browser\n        br.addheaders = [\n            ('Referer', 'https://www.edelweiss.plus/'),\n            ('X-Requested-With', 'XMLHttpRequest'),\n            ('Cache-Control', 'no-cache'),\n            ('Pragma', 'no-cache'),\n        ]\n        if 'edelweiss' in identifiers:\n            items = [identifiers['edelweiss']]\n        else:\n            log.error('Currently Edelweiss returns random books for search queries')\n            return\n            query = self.create_query(log, title=title, authors=authors,\n                    identifiers=identifiers)\n            if not query:\n                log.error('Insufficient metadata to construct query')\n                return\n            log('Using query URL:', query)\n            try:\n                raw = br.open(query, timeout=timeout).read().decode('utf-8')\n            except Exception as e:\n                log.exception('Failed to make identify query: %r'%query)\n                return as_unicode(e)\n            items = re.search(r'window[.]items\\s*=\\s*(.+?);', raw)\n            if items is None:\n                log.error('Failed to get list of matching items')\n                log.debug('Response text:')\n                log.debug(raw)\n                return\n            items = json.loads(items.group(1))\n\n        if (not items and identifiers and title and authors and\n                not abort.is_set()):\n            return self.identify(log, result_queue, abort, title=title,\n                    authors=authors, timeout=timeout)\n\n        if not items:\n            return\n\n        workers = []\n        items = items[:5]\n        for i, item in enumerate(get_basic_data(self.browser, log, *items)):\n            sku = item['sku']\n            for isbn in item['isbns']:\n                self.cache_isbn_to_identifier(isbn, sku)\n            if item['cover']:\n                self.cache_identifier_to_cover_url(sku, item['cover'])\n            fmt = item['format'].lower()\n            if 'audio' in fmt or 'mp3' in fmt:\n                continue  # Audio-book, ignore\n            workers.append(Worker(item, i, result_queue, br.clone_browser(), timeout, log, self))\n\n        if not workers:\n            return\n\n        for w in workers:\n            w.start()\n            # Don't send all requests at the same time\n            time.sleep(0.1)\n\n        while not abort.is_set():\n            a_worker_is_alive = False\n            for w in workers:\n                w.join(0.2)\n                if abort.is_set():\n                    break\n                if w.is_alive():\n                    a_worker_is_alive = True\n            if not a_worker_is_alive:\n                break\n\n    # }}}\n\n    def download_cover(self, log, result_queue, abort,  # {{{\n            title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):\n        cached_url = self.get_cached_cover_url(identifiers)\n        if cached_url is None:\n            log.info('No cached cover found, running identify')\n            rq = Queue()\n            self.identify(log, rq, abort, title=title, authors=authors,\n                    identifiers=identifiers)\n            if abort.is_set():\n                return\n            results = []\n            while True:\n                try:\n                    results.append(rq.get_nowait())\n                except Empty:\n                    break\n            results.sort(key=self.identify_results_keygen(\n                title=title, authors=authors, identifiers=identifiers))\n            for mi in results:\n                cached_url = self.get_cached_cover_url(mi.identifiers)\n                if cached_url is not None:\n                    break\n        if cached_url is None:\n            log.info('No cover found')\n            return\n\n        if abort.is_set():\n            return\n        br = self.browser\n        log('Downloading cover from:', cached_url)\n        try:\n            cdata = br.open_novisit(cached_url, timeout=timeout).read()\n            result_queue.put((self, cdata))\n        except:\n            log.exception('Failed to download cover from:', cached_url)\n    # }}}\n\n\nif __name__ == '__main__':\n    from calibre.ebooks.metadata.sources.test import authors_test, comments_test, pubdate_test, test_identify_plugin, title_test\n    tests = [\n        (  # A title and author search\n         {'title': \"The Husband's Secret\", 'authors':['Liane Moriarty']},\n         [title_test(\"The Husband's Secret\", exact=True),\n          authors_test(['Liane Moriarty'])]\n        ),\n\n        (  # An isbn present in edelweiss\n         {'identifiers':{'isbn': '9780312621360'}, },\n         [title_test('Flame: A Sky Chasers Novel', exact=True),\n          authors_test(['Amy Kathleen Ryan'])]\n        ),\n\n        # Multiple authors and two part title and no general description\n        ({'identifiers':{'edelweiss':'0321180607'}},\n        [title_test('XQuery From the Experts: A Guide to the W3C XML Query Language', exact=True),\n         authors_test([\n            'Howard Katz', 'Don Chamberlin', 'Denise Draper', 'Mary Fernandez',\n            'Michael Kay', 'Jonathan Robie', 'Michael Rys', 'Jerome Simeon',\n            'Jim Tivy', 'Philip Wadler']),\n         pubdate_test(2003, 8, 22),\n         comments_test('Jérôme Siméon'), lambda mi: bool(mi.comments and 'No title summary' not in mi.comments)\n        ]),\n    ]\n    start, stop = 0, len(tests)\n\n    tests = tests[start:stop]\n    test_identify_plugin(Edelweiss.name, tests)\n", -  "google": "#!/usr/bin/env python\n# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai\n# License: GPLv3 Copyright: 2011, Kovid Goyal <kovid at kovidgoyal.net>\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport hashlib\nimport os\nimport re\nimport sys\nimport tempfile\nimport time\n\nimport regex\n\ntry:\n    from queue import Empty, Queue\nexcept ImportError:\n    from Queue import Empty, Queue\n\nfrom calibre import as_unicode, prepare_string_for_xml, replace_entities\nfrom calibre.ebooks.chardet import xml_to_unicode\nfrom calibre.ebooks.metadata import authors_to_string, check_isbn\nfrom calibre.ebooks.metadata.book.base import Metadata\nfrom calibre.ebooks.metadata.sources.base import Source\nfrom calibre.utils.cleantext import clean_ascii_chars\nfrom calibre.utils.localization import canonicalize_lang\n\nNAMESPACES = {\n    'openSearch': 'http://a9.com/-/spec/opensearchrss/1.0/',\n    'atom': 'http://www.w3.org/2005/Atom',\n    'dc': 'http://purl.org/dc/terms',\n    'gd': 'http://schemas.google.com/g/2005'\n}\n\n\ndef pretty_google_books_comments(raw):\n    raw = replace_entities(raw)\n    # Paragraphs in the comments are removed but whatever software googl uses\n    # to do this does not insert a space so we often find the pattern\n    # word.Capital in the comments which can be used to find paragraph markers.\n    parts = []\n    for x in re.split(r'([a-z)\"”])(\\.)([A-Z(\"“])', raw):\n        if x == '.':\n            parts.append('.</p>\\n\\n<p>')\n        else:\n            parts.append(prepare_string_for_xml(x))\n    raw = '<p>' + ''.join(parts) + '</p>'\n    return raw\n\n\ndef get_details(browser, url, timeout):  # {{{\n    try:\n        raw = browser.open_novisit(url, timeout=timeout).read()\n    except Exception as e:\n        gc = getattr(e, 'getcode', lambda: -1)\n        if gc() != 403:\n            raise\n        # Google is throttling us, wait a little\n        time.sleep(2)\n        raw = browser.open_novisit(url, timeout=timeout).read()\n\n    return raw\n# }}}\n\n\nxpath_cache = {}\n\n\ndef XPath(x):\n    ans = xpath_cache.get(x)\n    if ans is None:\n        from lxml import etree\n        ans = xpath_cache[x] = etree.XPath(x, namespaces=NAMESPACES)\n    return ans\n\n\ndef to_metadata(browser, log, entry_, timeout, running_a_test=False):  # {{{\n    from lxml import etree\n\n    # total_results  = XPath('//openSearch:totalResults')\n    # start_index    = XPath('//openSearch:startIndex')\n    # items_per_page = XPath('//openSearch:itemsPerPage')\n    entry = XPath('//atom:entry')\n    entry_id = XPath('descendant::atom:id')\n    url = XPath('descendant::atom:link[@rel=\"self\"]/@href')\n    creator = XPath('descendant::dc:creator')\n    identifier = XPath('descendant::dc:identifier')\n    title = XPath('descendant::dc:title')\n    date = XPath('descendant::dc:date')\n    publisher = XPath('descendant::dc:publisher')\n    subject = XPath('descendant::dc:subject')\n    description = XPath('descendant::dc:description')\n    language = XPath('descendant::dc:language')\n\n    # print(etree.tostring(entry_, pretty_print=True))\n\n    def get_text(extra, x):\n        try:\n            ans = x(extra)\n            if ans:\n                ans = ans[0].text\n                if ans and ans.strip():\n                    return ans.strip()\n        except:\n            log.exception('Programming error:')\n        return None\n\n    def get_extra_details():\n        raw = get_details(browser, details_url, timeout)\n        if running_a_test:\n            with open(os.path.join(tempfile.gettempdir(), 'Google-' + details_url.split('/')[-1] + '.xml'), 'wb') as f:\n                f.write(raw)\n                print('Book details saved to:', f.name, file=sys.stderr)\n        feed = etree.fromstring(\n            xml_to_unicode(clean_ascii_chars(raw), strip_encoding_pats=True)[0],\n            parser=etree.XMLParser(recover=True, no_network=True, resolve_entities=False)\n        )\n        return entry(feed)[0]\n\n    if isinstance(entry_, str):\n        google_id = entry_\n        details_url = 'https://www.google.com/books/feeds/volumes/' + google_id\n        extra = get_extra_details()\n        title_ = ': '.join([x.text for x in title(extra)]).strip()\n        authors = [x.text.strip() for x in creator(extra) if x.text]\n    else:\n        id_url = entry_id(entry_)[0].text\n        google_id = id_url.split('/')[-1]\n        details_url = url(entry_)[0]\n        title_ = ': '.join([x.text for x in title(entry_)]).strip()\n        authors = [x.text.strip() for x in creator(entry_) if x.text]\n        if not id_url or not title:\n            # Silently discard this entry\n            return None\n        extra = None\n\n    if not authors:\n        authors = [_('Unknown')]\n    if not title:\n        return None\n    if extra is None:\n        extra = get_extra_details()\n    mi = Metadata(title_, authors)\n    mi.identifiers = {'google': google_id}\n    mi.comments = get_text(extra, description)\n    lang = canonicalize_lang(get_text(extra, language))\n    if lang:\n        mi.language = lang\n    mi.publisher = get_text(extra, publisher)\n\n    # ISBN\n    isbns = []\n    for x in identifier(extra):\n        t = type('')(x.text).strip()\n        if t[:5].upper() in ('ISBN:', 'LCCN:', 'OCLC:'):\n            if t[:5].upper() == 'ISBN:':\n                t = check_isbn(t[5:])\n                if t:\n                    isbns.append(t)\n    if isbns:\n        mi.isbn = sorted(isbns, key=len)[-1]\n    mi.all_isbns = isbns\n\n    # Tags\n    try:\n        btags = [x.text for x in subject(extra) if x.text]\n        tags = []\n        for t in btags:\n            atags = [y.strip() for y in t.split('/')]\n            for tag in atags:\n                if tag not in tags:\n                    tags.append(tag)\n    except:\n        log.exception('Failed to parse tags:')\n        tags = []\n    if tags:\n        mi.tags = [x.replace(',', ';') for x in tags]\n\n    # pubdate\n    pubdate = get_text(extra, date)\n    if pubdate:\n        from calibre.utils.date import parse_date, utcnow\n        try:\n            default = utcnow().replace(day=15)\n            mi.pubdate = parse_date(pubdate, assume_utc=True, default=default)\n        except:\n            log.error('Failed to parse pubdate %r' % pubdate)\n\n    # Cover\n    mi.has_google_cover = None\n    for x in extra.xpath(\n        '//*[@href and @rel=\"http://schemas.google.com/books/2008/thumbnail\"]'\n    ):\n        mi.has_google_cover = x.get('href')\n        break\n\n    return mi\n\n# }}}\n\n\nclass GoogleBooks(Source):\n\n    name = 'Google'\n    version = (1, 1, 2)\n    minimum_calibre_version = (2, 80, 0)\n    description = _('Downloads metadata and covers from Google Books')\n\n    capabilities = frozenset({'identify'})\n    touched_fields = frozenset({\n        'title', 'authors', 'tags', 'pubdate', 'comments', 'publisher',\n        'identifier:isbn', 'identifier:google', 'languages'\n    })\n    supports_gzip_transfer_encoding = True\n    cached_cover_url_is_reliable = False\n\n    GOOGLE_COVER = 'https://books.google.com/books?id=%s&printsec=frontcover&img=1'\n\n    DUMMY_IMAGE_MD5 = frozenset(\n        ('0de4383ebad0adad5eeb8975cd796657', 'a64fa89d7ebc97075c1d363fc5fea71f')\n    )\n\n    def get_book_url(self, identifiers):  # {{{\n        goog = identifiers.get('google', None)\n        if goog is not None:\n            return ('google', goog, 'https://books.google.com/books?id=%s' % goog)\n    # }}}\n\n    def id_from_url(self, url):  # {{{\n        from polyglot.urllib import parse_qs, urlparse\n        purl = urlparse(url)\n        if purl.netloc == 'books.google.com':\n            q = parse_qs(purl.query)\n            gid = q.get('id')\n            if gid:\n                return 'google', gid[0]\n    # }}}\n\n    def create_query(self, title=None, authors=None, identifiers={}, capitalize_isbn=False):  # {{{\n        try:\n            from urllib.parse import urlencode\n        except ImportError:\n            from urllib import urlencode\n        BASE_URL = 'https://books.google.com/books/feeds/volumes?'\n        isbn = check_isbn(identifiers.get('isbn', None))\n        q = ''\n        if isbn is not None:\n            q += ('ISBN:' if capitalize_isbn else 'isbn:') + isbn\n        elif title or authors:\n\n            def build_term(prefix, parts):\n                return ' '.join('in' + prefix + ':' + x for x in parts)\n\n            title_tokens = list(self.get_title_tokens(title))\n            if title_tokens:\n                q += build_term('title', title_tokens)\n            author_tokens = list(self.get_author_tokens(authors, only_first_author=True))\n            if author_tokens:\n                q += ('+' if q else '') + build_term('author', author_tokens)\n\n        if not q:\n            return None\n        if not isinstance(q, bytes):\n            q = q.encode('utf-8')\n        return BASE_URL + urlencode({\n            'q': q,\n            'max-results': 20,\n            'start-index': 1,\n            'min-viewability': 'none',\n        })\n\n    # }}}\n\n    def download_cover(  # {{{\n        self,\n        log,\n        result_queue,\n        abort,\n        title=None,\n        authors=None,\n        identifiers={},\n        timeout=30,\n        get_best_cover=False\n    ):\n        cached_url = self.get_cached_cover_url(identifiers)\n        if cached_url is None:\n            log.info('No cached cover found, running identify')\n            rq = Queue()\n            self.identify(\n                log,\n                rq,\n                abort,\n                title=title,\n                authors=authors,\n                identifiers=identifiers\n            )\n            if abort.is_set():\n                return\n            results = []\n            while True:\n                try:\n                    results.append(rq.get_nowait())\n                except Empty:\n                    break\n            results.sort(\n                key=self.identify_results_keygen(\n                    title=title, authors=authors, identifiers=identifiers\n                )\n            )\n            for mi in results:\n                cached_url = self.get_cached_cover_url(mi.identifiers)\n                if cached_url is not None:\n                    break\n        if cached_url is None:\n            log.info('No cover found')\n            return\n\n        br = self.browser\n        for candidate in (0, 1):\n            if abort.is_set():\n                return\n            url = cached_url + '&zoom={}'.format(candidate)\n            log('Downloading cover from:', cached_url)\n            try:\n                cdata = br.open_novisit(url, timeout=timeout).read()\n                if cdata:\n                    if hashlib.md5(cdata).hexdigest() in self.DUMMY_IMAGE_MD5:\n                        log.warning('Google returned a dummy image, ignoring')\n                    else:\n                        result_queue.put((self, cdata))\n                        break\n            except Exception:\n                log.exception('Failed to download cover from:', cached_url)\n\n    # }}}\n\n    def get_cached_cover_url(self, identifiers):  # {{{\n        url = None\n        goog = identifiers.get('google', None)\n        if goog is None:\n            isbn = identifiers.get('isbn', None)\n            if isbn is not None:\n                goog = self.cached_isbn_to_identifier(isbn)\n        if goog is not None:\n            url = self.cached_identifier_to_cover_url(goog)\n\n        return url\n\n    # }}}\n\n    def postprocess_downloaded_google_metadata(self, ans, relevance=0):  # {{{\n        if not isinstance(ans, Metadata):\n            return ans\n        ans.source_relevance = relevance\n        goog = ans.identifiers['google']\n        for isbn in getattr(ans, 'all_isbns', []):\n            self.cache_isbn_to_identifier(isbn, goog)\n        if getattr(ans, 'has_google_cover', False):\n            self.cache_identifier_to_cover_url(goog, self.GOOGLE_COVER % goog)\n        if ans.comments:\n            ans.comments = pretty_google_books_comments(ans.comments)\n        self.clean_downloaded_metadata(ans)\n        return ans\n    # }}}\n\n    def get_all_details(  # {{{\n        self,\n        br,\n        log,\n        entries,\n        abort,\n        result_queue,\n        timeout\n    ):\n        from lxml import etree\n        for relevance, i in enumerate(entries):\n            try:\n                ans = self.postprocess_downloaded_google_metadata(to_metadata(br, log, i, timeout, self.running_a_test), relevance)\n                if isinstance(ans, Metadata):\n                    result_queue.put(ans)\n            except Exception:\n                log.exception(\n                    'Failed to get metadata for identify entry:', etree.tostring(i)\n                )\n            if abort.is_set():\n                break\n\n    # }}}\n\n    def identify_via_web_search(  # {{{\n        self,\n        log,\n        result_queue,\n        abort,\n        title=None,\n        authors=None,\n        identifiers={},\n        timeout=30\n    ):\n        from calibre.utils.filenames import ascii_text\n        isbn = check_isbn(identifiers.get('isbn', None))\n        q = []\n        strip_punc_pat = regex.compile(r'[\\p{C}|\\p{M}|\\p{P}|\\p{S}|\\p{Z}]+', regex.UNICODE)\n        google_ids = []\n        check_tokens = set()\n        has_google_id = 'google' in identifiers\n\n        def to_check_tokens(*tokens):\n            for t in tokens:\n                if len(t) < 3:\n                    continue\n                t = t.lower()\n                if t in ('and', 'not', 'the'):\n                    continue\n                yield ascii_text(strip_punc_pat.sub('', t))\n\n        if has_google_id:\n            google_ids.append(identifiers['google'])\n        elif isbn is not None:\n            q.append(isbn)\n        elif title or authors:\n            title_tokens = list(self.get_title_tokens(title))\n            if title_tokens:\n                q += title_tokens\n                check_tokens |= set(to_check_tokens(*title_tokens))\n            author_tokens = list(self.get_author_tokens(authors, only_first_author=True))\n            if author_tokens:\n                q += author_tokens\n                check_tokens |= set(to_check_tokens(*author_tokens))\n        if not q and not google_ids:\n            return None\n        from calibre.ebooks.metadata.sources.update import search_engines_module\n        se = search_engines_module()\n        br = se.google_specialize_browser(se.browser())\n        if not has_google_id:\n            url = se.google_format_query(q, site='books.google.com')\n            log('Making query:', url)\n            r = []\n            root = se.query(br, url, 'google', timeout=timeout, save_raw=r.append)\n            pat = re.compile(r'id=([^&]+)')\n            for q in se.google_parse_results(root, r[0], log=log, ignore_uncached=False):\n                m = pat.search(q.url)\n                if m is None or not q.url.startswith('https://books.google'):\n                    continue\n                google_ids.append(m.group(1))\n\n        if not google_ids and isbn and (title or authors):\n            return self.identify_via_web_search(log, result_queue, abort, title, authors, {}, timeout)\n        found = False\n        seen = set()\n        for relevance, gid in enumerate(google_ids):\n            if gid in seen:\n                continue\n            seen.add(gid)\n            try:\n                ans = to_metadata(br, log, gid, timeout, self.running_a_test)\n                if isinstance(ans, Metadata):\n                    if isbn:\n                        if isbn not in ans.all_isbns:\n                            log('Excluding', ans.title, 'by', authors_to_string(ans.authors), 'as it does not match the ISBN:', isbn,\n                                'not in', ' '.join(ans.all_isbns))\n                            continue\n                    elif check_tokens:\n                        candidate = set(to_check_tokens(*self.get_title_tokens(ans.title)))\n                        candidate |= set(to_check_tokens(*self.get_author_tokens(ans.authors)))\n                        if candidate.intersection(check_tokens) != check_tokens:\n                            log('Excluding', ans.title, 'by', authors_to_string(ans.authors), 'as it does not match the query')\n                            continue\n                    ans = self.postprocess_downloaded_google_metadata(ans, relevance)\n                    result_queue.put(ans)\n                    found = True\n            except:\n                log.exception('Failed to get metadata for google books id:', gid)\n            if abort.is_set():\n                break\n        if not found and isbn and (title or authors):\n            return self.identify_via_web_search(log, result_queue, abort, title, authors, {}, timeout)\n    # }}}\n\n    def identify(  # {{{\n        self,\n        log,\n        result_queue,\n        abort,\n        title=None,\n        authors=None,\n        identifiers={},\n        timeout=30\n    ):\n        from lxml import etree\n        entry = XPath('//atom:entry')\n        identifiers = identifiers.copy()\n        br = self.browser\n        if 'google' in identifiers:\n            try:\n                ans = to_metadata(br, log, identifiers['google'], timeout, self.running_a_test)\n                if isinstance(ans, Metadata):\n                    self.postprocess_downloaded_google_metadata(ans)\n                    result_queue.put(ans)\n                    return\n            except Exception:\n                log.exception('Failed to get metadata for Google identifier:', identifiers['google'])\n            del identifiers['google']\n\n        query = self.create_query(\n            title=title, authors=authors, identifiers=identifiers\n        )\n        if not query:\n            log.error('Insufficient metadata to construct query')\n            return\n\n        def make_query(query):\n            log('Making query:', query)\n            try:\n                raw = br.open_novisit(query, timeout=timeout).read()\n            except Exception as e:\n                log.exception('Failed to make identify query: %r' % query)\n                return False, as_unicode(e)\n\n            try:\n                feed = etree.fromstring(\n                    xml_to_unicode(clean_ascii_chars(raw), strip_encoding_pats=True)[0],\n                    parser=etree.XMLParser(recover=True, no_network=True, resolve_entities=False)\n                )\n                return True, entry(feed)\n            except Exception as e:\n                log.exception('Failed to parse identify results')\n                return False, as_unicode(e)\n        ok, entries = make_query(query)\n        if not ok:\n            return entries\n        if not entries and not abort.is_set():\n            log('No results found, doing a web search instead')\n            return self.identify_via_web_search(log, result_queue, abort, title, authors, identifiers, timeout)\n\n        # There is no point running these queries in threads as google\n        # throttles requests returning 403 Forbidden errors\n        self.get_all_details(br, log, entries, abort, result_queue, timeout)\n\n    # }}}\n\n\nif __name__ == '__main__':  # tests {{{\n    # To run these test use:\n    # calibre-debug src/calibre/ebooks/metadata/sources/google.py\n    from calibre.ebooks.metadata.sources.test import authors_test, test_identify_plugin, title_test\n    tests = [\n    ({\n        'identifiers': {'google': 's7NIrgEACAAJ'},\n    }, [title_test('Ride Every Stride', exact=False)]),\n\n    ({\n        'identifiers': {'isbn': '0743273567'},\n        'title': 'Great Gatsby',\n        'authors': ['Fitzgerald']\n    }, [\n        title_test('The great gatsby', exact=True),\n        authors_test(['F. Scott Fitzgerald'])\n    ]),\n\n    ({\n        'title': 'Flatland',\n        'authors': ['Abbott']\n    }, [title_test('Flatland', exact=False)]),\n\n    ({\n        'title': 'The Blood Red Indian Summer: A Berger and Mitry Mystery',\n        'authors': ['David Handler'],\n    }, [title_test('The Blood Red Indian Summer: A Berger and Mitry Mystery')\n    ]),\n\n    ({\n        # requires using web search to find the book\n        'title': 'Dragon Done It',\n        'authors': ['Eric Flint'],\n    }, [\n        title_test('The dragon done it', exact=True),\n        authors_test(['Eric Flint', 'Mike Resnick'])\n    ]),\n\n    ]\n    test_identify_plugin(GoogleBooks.name, tests[:])\n\n# }}}\n", +  "edelweiss": "#!/usr/bin/env python\n# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\n__license__   = 'GPL v3'\n__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'\n__docformat__ = 'restructuredtext en'\n\nimport re\nimport time\nfrom threading import Thread\n\ntry:\n    from queue import Empty, Queue\nexcept ImportError:\n    from Queue import Empty, Queue\n\nfrom calibre import as_unicode, random_user_agent\nfrom calibre.ebooks.metadata import check_isbn\nfrom calibre.ebooks.metadata.sources.base import Source\n\n\ndef clean_html(raw):\n    from calibre.ebooks.chardet import xml_to_unicode\n    from calibre.utils.cleantext import clean_ascii_chars\n    return clean_ascii_chars(xml_to_unicode(raw, strip_encoding_pats=True,\n                                resolve_entities=True, assume_utf8=True)[0])\n\n\ndef parse_html(raw):\n    raw = clean_html(raw)\n    from html5_parser import parse\n    return parse(raw)\n\n\ndef astext(node):\n    from lxml import etree\n    return etree.tostring(node, method='text', encoding='unicode',\n                          with_tail=False).strip()\n\n\nclass Worker(Thread):  # {{{\n\n    def __init__(self, basic_data, relevance, result_queue, br, timeout, log, plugin):\n        Thread.__init__(self)\n        self.daemon = True\n        self.basic_data = basic_data\n        self.br, self.log, self.timeout = br, log, timeout\n        self.result_queue, self.plugin, self.sku = result_queue, plugin, self.basic_data['sku']\n        self.relevance = relevance\n\n    def run(self):\n        url = ('https://www.edelweiss.plus/GetTreelineControl.aspx?controlName=/uc/product/two_Enhanced.ascx&'\n        'sku={0}&idPrefix=content_1_{0}&mode=0'.format(self.sku))\n        try:\n            raw = self.br.open_novisit(url, timeout=self.timeout).read()\n        except Exception:\n            self.log.exception('Failed to load comments page: %r'%url)\n            return\n\n        try:\n            mi = self.parse(raw)\n            mi.source_relevance = self.relevance\n            self.plugin.clean_downloaded_metadata(mi)\n            self.result_queue.put(mi)\n        except Exception:\n            self.log.exception('Failed to parse details for sku: %s'%self.sku)\n\n    def parse(self, raw):\n        from calibre.ebooks.metadata.book.base import Metadata\n        from calibre.utils.date import UNDEFINED_DATE\n        root = parse_html(raw)\n        mi = Metadata(self.basic_data['title'], self.basic_data['authors'])\n\n        # Identifiers\n        if self.basic_data['isbns']:\n            mi.isbn = self.basic_data['isbns'][0]\n        mi.set_identifier('edelweiss', self.sku)\n\n        # Tags\n        if self.basic_data['tags']:\n            mi.tags = self.basic_data['tags']\n            mi.tags = [t[1:].strip() if t.startswith('&') else t for t in mi.tags]\n\n        # Publisher\n        mi.publisher = self.basic_data['publisher']\n\n        # Pubdate\n        if self.basic_data['pubdate'] and self.basic_data['pubdate'].year != UNDEFINED_DATE:\n            mi.pubdate = self.basic_data['pubdate']\n\n        # Rating\n        if self.basic_data['rating']:\n            mi.rating = self.basic_data['rating']\n\n        # Comments\n        comments = ''\n        for cid in ('summary', 'contributorbio', 'quotes_reviews'):\n            cid = 'desc_{}{}-content'.format(cid, self.sku)\n            div = root.xpath('//*[@id=\"{}\"]'.format(cid))\n            if div:\n                comments += self.render_comments(div[0])\n        if comments:\n            mi.comments = comments\n\n        mi.has_cover = self.plugin.cached_identifier_to_cover_url(self.sku) is not None\n        return mi\n\n    def render_comments(self, desc):\n        from lxml import etree\n\n        from calibre.library.comments import sanitize_comments_html\n        for c in desc.xpath('descendant::noscript'):\n            c.getparent().remove(c)\n        for a in desc.xpath('descendant::a[@href]'):\n            del a.attrib['href']\n            a.tag = 'span'\n        desc = etree.tostring(desc, method='html', encoding='unicode').strip()\n\n        # remove all attributes from tags\n        desc = re.sub(r'<([a-zA-Z0-9]+)\\s[^>]+>', r'<\\1>', desc)\n        # Collapse whitespace\n        # desc = re.sub(r'\\n+', '\\n', desc)\n        # desc = re.sub(r' +', ' ', desc)\n        # Remove comments\n        desc = re.sub(r'(?s)<!--.*?-->', '', desc)\n        return sanitize_comments_html(desc)\n# }}}\n\n\ndef get_basic_data(browser, log, *skus):\n    from mechanize import Request\n\n    from calibre.utils.date import parse_only_date\n    zeroes = ','.join('0' for sku in skus)\n    data = {\n            'skus': ','.join(skus),\n            'drc': zeroes,\n            'startPosition': '0',\n            'sequence': '1',\n            'selected': zeroes,\n            'itemID': '0',\n            'orderID': '0',\n            'mailingID': '',\n            'tContentWidth': '926',\n            'originalOrder': ','.join(type('')(i) for i in range(len(skus))),\n            'selectedOrderID': '0',\n            'selectedSortColumn': '0',\n            'listType': '1',\n            'resultType': '32',\n            'blockView': '1',\n    }\n    items_data_url = 'https://www.edelweiss.plus/GetTreelineControl.aspx?controlName=/uc/listviews/ListView_Title_Multi.ascx'\n    req = Request(items_data_url, data)\n    response = browser.open_novisit(req)\n    raw = response.read()\n    root = parse_html(raw)\n    for item in root.xpath('//div[@data-priority]'):\n        row = item.getparent().getparent()\n        sku = item.get('id').split('-')[-1]\n        isbns = [x.strip() for x in row.xpath('descendant::*[contains(@class, \"pev_sku\")]/text()')[0].split(',') if check_isbn(x.strip())]\n        isbns.sort(key=len, reverse=True)\n        try:\n            tags = [x.strip() for x in astext(row.xpath('descendant::*[contains(@class, \"pev_categories\")]')[0]).split('/')]\n        except IndexError:\n            tags = []\n        rating = 0\n        for bar in row.xpath('descendant::*[contains(@class, \"bgdColorCommunity\")]/@style'):\n            m = re.search(r'width: (\\d+)px;.*max-width: (\\d+)px', bar)\n            if m is not None:\n                rating = float(m.group(1)) / float(m.group(2))\n                break\n        try:\n            pubdate = parse_only_date(astext(row.xpath('descendant::*[contains(@class, \"pev_shipDate\")]')[0]\n                ).split(':')[-1].split(u'\\xa0')[-1].strip(), assume_utc=True)\n        except Exception:\n            log.exception('Error parsing published date')\n            pubdate = None\n        authors = []\n        for x in [x.strip() for x in row.xpath('descendant::*[contains(@class, \"pev_contributor\")]/@title')]:\n            authors.extend(a.strip() for a in x.split(','))\n        entry = {\n                'sku': sku,\n                'cover': row.xpath('descendant::img/@src')[0].split('?')[0],\n                'publisher': astext(row.xpath('descendant::*[contains(@class, \"headerPublisher\")]')[0]),\n                'title': astext(row.xpath('descendant::*[@id=\"title_{}\"]'.format(sku))[0]),\n                'authors': authors,\n                'isbns': isbns,\n                'tags': tags,\n                'pubdate': pubdate,\n                'format': ' '.join(row.xpath('descendant::*[contains(@class, \"pev_format\")]/text()')).strip(),\n                'rating': rating,\n        }\n        if entry['cover'].startswith('/'):\n            entry['cover'] = None\n        yield entry\n\n\nclass Edelweiss(Source):\n\n    name = 'Edelweiss'\n    version = (2, 0, 1)\n    minimum_calibre_version = (3, 6, 0)\n    description = _('Downloads metadata and covers from Edelweiss - A catalog updated by book publishers')\n\n    capabilities = frozenset(['identify', 'cover'])\n    touched_fields = frozenset([\n        'title', 'authors', 'tags', 'pubdate', 'comments', 'publisher',\n        'identifier:isbn', 'identifier:edelweiss', 'rating'])\n    supports_gzip_transfer_encoding = True\n    has_html_comments = True\n\n    @property\n    def user_agent(self):\n        # Pass in an index to random_user_agent() to test with a particular\n        # user agent\n        return random_user_agent(allow_ie=False)\n\n    def _get_book_url(self, sku):\n        if sku:\n            return 'https://www.edelweiss.plus/#sku={}&page=1'.format(sku)\n\n    def get_book_url(self, identifiers):  # {{{\n        sku = identifiers.get('edelweiss', None)\n        if sku:\n            return 'edelweiss', sku, self._get_book_url(sku)\n\n    # }}}\n\n    def get_cached_cover_url(self, identifiers):  # {{{\n        sku = identifiers.get('edelweiss', None)\n        if not sku:\n            isbn = identifiers.get('isbn', None)\n            if isbn is not None:\n                sku = self.cached_isbn_to_identifier(isbn)\n        return self.cached_identifier_to_cover_url(sku)\n    # }}}\n\n    def create_query(self, log, title=None, authors=None, identifiers={}):\n        try:\n            from urllib.parse import urlencode\n        except ImportError:\n            from urllib import urlencode\n        import time\n        BASE_URL = ('https://www.edelweiss.plus/GetTreelineControl.aspx?'\n        'controlName=/uc/listviews/controls/ListView_data.ascx&itemID=0&resultType=32&dashboardType=8&itemType=1&dataType=products&keywordSearch&')\n        keywords = []\n        isbn = check_isbn(identifiers.get('isbn', None))\n        if isbn is not None:\n            keywords.append(isbn)\n        elif title:\n            title_tokens = list(self.get_title_tokens(title))\n            if title_tokens:\n                keywords.extend(title_tokens)\n            author_tokens = self.get_author_tokens(authors, only_first_author=True)\n            if author_tokens:\n                keywords.extend(author_tokens)\n        if not keywords:\n            return None\n        params = {\n            'q': (' '.join(keywords)).encode('utf-8'),\n            '_': type('')(int(time.time()))\n        }\n        return BASE_URL+urlencode(params)\n\n    # }}}\n\n    def identify(self, log, result_queue, abort, title=None, authors=None,  # {{{\n            identifiers={}, timeout=30):\n        import json\n\n        br = self.browser\n        br.addheaders = [\n            ('Referer', 'https://www.edelweiss.plus/'),\n            ('X-Requested-With', 'XMLHttpRequest'),\n            ('Cache-Control', 'no-cache'),\n            ('Pragma', 'no-cache'),\n        ]\n        if 'edelweiss' in identifiers:\n            items = [identifiers['edelweiss']]\n        else:\n            log.error('Currently Edelweiss returns random books for search queries')\n            return\n            query = self.create_query(log, title=title, authors=authors,\n                    identifiers=identifiers)\n            if not query:\n                log.error('Insufficient metadata to construct query')\n                return\n            log('Using query URL:', query)\n            try:\n                raw = br.open(query, timeout=timeout).read().decode('utf-8')\n            except Exception as e:\n                log.exception('Failed to make identify query: %r'%query)\n                return as_unicode(e)\n            items = re.search(r'window[.]items\\s*=\\s*(.+?);', raw)\n            if items is None:\n                log.error('Failed to get list of matching items')\n                log.debug('Response text:')\n                log.debug(raw)\n                return\n            items = json.loads(items.group(1))\n\n        if (not items and identifiers and title and authors and\n                not abort.is_set()):\n            return self.identify(log, result_queue, abort, title=title,\n                    authors=authors, timeout=timeout)\n\n        if not items:\n            return\n\n        workers = []\n        items = items[:5]\n        for i, item in enumerate(get_basic_data(self.browser, log, *items)):\n            sku = item['sku']\n            for isbn in item['isbns']:\n                self.cache_isbn_to_identifier(isbn, sku)\n            if item['cover']:\n                self.cache_identifier_to_cover_url(sku, item['cover'])\n            fmt = item['format'].lower()\n            if 'audio' in fmt or 'mp3' in fmt:\n                continue  # Audio-book, ignore\n            workers.append(Worker(item, i, result_queue, br.clone_browser(), timeout, log, self))\n\n        if not workers:\n            return\n\n        for w in workers:\n            w.start()\n            # Don't send all requests at the same time\n            time.sleep(0.1)\n\n        while not abort.is_set():\n            a_worker_is_alive = False\n            for w in workers:\n                w.join(0.2)\n                if abort.is_set():\n                    break\n                if w.is_alive():\n                    a_worker_is_alive = True\n            if not a_worker_is_alive:\n                break\n\n    # }}}\n\n    def download_cover(self, log, result_queue, abort,  # {{{\n            title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):\n        cached_url = self.get_cached_cover_url(identifiers)\n        if cached_url is None:\n            log.info('No cached cover found, running identify')\n            rq = Queue()\n            self.identify(log, rq, abort, title=title, authors=authors,\n                    identifiers=identifiers)\n            if abort.is_set():\n                return\n            results = []\n            while True:\n                try:\n                    results.append(rq.get_nowait())\n                except Empty:\n                    break\n            results.sort(key=self.identify_results_keygen(\n                title=title, authors=authors, identifiers=identifiers))\n            for mi in results:\n                cached_url = self.get_cached_cover_url(mi.identifiers)\n                if cached_url is not None:\n                    break\n        if cached_url is None:\n            log.info('No cover found')\n            return\n\n        if abort.is_set():\n            return\n        br = self.browser\n        log('Downloading cover from:', cached_url)\n        try:\n            cdata = br.open_novisit(cached_url, timeout=timeout).read()\n            result_queue.put((self, cdata))\n        except Exception:\n            log.exception('Failed to download cover from:', cached_url)\n    # }}}\n\n\nif __name__ == '__main__':\n    from calibre.ebooks.metadata.sources.test import authors_test, comments_test, pubdate_test, test_identify_plugin, title_test\n    tests = [\n        (  # A title and author search\n         {'title': \"The Husband's Secret\", 'authors':['Liane Moriarty']},\n         [title_test(\"The Husband's Secret\", exact=True),\n          authors_test(['Liane Moriarty'])]\n        ),\n\n        (  # An isbn present in edelweiss\n         {'identifiers':{'isbn': '9780312621360'}, },\n         [title_test('Flame: A Sky Chasers Novel', exact=True),\n          authors_test(['Amy Kathleen Ryan'])]\n        ),\n\n        # Multiple authors and two part title and no general description\n        ({'identifiers':{'edelweiss':'0321180607'}},\n        [title_test('XQuery From the Experts: A Guide to the W3C XML Query Language', exact=True),\n         authors_test([\n            'Howard Katz', 'Don Chamberlin', 'Denise Draper', 'Mary Fernandez',\n            'Michael Kay', 'Jonathan Robie', 'Michael Rys', 'Jerome Simeon',\n            'Jim Tivy', 'Philip Wadler']),\n         pubdate_test(2003, 8, 22),\n         comments_test('Jérôme Siméon'), lambda mi: bool(mi.comments and 'No title summary' not in mi.comments)\n        ]),\n    ]\n    start, stop = 0, len(tests)\n\n    tests = tests[start:stop]\n    test_identify_plugin(Edelweiss.name, tests)\n", +  "google": "#!/usr/bin/env python\n# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai\n# License: GPLv3 Copyright: 2011, Kovid Goyal <kovid at kovidgoyal.net>\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport hashlib\nimport os\nimport re\nimport sys\nimport tempfile\nimport time\n\nimport regex\n\ntry:\n    from queue import Empty, Queue\nexcept ImportError:\n    from Queue import Empty, Queue\n\nfrom calibre import as_unicode, prepare_string_for_xml, replace_entities\nfrom calibre.ebooks.chardet import xml_to_unicode\nfrom calibre.ebooks.metadata import authors_to_string, check_isbn\nfrom calibre.ebooks.metadata.book.base import Metadata\nfrom calibre.ebooks.metadata.sources.base import Source\nfrom calibre.utils.cleantext import clean_ascii_chars\nfrom calibre.utils.localization import canonicalize_lang\n\nNAMESPACES = {\n    'openSearch': 'http://a9.com/-/spec/opensearchrss/1.0/',\n    'atom': 'http://www.w3.org/2005/Atom',\n    'dc': 'http://purl.org/dc/terms',\n    'gd': 'http://schemas.google.com/g/2005'\n}\n\n\ndef pretty_google_books_comments(raw):\n    raw = replace_entities(raw)\n    # Paragraphs in the comments are removed but whatever software googl uses\n    # to do this does not insert a space so we often find the pattern\n    # word.Capital in the comments which can be used to find paragraph markers.\n    parts = []\n    for x in re.split(r'([a-z)\"”])(\\.)([A-Z(\"“])', raw):\n        if x == '.':\n            parts.append('.</p>\\n\\n<p>')\n        else:\n            parts.append(prepare_string_for_xml(x))\n    raw = '<p>' + ''.join(parts) + '</p>'\n    return raw\n\n\ndef get_details(browser, url, timeout):  # {{{\n    try:\n        raw = browser.open_novisit(url, timeout=timeout).read()\n    except Exception as e:\n        gc = getattr(e, 'getcode', lambda: -1)\n        if gc() != 403:\n            raise\n        # Google is throttling us, wait a little\n        time.sleep(2)\n        raw = browser.open_novisit(url, timeout=timeout).read()\n\n    return raw\n# }}}\n\n\nxpath_cache = {}\n\n\ndef XPath(x):\n    ans = xpath_cache.get(x)\n    if ans is None:\n        from lxml import etree\n        ans = xpath_cache[x] = etree.XPath(x, namespaces=NAMESPACES)\n    return ans\n\n\ndef to_metadata(browser, log, entry_, timeout, running_a_test=False):  # {{{\n    from lxml import etree\n\n    # total_results  = XPath('//openSearch:totalResults')\n    # start_index    = XPath('//openSearch:startIndex')\n    # items_per_page = XPath('//openSearch:itemsPerPage')\n    entry = XPath('//atom:entry')\n    entry_id = XPath('descendant::atom:id')\n    url = XPath('descendant::atom:link[@rel=\"self\"]/@href')\n    creator = XPath('descendant::dc:creator')\n    identifier = XPath('descendant::dc:identifier')\n    title = XPath('descendant::dc:title')\n    date = XPath('descendant::dc:date')\n    publisher = XPath('descendant::dc:publisher')\n    subject = XPath('descendant::dc:subject')\n    description = XPath('descendant::dc:description')\n    language = XPath('descendant::dc:language')\n\n    # print(etree.tostring(entry_, pretty_print=True))\n\n    def get_text(extra, x):\n        try:\n            ans = x(extra)\n            if ans:\n                ans = ans[0].text\n                if ans and ans.strip():\n                    return ans.strip()\n        except Exception:\n            log.exception('Programming error:')\n        return None\n\n    def get_extra_details():\n        raw = get_details(browser, details_url, timeout)\n        if running_a_test:\n            with open(os.path.join(tempfile.gettempdir(), 'Google-' + details_url.split('/')[-1] + '.xml'), 'wb') as f:\n                f.write(raw)\n                print('Book details saved to:', f.name, file=sys.stderr)\n        feed = etree.fromstring(\n            xml_to_unicode(clean_ascii_chars(raw), strip_encoding_pats=True)[0],\n            parser=etree.XMLParser(recover=True, no_network=True, resolve_entities=False)\n        )\n        return entry(feed)[0]\n\n    if isinstance(entry_, str):\n        google_id = entry_\n        details_url = 'https://www.google.com/books/feeds/volumes/' + google_id\n        extra = get_extra_details()\n        title_ = ': '.join([x.text for x in title(extra)]).strip()\n        authors = [x.text.strip() for x in creator(extra) if x.text]\n    else:\n        id_url = entry_id(entry_)[0].text\n        google_id = id_url.split('/')[-1]\n        details_url = url(entry_)[0]\n        title_ = ': '.join([x.text for x in title(entry_)]).strip()\n        authors = [x.text.strip() for x in creator(entry_) if x.text]\n        if not id_url or not title:\n            # Silently discard this entry\n            return None\n        extra = None\n\n    if not authors:\n        authors = [_('Unknown')]\n    if not title:\n        return None\n    if extra is None:\n        extra = get_extra_details()\n    mi = Metadata(title_, authors)\n    mi.identifiers = {'google': google_id}\n    mi.comments = get_text(extra, description)\n    lang = canonicalize_lang(get_text(extra, language))\n    if lang:\n        mi.language = lang\n    mi.publisher = get_text(extra, publisher)\n\n    # ISBN\n    isbns = []\n    for x in identifier(extra):\n        t = type('')(x.text).strip()\n        if t[:5].upper() in ('ISBN:', 'LCCN:', 'OCLC:'):\n            if t[:5].upper() == 'ISBN:':\n                t = check_isbn(t[5:])\n                if t:\n                    isbns.append(t)\n    if isbns:\n        mi.isbn = sorted(isbns, key=len)[-1]\n    mi.all_isbns = isbns\n\n    # Tags\n    try:\n        btags = [x.text for x in subject(extra) if x.text]\n        tags = []\n        for t in btags:\n            atags = [y.strip() for y in t.split('/')]\n            for tag in atags:\n                if tag not in tags:\n                    tags.append(tag)\n    except Exception:\n        log.exception('Failed to parse tags:')\n        tags = []\n    if tags:\n        mi.tags = [x.replace(',', ';') for x in tags]\n\n    # pubdate\n    pubdate = get_text(extra, date)\n    if pubdate:\n        from calibre.utils.date import parse_date, utcnow\n        try:\n            default = utcnow().replace(day=15)\n            mi.pubdate = parse_date(pubdate, assume_utc=True, default=default)\n        except Exception:\n            log.error('Failed to parse pubdate %r' % pubdate)\n\n    # Cover\n    mi.has_google_cover = None\n    for x in extra.xpath(\n        '//*[@href and @rel=\"http://schemas.google.com/books/2008/thumbnail\"]'\n    ):\n        mi.has_google_cover = x.get('href')\n        break\n\n    return mi\n\n# }}}\n\n\nclass GoogleBooks(Source):\n\n    name = 'Google'\n    version = (1, 1, 2)\n    minimum_calibre_version = (2, 80, 0)\n    description = _('Downloads metadata and covers from Google Books')\n\n    capabilities = frozenset({'identify'})\n    touched_fields = frozenset({\n        'title', 'authors', 'tags', 'pubdate', 'comments', 'publisher',\n        'identifier:isbn', 'identifier:google', 'languages'\n    })\n    supports_gzip_transfer_encoding = True\n    cached_cover_url_is_reliable = False\n\n    GOOGLE_COVER = 'https://books.google.com/books?id=%s&printsec=frontcover&img=1'\n\n    DUMMY_IMAGE_MD5 = frozenset(\n        ('0de4383ebad0adad5eeb8975cd796657', 'a64fa89d7ebc97075c1d363fc5fea71f')\n    )\n\n    def get_book_url(self, identifiers):  # {{{\n        goog = identifiers.get('google', None)\n        if goog is not None:\n            return ('google', goog, 'https://books.google.com/books?id=%s' % goog)\n    # }}}\n\n    def id_from_url(self, url):  # {{{\n        from polyglot.urllib import parse_qs, urlparse\n        purl = urlparse(url)\n        if purl.netloc == 'books.google.com':\n            q = parse_qs(purl.query)\n            gid = q.get('id')\n            if gid:\n                return 'google', gid[0]\n    # }}}\n\n    def create_query(self, title=None, authors=None, identifiers={}, capitalize_isbn=False):  # {{{\n        try:\n            from urllib.parse import urlencode\n        except ImportError:\n            from urllib import urlencode\n        BASE_URL = 'https://books.google.com/books/feeds/volumes?'\n        isbn = check_isbn(identifiers.get('isbn', None))\n        q = ''\n        if isbn is not None:\n            q += ('ISBN:' if capitalize_isbn else 'isbn:') + isbn\n        elif title or authors:\n\n            def build_term(prefix, parts):\n                return ' '.join('in' + prefix + ':' + x for x in parts)\n\n            title_tokens = list(self.get_title_tokens(title))\n            if title_tokens:\n                q += build_term('title', title_tokens)\n            author_tokens = list(self.get_author_tokens(authors, only_first_author=True))\n            if author_tokens:\n                q += ('+' if q else '') + build_term('author', author_tokens)\n\n        if not q:\n            return None\n        if not isinstance(q, bytes):\n            q = q.encode('utf-8')\n        return BASE_URL + urlencode({\n            'q': q,\n            'max-results': 20,\n            'start-index': 1,\n            'min-viewability': 'none',\n        })\n\n    # }}}\n\n    def download_cover(  # {{{\n        self,\n        log,\n        result_queue,\n        abort,\n        title=None,\n        authors=None,\n        identifiers={},\n        timeout=30,\n        get_best_cover=False\n    ):\n        cached_url = self.get_cached_cover_url(identifiers)\n        if cached_url is None:\n            log.info('No cached cover found, running identify')\n            rq = Queue()\n            self.identify(\n                log,\n                rq,\n                abort,\n                title=title,\n                authors=authors,\n                identifiers=identifiers\n            )\n            if abort.is_set():\n                return\n            results = []\n            while True:\n                try:\n                    results.append(rq.get_nowait())\n                except Empty:\n                    break\n            results.sort(\n                key=self.identify_results_keygen(\n                    title=title, authors=authors, identifiers=identifiers\n                )\n            )\n            for mi in results:\n                cached_url = self.get_cached_cover_url(mi.identifiers)\n                if cached_url is not None:\n                    break\n        if cached_url is None:\n            log.info('No cover found')\n            return\n\n        br = self.browser\n        for candidate in (0, 1):\n            if abort.is_set():\n                return\n            url = cached_url + '&zoom={}'.format(candidate)\n            log('Downloading cover from:', cached_url)\n            try:\n                cdata = br.open_novisit(url, timeout=timeout).read()\n                if cdata:\n                    if hashlib.md5(cdata).hexdigest() in self.DUMMY_IMAGE_MD5:\n                        log.warning('Google returned a dummy image, ignoring')\n                    else:\n                        result_queue.put((self, cdata))\n                        break\n            except Exception:\n                log.exception('Failed to download cover from:', cached_url)\n\n    # }}}\n\n    def get_cached_cover_url(self, identifiers):  # {{{\n        url = None\n        goog = identifiers.get('google', None)\n        if goog is None:\n            isbn = identifiers.get('isbn', None)\n            if isbn is not None:\n                goog = self.cached_isbn_to_identifier(isbn)\n        if goog is not None:\n            url = self.cached_identifier_to_cover_url(goog)\n\n        return url\n\n    # }}}\n\n    def postprocess_downloaded_google_metadata(self, ans, relevance=0):  # {{{\n        if not isinstance(ans, Metadata):\n            return ans\n        ans.source_relevance = relevance\n        goog = ans.identifiers['google']\n        for isbn in getattr(ans, 'all_isbns', []):\n            self.cache_isbn_to_identifier(isbn, goog)\n        if getattr(ans, 'has_google_cover', False):\n            self.cache_identifier_to_cover_url(goog, self.GOOGLE_COVER % goog)\n        if ans.comments:\n            ans.comments = pretty_google_books_comments(ans.comments)\n        self.clean_downloaded_metadata(ans)\n        return ans\n    # }}}\n\n    def get_all_details(  # {{{\n        self,\n        br,\n        log,\n        entries,\n        abort,\n        result_queue,\n        timeout\n    ):\n        from lxml import etree\n        for relevance, i in enumerate(entries):\n            try:\n                ans = self.postprocess_downloaded_google_metadata(to_metadata(br, log, i, timeout, self.running_a_test), relevance)\n                if isinstance(ans, Metadata):\n                    result_queue.put(ans)\n            except Exception:\n                log.exception(\n                    'Failed to get metadata for identify entry:', etree.tostring(i)\n                )\n            if abort.is_set():\n                break\n\n    # }}}\n\n    def identify_via_web_search(  # {{{\n        self,\n        log,\n        result_queue,\n        abort,\n        title=None,\n        authors=None,\n        identifiers={},\n        timeout=30\n    ):\n        from calibre.utils.filenames import ascii_text\n        isbn = check_isbn(identifiers.get('isbn', None))\n        q = []\n        strip_punc_pat = regex.compile(r'[\\p{C}|\\p{M}|\\p{P}|\\p{S}|\\p{Z}]+', regex.UNICODE)\n        google_ids = []\n        check_tokens = set()\n        has_google_id = 'google' in identifiers\n\n        def to_check_tokens(*tokens):\n            for t in tokens:\n                if len(t) < 3:\n                    continue\n                t = t.lower()\n                if t in ('and', 'not', 'the'):\n                    continue\n                yield ascii_text(strip_punc_pat.sub('', t))\n\n        if has_google_id:\n            google_ids.append(identifiers['google'])\n        elif isbn is not None:\n            q.append(isbn)\n        elif title or authors:\n            title_tokens = list(self.get_title_tokens(title))\n            if title_tokens:\n                q += title_tokens\n                check_tokens |= set(to_check_tokens(*title_tokens))\n            author_tokens = list(self.get_author_tokens(authors, only_first_author=True))\n            if author_tokens:\n                q += author_tokens\n                check_tokens |= set(to_check_tokens(*author_tokens))\n        if not q and not google_ids:\n            return None\n        from calibre.ebooks.metadata.sources.update import search_engines_module\n        se = search_engines_module()\n        br = se.google_specialize_browser(se.browser())\n        if not has_google_id:\n            url = se.google_format_query(q, site='books.google.com')\n            log('Making query:', url)\n            r = []\n            root = se.query(br, url, 'google', timeout=timeout, save_raw=r.append)\n            pat = re.compile(r'id=([^&]+)')\n            for q in se.google_parse_results(root, r[0], log=log, ignore_uncached=False):\n                m = pat.search(q.url)\n                if m is None or not q.url.startswith('https://books.google'):\n                    continue\n                google_ids.append(m.group(1))\n\n        if not google_ids and isbn and (title or authors):\n            return self.identify_via_web_search(log, result_queue, abort, title, authors, {}, timeout)\n        found = False\n        seen = set()\n        for relevance, gid in enumerate(google_ids):\n            if gid in seen:\n                continue\n            seen.add(gid)\n            try:\n                ans = to_metadata(br, log, gid, timeout, self.running_a_test)\n                if isinstance(ans, Metadata):\n                    if isbn:\n                        if isbn not in ans.all_isbns:\n                            log('Excluding', ans.title, 'by', authors_to_string(ans.authors), 'as it does not match the ISBN:', isbn,\n                                'not in', ' '.join(ans.all_isbns))\n                            continue\n                    elif check_tokens:\n                        candidate = set(to_check_tokens(*self.get_title_tokens(ans.title)))\n                        candidate |= set(to_check_tokens(*self.get_author_tokens(ans.authors)))\n                        if candidate.intersection(check_tokens) != check_tokens:\n                            log('Excluding', ans.title, 'by', authors_to_string(ans.authors), 'as it does not match the query')\n                            continue\n                    ans = self.postprocess_downloaded_google_metadata(ans, relevance)\n                    result_queue.put(ans)\n                    found = True\n            except Exception:\n                log.exception('Failed to get metadata for google books id:', gid)\n            if abort.is_set():\n                break\n        if not found and isbn and (title or authors):\n            return self.identify_via_web_search(log, result_queue, abort, title, authors, {}, timeout)\n    # }}}\n\n    def identify(  # {{{\n        self,\n        log,\n        result_queue,\n        abort,\n        title=None,\n        authors=None,\n        identifiers={},\n        timeout=30\n    ):\n        from lxml import etree\n        entry = XPath('//atom:entry')\n        identifiers = identifiers.copy()\n        br = self.browser\n        if 'google' in identifiers:\n            try:\n                ans = to_metadata(br, log, identifiers['google'], timeout, self.running_a_test)\n                if isinstance(ans, Metadata):\n                    self.postprocess_downloaded_google_metadata(ans)\n                    result_queue.put(ans)\n                    return\n            except Exception:\n                log.exception('Failed to get metadata for Google identifier:', identifiers['google'])\n            del identifiers['google']\n\n        query = self.create_query(\n            title=title, authors=authors, identifiers=identifiers\n        )\n        if not query:\n            log.error('Insufficient metadata to construct query')\n            return\n\n        def make_query(query):\n            log('Making query:', query)\n            try:\n                raw = br.open_novisit(query, timeout=timeout).read()\n            except Exception as e:\n                log.exception('Failed to make identify query: %r' % query)\n                return False, as_unicode(e)\n\n            try:\n                feed = etree.fromstring(\n                    xml_to_unicode(clean_ascii_chars(raw), strip_encoding_pats=True)[0],\n                    parser=etree.XMLParser(recover=True, no_network=True, resolve_entities=False)\n                )\n                return True, entry(feed)\n            except Exception as e:\n                log.exception('Failed to parse identify results')\n                return False, as_unicode(e)\n        ok, entries = make_query(query)\n        if not ok:\n            return entries\n        if not entries and not abort.is_set():\n            log('No results found, doing a web search instead')\n            return self.identify_via_web_search(log, result_queue, abort, title, authors, identifiers, timeout)\n\n        # There is no point running these queries in threads as google\n        # throttles requests returning 403 Forbidden errors\n        self.get_all_details(br, log, entries, abort, result_queue, timeout)\n\n    # }}}\n\n\nif __name__ == '__main__':  # tests {{{\n    # To run these test use:\n    # calibre-debug src/calibre/ebooks/metadata/sources/google.py\n    from calibre.ebooks.metadata.sources.test import authors_test, test_identify_plugin, title_test\n    tests = [\n    ({\n        'identifiers': {'google': 's7NIrgEACAAJ'},\n    }, [title_test('Ride Every Stride', exact=False)]),\n\n    ({\n        'identifiers': {'isbn': '0743273567'},\n        'title': 'Great Gatsby',\n        'authors': ['Fitzgerald']\n    }, [\n        title_test('The great gatsby', exact=True),\n        authors_test(['F. Scott Fitzgerald'])\n    ]),\n\n    ({\n        'title': 'Flatland',\n        'authors': ['Abbott']\n    }, [title_test('Flatland', exact=False)]),\n\n    ({\n        'title': 'The Blood Red Indian Summer: A Berger and Mitry Mystery',\n        'authors': ['David Handler'],\n    }, [title_test('The Blood Red Indian Summer: A Berger and Mitry Mystery')\n    ]),\n\n    ({\n        # requires using web search to find the book\n        'title': 'Dragon Done It',\n        'authors': ['Eric Flint'],\n    }, [\n        title_test('The dragon done it', exact=True),\n        authors_test(['Eric Flint', 'Mike Resnick'])\n    ]),\n\n    ]\n    test_identify_plugin(GoogleBooks.name, tests[:])\n\n# }}}\n",    "google_images": "#!/usr/bin/env python\n# vim:fileencoding=UTF-8\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\n__license__   = 'GPL v3'\n__copyright__ = '2013, Kovid Goyal <kovid@kovidgoyal.net>'\n__docformat__ = 'restructuredtext en'\n\nfrom collections import OrderedDict\n\nfrom calibre import random_user_agent\nfrom calibre.ebooks.metadata.sources.base import Option, Source\n\n\ndef parse_html(raw):\n    try:\n        from html5_parser import parse\n    except ImportError:\n        # Old versions of calibre\n        import html5lib\n        return html5lib.parse(raw, treebuilder='lxml', namespaceHTMLElements=False)\n    else:\n        return parse(raw)\n\n\ndef imgurl_from_id(raw, tbnid):\n    from json import JSONDecoder\n    q = '\"{}\",['.format(tbnid)\n    start_pos = raw.index(q)\n    if start_pos < 100:\n        return\n    jd = JSONDecoder()\n    data = jd.raw_decode('[' + raw[start_pos:])[0]\n    # from pprint import pprint\n    # pprint(data)\n    url_num = 0\n    for x in data:\n        if isinstance(x, list) and len(x) == 3:\n            q = x[0]\n            if hasattr(q, 'lower') and q.lower().startswith('http'):\n                url_num += 1\n                if url_num > 1:\n                    return q\n\n\ndef parse_google_markup(raw):\n    root = parse_html(raw)\n    # newer markup pages use data-docid not data-tbnid\n    results = root.xpath('//div/@data-tbnid') or root.xpath('//div/@data-docid')\n    ans = OrderedDict()\n    for tbnid in results:\n        try:\n            imgurl = imgurl_from_id(raw, tbnid)\n        except Exception:\n            continue\n        if imgurl:\n            ans[imgurl] = True\n    return list(ans)\n\n\nclass GoogleImages(Source):\n\n    name = 'Google Images'\n    version = (1, 0, 6)\n    minimum_calibre_version = (2, 80, 0)\n    description = _('Downloads covers from a Google Image search. Useful to find larger/alternate covers.')\n    capabilities = frozenset(['cover'])\n    can_get_multiple_covers = True\n    supports_gzip_transfer_encoding = True\n    options = (Option('max_covers', 'number', 5, _('Maximum number of covers to get'),\n                      _('The maximum number of covers to process from the Google search result')),\n               Option('size', 'choices', 'svga', _('Cover size'),\n                      _('Search for covers larger than the specified size'),\n                      choices=OrderedDict((\n                          ('any', _('Any size'),),\n                          ('l', _('Large'),),\n                          ('qsvga', _('Larger than %s')%'400x300',),\n                          ('vga', _('Larger than %s')%'640x480',),\n                          ('svga', _('Larger than %s')%'600x800',),\n                          ('xga', _('Larger than %s')%'1024x768',),\n                          ('2mp', _('Larger than %s')%'2 MP',),\n                          ('4mp', _('Larger than %s')%'4 MP',),\n                      ))),\n    )\n\n    def download_cover(self, log, result_queue, abort,\n            title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):\n        if not title:\n            return\n        timeout = max(60, timeout)  # Needs at least a minute\n        title = ' '.join(self.get_title_tokens(title))\n        author = ' '.join(self.get_author_tokens(authors))\n        urls = self.get_image_urls(title, author, log, abort, timeout)\n        self.download_multiple_covers(title, authors, urls, get_best_cover, timeout, result_queue, abort, log)\n\n    @property\n    def user_agent(self):\n        return random_user_agent(allow_ie=False)\n\n    def get_image_urls(self, title, author, log, abort, timeout):\n        from calibre.utils.cleantext import clean_ascii_chars\n        try:\n            from urllib.parse import urlencode\n        except ImportError:\n            from urllib import urlencode\n        br = self.browser\n        q = urlencode({'as_q': ('%s %s'%(title, author)).encode('utf-8')})\n        if isinstance(q, bytes):\n            q = q.decode('utf-8')\n        sz = self.prefs['size']\n        if sz == 'any':\n            sz = ''\n        elif sz == 'l':\n            sz = 'isz:l,'\n        else:\n            sz = 'isz:lt,islt:%s,' % sz\n        # See https://www.google.com/advanced_image_search to understand this\n        # URL scheme\n        url = 'https://www.google.com/search?as_st=y&tbm=isch&{}&as_epq=&as_oq=&as_eq=&cr=&as_sitesearch=&safe=images&tbs={}iar:t,ift:jpg'.format(q, sz)\n        log('Search URL: ' + url)\n        # See https://github.com/benbusby/whoogle-search/pull/1054 for cookies\n        br.set_simple_cookie('CONSENT', 'PENDING+987', '.google.com', path='/')\n        template = b'\\x08\\x01\\x128\\x08\\x14\\x12+boq_identityfrontenduiserver_20231107.05_p0\\x1a\\x05en-US \\x03\\x1a\\x06\\x08\\x80\\xf1\\xca\\xaa\\x06'\n        from base64 import standard_b64encode\n        from datetime import date\n        template.replace(b'20231107', date.today().strftime('%Y%m%d').encode('ascii'))\n        br.set_simple_cookie('SOCS', standard_b64encode(template).decode('ascii').rstrip('='), '.google.com', path='/')\n        # br.set_debug_http(True)\n        raw = clean_ascii_chars(br.open(url).read().decode('utf-8'))\n        # with open('/t/raw.html', 'w') as f:\n        #     f.write(raw)\n        return parse_google_markup(raw)\n\n\ndef test_raw():\n    import sys\n    raw = open(sys.argv[-1]).read()\n    for x in parse_google_markup(raw):\n        print(x)\n\n\ndef test(title='Star Trek: Section 31: Control', authors=('David Mack',)):\n    try:\n        from queue import Queue\n    except ImportError:\n        from Queue import Queue\n    from threading import Event\n\n    from calibre.utils.logging import default_log\n    p = GoogleImages(None)\n    p.log = default_log\n    rq = Queue()\n    p.download_cover(default_log, rq, Event(), title=title, authors=authors)\n    print('Downloaded', rq.qsize(), 'covers')\n\n\nif __name__ == '__main__':\n    test()\n",    "hashes": { -    "amazon": "cb6b4178d198ae60ab1017e03a45d9d839899057", +    "amazon": "6be10d2caceffdc42f092e32b0eaef81f9261c23",      "big_book_search": "7a8b67c0f19ecbfe8a9d28b961aab1119f31c3e3", -    "edelweiss": "54f2d2d6d00d4a7081e72d08d8b7b4bb4288cb53", -    "google": "d7688a11f00e15ed8f9786e97cc74fe9184b9300", +    "edelweiss": "640a39d0926dfdaa72f54160a1db5323b4d7c164", +    "google": "61283b42dd070d2a5e0bdee00336d3492db4a124",      "google_images": "4244dd8267cb6215c7dfd2da166c6e02b1db31ea",      "openlibrary": "239077a692701cbf0281e7a2e64306cd00217410",      "search_engines": "9f1dbe2c712c5944b63f700dd8831b9c18231039" diff --git a/dotfiles/system/.config/calibre/metadata_sources/global.json b/dotfiles/system/.config/calibre/metadata_sources/global.json index fe23e6c..be7665d 100644 --- a/dotfiles/system/.config/calibre/metadata_sources/global.json +++ b/dotfiles/system/.config/calibre/metadata_sources/global.json @@ -8,8 +8,8 @@      ]    },    "ignore_fields": [ -    "series", -    "rating" +    "rating", +    "series"    ],    "tag_map_rules": [      { diff --git a/dotfiles/system/.config/calibre/plugins/Favourites Menu.json b/dotfiles/system/.config/calibre/plugins/Favourites Menu.json index 5505bfc..14daac7 100644 --- a/dotfiles/system/.config/calibre/plugins/Favourites Menu.json +++ b/dotfiles/system/.config/calibre/plugins/Favourites Menu.json @@ -1,15 +1,24 @@  {    "menus": [      { -      "display": "Reading List", +      "display": "Convert books",        "path": [ -        "Reading List" +        "Convert Books" +      ] +    }, +    null, +    { +      "display": "Get books", +      "path": [ +        "Store"        ]      }, +    null,      { -      "display": "Plugin updates*", +      "display": "Start Content server",        "path": [ -        "Plugin Updater" +        "Connect Share", +        "Start Content server"        ]      },      { @@ -18,36 +27,64 @@          "Extract ISBN"        ]      }, +    null,      { -      "display": "Clean Comments", +      "display": "Add to default list",        "path": [ -        "Clean Comments" +        "Reading List", +        "Add to default list"        ]      },      { -      "display": "Find Duplicates", +      "display": "Remove from default list",        "path": [ -        "Find Duplicates" +        "Reading List", +        "Remove from default list"        ]      },      { -      "display": "Convert books", +      "display": "View default list",        "path": [ -        "Convert Books" +        "Reading List", +        "View default list"        ]      }, +    null,      { -      "display": "Get books", +      "display": "Email to cjennings_paperwhite@kindle.com",        "path": [ -        "Store" +        "Connect Share", +        "Email to cjennings_paperwhite@kindle.com"        ]      },      null,      { -      "display": "Start Content server", +      "display": "Find book duplicates...",        "path": [ -        "Connect Share", -        "Start Content server" +        "Find Duplicates", +        "Find book duplicates..." +      ] +    }, +    { +      "display": "Check library", +      "path": [ +        "Choose Library", +        "Library maintenance", +        "Check library" +      ] +    }, +    { +      "display": "Download all scheduled news sources", +      "path": [ +        "Fetch News", +        "Download all scheduled news sources" +      ] +    }, +    { +      "display": "Schedule news download", +      "path": [ +        "Fetch News", +        "Schedule news download"        ]      }    ] diff --git a/dotfiles/system/.config/calibre/scheduler.xml b/dotfiles/system/.config/calibre/scheduler.xml new file mode 100644 index 0000000..f3c64a3 --- /dev/null +++ b/dotfiles/system/.config/calibre/scheduler.xml @@ -0,0 +1,12 @@ +<?xml version='1.0' encoding='utf-8'?> +<recipe_collection xmlns="http://calibre-ebook.com/recipe_collection"> + +	<scheduled_recipe id="builtin:nytimes" title="The New York Times (Web)" last_downloaded="2025-07-22T11:00:45.395725+00:00"><schedule type="days_of_week">0,1,2,3,4,5,6:6:0</schedule></scheduled_recipe> + +	<recipe_customization keep_issues="7" id="builtin:nytimes" add_title_tag="yes" custom_tags="news" recipe_specific_options="{"web": "yes", "days": "1", "comp": "yes"}"/> + +	<scheduled_recipe id="builtin:economist" title="The Economist" last_downloaded="2025-07-22T11:07:33.441948+00:00"><schedule type="days_of_week">0,1,2,3,4,5,6:6:0</schedule></scheduled_recipe> + +	<recipe_customization keep_issues="7" id="builtin:economist" add_title_tag="yes" custom_tags="news" recipe_specific_options="{"res": "960"}"/> + +</recipe_collection>
\ No newline at end of file diff --git a/dotfiles/system/.config/calibre/smtp.py.json b/dotfiles/system/.config/calibre/smtp.py.json new file mode 100644 index 0000000..e4f8419 --- /dev/null +++ b/dotfiles/system/.config/calibre/smtp.py.json @@ -0,0 +1,18 @@ +{ +  "accounts": { +    "cjennings_paperwhite@kindle.com": [ +      "EPUB", +      true, +      true +    ] +  }, +  "aliases": {}, +  "encryption": "TLS", +  "from_": "c@cjennings.net", +  "relay_host": "127.0.0.1", +  "relay_password": "306c5275546b4e3766396f556d4d4342584d65786567", +  "relay_port": 1025, +  "relay_username": "c@cjennings.net", +  "subjects": {}, +  "tags": {} +}
\ No newline at end of file diff --git a/dotfiles/system/.config/calibre/viewer-webengine.json b/dotfiles/system/.config/calibre/viewer-webengine.json index 5559e3d..7822ccc 100644 --- a/dotfiles/system/.config/calibre/viewer-webengine.json +++ b/dotfiles/system/.config/calibre/viewer-webengine.json @@ -1,50 +1,50 @@  {    "geometry-of-main_window_geometry": {      "frame_geometry": { -      "height": 778, -      "width": 1280, +      "height": 981, +      "width": 1504,        "x": 0,        "y": 22      },      "full_screened": false,      "geometry": { -      "height": 778, -      "width": 1280, +      "height": 981, +      "width": 1504,        "x": 0,        "y": 22      },      "maximized": false,      "normal_geometry": { -      "height": 778, -      "width": 1280, +      "height": 981, +      "width": 1504,        "x": 0,        "y": 22      },      "qt": {        "__class__": "bytearray", -      "__value__": "AdnQywADAAAAAAAAAAAAFgAABP8AAAMfAAAAAAAAABYAAAT/AAADHwAAAAAAAAAABQAAAAAAAAAAFgAABP8AAAMf" +      "__value__": "AdnQywADAAAAAAAAAAAAFgAABd8AAAPqAAAAAAAAABYAAAXfAAAD6gAAAAAAAAAABeAAAAAAAAAAFgAABd8AAAPq"      },      "screen": {        "depth": 24, -      "device_pixel_ratio": 1.0, +      "device_pixel_ratio": 1.5,        "geometry_in_logical_pixels": { -        "height": 800, -        "width": 1280, +        "height": 1003, +        "width": 1504,          "x": 0,          "y": 0        },        "index_in_screens_list": 0, -      "manufacturer": "Lenovo Group Limited", +      "manufacturer": "",        "model": "", -      "name": "LVDS1", +      "name": "eDP-1",        "serial": "",        "size_in_logical_pixels": { -        "height": 800, -        "width": 1280 +        "height": 1003, +        "width": 1504        },        "virtual_geometry": { -        "height": 800, -        "width": 1280, +        "height": 1003, +        "width": 1504,          "x": 0,          "y": 0        } @@ -65,7 +65,7 @@    },    "main_window_state": {      "__class__": "bytearray", -    "__value__": "AAAA/wAAAAH9AAAAAgAAAAAAAAAAAAAAAPwCAAAAAvsAAAAQAHQAbwBjAC0AZABvAGMAawAAAAAA/////wAAAIcA////+wAAABYAcwBlAGEAcgBjAGgALQBkAG8AYwBrAAAAAAD/////AAAAlQD///8AAAABAAAAAAAAAAD8AgAAAAT7AAAAFgBsAG8AbwBrAHUAcAAtAGQAbwBjAGsAAAAAAP////8AAAB9AP////sAAAAcAGIAbwBvAGsAbQBhAHIAawBzAC0AZABvAGMAawAAAAAA/////wAAAOcA////+wAAABwAaQBuAHMAcABlAGMAdABvAHIALQBkAG8AYwBrAAAAAAD/////AAAAEwD////7AAAAHgBoAGkAZwBoAGwAaQBnAGgAdABzAC0AZABvAGMAawAAAAAA/////wAAAMkA////AAAFAAAAAwoAAAAEAAAABAAAAAgAAAAI/AAAAAEAAAAAAAAAAQAAAB4AYQBjAHQAaQBvAG4AcwBfAHQAbwBvAGwAYgBhAHICAAAAAP////8AAAAAAAAAAA==" +    "__value__": "AAAA/wAAAAH9AAAAAgAAAAAAAAAAAAAAAPwCAAAAAvsAAAAQAHQAbwBjAC0AZABvAGMAawAAAAAA/////wAAAIsA////+wAAABYAcwBlAGEAcgBjAGgALQBkAG8AYwBrAAAAAAD/////AAAAnAD///8AAAABAAAAAAAAAAD8AgAAAAT7AAAAFgBsAG8AbwBrAHUAcAAtAGQAbwBjAGsAAAAAAP////8AAACGAP////sAAAAcAGIAbwBvAGsAbQBhAHIAawBzAC0AZABvAGMAawAAAAAA/////wAAAPcA////+wAAABwAaQBuAHMAcABlAGMAdABvAHIALQBkAG8AYwBrAAAAAAD/////AAAAFAD////7AAAAHgBoAGkAZwBoAGwAaQBnAGgAdABzAC0AZABvAGMAawAAAAAA/////wAAANoA////AAAF4AAAA9UAAAAEAAAABAAAAAgAAAAI/AAAAAEAAAAAAAAAAQAAAB4AYQBjAHQAaQBvAG4AcwBfAHQAbwBvAGwAYgBhAHICAAAAAP////8AAAAAAAAAAA=="    },    "old_prefs_migrated": true,    "session_data": { @@ -102,22 +102,48 @@      "standalone_recently_opened": [        {          "authors": [ -          "G. R. F. Ferrari", -          "Tom Griffith" +          "Desconocido"          ], -        "key": "/home/cjennings/sync/books/Plato/The Republic (41225)/The Republic - Plato.epub", -        "pathtoebook": "/home/cjennings/sync/books/Plato/The Republic (41225)/The Republic - Plato.epub", -        "timestamp": "2025-05-09T08:17:25.900Z", -        "title": "Plato: The Republic" +        "key": "/home/cjennings/sync/books/Jill Vance Buroker/Kant's 'Critique of Pure Reason'_ An Introduction (43864)/Kant's 'Critique of Pure Reason'_ An Intro - Jill Vance Buroker.mobi", +        "pathtoebook": "/home/cjennings/sync/books/Jill Vance Buroker/Kant's 'Critique of Pure Reason'_ An Introduction (43864)/Kant's 'Critique of Pure Reason'_ An Intro - Jill Vance Buroker.mobi", +        "timestamp": "2025-07-18T04:13:33.770Z", +        "title": "Kants Critique of Pure Reason An Introduction Cambridge Introductions to Key Philosophical Texts Cambridge"        },        {          "authors": [ -          "test" +          "Leszek Kolakowski"          ], -        "key": "/home/cjennings/sync/books/test/test (40166)/test - test.epub", -        "pathtoebook": "/home/cjennings/sync/books/test/test (40166)/test - test.epub", -        "timestamp": "2024-06-07T19:30:17.821Z", -        "title": "test" +        "key": "/home/cjennings/sync/books/Leszek Kolakowski/Is God Happy__ Selected Essays (43040)/Is God Happy__ Selected Essays - Leszek Kolakowski.azw3", +        "pathtoebook": "/home/cjennings/sync/books/Leszek Kolakowski/Is God Happy__ Selected Essays (43040)/Is God Happy__ Selected Essays - Leszek Kolakowski.azw3", +        "timestamp": "2025-07-13T16:19:13.806Z", +        "title": "Is God Happy?: Selected Essays (Penguin Modern Classics)" +      }, +      { +        "authors": [ +          "Desconocido" +        ], +        "key": "/home/cjennings/sync/books/Desconocido/Routledge Aristotle And The Metaphysics (43652)/Routledge Aristotle And The Metaphysics - Desconocido.azw3", +        "pathtoebook": "/home/cjennings/sync/books/Desconocido/Routledge Aristotle And The Metaphysics (43652)/Routledge Aristotle And The Metaphysics - Desconocido.azw3", +        "timestamp": "2025-07-06T21:55:18.416Z", +        "title": "Routledge Aristotle And The Metaphysics" +      }, +      { +        "authors": [ +          "Habermas, Jürgen" +        ], +        "key": "/home/cjennings/sync/books/Habermas, Jurgen/The Philosophical Discourse of Modernity (40589)/The Philosophical Discourse of Modernity - Habermas, Jurgen.epub", +        "pathtoebook": "/home/cjennings/sync/books/Habermas, Jurgen/The Philosophical Discourse of Modernity (40589)/The Philosophical Discourse of Modernity - Habermas, Jurgen.epub", +        "timestamp": "2024-12-13T02:38:28.792Z", +        "title": "The Philosophical Discourse of Modernity" +      }, +      { +        "authors": [ +          "Tamsyn Muir" +        ], +        "key": "/home/cjennings/sync/books/Tamsyn Muir/Gideon the Ninth (40289)/Gideon the Ninth - Tamsyn Muir.epub", +        "pathtoebook": "/home/cjennings/sync/books/Tamsyn Muir/Gideon the Ninth (40289)/Gideon the Ninth - Tamsyn Muir.epub", +        "timestamp": "2024-11-15T19:06:33.047Z", +        "title": "Gideon the Ninth"        },        {          "key": "/home/cjennings/.local/opt/tor-browser/app/Browser/downloads/Love and Rockets #1 (1981) [Pyramid].cbz", diff --git a/dotfiles/system/.config/calibre/viewer/annots/33083ace2855943c7e4d7d188c47051f047f05e84d828fca5e5545396b94f14c.json b/dotfiles/system/.config/calibre/viewer/annots/33083ace2855943c7e4d7d188c47051f047f05e84d828fca5e5545396b94f14c.json new file mode 100644 index 0000000..0317109 --- /dev/null +++ b/dotfiles/system/.config/calibre/viewer/annots/33083ace2855943c7e4d7d188c47051f047f05e84d828fca5e5545396b94f14c.json @@ -0,0 +1 @@ +[{"pos": "epubcfi(/2/2/4/2@50:49.93)", "pos_type": "epubcfi", "timestamp": "2025-07-18T04:14:04.406842+00:00", "type": "last-read"}]
\ No newline at end of file diff --git a/dotfiles/system/.config/calibre/viewer/annots/ab0b0aa00cc90f53470da2761ea678a4ccacef1f5002917bda43970cd6096b19.json b/dotfiles/system/.config/calibre/viewer/annots/ab0b0aa00cc90f53470da2761ea678a4ccacef1f5002917bda43970cd6096b19.json new file mode 100644 index 0000000..ac7dcad --- /dev/null +++ b/dotfiles/system/.config/calibre/viewer/annots/ab0b0aa00cc90f53470da2761ea678a4ccacef1f5002917bda43970cd6096b19.json @@ -0,0 +1 @@ +[{"pos": "epubcfi(/2/2/4/2@50:49.93)", "pos_type": "epubcfi", "timestamp": "2025-07-13T16:19:15.135276+00:00", "type": "last-read"}]
\ No newline at end of file diff --git a/dotfiles/system/.config/calibre/viewer/annots/c5a80ad08eb5ae859fefd73672b6a7cddc243254b55897adfdd5671fe7b2aacf.json b/dotfiles/system/.config/calibre/viewer/annots/c5a80ad08eb5ae859fefd73672b6a7cddc243254b55897adfdd5671fe7b2aacf.json new file mode 100644 index 0000000..098752f --- /dev/null +++ b/dotfiles/system/.config/calibre/viewer/annots/c5a80ad08eb5ae859fefd73672b6a7cddc243254b55897adfdd5671fe7b2aacf.json @@ -0,0 +1 @@ +[{"pos": "epubcfi(/4/2/4/128/1:16)", "pos_type": "epubcfi", "timestamp": "2025-07-06T21:55:27.324496+00:00", "type": "last-read"}]
\ No newline at end of file | 
