Files
cbwebreader/comic/models.py
Ajurna 2647a0e31e Merge pull request #77
* added timestamp to comicstatus.

* added timestamp to comicstatus.
2022-09-19 12:18:46 +01:00

305 lines
11 KiB
Python

import io
import mimetypes
import uuid
import zipfile
from functools import reduce
from pathlib import Path
from typing import Optional, List, Union, Tuple, Final, IO
# noinspection PyPackageRequirements
import fitz
import rarfile
from PIL import Image, UnidentifiedImageError
from PIL.Image import Image as Image_type
from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.db import models
from django.db.models import UniqueConstraint
from django_boost.models.fields import AutoOneToOneField
from imagekit.models import ProcessedImageField
from imagekit.processors import ResizeToFill
from comic.errors import NotCompatibleArchive
if settings.UNRAR_TOOL:
rarfile.UNRAR_TOOL = settings.UNRAR_TOOL
class Directory(models.Model):
class Classification(models.IntegerChoices):
C_G = 0, 'G'
C_PG = 1, 'PG'
C_12 = 2, '12'
C_15 = 3, '15'
C_18 = 4, '18'
name = models.CharField(max_length=100)
parent = models.ForeignKey("Directory", null=True, blank=True, on_delete=models.CASCADE, to_field="selector")
selector = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True)
thumbnail = ProcessedImageField(upload_to='thumbs',
processors=[ResizeToFill(200, 300)],
format='JPEG',
options={'quality': 60},
null=True)
thumbnail_issue = models.ForeignKey("ComicBook", null=True,
on_delete=models.SET_NULL,
related_name='directory_thumbnail_issue')
thumbnail_index = models.PositiveIntegerField(default=0)
classification = models.PositiveSmallIntegerField(choices=Classification.choices, default=Classification.C_18)
class Meta:
ordering = ['name']
def __str__(self) -> str:
return f"Directory: {self.name}; {self.parent}"
@property
def title(self) -> str:
return self.name
@property
def type(self) -> str:
return 'Directory'
def generate_thumbnail(self) -> None:
book: ComicBook = ComicBook.objects.filter(directory=self).order_by('file_name').first()
if not book:
return
if not book.thumbnail:
book.generate_thumbnail()
self.thumbnail = book.thumbnail
self.save()
@property
def path(self) -> Path:
return self.get_path()
def get_path(self) -> Path:
path_items = self.get_path_items()
path_items.reverse()
if len(path_items) >= 2:
# pylint: disable=unnecessary-lambda
return reduce(lambda x, y: Path(x, y), path_items)
return Path(path_items[0])
def get_path_items(self, path_items: Optional[List] = None) -> List[Path]:
if path_items is None:
path_items = []
path_items.append(self.name)
if self.parent:
self.parent.get_path_items(path_items)
return path_items
def get_path_objects(self, path_items: Optional[List] = None) -> List["Directory"]:
if path_items is None:
path_items = []
path_items.append(self)
if self.parent:
self.parent.get_path_objects(path_items)
return path_items
class ComicBook(models.Model):
file_name = models.TextField()
date_added = models.DateTimeField(auto_now_add=True)
directory = models.ForeignKey(Directory, blank=True, null=True, on_delete=models.CASCADE, to_field="selector")
selector = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True)
version = models.IntegerField(default=1)
thumbnail = ProcessedImageField(upload_to='thumbs',
processors=[ResizeToFill(200, 300)],
format='JPEG',
options={'quality': 60},
null=True)
thumbnail_index = models.PositiveIntegerField(default=0)
page_count = models.IntegerField(default=0)
class Meta:
constraints = [
UniqueConstraint(fields=['directory', 'file_name'], name='one_comic_name_per_directory')
]
def __str__(self) -> str:
return self.file_name
@property
def title(self) -> str:
return self.file_name
@property
def type(self) -> str:
return 'ComicBook'
@property
def total(self) -> int:
return self.page_count
def get_pdf(self) -> Path:
base_dir = settings.COMIC_BOOK_VOLUME
if self.directory:
return Path(base_dir, self.directory.get_path(), self.file_name)
return Path(base_dir, self.file_name)
def get_image(self, page: int) -> Union[Tuple[IO[bytes], str], Tuple[bool, bool]]:
base_dir = settings.COMIC_BOOK_VOLUME
if self.directory:
archive_path = Path(base_dir, self.directory.path, self.file_name)
else:
archive_path = Path(base_dir, self.file_name)
try:
archive = rarfile.RarFile(archive_path)
except rarfile.NotRarFile:
# pylint: disable=consider-using-with
archive = zipfile.ZipFile(archive_path)
except zipfile.BadZipfile:
return False, False
file_name, file_mime = self.get_archive_files(archive)[page]
return archive.open(file_name), file_mime
def generate_thumbnail_pdf(self, page_index: int = 0) -> Tuple[io.BytesIO, Image_type, str]:
img, pil_data = self._get_pdf_image(page_index if page_index else 0)
return img, pil_data, 'Image/JPEG'
def generate_thumbnail_archive(self, page_index: int = 0) -> Union[Tuple[io.BytesIO, Image_type, str],
Tuple[bool, bool, bool]]:
img, content_type, pil_data = False, False, False
if page_index:
img, content_type = self.get_image(page_index)
pil_data = Image.open(img)
else:
for page_number in range(self.page_count):
try:
img, content_type = self.get_image(page_number)
pil_data = Image.open(img)
break
except UnidentifiedImageError:
continue
return img, pil_data, content_type
def generate_thumbnail(self, page_index: int = None) -> None:
if Path(self.file_name).suffix.lower() == '.pdf':
img, pil_data, content_type = self.generate_thumbnail_pdf(page_index if page_index else 0)
else:
img, pil_data, content_type = self.generate_thumbnail_archive(page_index)
if not img:
return
self.thumbnail = InMemoryUploadedFile(
img,
None,
f'{self.file_name}.jpg',
content_type,
pil_data.tell(),
None
)
self.save()
def _get_pdf_image(self, page_index: int) -> Tuple[io.BytesIO, Image_type]:
# noinspection PyUnresolvedReferences
doc = fitz.open(self.get_pdf())
page = doc[page_index]
pix = page.get_pixmap()
mode: Final = "RGBA" if pix.alpha else "RGB"
# noinspection PyTypeChecker
pil_data = Image.frombytes(mode, (pix.width, pix.height), pix.samples)
img = io.BytesIO()
pil_data.save(img, format="JPEG")
img.seek(0)
return img, pil_data
@staticmethod
def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
try:
book = ComicBook.objects.get(file_name=comic_file_path.name, version=0)
book.directory = directory
book.version = 1
book.save()
return book
except ComicBook.DoesNotExist:
pass
book = ComicBook(file_name=comic_file_path.name, directory=directory if directory else None)
book.save()
try:
archive, archive_type = book.get_archive()
except NotCompatibleArchive:
return comic_file_path
if archive_type == 'archive':
book.page_count = len(book.get_archive_files(archive))
elif archive_type == 'pdf':
book.page_count = archive.page_count
return book
@property
def get_archive_path(self) -> Path:
if self.directory:
return Path(settings.COMIC_BOOK_VOLUME, self.directory.get_path(), self.file_name)
return Path(settings.COMIC_BOOK_VOLUME, self.file_name)
def get_archive(self) -> Tuple[Union[rarfile.RarFile, zipfile.ZipFile, fitz.Document], str]:
archive_path = self.get_archive_path
try:
return rarfile.RarFile(archive_path), 'archive'
except rarfile.NotRarFile:
pass
try:
return zipfile.ZipFile(archive_path), 'archive'
except zipfile.BadZipFile:
pass
try:
# noinspection PyUnresolvedReferences
return fitz.open(str(archive_path)), 'pdf'
except RuntimeError:
pass
raise NotCompatibleArchive
def get_page_count(self) -> int:
archive, archive_type = self.get_archive()
if archive_type == 'archive':
return len(self.get_archive_files(archive))
elif archive_type == 'pdf':
return archive.page_count
@staticmethod
def get_archive_files(archive: Union[zipfile.ZipFile, rarfile.RarFile]) -> List[Tuple[str, str]]:
return [
(x, mimetypes.guess_type(x)[0]) for x in sorted(archive.namelist())
if not x.endswith('/') and mimetypes.guess_type(x)[0] and not x.endswith('xml')
]
class ComicStatus(models.Model):
user = models.ForeignKey(User, unique=False, null=False, on_delete=models.CASCADE)
comic = models.ForeignKey(ComicBook, unique=False, blank=False, null=False, on_delete=models.CASCADE,
to_field="selector")
last_read_page = models.IntegerField(default=0)
unread = models.BooleanField(default=True)
finished = models.BooleanField(default=False)
updated = models.DateTimeField(auto_now=True)
class Meta:
constraints = [
UniqueConstraint(fields=['user', 'comic'], name='one_per_user_per_comic')
]
def __str__(self) -> str:
return self.__repr__()
def __repr__(self) -> str:
return (
f"<ComicStatus:{self.user.username}:{self.comic.file_name}:{self.last_read_page}:"
f"{self.unread}:{self.finished}"
)
class UserMisc(models.Model):
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
feed_id = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True)
allowed_to_read = models.PositiveSmallIntegerField(default=Directory.Classification.C_18,
choices=Directory.Classification.choices)