Merge pull request #77

* added timestamp to comicstatus.

* added timestamp to comicstatus.
This commit is contained in:
2022-09-19 12:18:46 +01:00
committed by GitHub
parent 01e73cc9b3
commit 2647a0e31e
20 changed files with 463 additions and 304 deletions

View File

@@ -46,6 +46,7 @@ router.register(r'generate_thumbnail', rest.GenerateThumbnailViewSet, basename='
router.register(r'read', rest.ReadViewSet, basename='read')\
.register(r'image', rest.ImageViewSet, basename='image', parents_query_lookups=['selector'])
router.register(r'recent', rest.RecentComicsView, basename="recent")
router.register(r'history', rest.HistoryViewSet, basename='history')
router.register(r'action', rest.ActionViewSet, basename='action')
router.register(r'account', rest.AccountViewSet, basename='account')
router.register(r'directory', rest.DirectoryViewSet, basename='directory')

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from django.contrib import admin
from .models import Directory, ComicBook, ComicPage, ComicStatus, UserMisc
from .models import Directory, ComicBook, ComicStatus, UserMisc
@admin.register(Directory)
@@ -26,12 +26,6 @@ class ComicBookAdmin(admin.ModelAdmin):
search_fields = ['file_name']
@admin.register(ComicPage)
class ComicPageAdmin(admin.ModelAdmin):
list_display = ('id', 'Comic', 'index', 'page_file_name', 'content_type')
raw_id_fields = ('Comic',)
@admin.register(ComicStatus)
class ComicStatusAdmin(admin.ModelAdmin):
list_display = (

View File

@@ -1,114 +1,38 @@
from pathlib import Path
from typing import Optional, Union
from PIL import UnidentifiedImageError
from django.conf import settings
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandParser
from loguru import logger
from comic.models import ComicBook, Directory
from comic.processing import generate_directory
class Command(BaseCommand):
help = "Scan directories to Update Comic DB"
def __init__(self):
def __init__(self) -> None:
super().__init__()
self.OUTPUT = False
self.VERIFY_PAGES = False
def add_arguments(self, parser):
def add_arguments(self, parser: CommandParser) -> None:
parser.add_argument(
'--out',
action='store_true',
help='Output to console',
)
parser.add_argument(
'--verify_pages',
action='store_true',
help='Output to console',
)
def handle(self, *args, **options):
self.VERIFY_PAGES = options.get('verify_pages', False)
def handle(self, *args, **options) -> None:
self.OUTPUT = options.get('out', False)
self.scan_directory()
def scan_directory(self, directory=False):
"""
:type directory: Directory
"""
if not directory:
comic_dir = settings.COMIC_BOOK_VOLUME
else:
comic_dir = Path(settings.COMIC_BOOK_VOLUME, directory.path)
if directory:
books = ComicBook.objects.filter(directory=directory)
else:
books = ComicBook.objects.filter(directory__isnull=True)
for book in books:
Path(comic_dir, book.file_name).is_file()
if not Path(comic_dir, book.file_name).is_file():
book.delete()
for file in sorted(comic_dir.glob('*')):
if file.is_dir():
if self.OUTPUT:
logger.info(f"Scanning Directory {file}")
try:
if directory:
next_directory, created = Directory.objects.get_or_create(name=file.name, parent=directory)
else:
next_directory, created = Directory.objects.get_or_create(name=file.name, parent__isnull=True)
except Directory.MultipleObjectsReturned:
if directory:
next_directories = Directory.objects.filter(name=file.name, parent=directory)
else:
next_directories = Directory.objects.filter(name=file.name, parent__isnull=True)
next_directories = next_directories.order_by('id')
next_directory = next_directories.first()
next_directories.exclude(id=next_directory.id).delete()
logger.error(f'Duplicate Directory {file}')
created = False
if created:
next_directory.save()
self.scan_directory(next_directory)
else:
if file.suffix.lower() not in settings.SUPPORTED_FILES:
continue
if self.OUTPUT:
logger.info(f"Scanning File {file}")
try:
if directory:
try:
book = ComicBook.objects.get(file_name=file.name, directory=directory)
except ComicBook.MultipleObjectsReturned:
logger.error(f'Duplicate Comic {file}')
books = ComicBook.objects.filter(file_name=file.name, directory=directory).order_by('id')
book = books.first()
extra_books = books.exclude(id=book.id)
extra_books.delete()
if book.version == 0:
book.version = 1
book.save()
if self.VERIFY_PAGES:
logger.info(f'Verifing pages: {book}')
book.verify_pages()
else:
book = ComicBook.objects.get(file_name=file.name, directory__isnull=True)
if book.version == 0:
if directory:
book.directory = directory
book.version = 1
book.save()
if self.VERIFY_PAGES:
logger.info(f'Verifing pages: {book}')
book.verify_pages()
except ComicBook.DoesNotExist:
book = ComicBook.process_comic_book(file, directory)
try:
book.generate_thumbnail()
except UnidentifiedImageError:
book.generate_thumbnail(1)
except:
pass
def scan_directory(self, user: Optional[User] = None, directory: Optional[Directory] = None) -> None:
if not user:
user_model = get_user_model()
user = user_model.objects.first()
for item in generate_directory(user, directory):
item: Union[Directory, ComicBook]
if item.type == 'Directory':
logger.info(item)
self.scan_directory(user, item)

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.7 on 2022-09-15 09:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comic', '0046_comicbook_one_comic_name_per_directory'),
]
operations = [
migrations.AddField(
model_name='comicstatus',
name='updated',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.7 on 2022-09-15 09:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comic', '0047_comicstatus_updated'),
]
operations = [
migrations.AddField(
model_name='comicbook',
name='page_count',
field=models.IntegerField(default=0),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.0.7 on 2022-09-15 09:59
from django.db import migrations
from django.db.models import Count
def forwards_func(apps, schema_editor):
books = apps.get_model("comic", "ComicBook")
for book in books.objects.all().annotate(total_pages=Count('comicpage')):
book.page_count = book.total_pages
book.save()
class Migration(migrations.Migration):
dependencies = [
('comic', '0048_comicbook_page_count'),
]
operations = [
migrations.RunPython(forwards_func),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 4.0.7 on 2022-09-15 15:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('comic', '0049_populate_pages'),
]
operations = [
migrations.DeleteModel(
name='ComicPage',
),
]

View File

@@ -3,9 +3,8 @@ import mimetypes
import uuid
import zipfile
from functools import reduce
from itertools import zip_longest
from pathlib import Path
from typing import Optional, List, Union, Tuple, Final
from typing import Optional, List, Union, Tuple, Final, IO
# noinspection PyPackageRequirements
import fitz
@@ -17,7 +16,6 @@ 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.db.transaction import atomic
from django_boost.models.fields import AutoOneToOneField
from imagekit.models import ProcessedImageField
from imagekit.processors import ResizeToFill
@@ -93,7 +91,7 @@ class Directory(models.Model):
self.parent.get_path_items(path_items)
return path_items
def get_path_objects(self, path_items=None) -> List["Directory"]:
def get_path_objects(self, path_items: Optional[List] = None) -> List["Directory"]:
if path_items is None:
path_items = []
path_items.append(self)
@@ -114,6 +112,7 @@ class ComicBook(models.Model):
options={'quality': 60},
null=True)
thumbnail_index = models.PositiveIntegerField(default=0)
page_count = models.IntegerField(default=0)
class Meta:
constraints = [
@@ -131,13 +130,17 @@ class ComicBook(models.Model):
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.BytesIO, Image_type], Tuple[bool, bool]]:
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)
@@ -151,14 +154,8 @@ class ComicBook(models.Model):
except zipfile.BadZipfile:
return False, False
page_obj = ComicPage.objects.get(Comic=self, index=page)
try:
out = (archive.open(page_obj.page_file_name), page_obj.content_type)
except rarfile.NoRarEntry:
self.verify_pages()
page_obj = ComicPage.objects.get(Comic=self, index=page)
out = (archive.open(page_obj.page_file_name), page_obj.content_type)
return out
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)
@@ -171,7 +168,7 @@ class ComicBook(models.Model):
img, content_type = self.get_image(page_index)
pil_data = Image.open(img)
else:
for page_number in range(ComicPage.objects.filter(Comic=self).count()):
for page_number in range(self.page_count):
try:
img, content_type = self.get_image(page_number)
pil_data = Image.open(img)
@@ -211,10 +208,6 @@ class ComicBook(models.Model):
img.seek(0)
return img, pil_data
@property
def page_count(self) -> int:
return ComicPage.objects.filter(Comic=self).count()
@staticmethod
def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
try:
@@ -234,14 +227,10 @@ class ComicBook(models.Model):
return comic_file_path
if archive_type == 'archive':
book.verify_pages()
book.page_count = len(book.get_archive_files(archive))
elif archive_type == 'pdf':
with atomic():
for page_index in range(archive.page_count):
page = ComicPage(
Comic=book, index=page_index, page_file_name=page_index + 1, content_type='application/pdf'
)
page.save()
book.page_count = archive.page_count
return book
@property
@@ -268,56 +257,20 @@ class ComicBook(models.Model):
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) -> List[Tuple[str, str]]:
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]
if not x.endswith('/') and mimetypes.guess_type(x)[0] and not x.endswith('xml')
]
def verify_pages(self, pages: Optional["ComicPage"] = None) -> None:
if not pages:
pages = ComicPage.objects.filter(Comic=self)
archive, archive_type = self.get_archive()
if archive_type == 'pdf':
return
archive_files = self.get_archive_files(archive)
index = 0
for a_file, db_file in zip_longest(archive_files, pages):
if not a_file:
db_file.delete()
continue
if not db_file:
ComicPage(
Comic=self,
page_file_name=a_file[0],
index=index,
content_type=a_file[1]
).save()
index += 1
continue
changed = False
if a_file[0] != db_file.page_file_name:
db_file.page_file_name = a_file[0]
changed = True
if a_file[1] != db_file.content_type:
db_file.content_type = a_file[1]
changed = True
if changed:
db_file.save()
index += 1
class ComicPage(models.Model):
Comic = models.ForeignKey(ComicBook, on_delete=models.CASCADE)
index = models.IntegerField()
page_file_name = models.CharField(max_length=200, unique=False)
content_type = models.CharField(max_length=30)
class Meta:
ordering = ['index']
class ComicStatus(models.Model):
user = models.ForeignKey(User, unique=False, null=False, on_delete=models.CASCADE)
@@ -326,6 +279,7 @@ class ComicStatus(models.Model):
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 = [
@@ -342,9 +296,6 @@ class ComicStatus(models.Model):
)
# TODO: add support to reference items last being read
class UserMisc(models.Model):
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)

View File

@@ -1,17 +1,19 @@
import mimetypes
import zipfile
from itertools import chain
from pathlib import Path
from typing import NamedTuple, List
from typing import NamedTuple, List, Optional, Union
import rarfile
from django.conf import settings
from django.contrib.auth.models import User
from django.db.models import Count, Q, F, Case, When, PositiveSmallIntegerField
from django.db.models import Count, Q, F, Case, When, PositiveSmallIntegerField, QuerySet
from comic import models
from comic.errors import NotCompatibleArchive
def generate_directory(user: User, directory=None):
def generate_directory(user: User, directory: Optional[models.Directory] = None) -> List[QuerySet]:
dir_path = Path(settings.COMIC_BOOK_VOLUME, directory.path) if directory else settings.COMIC_BOOK_VOLUME
files = []
@@ -37,7 +39,6 @@ def generate_directory(user: User, directory=None):
models.ComicStatus.objects.bulk_create(new_status)
file_db_query = file_db_query.annotate(
total=Count('comicpage', distinct=True),
progress=F('comicstatus__last_read_page') + 1,
finished=F('comicstatus__finished'),
unread=F('comicstatus__unread'),
@@ -60,7 +61,7 @@ def generate_directory(user: User, directory=None):
return files
def clean_directories(directories, dir_path, directory=None):
def clean_directories(directories: QuerySet, dir_path: Path, directory: Optional[models.Directory] = None) -> None:
dir_db_set = set(Path(settings.COMIC_BOOK_VOLUME, x.path) for x in directories)
dir_list = set(x for x in sorted(dir_path.glob('*')) if x.is_dir())
# Create new directories db instances
@@ -72,7 +73,7 @@ def clean_directories(directories, dir_path, directory=None):
models.Directory.objects.get(name=stale_directory.name, parent=directory).delete()
def clean_files(files, user, dir_path, directory=None):
def clean_files(files: QuerySet, user: User, dir_path: Path, directory: Optional[models.Directory] = None) -> None:
file_list = set(x for x in sorted(dir_path.glob('*')) if x.is_file())
files_db_set = set(Path(dir_path, x.file_name) for x in files)
@@ -80,34 +81,23 @@ def clean_files(files, user, dir_path, directory=None):
books_to_add = []
for new_comic in file_list - files_db_set:
if new_comic.suffix.lower() in settings.SUPPORTED_FILES:
books_to_add.append(
models.ComicBook(file_name=new_comic.name, directory=directory)
)
new_book = models.ComicBook(file_name=new_comic.name, directory=directory)
archive, archive_type = new_book.get_archive()
try:
if archive_type == 'archive':
new_book.page_count = len(get_archive_files(archive))
elif archive_type == 'pdf':
new_book.page_count = archive.page_count
except NotCompatibleArchive:
pass
books_to_add.append(new_book)
models.ComicBook.objects.bulk_create(books_to_add)
pages_to_add = []
status_to_add = []
for book in books_to_add:
status_to_add.append(models.ComicStatus(user=user, comic=book))
try:
archive, archive_type = book.get_archive()
if archive_type == 'archive':
pages_to_add.extend([
models.ComicPage(
Comic=book, index=idx, page_file_name=page.file_name, content_type=page.mime_type
) for idx, page in enumerate(get_archive_files(archive))
])
elif archive_type == 'pdf':
pages_to_add.extend([
models.ComicPage(
Comic=book, index=idx, page_file_name=idx + 1, content_type='application/pdf'
) for idx in range(archive.page_count)
])
except NotCompatibleArchive:
pass
models.ComicStatus.objects.bulk_create(status_to_add)
models.ComicPage.objects.bulk_create(pages_to_add)
# Remove stale comic instances
for stale_comic in files_db_set - file_list:
@@ -119,7 +109,7 @@ class ArchiveFile(NamedTuple):
mime_type: str
def get_archive_files(archive) -> List[ArchiveFile]:
def get_archive_files(archive: Union[zipfile.ZipFile, rarfile.RarFile]) -> List[ArchiveFile]:
return [
ArchiveFile(x, mimetypes.guess_type(x)[0]) for x in sorted(archive.namelist())
if not x.endswith('/') and mimetypes.guess_type(x)[0]

View File

@@ -6,7 +6,7 @@ from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.db.models import Count, Case, When, F, PositiveSmallIntegerField, FileField, QuerySet
from django.db.models import Case, When, F, PositiveSmallIntegerField, FileField, QuerySet
from django.http import FileResponse
from drf_yasg.utils import swagger_auto_schema
from rest_framework import viewsets, serializers, mixins, permissions, status, renderers
@@ -180,12 +180,6 @@ class GenerateThumbnailViewSet(viewsets.ViewSet):
)
class PageSerializer(serializers.Serializer):
index = serializers.IntegerField()
page_file_name = serializers.CharField()
content_type = serializers.CharField()
class DirectionSerializer(serializers.Serializer):
route = serializers.ChoiceField(choices=['read', 'browse'])
selector = serializers.UUIDField(required=False)
@@ -197,7 +191,7 @@ class ReadSerializer(serializers.Serializer):
last_read_page = serializers.IntegerField()
prev_comic = DirectionSerializer()
next_comic = DirectionSerializer()
pages = PageSerializer(many=True)
pages = serializers.IntegerField()
class TypeSerializer(serializers.Serializer):
@@ -217,10 +211,13 @@ class ReadViewSet(viewsets.GenericViewSet):
def retrieve(self, request: Request, selector: UUID) -> Response:
comic = get_object_or_404(models.ComicBook, selector=selector)
_, _ = models.UserMisc.objects.get_or_create(user=request.user)
pages = models.ComicPage.objects.filter(Comic=comic)
comic_status, _ = models.ComicStatus.objects.get_or_create(comic=comic, user=request.user)
comic_list = list(models.ComicBook.objects.filter(directory=comic.directory).order_by('file_name'))
comic_index = comic_list.index(comic)
current_page_count = comic.get_page_count()
if comic.page_count != current_page_count:
comic.page_count = current_page_count
comic.save()
try:
prev_comic = {'route': 'browse', 'selector': comic.directory.selector} if comic_index == 0 else \
{'route': 'read', 'selector': comic_list[comic_index - 1].selector}
@@ -238,7 +235,7 @@ class ReadViewSet(viewsets.GenericViewSet):
"last_read_page": comic_status.last_read_page,
"prev_comic": prev_comic,
"next_comic": next_comic,
"pages": pages,
"pages": comic.page_count,
}
serializer = self.serializer_class(data)
return Response(serializer.data)
@@ -269,7 +266,7 @@ class ReadViewSet(viewsets.GenericViewSet):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
comic_status, _ = models.ComicStatus.objects.annotate(page_count=Count('comic__comicpage')) \
comic_status, _ = models.ComicStatus.objects.annotate(page_count=F('comic__page_count')) \
.get_or_create(comic_id=selector, user=request.user)
comic_status.last_read_page = serializer.data['page']
comic_status.unread = False
@@ -296,14 +293,14 @@ class PassthroughRenderer(renderers.BaseRenderer): # pylint: disable=too-few-pu
class ImageViewSet(viewsets.ViewSet):
queryset = models.ComicPage.objects.all()
queryset = models.ComicBook.objects.all()
lookup_field = 'page'
renderer_classes = [PassthroughRenderer]
@swagger_auto_schema(responses={status.HTTP_200_OK: "A Binary Image response"})
def retrieve(self, _request: Request, parent_lookup_selector: UUID, page: int) -> FileResponse:
book = models.ComicBook.objects.get(selector=parent_lookup_selector)
img, content = book.get_image(int(page))
img, content = book.get_image(int(page) - 1)
self.renderer_classes[0].media_type = content
return FileResponse(img, content_type=content)
@@ -315,14 +312,17 @@ class StandardResultsSetPagination(PageNumberPagination):
class RecentComicsSerializer(serializers.ModelSerializer):
total_pages = serializers.IntegerField()
unread = serializers.BooleanField()
finished = serializers.BooleanField()
last_read_page = serializers.IntegerField()
class Meta:
model = models.ComicBook
fields = ['file_name', 'date_added', 'selector', 'total_pages', 'unread', 'finished', 'last_read_page']
fields = ['file_name', 'date_added', 'selector', 'page_count', 'unread', 'finished', 'last_read_page']
class SearchTextQuerySerializer(serializers.Serializer):
search_text = serializers.CharField(required=False)
class RecentComicsView(mixins.ListModelMixin, viewsets.GenericViewSet):
@@ -339,7 +339,6 @@ class RecentComicsView(mixins.ListModelMixin, viewsets.GenericViewSet):
query = models.ComicBook.objects.all()
query = query.annotate(
total_pages=Count('comicpage'),
unread=Case(When(comicstatus__user=user, then='comicstatus__unread')),
finished=Case(When(comicstatus__user=user, then='comicstatus__finished')),
last_read_page=Case(When(comicstatus__user=user, then='comicstatus__last_read_page')) + 1,
@@ -353,6 +352,53 @@ class RecentComicsView(mixins.ListModelMixin, viewsets.GenericViewSet):
query = query.order_by('-date_added')
return query
@swagger_auto_schema(query_serializer=SearchTextQuerySerializer())
def list(self, request: Request, *args, **kwargs) -> Response:
return super().list(request, *args, **kwargs)
class HistorySerializer(serializers.ModelSerializer):
last_read_time = serializers.DateTimeField()
finished = serializers.BooleanField()
last_read_page = serializers.IntegerField()
class Meta:
model = models.ComicBook
fields = ['file_name', 'selector', 'page_count', 'last_read_time', 'finished', 'last_read_page']
class HistoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
queryset = models.ComicBook.objects.all()
serializer_class = HistorySerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self) -> QuerySet[models.ComicBook]:
user = self.request.user
if "search_text" in self.request.query_params:
query = models.ComicBook.objects.filter(file_name__icontains=self.request.query_params["search_text"])
else:
query = models.ComicBook.objects.all()
query = query.annotate(
last_read_page=Case(When(comicstatus__user=user, then='comicstatus__last_read_page')) + 1,
finished=Case(When(comicstatus__user=user, then='comicstatus__finished')),
unread=Case(When(comicstatus__user=user, then='comicstatus__unread')),
last_read_time=Case(When(comicstatus__user=user, then='comicstatus__updated')),
classification=Case(
When(directory__isnull=True, then=models.Directory.Classification.C_18),
default=F('directory__classification'),
output_field=PositiveSmallIntegerField(choices=models.Directory.Classification.choices)
)
)
query = query.filter(comicstatus__unread=False)
query = query.order_by('-comicstatus__updated')
return query
@swagger_auto_schema(query_serializer=SearchTextQuerySerializer())
def list(self, request: Request, *args, **kwargs) -> Response:
return super().list(request, *args, **kwargs)
class ActionSerializer(serializers.Serializer):
selectors = serializers.ListSerializer(child=serializers.UUIDField())
@@ -369,7 +415,7 @@ class ActionViewSet(viewsets.GenericViewSet):
if serializer.is_valid():
comics = self.get_comics(serializer.data['selectors'])
comic_status = models.ComicStatus.objects.filter(comic__selector__in=comics, user=request.user)
comic_status = comic_status.annotate(total_pages=Count('comic__comicpage'))
comic_status = comic_status.annotate(total_pages=F('comic__page_count'))
status_to_update = []
for c_status in comic_status:
c_status.last_read_page = c_status.total_pages - 1
@@ -378,8 +424,7 @@ class ActionViewSet(viewsets.GenericViewSet):
status_to_update.append(c_status)
comics.remove(str(c_status.comic_id))
for new_status in comics:
comic = models.ComicBook.objects.annotate(
total_pages=Count('comicpage')).get(selector=new_status)
comic = models.ComicBook.objects.get(selector=new_status)
obj, _ = models.ComicStatus.objects.get_or_create(comic=comic, user=request.user)
obj.unread = False
obj.finished = True

View File

@@ -25,7 +25,7 @@
<li><a class="dropdown-item" @click="updateComic('mark_unread')"><font-awesome-icon icon='book' /> Mark Un-read</a></li>
<li><a class="dropdown-item" @click="updateComic('mark_read')"><font-awesome-icon icon='book-open' /> Mark read</a></li>
<li><a class="dropdown-item" v-if="data.type === 'ComicBook'" @click="$emit('markPreviousRead', data.selector)"><font-awesome-icon icon='book' /><font-awesome-icon icon='turn-up' />Mark previous comics read</a></li>
<li><a class="dropdown-item" v-if="data.type === 'Directory'" data-bs-toggle="modal" :data-bs-target="'#'+data.selector"><font-awesome-icon icon='edit' />Edit comic</a></li>
<li><a class="dropdown-item" v-if="data.type === 'Directory'" data-bs-toggle="modal" :data-bs-target="'#id'+data.selector"><font-awesome-icon icon='edit' />Edit comic</a></li>
</ul>
</div>
</div>
@@ -33,7 +33,7 @@
</div>
</div>
<div class="modal fade" :id="data.selector" tabindex="-1" :aria-labelledby="data.selector+'-label'" aria-hidden="true" >
<div class="modal fade" :id="'id'+data.selector" tabindex="-1" :aria-labelledby="data.selector+'-label'" aria-hidden="true" >
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">

View File

@@ -0,0 +1,146 @@
<template>
<div class="container">
<div class="row">
<div class="col d-flex align-items-center">
<form class="form-inline ">
<label class="my-1 px-1" for="selectChoices">Show</label>
<select class="custom-select my-1 mr-sm-2 " id="selectChoices" v-model="this.page_size" @change="this.setPage(this.page)">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<label class="my-1 px-1" for="selectChoices">entries</label>
</form>
</div>
<div class="col d-flex justify-content-end">
<form class="form-inline">
<div class="form-floating">
<input type="text" class="form-control" id="floatingInput" placeholder="name@example.com" v-model="search_text" @keyup="this.debounceInput()">
<label for="floatingInput">Search</label>
</div>
</form>
</div>
</div>
<div class="row">
<caption>
<h2>Reading History</h2>
</caption>
</div>
<div class="row">
<table class="table table-striped table-bordered">
<caption>Recent Comics</caption>
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Comic</th>
<th scope="col">Date Read</th>
<th scope="col">status</th>
</tr>
</thead>
<tbody>
<template v-for="item in comics" :key="item.id">
<tr>
<th scope="row"><font-awesome-icon icon='book' class="" /></th>
<td><router-link :to="{name: 'read', params: { selector: item.selector }}" class="" >{{ item.file_name }}</router-link></td>
<td>{{ timeago(item.last_read_time) }}</td>
<td>{{ get_status(item) }}</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="row">
<div class="col">
Showing page {{ this.page }} of {{ this.page_count }} pages.
</div>
<div class="col d-flex justify-content-end">
<paginate
v-model="this.page"
:page-count="this.page_count"
:click-handler="this.setPage"
:prev-text="'Prev'"
:next-text="'Next'"
:container-class="'pagination '"
>
</paginate>
</div>
</div>
</div>
</template>
<script>
import Paginate from "vuejs-paginate-next";
import api from "@/api";
import * as timeago from "timeago.js";
export default {
name: "HistoryTable",
components: {
Paginate
},
data () {
return {
page: 1,
page_size: 10,
page_count: 2,
search_text: '',
comics: [],
timeout: null,
func_selected: 'choose',
feed_id: ''
}},
methods: {
updateComicList () {
let comic_list_url = '/api/history/'
let params = { params: { page: this.page, page_size: this.page_size } }
if (this.search_text) {
params.params.search_text = this.search_text
}
api.get(comic_list_url, params)
.then(response => {
this.comics = response.data.results
this.page_count = Math.ceil(response.data.count / this.page_size)
})
.catch((error) => {
if (error.response.data.detail === 'Invalid page.') {
this.setPage(1)
} else {
console.log(error)
}
})
},
timeago(input) {
return timeago.format(input)
},
get_status(item) {
if (item.unread || item.unread === null) {
return "Unread"
} else if (item.finished) {
return "Finished"
} else {
return item.last_read_page + ' / ' + item.page_count
}
},
setPage(page) {
this.page = page
this.updateComicList()
},
debounceInput() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.setPage(this.page)
}, 500)
},
},
mounted() {
this.updateComicList()
},
}
</script>
<style scoped>
</style>

View File

@@ -1,15 +1,15 @@
<template>
<div class="reveal" id="comic_box" ref="comic_box" >
<div id="slides_div" class="slides" ref="slides">
<section class="" v-for="page in pages" :key="page.index" :data-menu-title="page.page_file_name" hidden>
<img :data-src="'/api/read/' + selector + '/image/' + page.index + '/'" class="w-100" :alt="page.page_file_name">
<section class="" v-for="page in pages" :key="page" :data-menu-title="page" hidden>
<img :data-src="'/api/read/' + selector + '/image/' + page + '/'" class="w-100" :alt="page">
</section>
</div>
</div>
<div class="row navButtons pb-2">
<comic-paginate
v-model="paginate_page"
:page_count="pages.length"
:page_count="pages"
@setPage="this.setPage"
@prevComic="prevComic"
@nextComic="nextComic"
@@ -35,7 +35,7 @@ export default {
title: '',
prev_comic: {},
next_comic: {},
pages: [],
pages: 1,
}
},
props: {
@@ -130,7 +130,8 @@ export default {
plugins: [ ]
}).then(() => {
this.deck.slide(this.current_page)
this.deck.on( 'slidechanged', () => {
api.put(set_read_url, {page: this.current_page})
this.deck.on( 'slidechanged', (event) => {
this.$refs.comic_box.scrollIntoView({behavior: 'smooth'})
api.put(set_read_url, {page: event.indexh})
});

View File

@@ -13,6 +13,9 @@
<li class="nav-item">
<router-link :to="{name: 'recent'}" class="nav-link" >Recent</router-link>
</li>
<li class="nav-item">
<router-link :to="{name: 'history'}" class="nav-link" >History</router-link>
</li>
<li class="nav-item">
<router-link :to="{name: 'account'}" class="nav-link" >Account</router-link>
</li>

View File

@@ -131,7 +131,7 @@ export default {
} else if (item.finished) {
return "Finished"
} else {
return item.last_read_page + 1 + ' / ' + item.total_pages
return item.last_read_page + ' / ' + item.page_count
}
},
setPage(page) {

View File

@@ -6,6 +6,7 @@ const AccountView = () => import('@/views/AccountView')
const BrowseView = () => import('@/views/BrowseView')
const UserView = () => import('@/views/UserView')
const LoginView = () => import('@/views/LoginView')
const HistoryView = () => import('@/views/HistoryView')
const routes = [
{
@@ -37,6 +38,11 @@ const routes = [
name: 'recent',
component: RecentView
},
{
path: '/history',
name: 'history',
component: HistoryView
},
{
path: '/account',
name: 'account',

View File

@@ -0,0 +1,24 @@
<template>
<the-breadcrumbs :manual_crumbs="this.crumbs" />
<history-table />
</template>
<script>
import TheBreadcrumbs from "@/components/TheBreadcrumbs";
import HistoryTable from "@/components/HistoryTable";
export default {
name: "HistoryView",
components: {HistoryTable, TheBreadcrumbs},
data () {
return {
crumbs: [
{id: 0, selector: '', name: 'Home'},
{id: 1, selector: '', name: 'History'}
]
}},
}
</script>
<style scoped>
</style>

120
poetry.lock generated
View File

@@ -97,7 +97,7 @@ lxml = ["lxml"]
[[package]]
name = "certifi"
version = "2022.6.15"
version = "2022.9.14"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
@@ -236,14 +236,14 @@ Django = ">3.2"
[[package]]
name = "Django"
version = "4.0.7"
version = "4.1.1"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
category = "main"
optional = false
python-versions = ">=3.8"
[package.dependencies]
asgiref = ">=3.4.1,<4"
asgiref = ">=3.5.2,<4"
sqlparse = ">=0.2.2"
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
@@ -264,7 +264,7 @@ django = "*"
[[package]]
name = "django-boost"
version = "2.0"
version = "2.1"
description = "Django Extension library"
category = "main"
optional = false
@@ -314,7 +314,7 @@ tests = ["jinja2 (>=2.9.6)", "mock (==1.0.1)", "pep8 (==1.4.6)", "pytest (<4.0)"
[[package]]
name = "django-extensions"
version = "3.2.0"
version = "3.2.1"
description = "Extensions for Django"
category = "main"
optional = false
@@ -383,7 +383,7 @@ sqlparse = "*"
[[package]]
name = "django-sri"
version = "0.4.0"
version = "0.5.0"
description = "Subresource Integrity for Django"
category = "main"
optional = false
@@ -548,7 +548,7 @@ license = ["ukkonen"]
[[package]]
name = "idna"
version = "3.3"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
@@ -971,11 +971,11 @@ pylint = ">=1.7"
[[package]]
name = "PyMuPDF"
version = "1.18.19"
version = "1.20.2"
description = "Python bindings for the PDF toolkit and renderer MuPDF"
category = "main"
optional = false
python-versions = "*"
python-versions = ">=3.7"
[[package]]
name = "pyOpenSSL"
@@ -1016,11 +1016,11 @@ six = ">=1.5"
[[package]]
name = "python-dotenv"
version = "0.20.0"
version = "0.21.0"
description = "Read key-value pairs from a .env file and set them as environment variables"
category = "main"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.7"
[package.extras]
cli = ["click (>=5.0)"]
@@ -1169,7 +1169,7 @@ python-versions = ">=3.6,<4.0"
[[package]]
name = "traitlets"
version = "5.3.0"
version = "5.4.0"
description = ""
category = "dev"
optional = false
@@ -1236,7 +1236,7 @@ ua-parser = ">=0.10.0"
[[package]]
name = "virtualenv"
version = "20.16.4"
version = "20.16.5"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
@@ -1261,11 +1261,14 @@ python-versions = "*"
[[package]]
name = "Werkzeug"
version = "2.0.3"
version = "2.2.2"
description = "The comprehensive WSGI web application library."
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
MarkupSafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog"]
@@ -1292,7 +1295,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "85edbd1fe3ad53c66f95b372ad7e1891058831431f08de21f11197047ce8c51b"
content-hash = "8adcd11c8f30ed42dc6d187b3a3f5b0f020180c362d13670b39467523732819d"
[metadata.files]
appnope = [
@@ -1328,8 +1331,8 @@ beautifulsoup4 = [
{file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
]
certifi = [
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
{file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
{file = "certifi-2022.9.14-py3-none-any.whl", hash = "sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516"},
{file = "certifi-2022.9.14.tar.gz", hash = "sha256:36973885b9542e6bd01dea287b2b4b3b21236307c56324fcc3f1160f2d655ed5"},
]
cffi = [
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
@@ -1514,16 +1517,16 @@ dj-database-url = [
{file = "dj_database_url-1.0.0-py3-none-any.whl", hash = "sha256:cd354a3b7a9136d78d64c17b2aec369e2ae5616fbca6bfbe435ef15bb372ce39"},
]
Django = [
{file = "Django-4.0.7-py3-none-any.whl", hash = "sha256:41bd65a9e5f8a89cdbfa7a7bba45cd7431ae89e750af82dea8a35fd1a7ecbe66"},
{file = "Django-4.0.7.tar.gz", hash = "sha256:9c6d5ad36be798e562ddcaa6b17b1c3ff2d3c4f529a47432b69fb9a30f847461"},
{file = "Django-4.1.1-py3-none-any.whl", hash = "sha256:acb21fac9275f9972d81c7caf5761a89ec3ea25fe74545dd26b8a48cb3a0203e"},
{file = "Django-4.1.1.tar.gz", hash = "sha256:a153ffd5143bf26a877bfae2f4ec736ebd8924a46600ca089ad96b54a1d4e28e"},
]
django-appconf = [
{file = "django-appconf-1.0.5.tar.gz", hash = "sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4"},
{file = "django_appconf-1.0.5-py3-none-any.whl", hash = "sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d"},
]
django-boost = [
{file = "django_boost-2.0-py3-none-any.whl", hash = "sha256:266c22a5a7bdae480cfdc337b11073e1e906521a4f9c5b3dbb7e15bfeb916065"},
{file = "django_boost-2.0.tar.gz", hash = "sha256:6d6c2d7c34d54cfdbb6232d755967eabaaf8db5395bc003637e87a7f0e329016"},
{file = "django_boost-2.1-py3-none-any.whl", hash = "sha256:c59450d082bb6f7c130d0fcbf5cb9346c183d5d78856966838b17e02901e31b9"},
{file = "django_boost-2.1.tar.gz", hash = "sha256:a7d8defc2ca0eeebd08636abe58a21094f43fcc7c0ff020f9f8deec82d53a39f"},
]
django-bootstrap4 = [
{file = "django-bootstrap4-22.2.tar.gz", hash = "sha256:57bcbce53530ffd57a1c8bda74c8f3b56c859e085fb1f52a0e3e3a4f982f0960"},
@@ -1538,8 +1541,8 @@ django-csp = [
{file = "django_csp-3.7.tar.gz", hash = "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"},
]
django-extensions = [
{file = "django-extensions-3.2.0.tar.gz", hash = "sha256:7dc7cd1da50d83b76447a58f5d7e5c8e6cd83f21e9b7e5f97e6b644f4d4e21a6"},
{file = "django_extensions-3.2.0-py3-none-any.whl", hash = "sha256:4c234a7236e9e41c17d9036f6dae7a3a9b212527105b8a0d24b2459b267825f0"},
{file = "django-extensions-3.2.1.tar.gz", hash = "sha256:2a4f4d757be2563cd1ff7cfdf2e57468f5f931cc88b23cf82ca75717aae504a4"},
{file = "django_extensions-3.2.1-py3-none-any.whl", hash = "sha256:421464be390289513f86cb5e18eb43e5dc1de8b4c27ba9faa3b91261b0d67e09"},
]
django-filter = [
{file = "django-filter-22.1.tar.gz", hash = "sha256:ed473b76e84f7e83b2511bb2050c3efb36d135207d0128dfe3ae4b36e3594ba5"},
@@ -1558,8 +1561,8 @@ django-silk = [
{file = "django_silk-5.0.1-py3-none-any.whl", hash = "sha256:9dad85e783fcaaa1c97bebfa7ea01899428ded55b517d363f25ba87e43b5ce50"},
]
django-sri = [
{file = "django-sri-0.4.0.tar.gz", hash = "sha256:8a21b9808c351fe28d731ac1af9043b2525ba93d883aab888424cd8b121bbef1"},
{file = "django_sri-0.4.0-py3-none-any.whl", hash = "sha256:44e0fb6a33d767008098293014d89b380ee6ec65ffe034a89dcff8f199c5abb0"},
{file = "django-sri-0.5.0.tar.gz", hash = "sha256:9d9042a01f9314d308f8b40ea084768f55a182e2a82e2ea53412ca5f4433a28e"},
{file = "django_sri-0.5.0-py3-none-any.whl", hash = "sha256:c2621bed5660b5ac19ecf39b49e83df73625ba43d58fb5a35833f2100162819d"},
]
django-webpack-loader = [
{file = "django-webpack-loader-1.6.0.tar.gz", hash = "sha256:a29418ff41690035be10d2c94a0655dab099009c68b898839813d6a109b14d71"},
@@ -1610,8 +1613,8 @@ identify = [
{file = "identify-2.5.5.tar.gz", hash = "sha256:322a5699daecf7c6fd60e68852f36f2ecbb6a36ff6e6e973e0d2bb6fca203ee6"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
inflection = [
{file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"},
@@ -1917,30 +1920,27 @@ pylint-plugin-utils = [
{file = "pylint_plugin_utils-0.7-py3-none-any.whl", hash = "sha256:b3d43e85ab74c4f48bb46ae4ce771e39c3a20f8b3d56982ab17aa73b4f98d535"},
]
PyMuPDF = [
{file = "PyMuPDF-1.18.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ee8cc8aadaa818c9a5e2fb2a944c99a98822a7a3bc618d9c5d32f126874c0635"},
{file = "PyMuPDF-1.18.19-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:03ebf6fce6889df4708061a499b912909ead5e7bf1066f05b94458dcf164e3c3"},
{file = "PyMuPDF-1.18.19-cp310-cp310-win32.whl", hash = "sha256:f4bc63b0696c2f276703fadf3232d7ccaa01ab7548d1541fbc3762c573d3e1b5"},
{file = "PyMuPDF-1.18.19-cp310-cp310-win_amd64.whl", hash = "sha256:c2fc348061c14c79e546a207088ba8bf676dc8f1d302cb94cefe53d53c2a7808"},
{file = "PyMuPDF-1.18.19-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a795f40654a98459007148fa0660de5a9e1e1e0d12dc5d56bbdea481a1ec8fbc"},
{file = "PyMuPDF-1.18.19-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c802bdc3fdf690470a490cbcbf026895bb497f8049d33df47e1c64da939d46b3"},
{file = "PyMuPDF-1.18.19-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5044f9447686874f442a676776615b48b6a04d63a68dd99da21ad7a0efd5d13a"},
{file = "PyMuPDF-1.18.19-cp36-cp36m-win_amd64.whl", hash = "sha256:7a7fc4b4069934d7663dbd2bcf698f123b9bb0c4b4e25383a549692bee56f466"},
{file = "PyMuPDF-1.18.19-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc2df761b59ff06d16db7a5ce6d19bcab365efd4391eac96dab30ddda3a09f87"},
{file = "PyMuPDF-1.18.19-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:af2c6934eb8dc803c958d9742c7b64aa7d6287f764844f296dfdfe9e576e4df4"},
{file = "PyMuPDF-1.18.19-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d772141c9e007b887da54da5677e13975305bd85ae833eec35b923bed35ea9c8"},
{file = "PyMuPDF-1.18.19-cp37-cp37m-win32.whl", hash = "sha256:ea96c0129d6be8a289b80e7f0a5f6842e09c3dcb568a170297afb256cc387146"},
{file = "PyMuPDF-1.18.19-cp37-cp37m-win_amd64.whl", hash = "sha256:e7f1897600f7a56073b1370a1709f68b00c8700c4ef945bcffde1a09f58c2746"},
{file = "PyMuPDF-1.18.19-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c43b849c1daef36dc83667726518af3178c831c053e3e946e1989b5fb477bca"},
{file = "PyMuPDF-1.18.19-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:47a1588ce2296517019e8ba64279fe4cc03592d96fb1fc726ed9d0f5f01b5ef8"},
{file = "PyMuPDF-1.18.19-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc8e1f0c5fb54cb91ad3b630ef623834cb2e4b110c33eabccad9b8609053b20d"},
{file = "PyMuPDF-1.18.19-cp38-cp38-win32.whl", hash = "sha256:230d130f22a91da0a897c6b7e9b3523d24dc512ee32568a40d0df0e2cf2562c0"},
{file = "PyMuPDF-1.18.19-cp38-cp38-win_amd64.whl", hash = "sha256:7c0f145445b3ef8eb45794bf0f86fcd278696da5a6b879666b690e4856f5e481"},
{file = "PyMuPDF-1.18.19-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c12e1870841a8746f023a64ef6d8a44fa02324d17252c3e182f639a9d5530261"},
{file = "PyMuPDF-1.18.19-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:69308316b031aace9e490e9010d6c5334129eed47b47804da72d5d07eb5b2f9a"},
{file = "PyMuPDF-1.18.19-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616533cff3605c22a327488b4470f9929da675b006d7a1498f8d7ed8bf91f386"},
{file = "PyMuPDF-1.18.19-cp39-cp39-win32.whl", hash = "sha256:f407dbeecdefdfd46b9df7b083da71558143859216dc9891c40ea9cd2cf49a04"},
{file = "PyMuPDF-1.18.19-cp39-cp39-win_amd64.whl", hash = "sha256:439b972026fbe8636aed0fe9d2cabb321542fa92bc48cd4c96dbdd2508fc41ee"},
{file = "PyMuPDF-1.18.19.tar.gz", hash = "sha256:ecc684e9c45bd4072f538cc42998cfda4d00f066ba009226e8a212b112d9992c"},
{file = "PyMuPDF-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:18077adb58b7004bb396f952d27c89c6bf5dc521b4056ba4f95772c7e900a57a"},
{file = "PyMuPDF-1.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a82438b44723e7dcb47d156cf0299fcf3d09e970ee469081c3f9d79b8c40ce3"},
{file = "PyMuPDF-1.20.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73dbf73ac1c5cbf99dec903c98bc18eb30c8fe1d703ec5531296a5308700f001"},
{file = "PyMuPDF-1.20.2-cp310-cp310-win32.whl", hash = "sha256:1df02cca0fbb64c3dce4d6094f9ad91991d768ebe0c0efdee85294c7959db7c3"},
{file = "PyMuPDF-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:8074f005d247bbb6b43c0c1eba2a316d97cbf8345b456d37ee97c9cd2a7398cc"},
{file = "PyMuPDF-1.20.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4045847e830598269be448bda21dc8ea332fce974c4cf3b1b6ca0d6231f2de0d"},
{file = "PyMuPDF-1.20.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5824103cf2ea3b492cb5b3c66ab16d61db65363ebdfb7ed28a2af93cb48dee4f"},
{file = "PyMuPDF-1.20.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c50b8408dbbb921cfce7145ac0ae0e150955f9b2deed8cdddf03e830ca1e6a2"},
{file = "PyMuPDF-1.20.2-cp37-cp37m-win32.whl", hash = "sha256:1bd2004a64f110dac255a86ec45245f17e84d81cf6f8f67608e45455cd1b5697"},
{file = "PyMuPDF-1.20.2-cp37-cp37m-win_amd64.whl", hash = "sha256:acadb4e61776cc2d6dd0196675db324411fccd64e8d68399dc8e6ca2cb943f49"},
{file = "PyMuPDF-1.20.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b07f6c389c690f28c79830a14c294bbc1679bd31edc4b6a42511c415434cbd"},
{file = "PyMuPDF-1.20.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d29a7db4b15dcb7632bb6afa197f12f95244ab15f7c73ccb219e3f7df4e79fa"},
{file = "PyMuPDF-1.20.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2897fb4160b5fe2f80739ca9e101677e5694b5dcfda8026b0eb0b7e2db5615"},
{file = "PyMuPDF-1.20.2-cp38-cp38-win32.whl", hash = "sha256:2af5b16d2f7f78a8800906e30d650dbdbf38209e988e0796b57d1a938a59209a"},
{file = "PyMuPDF-1.20.2-cp38-cp38-win_amd64.whl", hash = "sha256:071732c5139150f1f7ba95d25c0b69919dee8494adc88a914872da5272b974d7"},
{file = "PyMuPDF-1.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5afa38414c3cf10daaa627f3fc7e8653efbbc2e5f58335a37e743a5c52698d0"},
{file = "PyMuPDF-1.20.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddf7d617fba1b345d9aee8d533888e340ffa3f784910453608bc5541089023c"},
{file = "PyMuPDF-1.20.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c0ee0df665849f8686981fcc8f836afac2302bb6a7aaf330e73fd5694017d9d"},
{file = "PyMuPDF-1.20.2-cp39-cp39-win32.whl", hash = "sha256:e7214cf1870238d39ca008eda7c612b1db7228d7e913cb1a028e7a28175bef41"},
{file = "PyMuPDF-1.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:e9cb960e234498a4e91e4b56e2e71f4c56fd0045c7a71315df8e03a33212dbeb"},
{file = "PyMuPDF-1.20.2.tar.gz", hash = "sha256:02eedf01f57c6bafb5e8667cea0088a2d2522643c47100f1908bec3a68a84888"},
]
pyOpenSSL = [
{file = "pyOpenSSL-22.0.0-py2.py3-none-any.whl", hash = "sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0"},
@@ -1955,8 +1955,8 @@ python-dateutil = [
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
python-dotenv = [
{file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"},
{file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"},
{file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"},
{file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"},
]
pytz = [
{file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"},
@@ -2074,8 +2074,8 @@ tomlkit = [
{file = "tomlkit-0.11.4.tar.gz", hash = "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83"},
]
traitlets = [
{file = "traitlets-5.3.0-py3-none-any.whl", hash = "sha256:65fa18961659635933100db8ca120ef6220555286949774b9cfc106f941d1c7a"},
{file = "traitlets-5.3.0.tar.gz", hash = "sha256:0bb9f1f9f017aa8ec187d8b1b2a7a6626a2a1d877116baba52a129bfa124f8e2"},
{file = "traitlets-5.4.0-py3-none-any.whl", hash = "sha256:93663cc8236093d48150e2af5e2ed30fc7904a11a6195e21bab0408af4e6d6c8"},
{file = "traitlets-5.4.0.tar.gz", hash = "sha256:3f2c4e435e271592fe4390f1746ea56836e3a080f84e7833f0f801d9613fec39"},
]
typing-extensions = [
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
@@ -2102,16 +2102,16 @@ user-agents = [
{file = "user_agents-2.2.0-py3-none-any.whl", hash = "sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7"},
]
virtualenv = [
{file = "virtualenv-20.16.4-py3-none-any.whl", hash = "sha256:035ed57acce4ac35c82c9d8802202b0e71adac011a511ff650cbcf9635006a22"},
{file = "virtualenv-20.16.4.tar.gz", hash = "sha256:014f766e4134d0008dcaa1f95bafa0fb0f575795d07cae50b1bee514185d6782"},
{file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"},
{file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
Werkzeug = [
{file = "Werkzeug-2.0.3-py3-none-any.whl", hash = "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8"},
{file = "Werkzeug-2.0.3.tar.gz", hash = "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c"},
{file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"},
{file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"},
]
win32-setctime = [
{file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},

View File

@@ -3,30 +3,30 @@ line_length = 119
[tool.poetry]
name = "cbwebreader"
version = "1.0.6"
version = "1.1.0"
description = "CBR/Z Web Reader"
authors = ["ajurna <ajurna@gmail.com>"]
license = "Creative Commons Attribution-ShareAlike 4.0 International License"
[tool.poetry.dependencies]
python = "^3.10"
Django = "4.0.7"
Django = "4.1.1"
gunicorn = "^20.0.4"
dj-database-url = "^1.0.0"
python-dotenv = "^0.20.0"
python-dotenv = "^0.21.0"
loguru = "^0.6.0"
django-silk = "^5.0.0"
mysqlclient = "^2.0.1"
psycopg2 = "^2.8.6"
rarfile = "^4.0"
django-extensions = "^3.2.0"
django-extensions = "^3.2.1"
Pillow = "^9.1.1"
django-imagekit = "^4.0.2"
PyMuPDF = "~1.18"
PyMuPDF = "~1.20.2"
django-bootstrap4 = "^22.1"
django-csp = "^3.7"
django-boost = "^2.0"
django-sri = "^0.4.0"
django-boost = "^2.1"
django-sri = "^0.5.0"
django-permissions-policy = "^4.9.0"
djangorestframework = "^3.13.1"
django-filter = "^22.1"
@@ -40,7 +40,7 @@ flake8-annotations = "^2.9.1"
[tool.poetry.dev-dependencies]
mypy = "^0.971"
Werkzeug = "<2.1"
Werkzeug = "^2.2"
pyOpenSSL = "^22.0.0"
ipython = "^8.4.0"
coverage = "^6.2"

View File

@@ -2,25 +2,25 @@ asgiref==3.5.2 ; python_version >= "3.10" and python_version < "4.0"
attrs==22.1.0 ; python_version >= "3.10" and python_version < "4.0"
autopep8==1.7.0 ; python_version >= "3.10" and python_version < "4.0"
beautifulsoup4==4.11.1 ; python_version >= "3.10" and python_version < "4.0"
certifi==2022.6.15 ; python_version >= "3.10" and python_version < "4"
certifi==2022.9.14 ; python_version >= "3.10" and python_version < "4"
charset-normalizer==2.1.1 ; python_version >= "3.10" and python_version < "4"
colorama==0.4.5 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32"
coreapi==2.3.3 ; python_version >= "3.10" and python_version < "4.0"
coreschema==0.0.4 ; python_version >= "3.10" and python_version < "4.0"
dj-database-url==1.0.0 ; python_version >= "3.10" and python_version < "4.0"
django-appconf==1.0.5 ; python_version >= "3.10" and python_version < "4.0"
django-boost==2.0 ; python_version >= "3.10" and python_version < "4.0"
django-boost==2.1 ; python_version >= "3.10" and python_version < "4.0"
django-bootstrap4==22.2 ; python_version >= "3.10" and python_version < "4.0"
django-cors-headers==3.13.0 ; python_version >= "3.10" and python_version < "4.0"
django-csp==3.7 ; python_version >= "3.10" and python_version < "4.0"
django-extensions==3.2.0 ; python_version >= "3.10" and python_version < "4.0"
django-extensions==3.2.1 ; python_version >= "3.10" and python_version < "4.0"
django-filter==22.1 ; python_version >= "3.10" and python_version < "4.0"
django-imagekit==4.1.0 ; python_version >= "3.10" and python_version < "4.0"
django-permissions-policy==4.13.0 ; python_version >= "3.10" and python_version < "4.0"
django-silk==5.0.1 ; python_version >= "3.10" and python_version < "4.0"
django-sri==0.4.0 ; python_version >= "3.10" and python_version < "4.0"
django-sri==0.5.0 ; python_version >= "3.10" and python_version < "4.0"
django-webpack-loader==1.6.0 ; python_version >= "3.10" and python_version < "4.0"
django==4.0.7 ; python_version >= "3.10" and python_version < "4.0"
django==4.1.1 ; python_version >= "3.10" and python_version < "4.0"
djangorestframework-simplejwt==5.2.0 ; python_version >= "3.10" and python_version < "4.0"
djangorestframework==3.13.1 ; python_version >= "3.10" and python_version < "4.0"
drf-extensions==0.7.1 ; python_version >= "3.10" and python_version < "4.0"
@@ -29,7 +29,7 @@ flake8-annotations==2.9.1 ; python_version >= "3.10" and python_version < "4.0"
flake8==5.0.4 ; python_version >= "3.10" and python_version < "4.0"
gprof2dot==2022.7.29 ; python_version >= "3.10" and python_version < "4.0"
gunicorn==20.1.0 ; python_version >= "3.10" and python_version < "4.0"
idna==3.3 ; python_version >= "3.10" and python_version < "4"
idna==3.4 ; python_version >= "3.10" and python_version < "4"
inflection==0.5.1 ; python_version >= "3.10" and python_version < "4.0"
itypes==1.2.0 ; python_version >= "3.10" and python_version < "4.0"
jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0"
@@ -44,10 +44,10 @@ psycopg2==2.9.3 ; python_version >= "3.10" and python_version < "4.0"
pycodestyle==2.9.1 ; python_version >= "3.10" and python_version < "4.0"
pyflakes==2.5.0 ; python_version >= "3.10" and python_version < "4.0"
pyjwt==2.4.0 ; python_version >= "3.10" and python_version < "4.0"
pymupdf==1.18.19 ; python_version >= "3.10" and python_version < "4.0"
pymupdf==1.20.2 ; python_version >= "3.10" and python_version < "4.0"
pyparsing==3.0.9 ; python_version >= "3.10" and python_version < "4.0"
python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0"
python-dotenv==0.20.0 ; python_version >= "3.10" and python_version < "4.0"
python-dotenv==0.21.0 ; python_version >= "3.10" and python_version < "4.0"
pytz==2022.2.1 ; python_version >= "3.10" and python_version < "4.0"
rarfile==4.0 ; python_version >= "3.10" and python_version < "4.0"
requests==2.28.1 ; python_version >= "3.10" and python_version < "4"