# SPDX-FileCopyrightText: 2019-2025 Valéry Febvre
# SPDX-License-Identifier: GPL-3.0-or-later
# Author: Valéry Febvre <vfebvre@easter-eggs.com>

import datetime
from gettext import gettext as _
import gc
import importlib
import json
import logging
import os
from pathlib import Path
import re
import shutil
import time

from modern_colorthief import get_palette
from PIL import Image
from PIL import ImageFilter
from PIL import ImageStat

from komikku.models.database import create_db_connection
from komikku.models.database import insert_row
from komikku.models.database import update_row
from komikku.models.database import update_rows
from komikku.servers.utils import get_server_class_name_by_id
from komikku.servers.utils import get_server_dir_name_by_id
from komikku.servers.utils import get_server_module_name_by_id
from komikku.utils import COVER_HEIGHT
from komikku.utils import COVER_WIDTH
from komikku.utils import get_cached_data_dir
from komikku.utils import get_data_dir
from komikku.utils import is_number
from komikku.utils import remove_number_leading_zero
from komikku.utils import trunc_filename

logger = logging.getLogger(__name__)


class Manga:
    _chapters = None
    _chapters_scanlators = None
    _server = None

    STATUSES = dict(
        complete=_('Complete'),
        ongoing=_('Ongoing'),
        suspended=_('Suspended'),
        hiatus=_('Hiatus'),
    )

    def __init__(self, server=None):
        if server:
            self._server = server

    @classmethod
    def get(cls, id_, server=None, db_conn=None):
        if db_conn is not None:
            row = db_conn.execute('SELECT * FROM mangas WHERE id = ?', (id_,)).fetchone()
        else:
            db_conn = create_db_connection()
            row = db_conn.execute('SELECT * FROM mangas WHERE id = ?', (id_,)).fetchone()
            db_conn.close()

        if row is None:
            return None

        manga = cls(server=server)
        for key in row.keys():
            setattr(manga, key, row[key])

        return manga

    @classmethod
    def new(cls, data, server, long_strip_detection):
        data = data.copy()
        chapters = data.pop('chapters')
        cover_url = data.pop('cover')

        # Remove search-specific data not saved in database
        for key in ('nb_chapters', 'nb_volumes', 'last_chapter', 'last_volume'):
            data.pop(key, None)
            continue

        # Fill data with internal data
        data.update(dict(
            in_library=0,
            # Add fake last_read date: allows to display recently added manga at the top of the library
            last_read=datetime.datetime.now(datetime.UTC),
        ))

        # Long strip detection (Webtoon)
        if long_strip_detection and server.is_long_strip(data):
            data.update(dict(
                reading_mode='webtoon',
                scaling='width',
            ))

        db_conn = create_db_connection()
        with db_conn:
            id_ = insert_row(db_conn, 'mangas', data)

            rank = 0
            for chapter_data in chapters:
                if not chapter_data.get('date'):
                    # Used today if not date is provided
                    chapter_data['date'] = datetime.date.today()

                chapter = Chapter.new(chapter_data, rank, id_, db_conn)
                if chapter is not None:
                    rank += 1

        db_conn.close()

        manga = cls.get(id_, server)

        if not os.path.exists(manga.path):
            os.makedirs(manga.path)

        manga._save_cover(cover_url)

        return manga

    @property
    def backdrop_colors_css(self):
        cover_path = self.cover_fs_path
        if cover_path is None:
            return None

        path = os.path.join(self.path, 'backdrop_colors.css')
        if os.path.exists(path):
            with open(path) as fp:
                data = fp.read()
                # CSS must be regenerated if old format is detected
                # Named colors are deprecated and will be removed in GTK5
                if '@define-color' not in data:
                    return data

        palette = get_palette(cover_path, color_count=2)
        if len(palette) != 2:
            # Single color image?
            return None

        colors = [':root {\n']
        for index, color in enumerate(reversed(palette)):
            colors.append(f'\t--backdrop-background-color-{index}: rgb({color[0]} {color[1]} {color[2]} / 100%);\n')  # noqa: E702, E231
        colors.append('\t--backdrop-background-color-2: var(--window-bg-color);\n')
        colors.append('}\n')

        with open(path, 'w') as fp:
            fp.writelines(colors)

        return ''.join(colors)

    @property
    def backdrop_image_fs_path(self):
        if self.cover_fs_path is None:
            return None

        path = os.path.join(self.path, 'backdrop_image.jpg')
        if os.path.exists(path):
            return path

        with Image.open(self.cover_fs_path) as image:
            image = image.convert('RGB').filter(ImageFilter.GaussianBlur(35))
            image.save(path, 'JPEG')

        return path

    @property
    def backdrop_info(self):
        if self.backdrop_image_fs_path is None:
            return None

        path = os.path.join(self.path, 'backdrop_info.json')
        if os.path.exists(path):
            with open(path) as fp:
                return json.load(fp)

        with Image.open(self.backdrop_image_fs_path) as image:
            stat = ImageStat.Stat(image.convert('L'))
            info = {
                # Luninance values used to apply an opacity on Picture depending of color scheme (dark/light)
                'luminance': [
                    min((stat.mean[0] + stat.extrema[0][0]) / 510, 0.7),
                    max((stat.mean[0] + stat.extrema[0][1]) / 510, 0.3),
                ]
            }
            with open(path, 'w') as fp:
                json.dump(info, fp)

        return info

    @property
    def categories(self):
        db_conn = create_db_connection()
        rows = db_conn.execute(
            'SELECT c.id FROM categories c JOIN categories_mangas_association cma ON cma.category_id = c.id WHERE cma.manga_id = ?',
            (self.id,)
        )

        categories = []
        for row in rows:
            categories.append(row['id'])

        db_conn.close()

        return categories

    @property
    def chapters(self):
        if self._chapters is None:
            db_conn = create_db_connection()
            if self.sort_order and self.sort_order.endswith('asc'):
                rows = db_conn.execute('SELECT * FROM chapters WHERE manga_id = ? ORDER BY rank ASC', (self.id,))
            else:
                rows = db_conn.execute('SELECT * FROM chapters WHERE manga_id = ? ORDER BY rank DESC', (self.id,))

            self._chapters = []
            for row in rows:
                self._chapters.append(Chapter(row=row, manga=self))

            db_conn.close()

        return self._chapters

    @property
    def chapters_scanlators(self):
        if self._chapters_scanlators is None:
            db_conn = create_db_connection()

            rows = db_conn.execute('SELECT DISTINCT scanlators, count(*) FROM chapters WHERE manga_id = ? GROUP BY scanlators', (self.id,))

            scanlators = {}
            for row in rows:
                if not row[0]:  # None or []
                    # Use 'Unknown' as virtual scanlator for chapters without scanlators defined
                    row = (['Unknown'], row[1])

                for scanlator in row[0]:
                    if scanlator not in scanlators:
                        scanlators[scanlator] = {
                            'name': scanlator,
                            'count': 0,
                        }

                    scanlators[scanlator]['count'] += row[1]

            self._chapters_scanlators = list(scanlators.values()) or None

            db_conn.close()

        return self._chapters_scanlators

    @property
    def class_name(self):
        return get_server_class_name_by_id(self.server_id)

    @property
    def cover_fs_path(self):
        path = os.path.join(self.path, 'cover.jpg')
        if os.path.exists(path):
            return path

        return None

    @property
    def dir_name(self):
        return get_server_dir_name_by_id(self.server_id)

    @property
    def is_local(self):
        return self.server_id == 'local'

    @property
    def module_name(self):
        return get_server_module_name_by_id(self.server_id)

    @property
    def nb_downloaded_chapters(self):
        db_conn = create_db_connection()
        row = db_conn.execute(
            'SELECT count() AS downloaded FROM chapters WHERE manga_id = ? AND downloaded = 1 and read = 0', (self.id,)).fetchone()
        db_conn.close()

        return row['downloaded']

    @property
    def nb_recent_chapters(self):
        db_conn = create_db_connection()
        row = db_conn.execute('SELECT count() AS recents FROM chapters WHERE manga_id = ? AND recent = 1', (self.id,)).fetchone()
        db_conn.close()

        return row['recents']

    @property
    def nb_unread_chapters(self):
        db_conn = create_db_connection()
        row = db_conn.execute('SELECT count() AS unread FROM chapters WHERE manga_id = ? AND read = 0', (self.id,)).fetchone()
        db_conn.close()

        return row['unread']

    @property
    def last_read_chapter(self):
        db_conn = create_db_connection()
        row = db_conn.execute(
            'SELECT * FROM chapters WHERE manga_id = ? AND last_read IS NOT NULL ORDER BY last_read DESC LIMIT 1', (self.id,)
        ).fetchone()
        db_conn.close()

        return Chapter(row=row, manga=self) if row else None

    @property
    def path(self):
        if self.in_library:
            return os.path.join(get_data_dir(), self.dir_name, trunc_filename(self.name))

        return os.path.join(get_cached_data_dir(), self.dir_name, trunc_filename(self.name))

    @property
    def server(self):
        if self._server is None:
            module = importlib.import_module('.' + self.module_name, package='komikku.servers')
            self._server = getattr(module, self.class_name)()

        return self._server

    def _save_cover(self, url):
        # Covers in landscape format are converted to portrait format
        if self.server.save_image(url, self.path, 'cover', COVER_WIDTH, COVER_HEIGHT):
            # Remove backdrop files (image, css, info)
            for file in Path(self.path).glob('backdrop_*'):
                os.unlink(file)

    def add_in_library(self):
        tmp_path = self.path

        self.update(dict(in_library=True))

        if self.is_local:
            # Move files
            for filename in os.listdir(tmp_path):
                dst_path = os.path.join(self.path, filename)
                if os.path.exists(dst_path):
                    os.remove(dst_path)
                shutil.move(os.path.join(tmp_path, filename), self.path)

            # Remove folder
            shutil.rmtree(tmp_path)
        else:
            # Move folder
            shutil.move(tmp_path, self.path)

    def delete(self):
        db_conn = create_db_connection()

        with db_conn:
            db_conn.execute('DELETE FROM mangas WHERE id = ?', (self.id, ))

        db_conn.close()

        # Delete folder except when server is 'local'
        if os.path.exists(self.path) and not self.is_local:
            shutil.rmtree(self.path)

    def get_next_chapter(self, chapter, direction=1):
        """
        :param chapter: reference chapter
        :param direction: -1 for preceding chapter, 1 for following chapter
        """
        assert direction in (-1, 1), 'Invalid direction value'

        db_conn = create_db_connection()

        op = '>' if direction == 1 else '<'
        order = 'ASC' if direction == 1 else 'DESC'
        if self.filters and self.filters.get('scanlators'):
            # Chapters must be filtered by scanlators (some scanlators are excluded)
            excluded_scanlators = self.filters['scanlators']

            # Subquery to get IDs of not filtered chapters
            scanlators_subquery = f"""
                SELECT DISTINCT c.id
                FROM chapters c, json_each(scanlators)
                WHERE json_each.value NOT IN ("{'", "'.join(excluded_scanlators)}") AND c.manga_id = {self.id}
            """
            if 'Unknown' not in excluded_scanlators:
                # Add chapters without scanlators defined
                scanlators_subquery += f"""
                    UNION
                    SELECT id FROM chapters WHERE (scanlators IS NULL OR scanlators->0 IS NULL) AND manga_id = {self.id}
                """
        else:
            scanlators_subquery = None

        if self.sort_order in ('asc', 'desc', None):
            if scanlators_subquery:
                row = db_conn.execute(
                    f'SELECT * FROM chapters WHERE manga_id = ? AND id IN ({scanlators_subquery}) AND rank {op} ? ORDER BY rank {order}',
                    (self.id, chapter.rank)
                ).fetchone()
            else:
                row = db_conn.execute(
                    f'SELECT * FROM chapters WHERE manga_id = ? AND rank {op} ? ORDER BY rank {order}',
                    (self.id, chapter.rank)
                ).fetchone()

        elif self.sort_order in ('date-asc', 'date-desc'):
            if scanlators_subquery:
                row = db_conn.execute(
                    f'SELECT * FROM chapters WHERE manga_id = ? AND id IN ({scanlators_subquery}) AND date {op} ? ORDER BY date {order}, id {order}',
                    (self.id, chapter.date)
                ).fetchone()
            else:
                row = db_conn.execute(
                    f'SELECT * FROM chapters WHERE manga_id = ? AND date {op} ? ORDER BY date {order}, id {order}',
                    (self.id, chapter.date)
                ).fetchone()

        elif self.sort_order in ('natural-asc', 'natural-desc'):
            if scanlators_subquery:
                row = db_conn.execute(
                    f'SELECT * FROM chapters WHERE manga_id = ? AND id IN ({scanlators_subquery}) AND title {op} ? COLLATE natsort ORDER BY title {order}, id {order}',
                    (self.id, chapter.title)
                ).fetchone()
            else:
                row = db_conn.execute(
                    f'SELECT * FROM chapters WHERE manga_id = ? AND title {op} ? COLLATE natsort ORDER BY title {order}, id {order}',
                    (self.id, chapter.title)
                ).fetchone()

        db_conn.close()

        if not row:
            return None

        return Chapter(row=row, manga=self)

    def toggle_category(self, category_id, active):
        db_conn = create_db_connection()
        with db_conn:
            if active:
                insert_row(db_conn, 'categories_mangas_association', dict(category_id=category_id, manga_id=self.id))
            else:
                db_conn.execute(
                    'DELETE FROM categories_mangas_association WHERE category_id = ? AND manga_id = ?',
                    (category_id, self.id,)
                )

        db_conn.close()

    def update(self, data):
        """
        Updates specific fields

        :param dict data: fields to update
        :return: True on success False otherwise
        """
        ret = False

        # Update
        for key in data:
            setattr(self, key, data[key])

        db_conn = create_db_connection()
        with db_conn:
            ret = update_row(db_conn, 'mangas', self.id, data)

        db_conn.close()

        return ret

    def update_full(self):
        """
        Updates manga

        :return: True on success False otherwise, chapters_changes, synced
        :rtype: tuple
        """
        chapters_changes = {
            'recent_ids': [],
            'nb_updated': 0,
            'nb_deleted': 0,
        }
        gone_chapters_ranks = []

        def get_free_rank(rank):
            if rank not in gone_chapters_ranks:
                return rank

            return get_free_rank(rank + 1)

        data = self.server.get_manga_data(dict(
            slug=self.slug,
            name=self.name,
            url=self.url,
            last_read=self.last_read
        ))
        gc.collect()

        if data is None:
            return False, chapters_changes, False

        synced = self.server.sync and data['last_read'] != self.last_read

        db_conn = create_db_connection()
        with db_conn:
            # Update chapters
            chapters_data = data.pop('chapters')

            # First, delete chapters that no longer exist on server EXCEPT those marked as downloaded
            # If server is 'local', chapters are always deleted
            chapters_slugs = [str(chapter_data['slug']) for chapter_data in chapters_data]
            rows = db_conn.execute('SELECT * FROM chapters WHERE manga_id = ?', (self.id,))
            for row in rows:
                if row['slug'] not in chapters_slugs:
                    gone_chapter = Chapter.get(row['id'], manga=self, db_conn=db_conn)
                    if not gone_chapter.downloaded or self.is_local:
                        # Chapter is not dowmloaded or server is 'local'
                        # Delete chapter
                        gone_chapter.delete(db_conn)
                        chapters_changes['nb_deleted'] += 1

                        logger.warning(
                            '[UPDATE] {0} ({1}): Delete chapter {2} (no longer available)'.format(
                                self.name, self.server_id, gone_chapter.title
                            )
                        )
                    else:
                        # Chapter is downloaded
                        # Keep track of rank because it must not be reused
                        gone_chapters_ranks.append(gone_chapter.rank)

            # Then, add or update chapters
            rank = 0
            for chapter_data in chapters_data:
                row = db_conn.execute(
                    'SELECT * FROM chapters WHERE manga_id = ? AND slug = ?', (self.id, chapter_data['slug'])
                ).fetchone()

                rank = get_free_rank(rank)
                if row:
                    # Update chapter
                    changes = {}

                    # Common fields
                    for key in ('title', 'num', 'num_volume', 'url', 'date', 'scanlators'):
                        if row[key] != chapter_data.get(key):
                            if key in ('num', 'num_volume'):
                                num = str(chapter_data.get(key))
                                changes[key] = remove_number_leading_zero(num) if is_number(num) else None
                            else:
                                changes[key] = chapter_data.get(key)

                    if row['rank'] != rank:
                        changes['rank'] = rank

                    if changes:
                        chapters_changes['nb_updated'] += 1

                    # Sync fields
                    for key in ('last_page_read_index', 'last_read', 'read'):
                        if chapter_data.get(key) and row[key] != chapter_data[key]:
                            changes[key] = chapter_data[key]

                    if changes:
                        update_row(db_conn, 'chapters', row['id'], changes)

                    rank += 1
                else:
                    # Add new chapter

                    # Ensure chapter num and volume num are numbers
                    for key in ('num', 'num_volume'):
                        if chapter_data.get(key) is not None:
                            num = str(chapter_data[key])
                            chapter_data[key] = remove_number_leading_zero(num) if is_number(num) else None

                    # Used today if not date is provided
                    if not chapter_data.get('date'):
                        chapter_data['date'] = datetime.date.today()

                    chapter_data.update(dict(
                        manga_id=self.id,
                        rank=rank,
                        downloaded=chapter_data.get('downloaded', 0),
                        recent=1,
                        read=0,
                    ))
                    id_ = insert_row(db_conn, 'chapters', chapter_data)
                    if id_ is not None:
                        chapters_changes['recent_ids'].append(id_)
                        rank += 1

                        logger.info('[UPDATE] {0} ({1}): Add new chapter {2}'.format(self.name, self.server_id, chapter_data['title']))

            if chapters_changes['recent_ids'] or chapters_changes['nb_updated'] or chapters_changes['nb_deleted']:
                data['last_update'] = datetime.datetime.now(datetime.UTC)

            # Update cover
            cover = data.pop('cover', None)
            if cover:
                self._save_cover(cover)

            # Convert synopsis Markdown links (if any) into HTML <a> tags
            if data.get('synopsis'):
                data['synopsis'] = re.sub(
                    r'\[([^\[\]]*)\]\((.*?)\)',
                    r'<a href="\g<2>">\g<1></a>',
                    data['synopsis'],
                    flags=re.M
                )

            # Clear cache
            self._chapters = None
            self._chapters_scanlators = None

            # Store old path
            old_path = self.path

            # Update
            for key in data:
                setattr(self, key, data[key])

            update_row(db_conn, 'mangas', self.id, data)

            if old_path != self.path:
                # Manga name changes, manga folder must be renamed too
                os.rename(old_path, self.path)

        db_conn.close()

        return True, chapters_changes, synced


class Chapter:
    _manga = None

    def __init__(self, row=None, manga=None):
        if row is not None:
            if manga:
                self._manga = manga
            for key in row.keys():
                setattr(self, key, row[key])

    @classmethod
    def get(cls, id_, manga=None, db_conn=None):
        if db_conn is not None:
            row = db_conn.execute('SELECT * FROM chapters WHERE id = ?', (id_,)).fetchone()
        else:
            db_conn = create_db_connection()
            row = db_conn.execute('SELECT * FROM chapters WHERE id = ?', (id_,)).fetchone()
            db_conn.close()

        if row is None:
            return None

        return cls(row, manga)

    @classmethod
    def new(cls, data, rank, manga_id, db_conn=None):
        # Fill data with internal data
        data = data.copy()
        data.update(dict(
            manga_id=manga_id,
            rank=rank,
            downloaded=data.get('downloaded', 0),
            recent=0,
            read=0,
        ))

        if db_conn is not None:
            id_ = insert_row(db_conn, 'chapters', data)
        else:
            db_conn = create_db_connection()

            with db_conn:
                id_ = insert_row(db_conn, 'chapters', data)

        chapter = cls.get(id_, db_conn=db_conn) if id_ is not None else None

        return chapter

    @property
    def clearable(self):
        # Not clearable if server is 'local'
        return os.path.exists(self.path) and not self.is_local

    @property
    def is_local(self):
        return self.manga.is_local

    @property
    def manga(self):
        if self._manga is None:
            self._manga = Manga.get(self.manga_id)

        return self._manga

    @property
    def number(self):
        """ Returns chapter number"""
        if self.num and is_number(self.num):
            return remove_number_leading_zero(self.num)

        logger.warning(f'{self.manga.name} serie ({self.manga.server_id}) do not support tracking (no chapter num)?')

        return None

    @property
    def path(self):
        # BEWARE: self.slug may contain '/' characters
        # os.makedirs() must be used to create chapter's folder
        name = '/'.join([trunc_filename(part) for part in self.slug.split('/')])

        return os.path.join(self.manga.path, name)

    def clear(self, reset=False):
        """
        Clear (erase files on disk) and optionally reset

        :param bool reset: reset
        :return: True on success False otherwise
        """
        if self.clearable:
            shutil.rmtree(self.path)

        data = dict(
            downloaded=0,
        )
        if reset:
            data.update(dict(
                pages=None,
                read_progress=None,
                read=0,
                last_read=None,
                last_page_read_index=None,
            ))

        return self.update(data)

    @staticmethod
    def clear_many(chapters, reset=False):
        # Assume all chapters belong to the same manga
        manga = chapters[0].manga
        ids = []
        data = []

        for chapter in chapters:
            # Delete folder except when server is 'local'
            if os.path.exists(chapter.path) and not manga.is_local:
                shutil.rmtree(chapter.path)

            ids.append(chapter.id)

            updated_data = dict(
                downloaded=0,
            )
            if reset:
                updated_data.update(dict(
                    pages=None,
                    read_progress=None,
                    read=0,
                    last_read=None,
                    last_page_read_index=None,
                ))
            data.append(updated_data)

        db_conn = create_db_connection()
        with db_conn:
            update_rows(db_conn, 'chapters', ids, data)

        db_conn.close()

    def delete(self, db_conn=None):
        if db_conn is not None:
            db_conn.execute('DELETE FROM chapters WHERE id = ?', (self.id, ))
        else:
            db_conn = create_db_connection()

            with db_conn:
                db_conn.execute('DELETE FROM chapters WHERE id = ?', (self.id, ))

            db_conn.close()

        if os.path.exists(self.path):
            shutil.rmtree(self.path)

    def get_page(self, index):
        page_path = self.get_page_path(index)
        if page_path:
            return page_path, None

        page = self.pages[index]

        start = time.perf_counter()
        data = self.manga.server.get_manga_chapter_page_image(self.manga.slug, self.manga.name, self.slug, page)
        rtime = time.perf_counter() - start
        gc.collect()

        if data is None:
            return None, None

        if not os.path.exists(self.path):
            os.makedirs(self.path, exist_ok=True)

        image = data['buffer']
        page_path = os.path.join(self.path, data['name'])
        with open(page_path, 'wb') as fp:
            fp.write(image)

        updated_data = {}

        # If page name can't be retrieved from `image` or `slug`, we store its name
        retrievable = False
        if page.get('image') and data['name'] == page['image'].split('?')[0].split('/')[-1]:
            retrievable = True
        elif page.get('slug') and data['name'] == page['slug'].split('/')[-1]:
            retrievable = True
        if not retrievable:
            self.pages[index]['name'] = data['name']
            updated_data['pages'] = self.pages

        downloaded = len(next(os.walk(self.path))[2]) == len(self.pages)
        if downloaded != self.downloaded:
            updated_data['downloaded'] = downloaded

        if updated_data:
            self.update(updated_data)

        return page_path, rtime

    def get_page_data(self, index):
        """
        Return page image data: buffer, MIME type, name

        Useful for locally stored manga. Image data (bytes) are retrieved directly from archive.
        """
        return self.manga.server.get_manga_chapter_page_image(self.manga.slug, self.manga.name, self.slug, self.pages[index])

    def get_page_path(self, index):
        if not self.pages:
            return None

        page = self.pages[index]

        # Get image name
        if page.get('name'):
            name = page['name']

        elif page.get('image') and page['image'].split('/')[-1]:
            # Extract from URL (relative or absolute)
            name = page['image'].split('/')[-1]
            # Remove query string
            name = name.split('?')[0]

        elif page.get('slug') and page['slug'].split('/')[-1]:
            name = page['slug'].split('/')[-1]

        else:
            return None

        path = os.path.join(self.path, name)

        return path if os.path.exists(path) else None

    def update(self, data):
        """
        Updates specific fields

        :param dict data: fields to update
        :return: True on success False otherwise
        """
        ret = False

        for key in data:
            setattr(self, key, data[key])

        db_conn = create_db_connection()
        with db_conn:
            ret = update_row(db_conn, 'chapters', self.id, data)

        db_conn.close()

        return ret

    def update_full(self):
        """
        Updates chapter

        Fetches server and saves chapter data

        :return: True on success False otherwise
        """
        if self.pages:
            return True

        data = self.manga.server.get_manga_chapter_data(self.manga.slug, self.manga.name, self.slug, self.url)
        gc.collect()

        if data is None or not data['pages']:
            return False

        return self.update(data)
