Rewrite of Comic lists so that tehy have thumbnails. (#26)

This commit is contained in:
2021-04-23 18:21:25 +01:00
committed by GitHub
parent 5b0a1c7c14
commit d40c8e270c
25 changed files with 4588 additions and 270 deletions

View File

@@ -1,13 +1,11 @@
import os
from os.path import isdir
from pathlib import Path
from PIL import UnidentifiedImageError
from django.conf import settings
from django.core.management.base import BaseCommand
from loguru import logger
from django.core.management.base import BaseCommand
from django.conf import settings
from comic.models import ComicBook, Directory, ComicStatus
from comic.models import ComicBook, Directory
class Command(BaseCommand):
@@ -15,6 +13,7 @@ class Command(BaseCommand):
def __init__(self):
super().__init__()
self.OUTPUT = False
def add_arguments(self, parser):
parser.add_argument(
@@ -91,4 +90,10 @@ class Command(BaseCommand):
book.version = 1
book.save()
except ComicBook.DoesNotExist:
ComicBook.process_comic_book(file, directory)
book = ComicBook.process_comic_book(file, directory)
try:
book.generate_thumbnail()
except UnidentifiedImageError:
book.generate_thumbnail(1)
except:
pass

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2 on 2021-04-21 11:13
from django.db import migrations
import imagekit.models.fields
class Migration(migrations.Migration):
dependencies = [
('comic', '0021_delete_setting'),
]
operations = [
migrations.AddField(
model_name='comicbook',
name='thumbnail',
field=imagekit.models.fields.ProcessedImageField(null=True, upload_to='thumbs'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2 on 2021-04-21 17:44
from django.db import migrations
import imagekit.models.fields
class Migration(migrations.Migration):
dependencies = [
('comic', '0022_comicbook_thumbnail'),
]
operations = [
migrations.AddField(
model_name='directory',
name='thumbnail',
field=imagekit.models.fields.ProcessedImageField(null=True, upload_to='thumbs'),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.2 on 2021-04-22 07:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('comic', '0023_directory_thumbnail'),
]
operations = [
migrations.AddField(
model_name='comicbook',
name='thumbnail_index',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='directory',
name='thumbnail_index',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='directory',
name='thumbnail_issue',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='directory_thumbnail_issue', to='comic.comicbook'),
),
]

View File

@@ -8,15 +8,18 @@ from os import listdir
from pathlib import Path
from typing import Optional, List, Union, Tuple
import PyPDF4
import PyPDF4.utils
import rarfile
from PIL import Image
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.transaction import atomic
from django.utils.http import urlsafe_base64_encode
import PyPDF4
import PyPDF4.utils
import rarfile
from imagekit.models import ProcessedImageField
from imagekit.processors import ResizeToFill
from comic.errors import NotCompatibleArchive
@@ -28,6 +31,15 @@ class Directory(models.Model):
name = models.CharField(max_length=100)
parent = models.ForeignKey("Directory", null=True, blank=True, on_delete=models.CASCADE)
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)
class Meta:
ordering = ['name']
@@ -35,6 +47,39 @@ class Directory(models.Model):
def __str__(self):
return "Directory: {0}; {1}".format(self.name, self.parent)
def mark_read(self, user):
books = ComicBook.objects.filter(directory=self)
for book in books:
book.mark_read(user)
def mark_unread(self, user):
books = ComicBook.objects.filter(directory=self)
for book in books:
book.mark_unread(user)
def get_thumbnail_url(self):
if self.thumbnail:
return self.thumbnail.url
else:
self.generate_thumbnail()
return self.thumbnail.url
def generate_thumbnail(self):
book = ComicBook.objects.filter(directory=self).order_by('file_name').first()
if not book:
return
img, content_type = book.get_image(0)
pil_data = Image.open(img)
self.thumbnail = InMemoryUploadedFile(
img,
None,
f'{self.name}.jpg',
content_type,
pil_data.tell(),
None
)
self.save()
@property
def path(self) -> Path:
return self.get_path()
@@ -63,6 +108,10 @@ class Directory(models.Model):
self.parent.get_path_objects(p)
return p
@property
def url_safe_selector(self):
return urlsafe_base64_encode(self.selector.bytes)
class ComicBook(models.Model):
file_name = models.TextField()
@@ -70,12 +119,33 @@ class ComicBook(models.Model):
directory = models.ForeignKey(Directory, blank=True, null=True, on_delete=models.CASCADE)
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)
def __str__(self):
return self.file_name
def mark_read(self, user: User):
status, _ = ComicStatus.objects.get_or_create(comic=self, user=user)
status.mark_read()
def mark_unread(self, user: User):
status, _ = ComicStatus.objects.get_or_create(comic=self, user=user)
status.mark_unread()
def mark_previous(self, user):
books = ComicBook.objects.filter(directory=self.directory).order_by('file_name')
for book in books:
if book == self:
break
book.mark_read(user)
@property
def selector_string(self):
def url_safe_selector(self):
return urlsafe_base64_encode(self.selector.bytes)
def get_pdf(self):
@@ -99,6 +169,26 @@ class ComicBook(models.Model):
out = (archive.open(page_obj.page_file_name), page_obj.content_type)
return out
def get_thumbnail_url(self):
if self.thumbnail:
return self.thumbnail.url
else:
self.generate_thumbnail()
return self.thumbnail.url
def generate_thumbnail(self, page_index: int = 0):
img, content_type = self.get_image(page_index)
pil_data = Image.open(img)
self.thumbnail = InMemoryUploadedFile(
img,
None,
f'{self.file_name}.jpg',
content_type,
pil_data.tell(),
None
)
self.save()
def is_last_page(self, page):
if (self.page_count - 1) == page:
return True
@@ -209,22 +299,15 @@ class ComicBook(models.Model):
def __str__(self):
return self.name
@property
def pages(self):
return [cp for cp in ComicPage.objects.filter(Comic=self).order_by("index")]
def page_name(self, index):
return ComicPage.objects.get(Comic=self, index=index).page_file_name
@staticmethod
def process_comic_book(comic_file_name: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
"""
:type comic_file_name: str
:type comic_file_path: str
:type directory: Directory
"""
try:
book = ComicBook.objects.get(file_name=comic_file_name, version=0)
book = ComicBook.objects.get(file_name=comic_file_path.name, version=0)
book.directory = directory
book.version = 1
book.save()
@@ -232,12 +315,12 @@ class ComicBook(models.Model):
except ComicBook.DoesNotExist:
pass
book = ComicBook(file_name=comic_file_name, directory=directory if directory else None)
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_name
return comic_file_path
if archive_type == 'archive':
book.verify_pages()
@@ -340,9 +423,18 @@ class ComicStatus(models.Model):
unread = models.BooleanField(default=True)
finished = models.BooleanField(default=False)
@property
def read(self):
return self.last_read_page
def mark_read(self):
page_count = ComicPage.objects.filter(Comic=self.comic).count()
self.unread = False
self.finished = True
self.last_read_page = page_count - 1
self.save()
def mark_unread(self):
self.unread = True
self.finished = False
self.last_read_page = 0
self.save()
def __str__(self):
return self.__repr__()

View File

@@ -69,6 +69,7 @@
<script type="text/javascript" src="{% static "reveal.js/reveal.js" %}"></script>
<script type="text/javascript" src="{% static "reveal.js/plugin/menu/menu.js" %}"></script>
<script type="text/javascript" src="{% static "js/hammer.min.js" %}"></script>
<script type="text/javascript" src="{% static "js/isotope.pkgd.min.js" %}"></script>
{% block script %}
{% endblock %}

View File

@@ -1,121 +1,165 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container">
<form id="comic_form" method="post" action="/comic/edit/">
{% csrf_token %}
<table class="table table-bordered table-striped table-hover" id="comic_list">
<caption><h2>Comics</h2> mark selected issues as:
<select name="func" id="func_selector">
<option value="choose">Choose...</option>
<option value="read">Read</option>
<option value="unread">Un-Read</option>
</select>
</caption>
<thead>
<tr>
<th id="select-all"><input type="checkbox" id="select-all-cb"></th>
<th style="text-align: center;"><span class="fa fa-file"></span></th>
<th width="100%">File/Folder</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr class="clickable-row" data-href="/comic/">
<td></td>
<td></td>
<td>loading data</td>
<td></td>
</tr>
</tbody>
</table>
</form>
<div class="row">
<div class="input-group">
<input type="text" id="quicksearch" class="form-control" placeholder="Search" aria-label="Search list of comics" aria-describedby="button-addon4">
<div id="filters" class="input-group-append" id="button-addon4">
<button class="btn btn-outline-secondary filters" type="button" data-filter="*">All</button>
<button class="btn btn-outline-secondary filters" type="button" data-filter=".read">Read</button>
<button class="btn btn-outline-secondary filters" type="button" data-filter=".unread">Unread</button>
<div class="btn-group" role="group">
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<button type="button" class="btn btn-primary dropdown-item" title="Mark Un-Read" onclick="comic_action('{{ selector }}', 'Directory', 'mark_unread')"><i class="fas fa-book">Mark Un-Read</i></button>
<button type="button" class="btn btn-primary dropdown-item" title="Mark Read" onclick="comic_action('{{ selector }}', 'Directory', 'mark_read')"><i class="fas fa-book-open">Mark Read</i></button>
{# <button type="button" class="btn btn-primary dropdown-item" title="Edit Comic"><i class="fas fa-edit">Edit Comic</i></button>#}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container comic-container">
<div class="row grid">
{% for file in files %}
<div class="m-2 grid-item {% if file.percent == 100 %}read{% else %}unread{% endif %}">
<div class="card" style="width: 200px;">
{% if file.item_type == 'Directory' %}
<a href="{% url "comic_list" file.selector %}">
{% elif file.item_type == 'ComicBook' %}
<a href="{% url "read_comic" file.selector %}">
{% endif %}
{% if file.obj.thumbnail %}
<img src="{{file.obj.thumbnail.url}}" class="card-img-top" alt="{{ file.name }}" onerror="this.onerror=null;this.src='{% static "img/placeholder.png" %}';">
{% else %}
{% if file.item_type == 'Directory' %}
<img src="{% url 'directory_thumbnail' file.selector %}" class="card-img-top" alt="{{ file.name }}" onerror="this.onerror=null;this.src='{% static "img/placeholder.png" %}';">
{% elif file.item_type == 'ComicBook' %}
<img src="{% url 'comic_thumbnail' file.selector %}" class="card-img-top" alt="{{ file.name }}" onerror="this.onerror=null;this.src='{% static "img/placeholder.png" %}';">
{% endif %}
{% endif %}
</a>
<div class="card-body">
<h5 class="card-title">
{% if file.item_type == 'Directory' %}
<a href="{% url "comic_list" file.selector %}" class="search-name">
{% elif file.item_type == 'ComicBook' %}
<a href="{% url "read_comic" file.selector %}" class="search-name">
{% endif %}
{{ file.name }}
</a>
</h5>
<p class="card-text">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: {{ file.percent }}%;" aria-valuenow="{{ file.percent }}" aria-valuemin="0" aria-valuemax="100">{{ file.percent }}%</div>
</div>
</p>
<div class="btn-group" role="group" aria-label="Comic Actions">
<button type="button" class="btn btn-primary" title="Mark Un-Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_unread')"><i class="fas fa-book"></i></button>
<button type="button" class="btn btn-primary" title="Mark Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_read')"><i class="fas fa-book-open"></i></button>
<div class="btn-group" role="group">
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
</button>
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<button type="button" class="btn btn-primary dropdown-item" title="Mark Un-Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_unread')"><i class="fas fa-book">Mark Un-Read</i></button>
<button type="button" class="btn btn-primary dropdown-item" title="Mark Read" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_read')"><i class="fas fa-book-open">Mark Read</i></button>
{% if file.item_type != 'Directory' %}
<button type="button" class="btn btn-primary dropdown-item" title="Mark Previous Read"><i class="fas fa-book" onclick="comic_action('{{ file.selector }}', '{{ file.item_type }}', 'mark_previous')"><i class="fas fa-arrow-up">Mark Previous Read</i></i></button>
{% endif %}
{# <button type="button" class="btn btn-primary dropdown-item" title="Edit Comic"><i class="fas fa-edit">Edit Comic</i></button>#}
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(document).ready(function() {
var table = $('#comic_list').DataTable({
"processing": true,
"stateSave": true,
"ajax": {
"type": "POST",
"url": "{{ json_url }}",
"data": function ( d ) {
d.csrfmiddlewaretoken = Cookies.get('csrftoken');
}
},
"rowCallback": function( row, data, index ) {
var r = $(row);
var cols = $('td:nth-child(n+2)', row);
if (data['selector'] === '0') {
} else {
cols.attr('data-href', data['url']);
cols.attr('style', 'cursor: pointer;');
cols.click(function () {
window.document.location = $(this).data("href");
});
}
var tds = $('td:eq(0)', row);
if (data['type'] === 'directory') {
tds.html('');
} else {
tds.html('<input type="checkbox" name="selected" value="'+data['selector']+'" data-type="'+data['type']+'"/>');
var cb = $('input', tds);
cb.change(function() {
$(this).closest('tr').toggleClass('info')
});
}
},
"drawCallback": function( settings ) {
var tds = $('table tr td:first-child');
tds.click(function(event){
if (!$(event.target).is('input')) {
var $cb = $('input', this);
$cb.click();
}
});
},
"columns": [
{ "data" : "blank", "orderable": false },
{ "data" : "icon", "orderable": false },
{ "data" : "name" },
{"data": "label"}
],
"order": [[2, 'asc']]
var qsRegex;
var buttonFilter;
var $grid = $('.comic-container').isotope({
itemSelector: '.grid-item',
layoutMode: 'fitRows',
filter: function() {
var $this = $(this);
var searchResult = qsRegex ? $this.text().match( qsRegex ) : true;
var buttonResult = buttonFilter ? $this.is( buttonFilter ) : true;
return searchResult && buttonResult;
{#return searchResult#}
}
});
$(".clickable-row").click(function() {
window.document.location = $(this).data("href");
$('#filters').on( 'click', 'button', function() {
buttonFilter = $( this ).attr('data-filter');
sessionStorage.setItem(window.location.href+"button", buttonFilter);
$grid.isotope();
});
$('#func_selector').on('change', function() {
$.post('/comic/edit/', $('#comic_form').serialize())
.done(function(){
$('#func_selector').val('choose');
$('#select-all input').prop('checked', false);
table.ajax.reload();
}).fail(function(){
alert('Error Submitting Change');
})
});
$('#select-all').click(function(event){
var cb = $('input', this);
if (!$(event.target).is('input')) {
cb.click();
var $quicksearch = $('#quicksearch').keyup( debounce( function() {
qsRegex = new RegExp($quicksearch.val(), 'gi');
sessionStorage.setItem(window.location.href+'text', $quicksearch.val());
$grid.isotope();
}) );
// debounce so filtering doesn't happen every millisecond
function debounce( fn, threshold ) {
var timeout;
threshold = threshold || 100;
return function debounced() {
clearTimeout( timeout );
var args = arguments;
var _this = this;
function delayed() {
fn.apply( _this, args );
}
$('table tr td:first-child input').each(function(chkbx) {
row = $(this);
if (row.prop('checked') !== cb.prop('checked')){
row.click();
}
});
});
} );
timeout = setTimeout( delayed, threshold );
};
}
setInterval(function (){
$grid.isotope();
}, 1000)
let field = document.getElementById("quicksearch");
// See if we have an autosave value
// (this will only happen if the page is accidentally refreshed)
if (sessionStorage.getItem(window.location.href+'text') || sessionStorage.getItem(window.location.href+'button')) {
// Restore the contents of the text field
field.value = sessionStorage.getItem(window.location.href+'text');
qsRegex = new RegExp($quicksearch.val(), 'gi');
buttonFilter = sessionStorage.getItem(window.location.href+'button');
$grid.isotope();
}
// Listen for changes in the text field
field.addEventListener("change", function() {
// And save the results into the session storage object
});
function comic_action(selector, item_type, action) {
$.ajax({
url: '/comic/action/' + action + '/' + item_type + '/' + selector + '/',
success: function (){window.location.reload()}
})
}
</script>
{% endblock %}

View File

@@ -103,43 +103,23 @@ class ComicBookTests(TestCase):
folders = generate_directory(user)
dir1 = folders[0]
self.assertEqual(dir1.name, "test_folder")
self.assertEqual(dir1.type, "directory")
self.assertEqual("fa-folder-open", dir1.icon)
d = Directory.objects.get(name="test_folder", parent__isnull=True)
location = "/comic/{0}/".format(urlsafe_base64_encode(d.selector.bytes))
self.assertEqual(dir1.location, location)
self.assertEqual(dir1.label, '<center><span class="label label-default">Empty</span></center>')
self.assertEqual(dir1.item_type, "Directory")
dir2 = folders[1]
self.assertEqual(dir2.name, "test1.rar")
self.assertEqual(dir2.type, "book")
self.assertEqual("fa-book", dir2.icon)
c = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True)
location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes))
self.assertEqual(dir2.location, location)
self.assertEqual(dir2.label, '<center><span class="label label-default">Unread</span></center>')
self.assertEqual(dir2.item_type, "ComicBook")
dir3 = folders[2]
self.assertEqual(dir3.name, "test2.rar")
self.assertEqual(dir3.type, "book")
self.assertEqual("fa-book", dir3.icon)
c = ComicBook.objects.get(file_name="test2.rar", directory__isnull=True)
location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes))
self.assertEqual(dir3.location, location)
self.assertEqual(dir3.label, '<center><span class="label label-primary">3/4</span></center>')
self.assertEqual(dir2.item_type, "ComicBook")
dir4 = folders[3]
self.assertEqual(dir4.name, "test3.rar")
self.assertEqual(dir4.type, "book")
self.assertEqual("fa-book", dir3.icon)
c = ComicBook.objects.get(file_name="test3.rar", directory__isnull=True)
location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes))
self.assertEqual(dir4.location, location)
self.assertEqual(dir4.label, '<center><span class="label label-default">Unread</span></center>')
self.assertEqual(dir4.item_type, "ComicBook")
def test_pages(self):
book = ComicBook.objects.get(file_name="test1.rar")
pages = book.pages
pages = [cp for cp in ComicPage.objects.filter(Comic=book).order_by("index")]
self.assertEqual(pages[0].page_file_name, "img1.jpg")
self.assertEqual(pages[0].index, 0)
self.assertEqual(pages[1].page_file_name, "img2.png")
@@ -149,9 +129,6 @@ class ComicBookTests(TestCase):
self.assertEqual(pages[3].page_file_name, "img4.bmp")
self.assertEqual(pages[3].index, 3)
def test_page_name(self):
book = ComicBook.objects.get(file_name="test1.rar")
self.assertEqual(book.page_name(0), "img1.jpg")
def test_comic_list(self):
c = Client()
@@ -166,18 +143,6 @@ class ComicBookTests(TestCase):
response = c.get(f"/comic/{urlsafe_base64_encode(directory.selector.bytes)}/")
self.assertEqual(response.status_code, 200)
def test_comic_list_json(self):
c = Client()
response = c.post("/comic/list_json/")
self.assertEqual(response.status_code, 302)
c.login(username="test", password="test")
response = c.post("/comic/list_json/")
self.assertEqual(response.status_code, 200)
directory = Directory.objects.first()
response = c.post(f"/comic/list_json/{urlsafe_base64_encode(directory.selector.bytes)}/")
self.assertEqual(response.status_code, 200)
def test_recent_comics(self):
c = Client()
response = c.get("/comic/recent/")
@@ -240,7 +205,7 @@ class ComicBookTests(TestCase):
response = c.get("/comic/edit/")
self.assertEqual(response.status_code, 405)
req_data = {"comic_list_length": 10, "func": "unread", "selected": book.selector_string}
req_data = {"comic_list_length": 10, "func": "unread", "selected": book.url_safe_selector}
response = c.post("/comic/edit/", req_data)
self.assertEqual(response.status_code, 200)

View File

@@ -10,14 +10,15 @@ urlpatterns = [
path("settings/users/add/", views.user_add_page, name="add_users"),
path("account/", views.account_page, name="account"),
path("read/<comic_selector>/", views.read_comic, name="read_comic"),
path("read/<comic_selector>/thumb", views.comic_thumbnail, name="comic_thumbnail"),
path("set_page/<comic_selector>/<int:page>/", views.set_read_page, name="set_read_page"),
path("read/<comic_selector>/<int:page>/img", views.get_image, name="get_image"),
path("read/<comic_selector>/pdf", views.get_pdf, name="get_pdf"),
path("list_json/", views.comic_list_json, name="comic_list_json1"),
path("list_json/<directory_selector>/", views.comic_list_json, name="comic_list_json2"),
path("recent/", views.recent_comics, name="recent_comics"),
path("recent/json/", views.recent_comics_json, name="recent_comics_json"),
path("edit/", views.comic_edit, name="comic_edit"),
path("feed/<int:user_selector>/", feeds.RecentComics()),
path("<directory_selector>/", views.comic_list, name="comic_list"),
path("<directory_selector>/thumb", views.directory_thumbnail, name="directory_thumbnail"),
path("action/<operation>/<item_type>/<selector>/", views.perform_action, name="perform_action")
]

View File

@@ -1,6 +1,7 @@
from collections import OrderedDict
from os import listdir
from dataclasses import dataclass
from pathlib import Path
from typing import Union
from django.conf import settings
from django.db.models import Count, Q, F
@@ -78,44 +79,30 @@ def generate_breadcrumbs_from_menu(paths):
return output
@dataclass
class DirFile:
def __init__(self):
self.name = ""
self.icon = ""
self.location = ""
self.label = ""
self.type = ""
self.selector = ""
obj: Union[Directory, ComicBook]
name: str = ''
item_type: str = ''
percent: int = 0
selector: str = ''
def __str__(self):
return self.name
def populate_directory(self, directory, user):
self.name = directory.name
self.icon = "fa-folder-open"
self.selector = urlsafe_base64_encode(directory.selector.bytes)
self.location = "/comic/{0}/".format(self.selector)
self.label = generate_dir_status(directory.total, directory.total_read)
self.type = "directory"
def populate_comic(self, comic, user):
if type(comic) == str:
self.icon = "fa-exclamation-circle"
self.name = comic
self.selector = "0"
self.location = "/"
self.label = '<center><span class="label label-danger">Error</span></center>'
self.type = "book"
else:
self.icon = "fa-book"
self.name = comic.file_name
self.selector = urlsafe_base64_encode(comic.selector.bytes)
self.location = "/comic/read/{0}/".format(self.selector)
self.label = generate_label(comic)
self.type = "book"
def __repr__(self):
return f"<DirFile: {self.name}: {self.type}>"
def __post_init__(self):
self.item_type = type(self.obj).__name__
if hasattr(self.obj, 'total') and hasattr(self.obj, 'total_read'):
# because pages count from zero.
total_adjustment = 1
if isinstance(self.obj, Directory):
total_adjustment = 0
try:
self.percent = int((self.obj.total_read / (self.obj.total - total_adjustment)) * 100)
except ZeroDivisionError:
self.percent = 0
self.selector = self.obj.url_safe_selector
if isinstance(self.obj, Directory):
self.name = self.obj.name
elif isinstance(self.obj, ComicBook):
self.name = self.obj.file_name
def generate_directory(user, directory=False):
@@ -127,11 +114,9 @@ def generate_directory(user, directory=False):
files = []
if directory:
dir_path = Path(base_dir, directory.path)
# ordered_dir_list = sorted(dir_path.glob('*'))
dir_list = [x for x in sorted(dir_path.glob('*')) if Path(base_dir, directory.path, x).is_dir()]
else:
dir_path = base_dir
# ordered_dir_list = base_dir.glob('*')
dir_list = [x for x in sorted(dir_path.glob('*')) if Path(base_dir, x).is_dir()]
file_list = [x for x in sorted(dir_path.glob('*')) if x.is_file()]
if directory:
@@ -154,23 +139,19 @@ def generate_directory(user, directory=False):
ComicStatus.objects.bulk_create(new_status)
file_list_obj = file_list_obj.annotate(
total_pages=Count('comicpage', distinct=True),
last_read_page=F('comicstatus__last_read_page'),
total=Count('comicpage', distinct=True),
total_read=F('comicstatus__last_read_page'),
finished=F('comicstatus__finished'),
unread=F('comicstatus__unread'),
user=F('comicstatus__user')
).filter(Q(user__isnull=True) | Q(user=user.id))
for directory_obj in dir_list_obj:
df = DirFile()
df.populate_directory(directory_obj, user)
files.append(df)
files.append(DirFile(directory_obj))
dir_list.remove(Path(dir_path, directory_obj.name))
for file_obj in file_list_obj:
df = DirFile()
df.populate_comic(file_obj, user)
files.append(df)
files.append(DirFile(file_obj))
file_list.remove(Path(dir_path, file_obj.file_name))
for directory_name in dir_list:
@@ -181,18 +162,14 @@ def generate_directory(user, directory=False):
directory_obj.save()
directory_obj.total = 0
directory_obj.total_read = 0
df = DirFile()
df.populate_directory(directory_obj, user)
files.append(df)
files.append(DirFile(directory_obj))
for file_name in file_list:
if file_name.suffix.lower() in [".rar", ".zip", ".cbr", ".cbz", ".pdf"]:
book = ComicBook.process_comic_book(file_name.name, directory)
df = DirFile()
df.populate_comic(book, user)
files.append(df)
book = ComicBook.process_comic_book(file_name, directory)
files.append(DirFile(book))
files.sort(key=lambda x: x.name)
files.sort(key=lambda x: x.type, reverse=True)
files.sort(key=lambda x: x.item_type, reverse=True)
return files

View File

@@ -1,9 +1,11 @@
import json
import uuid
from PIL import Image
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.db.models import Max, Count, F
from django.db.transaction import atomic
from django.http import HttpResponse, FileResponse
@@ -31,60 +33,55 @@ from .util import (
def comic_list(request, directory_selector=False):
if User.objects.all().count() == 0:
return redirect("/comic/settings/")
# try:
# base_dir = Setting.objects.get(name="BASE_DIR").value
# except Setting.DoesNotExist:
# return redirect("/comic/settings/")
# if not path.isdir(base_dir):
# return redirect("/comic/settings/")
directory = None
if directory_selector:
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
directory = Directory.objects.get(selector=selector)
else:
directory = False
if directory:
title = generate_title_from_path(directory.path)
breadcrumbs = generate_breadcrumbs_from_path(directory)
json_url = "/comic/list_json/{0}/".format(directory_selector)
else:
title = generate_title_from_path("Home")
breadcrumbs = generate_breadcrumbs_from_path()
json_url = "/comic/list_json/"
files = generate_directory(request.user, directory)
return render(
request,
"comic/comic_list.html",
{"breadcrumbs": breadcrumbs, "menu": Menu(request.user, "Browse"), "title": title, "json_url": json_url},
{
"breadcrumbs": breadcrumbs,
"menu": Menu(request.user, "Browse"),
"title": title,
"files": files,
"selector": directory_selector if directory_selector else 'None'
},
)
@login_required
@require_POST
def comic_list_json(request, directory_selector=False):
icon_str = '<span class="fa {0}"></span>'
if directory_selector:
directory_selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
directory = Directory.objects.get(selector=directory_selector)
else:
directory = False
files = generate_directory(request.user, directory)
response_data = dict()
response_data["data"] = []
for file in files:
response_data["data"].append(
{
"blank": "",
"selector": file.selector,
"type": file.type,
"icon": icon_str.format(file.icon),
"name": file.name,
"label": file.label,
"url": file.location,
}
)
return HttpResponse(json.dumps(response_data), content_type="application/json")
def perform_action(request, operation, item_type, selector):
if operation not in ['mark_read', 'mark_unread', 'mark_previous']:
return HttpResponse(400)
elif operation == 'mark_previous' and item_type == 'Directory':
return HttpResponse(422)
try:
selector_uuid = uuid.UUID(bytes=urlsafe_base64_decode(selector))
except ValueError:
if item_type == 'Directory':
for book in ComicBook.objects.filter(directory__isnull=True):
getattr(book, operation)(request.user)
return HttpResponse(204)
if item_type == 'ComicBook':
book = get_object_or_404(ComicBook, selector=selector_uuid)
getattr(book, operation)(request.user)
return HttpResponse(204)
elif item_type == 'Directory':
directory = get_object_or_404(Directory, selector=selector_uuid)
getattr(directory, operation)(request.user)
return HttpResponse(204)
@login_required
@@ -328,6 +325,22 @@ def get_image(_, comic_selector, page):
return FileResponse(img, content_type=content)
@xframe_options_sameorigin
@login_required
def comic_thumbnail(_, comic_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = ComicBook.objects.get(selector=selector)
return redirect(book.get_thumbnail_url())
@xframe_options_sameorigin
@login_required
def directory_thumbnail(_, directory_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
folder = Directory.objects.get(selector=selector)
return redirect(folder.get_thumbnail_url())
@login_required
def get_pdf(_, comic_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))