11# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
2- # Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
2+ # Copyright (C) 2018-2025 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
33# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
44# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
55# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
6- # apetresc, nanu-c, mutschler
6+ # apetresc, nanu-c, mutschler, akharlamov
77#
88# This program is free software: you can redistribute it and/or modify
99# it under the terms of the GNU General Public License as published by
2121import os
2222import json
2323import mimetypes
24+ import uuid
25+ from pathlib import Path
26+
2427import chardet # dependency of requests
2528import copy
2629from importlib .metadata import metadata
2932from flask import session as flask_session
3033from flask_babel import gettext as _
3134from flask_babel import get_locale
35+
36+ from .config_sql import CONVERSION_SOURCE_FMT_PRIORITY
3237from .cw_login import login_user , logout_user , current_user
3338from flask_limiter import RateLimitExceeded
3439from flask_limiter .util import get_remote_address
4752from .helper import check_valid_domain , check_email , check_username , \
4853 get_book_cover , get_series_cover_thumbnail , get_download_link , send_mail , generate_random_password , \
4954 send_registration_mail , check_send_to_ereader , check_read_formats , tags_filters , reset_password , valid_email , \
50- edit_book_read_status , valid_password
55+ edit_book_read_status , valid_password , convert_book_format
5156from .pagination import Pagination
5257from .redirect import get_redirect_location
5358from .cw_babel import get_available_locale
59+ from .tasks .convert import TaskConvert
5460from .usermanagement import login_required_if_no_ano
5561from .kobo_sync_status import remove_synced_book
5662from .render_template import render_title_template
@@ -1566,6 +1572,39 @@ def profile():
15661572# ###################################Show single book ##################################################################
15671573
15681574
1575+ def _start_epub_convert_task (book_id , filepath , source_format ):
1576+ lib_dir = config .config_calibre_split_dir if config .config_calibre_split else config .config_calibre_dir
1577+ task = TaskConvert (
1578+ str (Path (lib_dir ) / filepath ),
1579+ book_id ,
1580+ f'Converting { book_id } to EPUB' ,
1581+ {
1582+ 'old_book_format' : source_format ,
1583+ 'new_book_format' : 'EPUB' ,
1584+ },
1585+ None ,
1586+ user = current_user .name ,
1587+ )
1588+ queued_task = WorkerThread .add (current_user , task )
1589+ return queued_task .task .id
1590+
1591+
1592+ def _select_conversion_source (book ):
1593+ cur_candidate = None
1594+ cur_name = None
1595+ cur_priority = - 1
1596+ for data in book .data :
1597+ if data .format in CONVERSION_SOURCE_FMT_PRIORITY :
1598+ priority = CONVERSION_SOURCE_FMT_PRIORITY .index (data .format )
1599+ else :
1600+ priority = - 1
1601+ if priority > cur_priority :
1602+ cur_candidate = data .format
1603+ cur_name = data .name
1604+ cur_priority = priority
1605+ return cur_candidate , cur_name
1606+
1607+
15691608@web .route ("/read/<int:book_id>/<book_format>" )
15701609@login_required_if_no_ano
15711610@viewer_required
@@ -1586,20 +1625,28 @@ def read_book(book_id, book_format):
15861625 bookmark = ub .session .query (ub .Bookmark ).filter (and_ (ub .Bookmark .user_id == int (current_user .id ),
15871626 ub .Bookmark .book_id == book_id ,
15881627 ub .Bookmark .format == book_format .upper ())).first ()
1589- if book_format .lower () == "epub" or book_format .lower () == "kepub" :
1628+ format_id = book_format .lower ()
1629+ if format_id in ["epub" , "kepub" ]:
15901630 log .debug ("Start [k]epub reader for %d" , book_id )
15911631 return render_title_template ('read.html' , bookid = book_id , title = book .title , bookmark = bookmark ,
15921632 book_format = book_format )
1593- elif book_format . lower () == "pdf" :
1633+ elif format_id == "pdf" :
15941634 log .debug ("Start pdf reader for %d" , book_id )
15951635 return render_title_template ('readpdf.html' , pdffile = book_id , title = book .title )
1596- elif book_format . lower () == "txt" :
1636+ elif format_id == "txt" :
15971637 log .debug ("Start txt reader for %d" , book_id )
15981638 return render_title_template ('readtxt.html' , txtfile = book_id , title = book .title )
1599- elif book_format . lower () in ["djvu" , "djv" ]:
1639+ elif format_id in ["djvu" , "djv" ]:
16001640 log .debug ("Start djvu reader for %d" , book_id )
16011641 return render_title_template ('readdjvu.html' , djvufile = book_id , title = book .title ,
1602- extension = book_format .lower ())
1642+ extension = format_id )
1643+ elif book_format .upper () in config .convertable_to ('EPUB' ):
1644+ log .debug ("Start conversion to epub for %d" , book_id )
1645+ src_format , name = _select_conversion_source (book )
1646+ task_id = _start_epub_convert_task (book_id , Path (book .path ) / name , src_format )
1647+
1648+ url = url_for ('web.show_book_converting' , book_id = book_id , task_id = task_id )
1649+ return redirect (url )
16031650 else :
16041651 for fileExt in constants .EXTENSIONS_AUDIO :
16051652 if book_format .lower () == fileExt :
@@ -1623,10 +1670,7 @@ def read_book(book_id, book_format):
16231670 category = "error" )
16241671 return redirect (url_for ("web.index" ))
16251672
1626-
1627- @web .route ("/book/<int:book_id>" )
1628- @login_required_if_no_ano
1629- def show_book (book_id ):
1673+ def _get_book_entry (book_id ):
16301674 entries = calibre_db .get_book_read_archived (book_id , config .config_read_column , allow_show_archived = True )
16311675 if entries :
16321676 read_book = entries [1 ]
@@ -1660,15 +1704,52 @@ def show_book(book_id):
16601704 if media_format .format .lower () in constants .EXTENSIONS_AUDIO :
16611705 entry .audio_entries .append (media_format .format .lower ())
16621706
1707+ return entry , book_in_shelves , cc
1708+ else :
1709+ return None , None , None
1710+
1711+
1712+ @web .route ("/book/<int:book_id>" )
1713+ @login_required_if_no_ano
1714+ def show_book (book_id ):
1715+ entry , shelves , cc = _get_book_entry (book_id )
1716+ if entry :
16631717 return render_title_template ('detail.html' ,
16641718 entry = entry ,
16651719 cc = cc ,
16661720 is_xhr = request .headers .get ('X-Requested-With' ) == 'XMLHttpRequest' ,
16671721 title = entry .title ,
1668- books_shelfs = book_in_shelves ,
1722+ books_shelfs = shelves ,
16691723 page = "book" )
16701724 else :
1671- log .debug ("Selected book is unavailable. File does not exist or is not accessible" )
1672- flash (_ ("Oops! Selected book is unavailable. File does not exist or is not accessible" ),
1673- category = "error" )
1674- return redirect (url_for ("web.index" ))
1725+ return _no_book_entry ()
1726+
1727+
1728+ def _no_book_entry ():
1729+ log .debug ("Selected book is unavailable. File does not exist or is not accessible" )
1730+ flash (_ ("Oops! Selected book is unavailable. File does not exist or is not accessible" ),
1731+ category = "error" )
1732+ return redirect (url_for ("web.index" ))
1733+
1734+
1735+ @web .route ("/book/<int:book_id>/converting/<task_id>" )
1736+ @login_required_if_no_ano
1737+ def show_book_converting (book_id , task_id ):
1738+ task_uuid = uuid .UUID (task_id )
1739+ task = WorkerThread .get_instance ().get_task (task_uuid )
1740+ complete = task and task .dead
1741+ if complete :
1742+ return redirect (url_for ("web.read_book" , book_id = book_id , book_format = 'epub' ))
1743+ else :
1744+ entry = _get_book_entry (book_id )
1745+ if entry :
1746+ return render_title_template (
1747+ "converting.html" ,
1748+ entry = entry [0 ],
1749+ book_id = book_id ,
1750+ task_id = task_id ,
1751+ )
1752+ else :
1753+ return _no_book_entry ()
1754+
1755+
0 commit comments