Skip to content

Commit 7af9cf6

Browse files
committed
Merge branch 'hardcover'
2 parents 6090df8 + 8e6496f commit 7af9cf6

File tree

36 files changed

+624
-244
lines changed

36 files changed

+624
-244
lines changed

.github/workflows/dockerhub-build-push-on-push.yml

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,63 @@
11
name: Build & Push - Dev
2-
# Automatically builds and pushes a multi-platform dev image based on commits involving key files
2+
# Automatically builds and pushes a multi-platform dev image based on commits involving key files in any branch
33

44
on:
55
push:
66
branches:
7-
- main
7+
- "**" # Runs on pushes to any branch
88
paths:
99
- "empty_library/**"
1010
- "root/**"
1111
- "scripts/**"
1212
- "**/Dockerfile"
13+
create:
14+
branches:
15+
- "**" # Runs when a new branch is created
1316

1417
jobs:
1518
build:
1619
runs-on: ubuntu-latest
20+
if: github.event.ref_type == 'branch' || github.event_name == 'push' # Ensures it runs for branch creation & push events
1721

1822
steps:
19-
- uses: actions/checkout@v4
23+
- name: Checkout correct branch
24+
uses: actions/checkout@v4
25+
with:
26+
ref: ${{ github.ref }}
27+
28+
- name: Determine Docker Image Tag
29+
id: tag
30+
run: |
31+
if [[ "${{ github.ref_name }}" == "main" ]]; then
32+
echo "IMAGE_TAG=dev" >> $GITHUB_ENV
33+
else
34+
echo "IMAGE_TAG=dev-${{ github.ref_name }}" >> $GITHUB_ENV
35+
fi
2036
2137
- name: DockerHub Login
2238
uses: docker/login-action@v3
2339
with:
2440
username: ${{ secrets.DOCKERHUB_USERNAME }}
2541
password: ${{ secrets.DOCKERHUB_PA_TOKEN }}
2642

43+
- name: Install QEMU
44+
run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
45+
2746
- name: Set up Docker Buildx
2847
uses: docker/setup-buildx-action@v3
2948

3049
- name: Build and push Docker image
3150
uses: docker/build-push-action@v6
3251
with:
52+
provenance: false # Disable provenance metadata to fix BuildKit issues
3353
context: .
3454
file: ./Dockerfile
3555
push: true
3656
build-args: |
3757
BUILD_DATE=${{ github.event.repository.updated_at }}
38-
VERSION=${{ vars.CURRENT_DEV_VERSION }}-DEV_BUILD-${{ vars.CURRENT_DEV_BUILD_NUM }}
58+
VERSION=${{ vars.CURRENT_DEV_VERSION }}-DEV_BUILD-${{ env.IMAGE_TAG }}-${{ vars.CURRENT_DEV_BUILD_NUM }}
3959
tags: |
40-
${{ secrets.DOCKERHUB_USERNAME }}/calibre-web-automated:dev
60+
${{ secrets.DOCKERHUB_USERNAME }}/calibre-web-automated:dev-${{ env.IMAGE_TAG }}
4161
4262
platforms: linux/amd64,linux/arm64
4363

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ And just like that, Calibre-Web Automated should be up and running! **HOWEVER**
295295

296296
- CWA has been designed to make switching over as easy as possible. To migrate your CW instance to CWA, simply:
297297
1. Stop your instance of CW if it's still running
298+
2. Whatever your `/books` bind was in Calibre-Web, should be your `/calibre-library` bind for CWA
298299
2. Mount the same `/config` folder in your Docker Compose that you were using for CW (or a copy of it to be extra safe)
299300
3. Mount the same folder containing your Calibre Library (the Docker Compose for the Linuxserver image of Calibre Web has this as `/books` by default)
300301
- And then you're done! All of your users, settings ect. should be automatically carried over into your new CWA instance! Enjoy!

root/app/calibre-web/cps/cwa_functions.py

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from flask import Blueprint, redirect, flash, url_for, request, send_from_directory, abort
1+
from flask import Blueprint, redirect, flash, url_for, request, send_from_directory, abort, jsonify, current_app
22
from flask_babel import gettext as _
33

44
from . import logger, config, constants, csrf
@@ -40,6 +40,7 @@
4040

4141
# Folder where the log files are stored
4242
LOG_ARCHIVE = "/config/log_archive"
43+
DIRS_JSON = "/app/calibre-web-automated/dirs.json"
4344

4445
##———————————————————————————END OF GLOBAL VARIABLES——————————————————————————##
4546

@@ -72,31 +73,66 @@ def cwa_switch_theme():
7273
config.config_default_show |= constants.DETAIL_RANDOM
7374

7475
config.save()
75-
return redirect("/", code=302)
76+
return redirect(url_for("web.index"), code=302)
7677

7778
##————————————————————————————————————————————————————————————————————————————##
7879
## ##
7980
## CWA LIBRARY REFRESH ##
8081
## ##
8182
##————————————————————————————————————————————————————————————————————————————##
8283

84+
def get_ingest_dir():
85+
with open(DIRS_JSON, 'r') as f:
86+
dirs = json.load(f)
87+
return dirs['ingest_folder']
88+
89+
def refresh_library(app):
90+
with app.app_context(): # Create app context for session
91+
ingest_dir = get_ingest_dir()
92+
result = subprocess.run(['python3', '/app/calibre-web-automated/scripts/ingest_processor.py', ingest_dir])
93+
return_code = result.returncode
94+
95+
# Add empty list for messages in app context if a list doesn't already exist
96+
if "library_refresh_messages" not in current_app.config:
97+
current_app.config["library_refresh_messages"] = []
98+
99+
if return_code == 2:
100+
message = "Library Refresh 🔄 The book ingest service is already running ✋ Please wait until it has finished before trying again ⌛"
101+
elif return_code == 0:
102+
message = "Library Refresh 🔄 Library refreshed & ingest process complete! ✅"
103+
else:
104+
message = "Library Refresh 🔄 An unexpected error occurred, check the logs ⛔"
105+
106+
# Display message to user in Web UI
107+
current_app.config["library_refresh_messages"].append(message)
108+
# Print result to docker log
109+
print(message.replace('Library Refresh 🔄', '[library-refresh]'), flush=True)
110+
111+
@csrf.exempt
83112
@library_refresh.route("/cwa-library-refresh", methods=["GET", "POST"])
84113
@login_required_if_no_ano
85114
def cwa_library_refresh():
86-
flash(_("Library Refresh: Initialising Book Ingest System, please wait..."), category="cwa_refresh")
87-
result = subprocess.run(['python3', '/app/calibre-web-automated/scripts/ingest_processor.py', '/cwa-book-ingest'])
88-
return_code = result.returncode
89-
90-
# if return_code == 100:
91-
# flash(_(f"Library Refresh: Ingest process complete. New books ingested."), category="cwa_refresh")
92-
if return_code == 2:
93-
flash(_("Library Refresh: The book ingest service is already running, please wait until it has finished before trying again."), category="cwa_refresh")
94-
elif return_code == 0:
95-
flash(_("Library Refresh: Library refreshed & ingest process complete."), category="cwa_refresh")
96-
else:
97-
flash(_("Library Refresh: An unexpected error occurred, check the logs."), category="cwa_refresh")
115+
print("[library-refresh] Library refresh manually triggered by user...", flush=True)
116+
app = current_app._get_current_object() # Get actual app instance
117+
118+
current_app.config["library_refresh_messages"] = []
119+
120+
# Run refresh_library() in a background thread
121+
library_refresh_thread = Thread(target=refresh_library, args=(app,))
122+
library_refresh_thread.start()
98123

99-
return redirect("/", code=302)
124+
return jsonify({"message": "Library Refresh 🔄 Checking for any books that may have been missed, please wait..."}), 200
125+
126+
@csrf.exempt
127+
@library_refresh.route("/cwa-library-refresh/messages", methods=["GET"])
128+
@login_required_if_no_ano
129+
def get_library_refresh_messages():
130+
messages = current_app.config.get("library_refresh_messages", [])
131+
132+
# Clear messages after they have been retrieved
133+
current_app.config["library_refresh_messages"] = []
134+
135+
return jsonify({"messages": messages})
100136

101137
##————————————————————————————————————————————————————————————————————————————##
102138
## ##
@@ -688,4 +724,4 @@ def get_status():
688724
progress = extract_progress(status)
689725
statusList = {'status':status,
690726
'progress':progress}
691-
return json.dumps(statusList)
727+
return json.dumps(statusList)

root/app/calibre-web/cps/editbooks.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,76 @@ def table_get_custom_enum(c_id):
214214
@edit_required
215215
def edit_list_book(param):
216216
vals = request.form.to_dict()
217+
return edit_book_param(param, vals)
218+
219+
@editbook.route("/ajax/editselectedbooks", methods=['POST'])
220+
@login_required_if_no_ano
221+
@edit_required
222+
def edit_selected_books():
223+
d = request.get_json()
224+
selections = d.get('selections')
225+
title = d.get('title')
226+
title_sort = d.get('title_sort')
227+
author_sort = d.get('author_sort')
228+
authors = d.get('authors')
229+
categories = d.get('categories')
230+
series = d.get('series')
231+
languages = d.get('languages')
232+
publishers = d.get('publishers')
233+
comments = d.get('comments')
234+
checkA = d.get('checkA')
235+
236+
if len(selections) != 0:
237+
for book_id in selections:
238+
vals = {
239+
"pk": book_id,
240+
"value": None,
241+
"checkA": checkA,
242+
}
243+
if title:
244+
vals['value'] = title
245+
edit_book_param('title', vals)
246+
if title_sort:
247+
vals['value'] = title_sort
248+
edit_book_param('sort', vals)
249+
if author_sort:
250+
vals['value'] = author_sort
251+
edit_book_param('author_sort', vals)
252+
if authors:
253+
vals['value'] = authors
254+
edit_book_param('authors', vals)
255+
if categories:
256+
vals['value'] = categories
257+
edit_book_param('tags', vals)
258+
if series:
259+
vals['value'] = series
260+
edit_book_param('series', vals)
261+
if languages:
262+
vals['value'] = languages
263+
edit_book_param('languages', vals)
264+
if publishers:
265+
vals['value'] = publishers
266+
edit_book_param('publishers', vals)
267+
if comments:
268+
vals['value'] = comments
269+
edit_book_param('comments', vals)
270+
return json.dumps({'success': True})
271+
return ""
272+
273+
# Separated from /editbooks so that /editselectedbooks can also use this
274+
#
275+
# param: the property of the book to be changed
276+
# vals - JSON Object:
277+
# {
278+
# 'pk': "the book id",
279+
# 'value': "changes value of param to what's passed here"
280+
# 'checkA': "Optional. Used to check if autosort author is enabled. Assumed as true if not passed"
281+
# 'checkT': "Optional. Used to check if autotitle author is enabled. Assumed as true if not passed"
282+
# }
283+
#
284+
@login_required_if_no_ano
285+
@edit_required
286+
def edit_book_param(param, vals):
217287
book = calibre_db.get_book(vals['pk'])
218288
sort_param = ""
219289
ret = ""
@@ -353,6 +423,66 @@ def simulate_merge_list_book():
353423
return ""
354424

355425

426+
@editbook.route("/ajax/displayselectedbooks", methods=['POST'])
427+
@user_login_required
428+
@edit_required
429+
def display_selected_books():
430+
vals = request.get_json().get('selections')
431+
books = []
432+
if vals:
433+
for book_id in vals:
434+
books.append(calibre_db.get_book(book_id).title)
435+
return json.dumps({'books': books})
436+
return ""
437+
438+
@editbook.route("/ajax/archiveselectedbooks", methods=['POST'])
439+
@login_required_if_no_ano
440+
@edit_required
441+
def archive_selected_books():
442+
vals = request.get_json().get('selections')
443+
state = request.get_json().get('archive')
444+
if vals:
445+
for book_id in vals:
446+
is_archived = change_archived_books(book_id, state,
447+
message="Book {} archive bit set to: {}".format(book_id, state))
448+
if is_archived:
449+
kobo_sync_status.remove_synced_book(book_id)
450+
return json.dumps({'success': True})
451+
return ""
452+
453+
@editbook.route("/ajax/deleteselectedbooks", methods=['POST'])
454+
@user_login_required
455+
@edit_required
456+
def delete_selected_books():
457+
vals = request.get_json().get('selections')
458+
if vals:
459+
for book_id in vals:
460+
delete_book_from_table(book_id, "", True)
461+
return json.dumps({'success': True})
462+
return ""
463+
464+
@editbook.route("/ajax/readselectedbooks", methods=['POST'])
465+
@user_login_required
466+
@edit_required
467+
def read_selected_books():
468+
vals = request.get_json().get('selections')
469+
markAsRead = request.get_json().get('markAsRead')
470+
if vals:
471+
try:
472+
for book_id in vals:
473+
ret = helper.edit_book_read_status(book_id, markAsRead)
474+
475+
except (OperationalError, IntegrityError, StaleDataError) as e:
476+
calibre_db.session.rollback()
477+
log.error_or_exception("Database error: {}".format(e))
478+
ret = Response(json.dumps({'success': False,
479+
'msg': 'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}),
480+
mimetype='application/json')
481+
482+
return json.dumps({'success': True})
483+
return ""
484+
485+
356486
@editbook.route("/ajax/mergebooks", methods=['POST'])
357487
@user_login_required
358488
@edit_required

root/app/calibre-web/cps/render_template.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,9 @@ def cwa_update_notification() -> None:
147147

148148
update_available, current_version, tag_name = cwa_update_available()
149149
if update_available:
150-
message = f"⚡🚨 CWA UPDATE AVAILABLE! 🚨⚡ Current - {current_version} | Newest - {tag_name} | To update, just re-pull the image! This message will only display once per day"
150+
message = f"⚡🚨 CWA UPDATE AVAILABLE! 🚨⚡ Current - {current_version} | Newest - {tag_name} | To update, just re-pull the image! This message will only display once per day |"
151151
flash(_(message), category="cwa_update")
152+
print(f"[cwa-update-notification-service] {message}", flush=True)
152153

153154
with open('/app/cwa_update_notice', 'w') as f:
154155
f.write(current_date)
@@ -161,7 +162,10 @@ def cwa_update_notification() -> None:
161162
def render_title_template(*args, **kwargs):
162163
sidebar, simple = get_sidebar_config(kwargs)
163164
if current_user.role_admin():
164-
cwa_update_notification()
165+
try:
166+
cwa_update_notification()
167+
except Exception as e:
168+
print(f"[cwa-update-notification-service] The following error occurred when checking for available updates:\n{e}", flush=True)
165169
try:
166170
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, simple=simple,
167171
accept=config.config_upload_formats.split(','),

root/app/calibre-web/cps/services/hardcover.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,15 @@ def get_user_book(self, ids):
108108
def update_reading_progress(self, identifiers, progress_percent):
109109
ids = self.parse_identifiers(identifiers)
110110
book = self.get_user_book(ids)
111-
if not book: # Book doesn't exist, add it in Reading status
111+
# Book doesn't exist, add it in Reading status
112+
if not book:
112113
book = self.add_book(ids, status=2)
113-
if book.get("status_id") != 2: # Book is either WTR or Read
114+
# Book is either WTR or Read, and we aren't finished reading
115+
if book.get("status_id") != 2 and progress_percent != 100:
114116
book = self.change_book_status(book, 2)
117+
# Book is already marked as read, and we are also done
118+
if book.get("status_id") == 3 and progress_percent == 100:
119+
return
115120
pages = book.get("edition",{}).get("pages",0)
116121
if pages:
117122
pages_read = round(pages * (progress_percent / 100))
@@ -220,16 +225,12 @@ def execute(self, query, variables=None):
220225
"query": query,
221226
"variables": variables or {}
222227
}
223-
224-
225228
response = requests.post(self.endpoint, json=payload, headers=self.headers)
226229
try:
227230
response.raise_for_status()
228231
except requests.exceptions.HTTPError as e:
229232
raise Exception(f"HTTP error occurred: {e}")
230-
231233
result = response.json()
232234
if "errors" in result:
233235
raise Exception(f"GraphQL error: {result['errors']}")
234-
235236
return result.get("data", {})

0 commit comments

Comments
 (0)