\n"
+"Language: pl\n"
+"Language-Team: \n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.6.0\n"
+
+#: cps/book_formats.py:129 cps/book_formats.py:130 cps/book_formats.py:134
+#: cps/book_formats.py:138 cps/converter.py:11 cps/converter.py:27
+msgid "not installed"
+msgstr "nie zainstalowane"
+
+#: cps/converter.py:22 cps/converter.py:38
+msgid "Excecution permissions missing"
+msgstr ""
+
+#: cps/converter.py:48
+msgid "not configured"
+msgstr ""
+
+#: cps/helper.py:58
+#, python-format
+msgid "%(format)s format not found for book id: %(book)d"
+msgstr ""
+
+#: cps/helper.py:70
+#, python-format
+msgid "%(format)s not found on Google Drive: %(fn)s"
+msgstr ""
+
+#: cps/helper.py:77 cps/helper.py:147 cps/templates/detail.html:44
+msgid "Send to Kindle"
+msgstr "Wyślij do Kindle"
+
+#: cps/helper.py:78 cps/helper.py:96
+msgid "This e-mail has been sent via Calibre-Web."
+msgstr ""
+
+#: cps/helper.py:89
+#, python-format
+msgid "%(format)s not found: %(fn)s"
+msgstr ""
+
+#: cps/helper.py:94
+msgid "Calibre-Web test e-mail"
+msgstr ""
+
+#: cps/helper.py:95
+msgid "Test e-mail"
+msgstr ""
+
+#: cps/helper.py:111
+msgid "Get Started with Calibre-Web"
+msgstr ""
+
+#: cps/helper.py:112
+#, python-format
+msgid "Registration e-mail for user: %(name)s"
+msgstr ""
+
+#: cps/helper.py:135 cps/helper.py:145
+msgid "Could not find any formats suitable for sending by e-mail"
+msgstr ""
+
+#: cps/helper.py:148
+#, python-format
+msgid "E-mail: %(book)s"
+msgstr ""
+
+#: cps/helper.py:150
+msgid "The requested file could not be read. Maybe wrong permissions?"
+msgstr ""
+
+#: cps/helper.py:250
+#, python-format
+msgid "Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgstr ""
+
+#: cps/helper.py:259
+#, python-format
+msgid "Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgstr ""
+
+#: cps/helper.py:281 cps/helper.py:290
+#, python-format
+msgid "File %(file)s not found on Google Drive"
+msgstr ""
+
+#: cps/helper.py:308
+#, python-format
+msgid "Book path %(path)s not found on Google Drive"
+msgstr ""
+
+#: cps/helper.py:565
+msgid "Error excecuting UnRar"
+msgstr ""
+
+#: cps/helper.py:567
+msgid "Unrar binary file not found"
+msgstr ""
+
+#: cps/helper.py:609
+msgid "Waiting"
+msgstr ""
+
+#: cps/helper.py:611
+msgid "Failed"
+msgstr ""
+
+#: cps/helper.py:613
+msgid "Started"
+msgstr ""
+
+#: cps/helper.py:615
+msgid "Finished"
+msgstr ""
+
+#: cps/helper.py:617
+msgid "Unknown Status"
+msgstr ""
+
+#: cps/helper.py:622
+msgid "E-mail: "
+msgstr ""
+
+#: cps/helper.py:624 cps/helper.py:628
+msgid "Convert: "
+msgstr ""
+
+#: cps/helper.py:626
+msgid "Upload: "
+msgstr ""
+
+#: cps/helper.py:630
+msgid "Unknown Task: "
+msgstr ""
+
+#: cps/web.py:1132 cps/web.py:2842
+msgid "Unknown"
+msgstr ""
+
+#: cps/web.py:1141 cps/web.py:1172 cps/web.py:1257
+msgid "HTTP Error"
+msgstr ""
+
+#: cps/web.py:1143 cps/web.py:1174 cps/web.py:1258
+msgid "Connection error"
+msgstr ""
+
+#: cps/web.py:1145 cps/web.py:1176 cps/web.py:1259
+msgid "Timeout while establishing connection"
+msgstr ""
+
+#: cps/web.py:1147 cps/web.py:1178 cps/web.py:1260
+msgid "General error"
+msgstr ""
+
+#: cps/web.py:1153
+msgid "Unexpected data while reading update information"
+msgstr ""
+
+#: cps/web.py:1160
+msgid "No update available. You already have the latest version installed"
+msgstr ""
+
+#: cps/web.py:1185
+msgid "A new update is available. Click on the button below to update to the latest version."
+msgstr ""
+
+#: cps/web.py:1235
+msgid "Could not fetch update information"
+msgstr ""
+
+#: cps/web.py:1250
+msgid "Requesting update package"
+msgstr "Żądanie o pakiet aktualizacji"
+
+#: cps/web.py:1251
+msgid "Downloading update package"
+msgstr "Pobieranie pakietu aktualizacji"
+
+#: cps/web.py:1252
+msgid "Unzipping update package"
+msgstr "Rozpakowywanie pakietu aktualizacji"
+
+#: cps/web.py:1253
+msgid "Replacing files"
+msgstr ""
+
+#: cps/web.py:1254
+msgid "Database connections are closed"
+msgstr "Połączenia z bazą danych zostały zakończone"
+
+#: cps/web.py:1255
+msgid "Stopping server"
+msgstr ""
+
+#: cps/web.py:1256
+msgid "Update finished, please press okay and reload page"
+msgstr "Aktualizacja zakończona, proszę nacisnąć OK i odświeżyć stronę"
+
+#: cps/web.py:1257 cps/web.py:1258 cps/web.py:1259 cps/web.py:1260
+msgid "Update failed:"
+msgstr ""
+
+#: cps/web.py:1283
+msgid "Recently Added Books"
+msgstr ""
+
+#: cps/web.py:1293
+msgid "Newest Books"
+msgstr ""
+
+#: cps/web.py:1305
+msgid "Oldest Books"
+msgstr ""
+
+#: cps/web.py:1317
+msgid "Books (A-Z)"
+msgstr ""
+
+#: cps/web.py:1328
+msgid "Books (Z-A)"
+msgstr ""
+
+#: cps/web.py:1357
+msgid "Hot Books (most downloaded)"
+msgstr "Najpopularniejsze książki (najczęściej pobierane)"
+
+#: cps/web.py:1370
+msgid "Best rated books"
+msgstr "Najlepiej oceniane książki"
+
+#: cps/templates/index.xml:39 cps/web.py:1383
+msgid "Random Books"
+msgstr "Losowe książki"
+
+#: cps/web.py:1398
+msgid "Author list"
+msgstr "Lista autorów"
+
+#: cps/web.py:1410 cps/web.py:1501 cps/web.py:1663 cps/web.py:2206
+msgid "Error opening eBook. File does not exist or file is not accessible:"
+msgstr "Błąd otwierania e-booka. Plik nie istnieje lub plik nie jest dostępny:"
+
+#: cps/web.py:1438
+msgid "Publisher list"
+msgstr ""
+
+#: cps/web.py:1452
+#, python-format
+msgid "Publisher: %(name)s"
+msgstr ""
+
+#: cps/templates/index.xml:83 cps/web.py:1484
+msgid "Series list"
+msgstr "Lista serii"
+
+#: cps/web.py:1499
+#, python-format
+msgid "Series: %(serie)s"
+msgstr "Seria: %(serie)s"
+
+#: cps/web.py:1528
+msgid "Available languages"
+msgstr "Dostępne języki"
+
+#: cps/web.py:1548
+#, python-format
+msgid "Language: %(name)s"
+msgstr "Język: %(name)s"
+
+#: cps/templates/index.xml:76 cps/web.py:1559
+msgid "Category list"
+msgstr "Lista kategorii"
+
+#: cps/web.py:1573
+#, python-format
+msgid "Category: %(name)s"
+msgstr "Kategoria: %(name)s"
+
+#: cps/templates/layout.html:71 cps/web.py:1699
+msgid "Tasks"
+msgstr ""
+
+#: cps/web.py:1733
+msgid "Statistics"
+msgstr "Statystyki"
+
+#: cps/web.py:1840
+msgid "Callback domain is not verified, please follow steps to verify domain in google developer console"
+msgstr ""
+
+#: cps/web.py:1915
+msgid "Server restarted, please reload page"
+msgstr "Serwer uruchomiony ponownie, proszę odświeżyć stronę"
+
+#: cps/web.py:1918
+msgid "Performing shutdown of server, please close window"
+msgstr "Wykonano wyłączenie serwera, proszę zamknąć okno"
+
+#: cps/web.py:1937
+msgid "Update done"
+msgstr "Aktualizacja zakończona"
+
+#: cps/web.py:2007
+msgid "Published after "
+msgstr ""
+
+#: cps/web.py:2014
+msgid "Published before "
+msgstr ""
+
+#: cps/web.py:2028
+#, python-format
+msgid "Rating <= %(rating)s"
+msgstr ""
+
+#: cps/web.py:2030
+#, python-format
+msgid "Rating >= %(rating)s"
+msgstr ""
+
+#: cps/web.py:2089 cps/web.py:2098
+msgid "search"
+msgstr "szukaj"
+
+#: cps/templates/index.xml:47 cps/templates/index.xml:51
+#: cps/templates/layout.html:146 cps/web.py:2165
+msgid "Read Books"
+msgstr "Przeczytane książki"
+
+#: cps/templates/index.xml:55 cps/templates/index.xml:59
+#: cps/templates/layout.html:148 cps/web.py:2168
+msgid "Unread Books"
+msgstr "Nieprzeczytane książki"
+
+#: cps/web.py:2216 cps/web.py:2218 cps/web.py:2220 cps/web.py:2232
+msgid "Read a Book"
+msgstr "Czytaj książkę"
+
+#: cps/web.py:2298 cps/web.py:3201
+msgid "Please fill out all fields!"
+msgstr "Proszę wypełnić wszystkie pola!"
+
+#: cps/web.py:2299 cps/web.py:2320 cps/web.py:2324 cps/web.py:2329
+#: cps/web.py:2331
+msgid "register"
+msgstr "rejestracja"
+
+#: cps/web.py:2319 cps/web.py:3417
+msgid "An unknown error occurred. Please try again later."
+msgstr ""
+
+#: cps/web.py:2322
+msgid "Your e-mail is not allowed to register"
+msgstr ""
+
+#: cps/web.py:2325
+msgid "Confirmation e-mail was send to your e-mail account."
+msgstr ""
+
+#: cps/web.py:2328
+msgid "This username or e-mail address is already in use."
+msgstr ""
+
+#: cps/web.py:2345 cps/web.py:2441
+#, python-format
+msgid "you are now logged in as: '%(nickname)s'"
+msgstr "Zalogowałeś się jako: '%(nickname)s'"
+
+#: cps/web.py:2350
+msgid "Wrong Username or Password"
+msgstr "Błędna nazwa użytkownika lub hasło"
+
+#: cps/web.py:2356 cps/web.py:2377
+msgid "login"
+msgstr "logowanie"
+
+#: cps/web.py:2389 cps/web.py:2420
+msgid "Token not found"
+msgstr ""
+
+#: cps/web.py:2397 cps/web.py:2428
+msgid "Token has expired"
+msgstr ""
+
+#: cps/web.py:2405
+msgid "Success! Please return to your device"
+msgstr ""
+
+#: cps/web.py:2455
+msgid "Please configure the SMTP mail settings first..."
+msgstr "Proszę najpierw skonfigurować ustawienia SMTP poczty e-mail..."
+
+#: cps/web.py:2459
+#, python-format
+msgid "Book successfully queued for sending to %(kindlemail)s"
+msgstr ""
+
+#: cps/web.py:2463
+#, python-format
+msgid "There was an error sending this book: %(res)s"
+msgstr "Wystąpił błąd podczas wysyłania tej książki: %(res)s"
+
+#: cps/web.py:2465 cps/web.py:3255
+msgid "Please configure your kindle e-mail address first..."
+msgstr ""
+
+#: cps/web.py:2476 cps/web.py:2528
+msgid "Invalid shelf specified"
+msgstr ""
+
+#: cps/web.py:2483
+#, python-format
+msgid "Sorry you are not allowed to add a book to the the shelf: %(shelfname)s"
+msgstr ""
+
+#: cps/web.py:2491
+msgid "You are not allowed to edit public shelves"
+msgstr ""
+
+#: cps/web.py:2500
+#, python-format
+msgid "Book is already part of the shelf: %(shelfname)s"
+msgstr ""
+
+#: cps/web.py:2514
+#, python-format
+msgid "Book has been added to shelf: %(sname)s"
+msgstr "Książka została dodana do półki: %(sname)s"
+
+#: cps/web.py:2533
+#, python-format
+msgid "You are not allowed to add a book to the the shelf: %(name)s"
+msgstr ""
+
+#: cps/web.py:2538
+msgid "User is not allowed to edit public shelves"
+msgstr ""
+
+#: cps/web.py:2556
+#, python-format
+msgid "Books are already part of the shelf: %(name)s"
+msgstr ""
+
+#: cps/web.py:2570
+#, python-format
+msgid "Books have been added to shelf: %(sname)s"
+msgstr ""
+
+#: cps/web.py:2572
+#, python-format
+msgid "Could not add books to shelf: %(sname)s"
+msgstr ""
+
+#: cps/web.py:2609
+#, python-format
+msgid "Book has been removed from shelf: %(sname)s"
+msgstr "Książka została usunięta z półki: %(sname)s"
+
+#: cps/web.py:2615
+#, python-format
+msgid "Sorry you are not allowed to remove a book from this shelf: %(sname)s"
+msgstr ""
+
+#: cps/web.py:2635 cps/web.py:2659
+#, python-format
+msgid "A shelf with the name '%(title)s' already exists."
+msgstr "Półka o nazwie '%(title)s' już istnieje."
+
+#: cps/web.py:2640
+#, python-format
+msgid "Shelf %(title)s created"
+msgstr "Półka %(title)s została utworzona"
+
+#: cps/web.py:2642 cps/web.py:2670
+msgid "There was an error"
+msgstr "Wystąpił błąd"
+
+#: cps/web.py:2643 cps/web.py:2645
+msgid "create a shelf"
+msgstr "utwórz półkę"
+
+#: cps/web.py:2668
+#, python-format
+msgid "Shelf %(title)s changed"
+msgstr "Półka %(title)s została zmieniona"
+
+#: cps/web.py:2671 cps/web.py:2673
+msgid "Edit a shelf"
+msgstr "Edytuj półkę"
+
+#: cps/web.py:2694
+#, python-format
+msgid "successfully deleted shelf %(name)s"
+msgstr "pomyślnie usunięto półkę %(name)s"
+
+#: cps/web.py:2721
+#, python-format
+msgid "Shelf: '%(name)s'"
+msgstr "Półka: '%(name)s'"
+
+#: cps/web.py:2724
+msgid "Error opening shelf. Shelf does not exist or is not accessible"
+msgstr ""
+
+#: cps/web.py:2755
+#, python-format
+msgid "Change order of Shelf: '%(name)s'"
+msgstr "Zmieniono kolejność półki: '%(name)s'"
+
+#: cps/web.py:2784 cps/web.py:3207
+msgid "E-mail is not from valid domain"
+msgstr ""
+
+#: cps/web.py:2786 cps/web.py:2829 cps/web.py:2832
+#, python-format
+msgid "%(name)s's profile"
+msgstr "Profil użytkownika %(name)s"
+
+#: cps/web.py:2827
+msgid "Found an existing account for this e-mail address."
+msgstr ""
+
+#: cps/web.py:2830
+msgid "Profile updated"
+msgstr "Zaktualizowano profil"
+
+#: cps/web.py:2858
+msgid "Admin page"
+msgstr "Portal administracyjny"
+
+#: cps/web.py:2938 cps/web.py:3112
+msgid "Calibre-Web configuration updated"
+msgstr "Konfiguracja Calibre-Web została zaktualizowana"
+
+#: cps/templates/admin.html:100 cps/web.py:2951
+msgid "UI Configuration"
+msgstr ""
+
+#: cps/web.py:2969
+msgid "Import of optional Google Drive requirements missing"
+msgstr ""
+
+#: cps/web.py:2972
+msgid "client_secrets.json is missing or not readable"
+msgstr ""
+
+#: cps/web.py:2977 cps/web.py:3004
+msgid "client_secrets.json is not configured for web application"
+msgstr ""
+
+#: cps/templates/admin.html:99 cps/web.py:3007 cps/web.py:3033 cps/web.py:3045
+#: cps/web.py:3088 cps/web.py:3103 cps/web.py:3120 cps/web.py:3127
+#: cps/web.py:3142
+msgid "Basic Configuration"
+msgstr "Podstawowa konfiguracja"
+
+#: cps/web.py:3030
+msgid "Keyfile location is not valid, please enter correct path"
+msgstr ""
+
+#: cps/web.py:3042
+msgid "Certfile location is not valid, please enter correct path"
+msgstr ""
+
+#: cps/web.py:3085
+msgid "Logfile location is not valid, please enter correct path"
+msgstr ""
+
+#: cps/web.py:3124
+msgid "DB location is not valid, please enter correct path"
+msgstr "Lokalizacja bazy danych jest nieprawidłowa, wpisz poprawną ścieżkę"
+
+#: cps/templates/admin.html:33 cps/web.py:3203 cps/web.py:3209 cps/web.py:3225
+msgid "Add new user"
+msgstr "Dodaj nowego użytkownika"
+
+#: cps/web.py:3215
+#, python-format
+msgid "User '%(user)s' created"
+msgstr "Użytkownik '%(user)s' został utworzony"
+
+#: cps/web.py:3219
+msgid "Found an existing account for this e-mail address or nickname."
+msgstr ""
+
+#: cps/web.py:3243 cps/web.py:3257
+msgid "E-mail server settings updated"
+msgstr ""
+
+#: cps/web.py:3250
+#, python-format
+msgid "Test e-mail successfully send to %(kindlemail)s"
+msgstr ""
+
+#: cps/web.py:3253
+#, python-format
+msgid "There was an error sending the Test e-mail: %(res)s"
+msgstr ""
+
+#: cps/web.py:3258
+msgid "Edit e-mail server settings"
+msgstr ""
+
+#: cps/web.py:3283
+#, python-format
+msgid "User '%(nick)s' deleted"
+msgstr "Użytkownik '%(nick)s' został usunięty"
+
+#: cps/web.py:3392
+#, python-format
+msgid "User '%(nick)s' updated"
+msgstr "Użytkownik '%(nick)s' został zaktualizowany"
+
+#: cps/web.py:3395
+msgid "An unknown error occured."
+msgstr "Wystąpił nieznany błąd."
+
+#: cps/web.py:3397
+#, python-format
+msgid "Edit User %(nick)s"
+msgstr "Edytuj użytkownika %(nick)s"
+
+#: cps/web.py:3414
+#, python-format
+msgid "Password for user %(user)s reset"
+msgstr ""
+
+#: cps/web.py:3428 cps/web.py:3629
+msgid "Error opening eBook. File does not exist or file is not accessible"
+msgstr ""
+
+#: cps/web.py:3453 cps/web.py:3912
+msgid "edit metadata"
+msgstr "edytuj metadane"
+
+#: cps/web.py:3546 cps/web.py:3782
+#, python-format
+msgid "File extension '%(ext)s' is not allowed to be uploaded to this server"
+msgstr "Rozszerzenie pliku '%(ext)s' nie jest dozwolone do przesłania na ten serwer"
+
+#: cps/web.py:3550 cps/web.py:3786
+msgid "File to be uploaded must have an extension"
+msgstr "Plik do przesłania musi mieć rozszerzenie"
+
+#: cps/web.py:3562 cps/web.py:3806
+#, python-format
+msgid "Failed to create path %(path)s (Permission denied)."
+msgstr "Nie udało się utworzyć łącza %(path)s (Odmowa dostępu)."
+
+#: cps/web.py:3567
+#, python-format
+msgid "Failed to store file %(file)s."
+msgstr ""
+
+#: cps/web.py:3583
+#, python-format
+msgid "File format %(ext)s added to %(book)s"
+msgstr ""
+
+#: cps/web.py:3601
+#, python-format
+msgid "Failed to create path for cover %(path)s (Permission denied)."
+msgstr ""
+
+#: cps/web.py:3608
+#, python-format
+msgid "Failed to store cover-file %(cover)s."
+msgstr ""
+
+#: cps/web.py:3611
+msgid "Cover-file is not a valid image file"
+msgstr ""
+
+#: cps/web.py:3641 cps/web.py:3650 cps/web.py:3654
+msgid "unknown"
+msgstr ""
+
+#: cps/web.py:3673
+msgid "Cover is not a jpg file, can't save"
+msgstr ""
+
+#: cps/web.py:3721
+#, python-format
+msgid "%(langname)s is not a valid language"
+msgstr ""
+
+#: cps/web.py:3752
+msgid "Metadata successfully updated"
+msgstr ""
+
+#: cps/web.py:3761
+msgid "Error editing book, please check logfile for details"
+msgstr ""
+
+#: cps/web.py:3811
+#, python-format
+msgid "Failed to store file %(file)s (Permission denied)."
+msgstr "Nie można przechowywać pliku %(file)s (Odmowa dostępu)."
+
+#: cps/web.py:3816
+#, python-format
+msgid "Failed to delete file %(file)s (Permission denied)."
+msgstr "Nie udało się usunąć pliku %(file)s (Odmowa dostępu)."
+
+#: cps/web.py:3898
+#, python-format
+msgid "File %(file)s uploaded"
+msgstr ""
+
+#: cps/web.py:3928
+msgid "Source or destination format for conversion missing"
+msgstr ""
+
+#: cps/web.py:3938
+#, python-format
+msgid "Book successfully queued for converting to %(book_format)s"
+msgstr ""
+
+#: cps/web.py:3942
+#, python-format
+msgid "There was an error converting this book: %(res)s"
+msgstr ""
+
+#: cps/worker.py:287
+#, python-format
+msgid "Ebook-converter failed: %(error)s"
+msgstr ""
+
+#: cps/worker.py:298
+#, python-format
+msgid "Kindlegen failed with Error %(error)s. Message: %(message)s"
+msgstr ""
+
+#: cps/templates/admin.html:6
+msgid "User list"
+msgstr "Lista użytkowników"
+
+#: cps/templates/admin.html:9
+msgid "Nickname"
+msgstr "Nazwa użytkownika"
+
+#: cps/templates/admin.html:10
+msgid "E-mail"
+msgstr ""
+
+#: cps/templates/admin.html:11
+msgid "Kindle"
+msgstr "Kindle"
+
+#: cps/templates/admin.html:12
+msgid "DLS"
+msgstr "DLS"
+
+#: cps/templates/admin.html:13 cps/templates/layout.html:74
+msgid "Admin"
+msgstr "Portal administracyjny"
+
+#: cps/templates/admin.html:14 cps/templates/detail.html:22
+#: cps/templates/detail.html:31
+msgid "Download"
+msgstr "Pobierz"
+
+#: cps/templates/admin.html:15 cps/templates/layout.html:64
+msgid "Upload"
+msgstr "Wyślij"
+
+#: cps/templates/admin.html:16
+msgid "Edit"
+msgstr "Edytuj"
+
+#: cps/templates/admin.html:39
+msgid "SMTP e-mail server settings"
+msgstr ""
+
+#: cps/templates/admin.html:42 cps/templates/email_edit.html:11
+msgid "SMTP hostname"
+msgstr "Adres serwera SMTP"
+
+#: cps/templates/admin.html:43
+msgid "SMTP port"
+msgstr "Port serwera SMTP"
+
+#: cps/templates/admin.html:44
+msgid "SSL"
+msgstr "SSL"
+
+#: cps/templates/admin.html:45 cps/templates/email_edit.html:27
+msgid "SMTP login"
+msgstr "Nazwa użytkownika SMTP"
+
+#: cps/templates/admin.html:46
+msgid "From mail"
+msgstr "Wyślij z adresu e-mail"
+
+#: cps/templates/admin.html:56
+msgid "Change SMTP settings"
+msgstr "Zmień ustawienia SMTP"
+
+#: cps/templates/admin.html:62
+msgid "Configuration"
+msgstr "Konfiguracja"
+
+#: cps/templates/admin.html:65
+msgid "Calibre DB dir"
+msgstr "Folder bazy danych Calibre"
+
+#: cps/templates/admin.html:69
+msgid "Log level"
+msgstr ""
+
+#: cps/templates/admin.html:73
+msgid "Port"
+msgstr "Port"
+
+#: cps/templates/admin.html:79 cps/templates/config_view_edit.html:23
+msgid "Books per page"
+msgstr "Ilość książek na stronie"
+
+#: cps/templates/admin.html:83
+msgid "Uploading"
+msgstr "Wysyłanie"
+
+#: cps/templates/admin.html:87
+msgid "Anonymous browsing"
+msgstr ""
+
+#: cps/templates/admin.html:91
+msgid "Public registration"
+msgstr "Publiczna rejestracja"
+
+#: cps/templates/admin.html:95 cps/templates/remote_login.html:4
+msgid "Remote login"
+msgstr ""
+
+#: cps/templates/admin.html:106
+msgid "Administration"
+msgstr "Zarządzanie"
+
+#: cps/templates/admin.html:107
+msgid "Reconnect to Calibre DB"
+msgstr "Połącz ponownie z bazą danych Calibre"
+
+#: cps/templates/admin.html:108
+msgid "Restart Calibre-Web"
+msgstr "Uruchom ponownie Calibre Web"
+
+#: cps/templates/admin.html:109
+msgid "Stop Calibre-Web"
+msgstr "Zatrzymaj Calibre Web"
+
+#: cps/templates/admin.html:115
+msgid "Update"
+msgstr ""
+
+#: cps/templates/admin.html:119
+msgid "Version"
+msgstr ""
+
+#: cps/templates/admin.html:120
+msgid "Details"
+msgstr ""
+
+#: cps/templates/admin.html:126
+msgid "Current version"
+msgstr ""
+
+#: cps/templates/admin.html:132
+msgid "Check for update"
+msgstr "Sprawdź aktualizacje"
+
+#: cps/templates/admin.html:133
+msgid "Perform Update"
+msgstr "Wykonaj aktualizację"
+
+#: cps/templates/admin.html:145
+msgid "Do you really want to restart Calibre-Web?"
+msgstr "Na pewno chcesz uruchomić ponownie Calibre Web?"
+
+#: cps/templates/admin.html:150 cps/templates/admin.html:164
+#: cps/templates/admin.html:184 cps/templates/shelf.html:61
+msgid "Ok"
+msgstr "OK"
+
+#: cps/templates/admin.html:151 cps/templates/admin.html:165
+#: cps/templates/book_edit.html:178 cps/templates/book_edit.html:200
+#: cps/templates/config_edit.html:212 cps/templates/config_view_edit.html:168
+#: cps/templates/email_edit.html:40 cps/templates/email_edit.html:75
+#: cps/templates/shelf.html:62 cps/templates/shelf_edit.html:19
+#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:155
+msgid "Back"
+msgstr "Wróć"
+
+#: cps/templates/admin.html:163
+msgid "Do you really want to stop Calibre-Web?"
+msgstr "Na pewno chcesz zatrzymać Calibre Web?"
+
+#: cps/templates/admin.html:175
+msgid "Updating, please do not reload page"
+msgstr "Aktualizowanie, proszę nie odświeżać strony"
+
+#: cps/templates/author.html:15
+msgid "via"
+msgstr ""
+
+#: cps/templates/author.html:23
+msgid "In Library"
+msgstr ""
+
+#: cps/templates/author.html:69
+msgid "More by"
+msgstr ""
+
+#: cps/templates/book_edit.html:16
+msgid "Delete Book"
+msgstr ""
+
+#: cps/templates/book_edit.html:19
+msgid "Delete formats:"
+msgstr ""
+
+#: cps/templates/book_edit.html:22 cps/templates/book_edit.html:199
+#: cps/templates/email_edit.html:73 cps/templates/email_edit.html:74
+msgid "Delete"
+msgstr ""
+
+#: cps/templates/book_edit.html:30
+msgid "Convert book format:"
+msgstr ""
+
+#: cps/templates/book_edit.html:34
+msgid "Convert from:"
+msgstr ""
+
+#: cps/templates/book_edit.html:36 cps/templates/book_edit.html:43
+msgid "select an option"
+msgstr ""
+
+#: cps/templates/book_edit.html:41
+msgid "Convert to:"
+msgstr ""
+
+#: cps/templates/book_edit.html:50
+msgid "Convert book"
+msgstr ""
+
+#: cps/templates/book_edit.html:59 cps/templates/search_form.html:6
+msgid "Book Title"
+msgstr "Tytuł książki"
+
+#: cps/templates/book_edit.html:63 cps/templates/book_edit.html:259
+#: cps/templates/book_edit.html:277 cps/templates/search_form.html:10
+msgid "Author"
+msgstr "Autor"
+
+#: cps/templates/book_edit.html:67 cps/templates/book_edit.html:264
+#: cps/templates/book_edit.html:279 cps/templates/search_form.html:106
+msgid "Description"
+msgstr "Opis"
+
+#: cps/templates/book_edit.html:71 cps/templates/search_form.html:33
+msgid "Tags"
+msgstr "Tagi"
+
+#: cps/templates/book_edit.html:75 cps/templates/layout.html:157
+#: cps/templates/search_form.html:53
+msgid "Series"
+msgstr "Seria"
+
+#: cps/templates/book_edit.html:79
+msgid "Series id"
+msgstr "ID serii"
+
+#: cps/templates/book_edit.html:83
+msgid "Rating"
+msgstr "Ocena"
+
+#: cps/templates/book_edit.html:87
+msgid "Cover URL (jpg, cover is downloaded and stored in database, field is afterwards empty again)"
+msgstr ""
+
+#: cps/templates/book_edit.html:91
+msgid "Upload Cover from local drive"
+msgstr ""
+
+#: cps/templates/book_edit.html:96 cps/templates/detail.html:135
+msgid "Publishing date"
+msgstr "Data publikacji"
+
+#: cps/templates/book_edit.html:103 cps/templates/book_edit.html:261
+#: cps/templates/book_edit.html:278 cps/templates/detail.html:127
+#: cps/templates/search_form.html:14
+msgid "Publisher"
+msgstr "Wydawca"
+
+#: cps/templates/book_edit.html:107 cps/templates/user_edit.html:31
+msgid "Language"
+msgstr "Język"
+
+#: cps/templates/book_edit.html:117 cps/templates/search_form.html:117
+msgid "Yes"
+msgstr "Tak"
+
+#: cps/templates/book_edit.html:118 cps/templates/search_form.html:118
+msgid "No"
+msgstr "Nie"
+
+#: cps/templates/book_edit.html:164
+msgid "Upload format"
+msgstr ""
+
+#: cps/templates/book_edit.html:173
+msgid "view book after edit"
+msgstr "wyświetl książkę po edycji"
+
+#: cps/templates/book_edit.html:176 cps/templates/book_edit.html:212
+msgid "Get metadata"
+msgstr "Uzyskaj metadane"
+
+#: cps/templates/book_edit.html:177 cps/templates/config_edit.html:210
+#: cps/templates/config_view_edit.html:167 cps/templates/login.html:20
+#: cps/templates/search_form.html:153 cps/templates/shelf_edit.html:17
+#: cps/templates/user_edit.html:153
+msgid "Submit"
+msgstr "Wyślij"
+
+#: cps/templates/book_edit.html:191
+msgid "Are you really sure?"
+msgstr ""
+
+#: cps/templates/book_edit.html:194
+msgid "Book will be deleted from Calibre database"
+msgstr ""
+
+#: cps/templates/book_edit.html:195
+msgid "and from hard disk"
+msgstr ""
+
+#: cps/templates/book_edit.html:215
+msgid "Keyword"
+msgstr "Słowo kluczowe"
+
+#: cps/templates/book_edit.html:216
+msgid " Search keyword "
+msgstr " Szukaj słowa kluczowego "
+
+#: cps/templates/book_edit.html:218 cps/templates/layout.html:46
+msgid "Go!"
+msgstr "Idź!"
+
+#: cps/templates/book_edit.html:222
+msgid "Click the cover to load metadata to the form"
+msgstr "Kliknij okładkę, aby załadować metadane do formularza"
+
+#: cps/templates/book_edit.html:234 cps/templates/book_edit.html:274
+msgid "Loading..."
+msgstr "Ładowanie..."
+
+#: cps/templates/book_edit.html:239 cps/templates/layout.html:224
+msgid "Close"
+msgstr "Zamknij"
+
+#: cps/templates/book_edit.html:266 cps/templates/book_edit.html:280
+msgid "Source"
+msgstr "Źródło"
+
+#: cps/templates/book_edit.html:275
+msgid "Search error!"
+msgstr "Błąd wyszukiwania!"
+
+#: cps/templates/book_edit.html:276
+msgid "No Result(s) found! Please try aonther keyword."
+msgstr ""
+
+#: cps/templates/config_edit.html:12
+msgid "Library Configuration"
+msgstr ""
+
+#: cps/templates/config_edit.html:19
+msgid "Location of Calibre database"
+msgstr "Lokalizacja bazy danych Calibre"
+
+#: cps/templates/config_edit.html:24
+msgid "Use Google Drive?"
+msgstr "Użyć dysku Google?"
+
+#: cps/templates/config_edit.html:30
+msgid "Google Drive config problem"
+msgstr ""
+
+#: cps/templates/config_edit.html:36
+msgid "Authenticate Google Drive"
+msgstr ""
+
+#: cps/templates/config_edit.html:40
+msgid "Please finish Google Drive setup after login"
+msgstr ""
+
+#: cps/templates/config_edit.html:44
+msgid "Google Drive Calibre folder"
+msgstr ""
+
+#: cps/templates/config_edit.html:52
+#, fuzzy
+msgid "Metadata Watch Channel ID"
+msgstr "Metadane Watch Channel ID"
+
+#: cps/templates/config_edit.html:55
+msgid "Revoke"
+msgstr ""
+
+#: cps/templates/config_edit.html:73
+msgid "Server Configuration"
+msgstr ""
+
+#: cps/templates/config_edit.html:80
+msgid "Server Port"
+msgstr "Port serwera"
+
+#: cps/templates/config_edit.html:84
+msgid "SSL certfile location (leave it empty for non-SSL Servers)"
+msgstr ""
+
+#: cps/templates/config_edit.html:88
+msgid "SSL Keyfile location (leave it empty for non-SSL Servers)"
+msgstr ""
+
+#: cps/templates/config_edit.html:99
+msgid "Logfile Configuration"
+msgstr ""
+
+#: cps/templates/config_edit.html:106
+msgid "Log Level"
+msgstr "Poziom logów"
+
+#: cps/templates/config_edit.html:115
+msgid "Location and name of logfile (calibre-web.log for no entry)"
+msgstr ""
+
+#: cps/templates/config_edit.html:126
+msgid "Feature Configuration"
+msgstr ""
+
+#: cps/templates/config_edit.html:134
+msgid "Enable uploading"
+msgstr "Włącz wysyłanie"
+
+#: cps/templates/config_edit.html:138
+msgid "Enable anonymous browsing"
+msgstr "Włącz anonimowe przeglądanie"
+
+#: cps/templates/config_edit.html:142
+msgid "Enable public registration"
+msgstr "Włącz publiczną rejestrację"
+
+#: cps/templates/config_edit.html:146
+msgid "Enable remote login (\"magic link\")"
+msgstr ""
+
+#: cps/templates/config_edit.html:151
+msgid "Use"
+msgstr ""
+
+#: cps/templates/config_edit.html:152
+msgid "Obtain an API Key"
+msgstr ""
+
+#: cps/templates/config_edit.html:156
+msgid "Goodreads API Key"
+msgstr ""
+
+#: cps/templates/config_edit.html:160
+msgid "Goodreads API Secret"
+msgstr ""
+
+#: cps/templates/config_edit.html:173
+msgid "External binaries"
+msgstr ""
+
+#: cps/templates/config_edit.html:181
+msgid "No converter"
+msgstr ""
+
+#: cps/templates/config_edit.html:183
+msgid "Use Kindlegen"
+msgstr ""
+
+#: cps/templates/config_edit.html:185
+msgid "Use calibre's ebook converter"
+msgstr ""
+
+#: cps/templates/config_edit.html:189
+msgid "E-Book converter settings"
+msgstr ""
+
+#: cps/templates/config_edit.html:193
+msgid "Path to convertertool"
+msgstr ""
+
+#: cps/templates/config_edit.html:199
+msgid "Location of Unrar binary"
+msgstr ""
+
+#: cps/templates/config_edit.html:215 cps/templates/layout.html:82
+#: cps/templates/login.html:4
+msgid "Login"
+msgstr "Zaloguj się"
+
+#: cps/templates/config_view_edit.html:12
+msgid "View Configuration"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:19 cps/templates/layout.html:133
+#: cps/templates/layout.html:134 cps/templates/shelf_edit.html:7
+msgid "Title"
+msgstr "Tytuł"
+
+#: cps/templates/config_view_edit.html:27
+msgid "No. of random books to show"
+msgstr "Liczba losowych książek do pokazania"
+
+#: cps/templates/config_view_edit.html:31
+msgid "Regular expression for ignoring columns"
+msgstr "Wyrażenie regularne dla ignorowanych kolumn"
+
+#: cps/templates/config_view_edit.html:35
+msgid "Link read/unread status to Calibre column"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:44
+msgid "Regular expression for title sorting"
+msgstr "Wyrażenie regularne dla tytułu sortującego"
+
+#: cps/templates/config_view_edit.html:48
+msgid "Tags for Mature Content"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:62
+msgid "Default settings for new users"
+msgstr "Domyślne ustawienia dla nowych użytkowników"
+
+#: cps/templates/config_view_edit.html:70 cps/templates/user_edit.html:110
+msgid "Admin user"
+msgstr "Użytkownik z uprawnieniami administratora"
+
+#: cps/templates/config_view_edit.html:74 cps/templates/user_edit.html:119
+msgid "Allow Downloads"
+msgstr "Zezwalaj na pobieranie"
+
+#: cps/templates/config_view_edit.html:78 cps/templates/user_edit.html:123
+msgid "Allow Uploads"
+msgstr "Zezwalaj na wysyłanie"
+
+#: cps/templates/config_view_edit.html:82 cps/templates/user_edit.html:127
+msgid "Allow Edit"
+msgstr "Zezwalaj na edycję"
+
+#: cps/templates/config_view_edit.html:86 cps/templates/user_edit.html:131
+msgid "Allow Delete books"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:90 cps/templates/user_edit.html:136
+msgid "Allow Changing Password"
+msgstr "Zezwalaj na zmianę hasła"
+
+#: cps/templates/config_view_edit.html:94 cps/templates/user_edit.html:140
+msgid "Allow Editing Public Shelfs"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:104
+msgid "Default visibilities for new users"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:112 cps/templates/user_edit.html:58
+msgid "Show random books"
+msgstr "Pokaż losowe książki"
+
+#: cps/templates/config_view_edit.html:116 cps/templates/user_edit.html:62
+msgid "Show recent books"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:120 cps/templates/user_edit.html:66
+msgid "Show sorted books"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:124 cps/templates/user_edit.html:70
+msgid "Show hot books"
+msgstr "Pokaż najpopularniejsze książki"
+
+#: cps/templates/config_view_edit.html:128 cps/templates/user_edit.html:74
+msgid "Show best rated books"
+msgstr "Pokaż najlepiej ocenione książki"
+
+#: cps/templates/config_view_edit.html:132 cps/templates/user_edit.html:78
+msgid "Show language selection"
+msgstr "Pokaż wybór języka"
+
+#: cps/templates/config_view_edit.html:136 cps/templates/user_edit.html:82
+msgid "Show series selection"
+msgstr "Pokaż wybór serii"
+
+#: cps/templates/config_view_edit.html:140 cps/templates/user_edit.html:86
+msgid "Show category selection"
+msgstr "Pokaż wybór kategorii"
+
+#: cps/templates/config_view_edit.html:144 cps/templates/user_edit.html:90
+msgid "Show author selection"
+msgstr "Pokaż wybór autora"
+
+#: cps/templates/config_view_edit.html:148 cps/templates/user_edit.html:94
+msgid "Show publisher selection"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:152 cps/templates/user_edit.html:98
+msgid "Show read and unread"
+msgstr "Pokaż przeczytane i nieprzeczytane"
+
+#: cps/templates/config_view_edit.html:156 cps/templates/user_edit.html:102
+msgid "Show random books in detail view"
+msgstr "Pokaz losowe książki w widoku szczegółowym"
+
+#: cps/templates/config_view_edit.html:160 cps/templates/user_edit.html:115
+msgid "Show mature content"
+msgstr ""
+
+#: cps/templates/detail.html:49
+msgid "Read in browser"
+msgstr "Czytaj w przeglądarce"
+
+#: cps/templates/detail.html:88
+msgid "Book"
+msgstr "Książka"
+
+#: cps/templates/detail.html:88
+msgid "of"
+msgstr "z"
+
+#: cps/templates/detail.html:94
+msgid "language"
+msgstr "język"
+
+#: cps/templates/detail.html:172
+msgid "Read"
+msgstr "Czytaj"
+
+#: cps/templates/detail.html:182
+msgid "Description:"
+msgstr "Opis:"
+
+#: cps/templates/detail.html:195 cps/templates/search.html:14
+msgid "Add to shelf"
+msgstr "Dodaj do półki"
+
+#: cps/templates/detail.html:257
+msgid "Edit metadata"
+msgstr "Edytuj metadane"
+
+#: cps/templates/email_edit.html:15
+msgid "SMTP port (usually 25 for plain SMTP and 465 for SSL and 587 for STARTTLS)"
+msgstr "Port serwera SMTP (używane 25 dla jawnego SMTP i 465 dla połączenia SSL i 587 dla połączenia STARTTLS)"
+
+#: cps/templates/email_edit.html:19
+msgid "Encryption"
+msgstr "Szyfrowanie"
+
+#: cps/templates/email_edit.html:21
+msgid "None"
+msgstr "Nic"
+
+#: cps/templates/email_edit.html:22
+msgid "STARTTLS"
+msgstr "STARTTLS"
+
+#: cps/templates/email_edit.html:23
+msgid "SSL/TLS"
+msgstr "SSL/TLS"
+
+#: cps/templates/email_edit.html:31
+msgid "SMTP password"
+msgstr "Hasło SMTP"
+
+#: cps/templates/email_edit.html:35
+msgid "From e-mail"
+msgstr "Z adresu e-mail"
+
+#: cps/templates/email_edit.html:38
+msgid "Save settings"
+msgstr "Zapisz ustawienia"
+
+#: cps/templates/email_edit.html:39
+msgid "Save settings and send Test E-Mail"
+msgstr "Zapisz ustawienia i wyślij testową wiadomość e-mail"
+
+#: cps/templates/email_edit.html:43
+msgid "Allowed domains for registering"
+msgstr ""
+
+#: cps/templates/email_edit.html:47
+msgid "Enter domainname"
+msgstr ""
+
+#: cps/templates/email_edit.html:55
+msgid "Add Domain"
+msgstr ""
+
+#: cps/templates/email_edit.html:58
+msgid "Add"
+msgstr ""
+
+#: cps/templates/email_edit.html:72
+msgid "Do you really want to delete this domain rule?"
+msgstr ""
+
+#: cps/templates/feed.xml:21 cps/templates/layout.html:208
+msgid "Next"
+msgstr "Następne"
+
+#: cps/templates/feed.xml:33 cps/templates/index.xml:11
+#: cps/templates/layout.html:43 cps/templates/layout.html:44
+msgid "Search"
+msgstr "Szukaj"
+
+#: cps/templates/index.html:5
+msgid "Discover (Random Books)"
+msgstr "Odkrywaj (losowe książki)"
+
+#: cps/templates/index.xml:6
+msgid "Start"
+msgstr "Rozpocznij"
+
+#: cps/templates/index.xml:18 cps/templates/layout.html:139
+msgid "Hot Books"
+msgstr "Najpopularniejsze książki"
+
+#: cps/templates/index.xml:22
+msgid "Popular publications from this catalog based on Downloads."
+msgstr "Popularne publikacje z tego katalogu bazujące na pobranych."
+
+#: cps/templates/index.xml:25 cps/templates/layout.html:142
+msgid "Best rated Books"
+msgstr "Najlepiej ocenione książki"
+
+#: cps/templates/index.xml:29
+msgid "Popular publications from this catalog based on Rating."
+msgstr "Popularne publikacje z tego katalogu bazujące na ocenach."
+
+#: cps/templates/index.xml:32
+msgid "New Books"
+msgstr "Nowe książki"
+
+#: cps/templates/index.xml:36
+msgid "The latest Books"
+msgstr "Ostatnie książki"
+
+#: cps/templates/index.xml:43
+msgid "Show Random Books"
+msgstr "Pokazuj losowe książki"
+
+#: cps/templates/index.xml:62 cps/templates/layout.html:160
+msgid "Authors"
+msgstr "Autorzy"
+
+#: cps/templates/index.xml:66
+msgid "Books ordered by Author"
+msgstr "Książki sortowane według autorów"
+
+#: cps/templates/index.xml:69 cps/templates/layout.html:163
+msgid "Publishers"
+msgstr ""
+
+#: cps/templates/index.xml:73
+msgid "Books ordered by publisher"
+msgstr ""
+
+#: cps/templates/index.xml:80
+msgid "Books ordered by category"
+msgstr "Książki sortowane według kategorii"
+
+#: cps/templates/index.xml:87
+msgid "Books ordered by series"
+msgstr "Książki sortowane według serii"
+
+#: cps/templates/index.xml:90 cps/templates/layout.html:169
+msgid "Public Shelves"
+msgstr "Publiczne półki"
+
+#: cps/templates/index.xml:94
+msgid "Books organized in public shelfs, visible to everyone"
+msgstr ""
+
+#: cps/templates/index.xml:98 cps/templates/layout.html:173
+msgid "Your Shelves"
+msgstr "Twoje półki"
+
+#: cps/templates/index.xml:102
+msgid "User's own shelfs, only visible to the current user himself"
+msgstr ""
+
+#: cps/templates/layout.html:33
+msgid "Toggle navigation"
+msgstr "Przełącz nawigację"
+
+#: cps/templates/layout.html:54
+msgid "Advanced Search"
+msgstr "Zaawansowane wyszukiwanie"
+
+#: cps/templates/layout.html:78
+msgid "Logout"
+msgstr "Wyloguj się"
+
+#: cps/templates/layout.html:83 cps/templates/register.html:14
+msgid "Register"
+msgstr "Zarejestruj się"
+
+#: cps/templates/layout.html:108
+msgid "Uploading..."
+msgstr ""
+
+#: cps/templates/layout.html:109
+msgid "please don't refresh the page"
+msgstr ""
+
+#: cps/templates/layout.html:120
+msgid "Browse"
+msgstr "Przeglądaj"
+
+#: cps/templates/layout.html:122
+msgid "Recently Added"
+msgstr ""
+
+#: cps/templates/layout.html:127
+msgid "Sorted Books"
+msgstr ""
+
+#: cps/templates/layout.html:131 cps/templates/layout.html:132
+#: cps/templates/layout.html:133 cps/templates/layout.html:134
+msgid "Sort By"
+msgstr ""
+
+#: cps/templates/layout.html:131
+msgid "Newest"
+msgstr ""
+
+#: cps/templates/layout.html:132
+msgid "Oldest"
+msgstr ""
+
+#: cps/templates/layout.html:133
+msgid "Ascending"
+msgstr ""
+
+#: cps/templates/layout.html:134
+msgid "Descending"
+msgstr ""
+
+#: cps/templates/layout.html:151
+msgid "Discover"
+msgstr "Odkrywaj"
+
+#: cps/templates/layout.html:154
+msgid "Categories"
+msgstr "Kategorie"
+
+#: cps/templates/layout.html:166 cps/templates/search_form.html:74
+msgid "Languages"
+msgstr "Języki"
+
+#: cps/templates/layout.html:178
+msgid "Create a Shelf"
+msgstr "Utwórz półkę"
+
+#: cps/templates/layout.html:179 cps/templates/stats.html:3
+msgid "About"
+msgstr "O programie"
+
+#: cps/templates/layout.html:193
+msgid "Previous"
+msgstr ""
+
+#: cps/templates/layout.html:220
+msgid "Book Details"
+msgstr ""
+
+#: cps/templates/login.html:8 cps/templates/login.html:9
+#: cps/templates/register.html:7 cps/templates/user_edit.html:8
+msgid "Username"
+msgstr "Nazwa użytkownika"
+
+#: cps/templates/login.html:12 cps/templates/login.html:13
+#: cps/templates/user_edit.html:21
+msgid "Password"
+msgstr "Hasło"
+
+#: cps/templates/login.html:17
+msgid "Remember me"
+msgstr "Zapamiętaj mnie"
+
+#: cps/templates/login.html:22
+msgid "Log in with magic link"
+msgstr ""
+
+#: cps/templates/osd.xml:5
+msgid "Calibre-Web ebook catalog"
+msgstr ""
+
+#: cps/templates/read.html:69 cps/templates/readcbr.html:79
+#: cps/templates/readcbr.html:103
+msgid "Settings"
+msgstr ""
+
+#: cps/templates/read.html:72
+#, fuzzy
+msgid "Reflow text when sidebars are open."
+msgstr "Tekst pływający, gdy paski boczne są otwarte."
+
+#: cps/templates/readcbr.html:84
+msgid "Keyboard Shortcuts"
+msgstr ""
+
+#: cps/templates/readcbr.html:87
+msgid "Previous Page"
+msgstr ""
+
+#: cps/templates/readcbr.html:88
+msgid "Next Page"
+msgstr ""
+
+#: cps/templates/readcbr.html:89
+msgid "Scale to Best"
+msgstr ""
+
+#: cps/templates/readcbr.html:90
+msgid "Scale to Width"
+msgstr ""
+
+#: cps/templates/readcbr.html:91
+msgid "Scale to Height"
+msgstr ""
+
+#: cps/templates/readcbr.html:92
+msgid "Scale to Native"
+msgstr ""
+
+#: cps/templates/readcbr.html:93
+msgid "Rotate Right"
+msgstr ""
+
+#: cps/templates/readcbr.html:94
+msgid "Rotate Left"
+msgstr ""
+
+#: cps/templates/readcbr.html:95
+msgid "Flip Image"
+msgstr ""
+
+#: cps/templates/readcbr.html:108 cps/templates/user_edit.html:39
+msgid "Theme"
+msgstr ""
+
+#: cps/templates/readcbr.html:111
+msgid "Light"
+msgstr ""
+
+#: cps/templates/readcbr.html:112
+msgid "Dark"
+msgstr ""
+
+#: cps/templates/readcbr.html:117
+msgid "Scale"
+msgstr ""
+
+#: cps/templates/readcbr.html:120
+msgid "Best"
+msgstr ""
+
+#: cps/templates/readcbr.html:121
+msgid "Width"
+msgstr ""
+
+#: cps/templates/readcbr.html:122
+msgid "Height"
+msgstr ""
+
+#: cps/templates/readcbr.html:123
+msgid "Native"
+msgstr ""
+
+#: cps/templates/readcbr.html:128
+msgid "Rotate"
+msgstr ""
+
+#: cps/templates/readcbr.html:139
+msgid "Flip"
+msgstr ""
+
+#: cps/templates/readcbr.html:142
+msgid "Horizontal"
+msgstr ""
+
+#: cps/templates/readcbr.html:143
+msgid "Vertical"
+msgstr ""
+
+#: cps/templates/readpdf.html:29
+msgid "PDF.js viewer"
+msgstr "PDF.js viewer"
+
+#: cps/templates/readtxt.html:6
+msgid "Basic txt Reader"
+msgstr "Podstawowy czytnik txt"
+
+#: cps/templates/register.html:4
+msgid "Register a new account"
+msgstr "Zarejestruj nowe konto"
+
+#: cps/templates/register.html:8
+msgid "Choose a username"
+msgstr "Wybierz nazwę użytkownika"
+
+#: cps/templates/register.html:11 cps/templates/user_edit.html:13
+msgid "E-mail address"
+msgstr ""
+
+#: cps/templates/register.html:12
+msgid "Your email address"
+msgstr "Twój adres e-mail"
+
+#: cps/templates/remote_login.html:6
+msgid "Using your another device, visit"
+msgstr ""
+
+#: cps/templates/remote_login.html:6
+msgid "and log in"
+msgstr ""
+
+#: cps/templates/remote_login.html:9
+msgid "Once you do so, you will automatically get logged in on this device."
+msgstr ""
+
+#: cps/templates/search.html:5
+msgid "No Results for:"
+msgstr "Brak wyników dla:"
+
+#: cps/templates/search.html:6
+msgid "Please try a different search"
+msgstr "Proszę wypróbować podobne wyszukiwanie"
+
+#: cps/templates/search.html:8
+msgid "Results for:"
+msgstr "Wyniki dla:"
+
+#: cps/templates/search_form.html:19
+msgid "Publishing date from"
+msgstr ""
+
+#: cps/templates/search_form.html:26
+msgid "Publishing date to"
+msgstr ""
+
+#: cps/templates/search_form.html:43
+msgid "Exclude Tags"
+msgstr "Wyklucz tagi"
+
+#: cps/templates/search_form.html:63
+msgid "Exclude Series"
+msgstr "Wyklucz serie"
+
+#: cps/templates/search_form.html:84
+msgid "Exclude Languages"
+msgstr "Wyklucz języki"
+
+#: cps/templates/search_form.html:97
+msgid "Rating bigger than"
+msgstr ""
+
+#: cps/templates/search_form.html:101
+msgid "Rating less than"
+msgstr ""
+
+#: cps/templates/shelf.html:7
+msgid "Delete this Shelf"
+msgstr "Usuń tą półkę"
+
+#: cps/templates/shelf.html:8
+msgid "Edit Shelf"
+msgstr ""
+
+#: cps/templates/shelf.html:9 cps/templates/shelf_order.html:11
+msgid "Change order"
+msgstr "Zmień sortowanie"
+
+#: cps/templates/shelf.html:56
+msgid "Do you really want to delete the shelf?"
+msgstr ""
+
+#: cps/templates/shelf.html:59
+msgid "Shelf will be lost for everybody and forever!"
+msgstr ""
+
+#: cps/templates/shelf_edit.html:13
+msgid "should the shelf be public?"
+msgstr "półka powinna być publiczna?"
+
+#: cps/templates/shelf_order.html:5
+msgid "Drag 'n drop to rearrange order"
+msgstr "Przeciągnij i upuść, aby zmienić kolejność"
+
+#: cps/templates/stats.html:7
+msgid "Calibre library statistics"
+msgstr "Statystyki biblioteki Calibre"
+
+#: cps/templates/stats.html:12
+msgid "Books in this Library"
+msgstr "Książki"
+
+#: cps/templates/stats.html:16
+msgid "Authors in this Library"
+msgstr "Autorzy"
+
+#: cps/templates/stats.html:20
+msgid "Categories in this Library"
+msgstr "Kategorie"
+
+#: cps/templates/stats.html:24
+msgid "Series in this Library"
+msgstr "Serie"
+
+#: cps/templates/stats.html:28
+msgid "Linked libraries"
+msgstr "Załączone biblioteki"
+
+#: cps/templates/stats.html:32
+msgid "Program library"
+msgstr "Biblioteka programu"
+
+#: cps/templates/stats.html:33
+msgid "Installed Version"
+msgstr "Zainstalowana wersja"
+
+#: cps/templates/tasks.html:7
+msgid "Tasks list"
+msgstr ""
+
+#: cps/templates/tasks.html:12
+msgid "User"
+msgstr ""
+
+#: cps/templates/tasks.html:14
+msgid "Task"
+msgstr ""
+
+#: cps/templates/tasks.html:15
+msgid "Status"
+msgstr ""
+
+#: cps/templates/tasks.html:16
+msgid "Progress"
+msgstr ""
+
+#: cps/templates/tasks.html:17
+msgid "Runtime"
+msgstr ""
+
+#: cps/templates/tasks.html:18
+msgid "Starttime"
+msgstr ""
+
+#: cps/templates/tasks.html:24
+msgid "Delete finished tasks"
+msgstr ""
+
+#: cps/templates/tasks.html:25
+msgid "Hide all tasks"
+msgstr ""
+
+#: cps/templates/user_edit.html:18
+msgid "Reset user Password"
+msgstr ""
+
+#: cps/templates/user_edit.html:27
+msgid "Kindle E-Mail"
+msgstr "Adres e-mail Kindle"
+
+#: cps/templates/user_edit.html:41
+msgid "Standard Theme"
+msgstr ""
+
+#: cps/templates/user_edit.html:42
+msgid "caliBlur! Dark Theme (Beta)"
+msgstr ""
+
+#: cps/templates/user_edit.html:47
+msgid "Show books with language"
+msgstr "Pokaż książki w języku"
+
+#: cps/templates/user_edit.html:49
+msgid "Show all"
+msgstr "Pokaż wszystko"
+
+#: cps/templates/user_edit.html:147
+msgid "Delete this user"
+msgstr "Usuń tego użytkownika"
+
+#: cps/templates/user_edit.html:162
+msgid "Recent Downloads"
+msgstr "Ostatnio pobierane"
+
+#~ msgid "Convert: %s"
+#~ msgstr ""
+
+#~ msgid "%s: %s"
+#~ msgstr ""
+
+#~ msgid "E-Mail: %(book)s"
+#~ msgstr ""
+
+#~ msgid "E-mail: %stitle"
+#~ msgstr ""
+
+#~ msgid "Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+#~ msgstr ""
+
+#~ msgid "Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+#~ msgstr ""
+
+#~ msgid "Password for user %(user)s reset"
+#~ msgstr ""
+
+#~ msgid "Password for user %s reset"
+#~ msgstr ""
+
+#~ msgid "Rename title from: '%(src)s' to '%(src)s' failed with error: %(error)s"
+#~ msgstr ""
+
+#~ msgid "Rename author from: '%(src)s' to '%(src)s' failed with error: %(error)s"
+#~ msgstr ""
+
+#~ msgid "Failed to create path for cover %(cover)s (Permission denied)."
+#~ msgstr ""
+
+#~ msgid "File extension '%s' is not allowed to be uploaded to this server"
+#~ msgstr ""
+
+#~ msgid "File extension \"%(ext)s\" is not allowed to be uploaded to this server"
+#~ msgstr "Rozszerzenie pliku \"%(ext)s\" nie jest dozwolone do przesłania na ten serwer"
+
+#~ msgid "Current commit timestamp"
+#~ msgstr "Znacznik czasowy zainstalowanej wersji"
+
+#~ msgid "Newest commit timestamp"
+#~ msgstr "Znacznik czasowy nowej wersji"
+
+#~ msgid "Convert: %(book)s"
+#~ msgstr ""
+
+#~ msgid "Convert to %(format)s: %(book)s"
+#~ msgstr ""
+
+#~ msgid "Files are replaced"
+#~ msgstr "Pliki zostały zastąpione"
+
+#~ msgid "Server is stopped"
+#~ msgstr "Serwer jest zatrzymany"
+
+#~ msgid "Convertertool %(converter)s not found"
+#~ msgstr ""
+
+#~ msgid "Choose a password"
+#~ msgstr "Wybierz hasło"
+
diff --git a/src/cps/translations/ru/LC_MESSAGES/messages.mo b/src/cps/translations/ru/LC_MESSAGES/messages.mo
new file mode 100644
index 0000000..7b01b1f
Binary files /dev/null and b/src/cps/translations/ru/LC_MESSAGES/messages.mo differ
diff --git a/src/cps/translations/ru/LC_MESSAGES/messages.po b/src/cps/translations/ru/LC_MESSAGES/messages.po
new file mode 100644
index 0000000..5657f41
--- /dev/null
+++ b/src/cps/translations/ru/LC_MESSAGES/messages.po
@@ -0,0 +1,1947 @@
+# Перевод на русский язык для Calibre-Web.
+# Copyright (C) 2017 Pavel Korovin
+# This file is distributed under the same license as the Calibre-Web project
+# Pavel Korovin , 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Calibre-Web\n"
+"Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n"
+"POT-Creation-Date: 2018-11-03 14:03+0100\n"
+"PO-Revision-Date: 2017-04-30 00:47+0300\n"
+"Last-Translator: Pavel Korovin
\n"
+"Language: ru\n"
+"Language-Team: \n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.6.0\n"
+
+#: cps/book_formats.py:129 cps/book_formats.py:130 cps/book_formats.py:134
+#: cps/book_formats.py:138 cps/converter.py:11 cps/converter.py:27
+msgid "not installed"
+msgstr "не установлено"
+
+#: cps/converter.py:22 cps/converter.py:38
+msgid "Excecution permissions missing"
+msgstr "Отсутствуют разрешения на выполнение"
+
+#: cps/converter.py:48
+msgid "not configured"
+msgstr ""
+
+#: cps/helper.py:58
+#, python-format
+msgid "%(format)s format not found for book id: %(book)d"
+msgstr "%(format)s форма не найден для книги с id: %(book)d"
+
+#: cps/helper.py:70
+#, python-format
+msgid "%(format)s not found on Google Drive: %(fn)s"
+msgstr "%(format)s не найден на Google Drive: %(fn)s"
+
+#: cps/helper.py:77 cps/helper.py:147 cps/templates/detail.html:44
+msgid "Send to Kindle"
+msgstr "Отправить на Kindle"
+
+#: cps/helper.py:78 cps/helper.py:96
+msgid "This e-mail has been sent via Calibre-Web."
+msgstr "Это электронное письмо было отправлено через Caliber-Web."
+
+#: cps/helper.py:89
+#, python-format
+msgid "%(format)s not found: %(fn)s"
+msgstr "%(format)s не найден: %(fn)s"
+
+#: cps/helper.py:94
+msgid "Calibre-Web test e-mail"
+msgstr "Тестовый e-mail для Calibre-Web"
+
+#: cps/helper.py:95
+msgid "Test e-mail"
+msgstr "Тестовый e-mail"
+
+#: cps/helper.py:111
+msgid "Get Started with Calibre-Web"
+msgstr "Начать работать с Calibre-Web"
+
+#: cps/helper.py:112
+#, python-format
+msgid "Registration e-mail for user: %(name)s"
+msgstr "Регистрационный e-mail для пользователя: %(name)s"
+
+#: cps/helper.py:135 cps/helper.py:145
+msgid "Could not find any formats suitable for sending by e-mail"
+msgstr "Не удалось найти форматы, которые подходят для отправки по e-mail"
+
+#: cps/helper.py:148
+#, python-format
+msgid "E-mail: %(book)s"
+msgstr "Эл. почта: %(book)s"
+
+#: cps/helper.py:150
+msgid "The requested file could not be read. Maybe wrong permissions?"
+msgstr "Запрашиваемый файл не может быть прочитан. Возможно не верные разрешения?"
+
+#: cps/helper.py:250
+#, python-format
+msgid "Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgstr "Переименовывание заголовка с: '%(src)s' на '%(dest)s' не удалось из-за ошибки: %(error)s"
+
+#: cps/helper.py:259
+#, python-format
+msgid "Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgstr "Переименовывание автора с: '%(src)s' на '%(dest)s' не удалось из-за ошибки: %(error)s"
+
+#: cps/helper.py:281 cps/helper.py:290
+#, python-format
+msgid "File %(file)s not found on Google Drive"
+msgstr "Файл %(file)s не найден на Google Drive"
+
+#: cps/helper.py:308
+#, python-format
+msgid "Book path %(path)s not found on Google Drive"
+msgstr "Путь книги %(path)s не найден на Google Drive"
+
+#: cps/helper.py:565
+msgid "Error excecuting UnRar"
+msgstr "Ошибка извлечения UnRar"
+
+#: cps/helper.py:567
+msgid "Unrar binary file not found"
+msgstr "Unrar двочиный файл не найден"
+
+#: cps/helper.py:609
+msgid "Waiting"
+msgstr "Ожидание"
+
+#: cps/helper.py:611
+msgid "Failed"
+msgstr "Неудачно"
+
+#: cps/helper.py:613
+msgid "Started"
+msgstr "Начало"
+
+#: cps/helper.py:615
+msgid "Finished"
+msgstr "Закончено"
+
+#: cps/helper.py:617
+msgid "Unknown Status"
+msgstr ""
+
+#: cps/helper.py:622
+msgid "E-mail: "
+msgstr ""
+
+#: cps/helper.py:624 cps/helper.py:628
+msgid "Convert: "
+msgstr ""
+
+#: cps/helper.py:626
+msgid "Upload: "
+msgstr ""
+
+#: cps/helper.py:630
+msgid "Unknown Task: "
+msgstr ""
+
+#: cps/web.py:1132 cps/web.py:2842
+msgid "Unknown"
+msgstr "Неизвестно"
+
+#: cps/web.py:1141 cps/web.py:1172 cps/web.py:1257
+msgid "HTTP Error"
+msgstr "Ошибка HTTP"
+
+#: cps/web.py:1143 cps/web.py:1174 cps/web.py:1258
+msgid "Connection error"
+msgstr "Ошибка соединения"
+
+#: cps/web.py:1145 cps/web.py:1176 cps/web.py:1259
+msgid "Timeout while establishing connection"
+msgstr "Таймаут при установлении соединения"
+
+#: cps/web.py:1147 cps/web.py:1178 cps/web.py:1260
+msgid "General error"
+msgstr "Общая ошибка"
+
+#: cps/web.py:1153
+msgid "Unexpected data while reading update information"
+msgstr "Некорректные данные при чтении информации об обновлении"
+
+#: cps/web.py:1160
+msgid "No update available. You already have the latest version installed"
+msgstr "Обновление недоступно. Вы используете самую последнюю версию"
+
+#: cps/web.py:1185
+msgid "A new update is available. Click on the button below to update to the latest version."
+msgstr "Доступно обновление. Нажмите на кнопку, что бы обновиться до последней версии."
+
+#: cps/web.py:1235
+msgid "Could not fetch update information"
+msgstr "Не удалось получить информацию об обновлении"
+
+#: cps/web.py:1250
+msgid "Requesting update package"
+msgstr "Проверка обновлений"
+
+#: cps/web.py:1251
+msgid "Downloading update package"
+msgstr "Загрузка обновлений"
+
+#: cps/web.py:1252
+msgid "Unzipping update package"
+msgstr "Распаковка обновлений"
+
+#: cps/web.py:1253
+msgid "Replacing files"
+msgstr ""
+
+#: cps/web.py:1254
+msgid "Database connections are closed"
+msgstr "Соеднинения с базой данных закрыты"
+
+#: cps/web.py:1255
+msgid "Stopping server"
+msgstr ""
+
+#: cps/web.py:1256
+msgid "Update finished, please press okay and reload page"
+msgstr "Обновления установлены, нажмите okay и перезагрузите страницу"
+
+#: cps/web.py:1257 cps/web.py:1258 cps/web.py:1259 cps/web.py:1260
+msgid "Update failed:"
+msgstr ""
+
+#: cps/web.py:1283
+msgid "Recently Added Books"
+msgstr "Недавно Добавленные Книги"
+
+#: cps/web.py:1293
+msgid "Newest Books"
+msgstr "Новые Книги"
+
+#: cps/web.py:1305
+msgid "Oldest Books"
+msgstr "Старые Книги"
+
+#: cps/web.py:1317
+msgid "Books (A-Z)"
+msgstr "Книги (А-Я)"
+
+#: cps/web.py:1328
+msgid "Books (Z-A)"
+msgstr "Книги (Я-А)"
+
+#: cps/web.py:1357
+msgid "Hot Books (most downloaded)"
+msgstr "Популярные книги (часто загружаемые)"
+
+#: cps/web.py:1370
+msgid "Best rated books"
+msgstr "Книги с наивысшим рейтингом"
+
+#: cps/templates/index.xml:39 cps/web.py:1383
+msgid "Random Books"
+msgstr "Случайный выбор"
+
+#: cps/web.py:1398
+msgid "Author list"
+msgstr "Авторы"
+
+#: cps/web.py:1410 cps/web.py:1501 cps/web.py:1663 cps/web.py:2206
+msgid "Error opening eBook. File does not exist or file is not accessible:"
+msgstr "Невозможно открыть книгу. Файл не существует или недоступен."
+
+#: cps/web.py:1438
+msgid "Publisher list"
+msgstr ""
+
+#: cps/web.py:1452
+#, python-format
+msgid "Publisher: %(name)s"
+msgstr ""
+
+#: cps/templates/index.xml:83 cps/web.py:1484
+msgid "Series list"
+msgstr "Серии"
+
+#: cps/web.py:1499
+#, python-format
+msgid "Series: %(serie)s"
+msgstr "Серии: %(serie)s"
+
+#: cps/web.py:1528
+msgid "Available languages"
+msgstr "Доступные языки"
+
+#: cps/web.py:1548
+#, python-format
+msgid "Language: %(name)s"
+msgstr "Язык: %(name)s"
+
+#: cps/templates/index.xml:76 cps/web.py:1559
+msgid "Category list"
+msgstr "Категории"
+
+#: cps/web.py:1573
+#, python-format
+msgid "Category: %(name)s"
+msgstr "Категория: %(name)s"
+
+#: cps/templates/layout.html:71 cps/web.py:1699
+msgid "Tasks"
+msgstr "Задания"
+
+#: cps/web.py:1733
+msgid "Statistics"
+msgstr "Статистика"
+
+#: cps/web.py:1840
+msgid "Callback domain is not verified, please follow steps to verify domain in google developer console"
+msgstr "Не удалось проверить домен обратного вызова, пожалуйста, выполните шаги для проверки домена в консоли разработчика Google."
+
+#: cps/web.py:1915
+msgid "Server restarted, please reload page"
+msgstr "Сервер перезагружен, пожалуйста, перезагрузите страницу"
+
+#: cps/web.py:1918
+msgid "Performing shutdown of server, please close window"
+msgstr "Производится остановка сервера, пожалуйста, закройте окно"
+
+#: cps/web.py:1937
+msgid "Update done"
+msgstr "Обновление закончено"
+
+#: cps/web.py:2007
+msgid "Published after "
+msgstr "Опубликовано до "
+
+#: cps/web.py:2014
+msgid "Published before "
+msgstr "Опубликовано после "
+
+#: cps/web.py:2028
+#, python-format
+msgid "Rating <= %(rating)s"
+msgstr "Рейтинг <= %(rating)s"
+
+#: cps/web.py:2030
+#, python-format
+msgid "Rating >= %(rating)s"
+msgstr "Рейтинг >= %(rating)s"
+
+#: cps/web.py:2089 cps/web.py:2098
+msgid "search"
+msgstr "поиск"
+
+#: cps/templates/index.xml:47 cps/templates/index.xml:51
+#: cps/templates/layout.html:146 cps/web.py:2165
+msgid "Read Books"
+msgstr "Прочитанные Книги"
+
+#: cps/templates/index.xml:55 cps/templates/index.xml:59
+#: cps/templates/layout.html:148 cps/web.py:2168
+msgid "Unread Books"
+msgstr "Непрочитанные Книги"
+
+#: cps/web.py:2216 cps/web.py:2218 cps/web.py:2220 cps/web.py:2232
+msgid "Read a Book"
+msgstr "Читать Книгу"
+
+#: cps/web.py:2298 cps/web.py:3201
+msgid "Please fill out all fields!"
+msgstr "Пожалуйста, заполните все поля!"
+
+#: cps/web.py:2299 cps/web.py:2320 cps/web.py:2324 cps/web.py:2329
+#: cps/web.py:2331
+msgid "register"
+msgstr "регистрация"
+
+#: cps/web.py:2319 cps/web.py:3417
+msgid "An unknown error occurred. Please try again later."
+msgstr "Неизвестная ошибка. Попробуйте позже."
+
+#: cps/web.py:2322
+msgid "Your e-mail is not allowed to register"
+msgstr "Ваш e-mail не подходит для регистрации"
+
+#: cps/web.py:2325
+msgid "Confirmation e-mail was send to your e-mail account."
+msgstr "Письмо с подтверждением отправлено вам на e-mail"
+
+#: cps/web.py:2328
+msgid "This username or e-mail address is already in use."
+msgstr "Этот никнейм или e-mail уже используются"
+
+#: cps/web.py:2345 cps/web.py:2441
+#, python-format
+msgid "you are now logged in as: '%(nickname)s'"
+msgstr "Вы вошли как пользователь '%(nickname)s'"
+
+#: cps/web.py:2350
+msgid "Wrong Username or Password"
+msgstr "Ошибка в имени пользователя или пароле"
+
+#: cps/web.py:2356 cps/web.py:2377
+msgid "login"
+msgstr "войти"
+
+#: cps/web.py:2389 cps/web.py:2420
+msgid "Token not found"
+msgstr "Ключ не найден"
+
+#: cps/web.py:2397 cps/web.py:2428
+msgid "Token has expired"
+msgstr "Ключ просрочен"
+
+#: cps/web.py:2405
+msgid "Success! Please return to your device"
+msgstr "Успешно! Пожалуйста, проверьте свое устройство"
+
+#: cps/web.py:2455
+msgid "Please configure the SMTP mail settings first..."
+msgstr "Пожалуйста, сначала сконфигурируйте параметры SMTP"
+
+#: cps/web.py:2459
+#, python-format
+msgid "Book successfully queued for sending to %(kindlemail)s"
+msgstr "Книга успешно поставлена в очередь для отправки на %(kindlemail)s"
+
+#: cps/web.py:2463
+#, python-format
+msgid "There was an error sending this book: %(res)s"
+msgstr "Ошибка при отправке книги: %(res)s"
+
+#: cps/web.py:2465 cps/web.py:3255
+msgid "Please configure your kindle e-mail address first..."
+msgstr "Пожалуйста, сначала настройте e-mail на вашем kindle..."
+
+#: cps/web.py:2476 cps/web.py:2528
+msgid "Invalid shelf specified"
+msgstr "Указана неверная полка"
+
+#: cps/web.py:2483
+#, python-format
+msgid "Sorry you are not allowed to add a book to the the shelf: %(shelfname)s"
+msgstr ""
+
+#: cps/web.py:2491
+msgid "You are not allowed to edit public shelves"
+msgstr ""
+
+#: cps/web.py:2500
+#, python-format
+msgid "Book is already part of the shelf: %(shelfname)s"
+msgstr ""
+
+#: cps/web.py:2514
+#, python-format
+msgid "Book has been added to shelf: %(sname)s"
+msgstr "Книга добавлена на книжную полку: %(sname)s"
+
+#: cps/web.py:2533
+#, python-format
+msgid "You are not allowed to add a book to the the shelf: %(name)s"
+msgstr "Вам не разрешено добавлять книгу на полку: %(name)s"
+
+#: cps/web.py:2538
+msgid "User is not allowed to edit public shelves"
+msgstr "Пользователь не может редактировать общедоступные полки"
+
+#: cps/web.py:2556
+#, python-format
+msgid "Books are already part of the shelf: %(name)s"
+msgstr "Книги уже размещены на полке: %(name)s"
+
+#: cps/web.py:2570
+#, python-format
+msgid "Books have been added to shelf: %(sname)s"
+msgstr "Книги добавлены в полку: %(sname)s"
+
+#: cps/web.py:2572
+#, python-format
+msgid "Could not add books to shelf: %(sname)s"
+msgstr "Не удалось добавить книги на полку: %(sname)s"
+
+#: cps/web.py:2609
+#, python-format
+msgid "Book has been removed from shelf: %(sname)s"
+msgstr "Книга удалена с полки: %(sname)s"
+
+#: cps/web.py:2615
+#, python-format
+msgid "Sorry you are not allowed to remove a book from this shelf: %(sname)s"
+msgstr "Извините, вы не можете удалить книгу с полки: %(sname)s"
+
+#: cps/web.py:2635 cps/web.py:2659
+#, python-format
+msgid "A shelf with the name '%(title)s' already exists."
+msgstr "Полка с названием '%(title)s' уже существует."
+
+#: cps/web.py:2640
+#, python-format
+msgid "Shelf %(title)s created"
+msgstr "Создана полка %(title)s"
+
+#: cps/web.py:2642 cps/web.py:2670
+msgid "There was an error"
+msgstr "Произошла ошибка"
+
+#: cps/web.py:2643 cps/web.py:2645
+msgid "create a shelf"
+msgstr "создать полку"
+
+#: cps/web.py:2668
+#, python-format
+msgid "Shelf %(title)s changed"
+msgstr "Колка %(title)s изменена"
+
+#: cps/web.py:2671 cps/web.py:2673
+msgid "Edit a shelf"
+msgstr "Изменить полку"
+
+#: cps/web.py:2694
+#, python-format
+msgid "successfully deleted shelf %(name)s"
+msgstr "удачно удалена полка %(name)s"
+
+#: cps/web.py:2721
+#, python-format
+msgid "Shelf: '%(name)s'"
+msgstr "Полка: '%(name)s'"
+
+#: cps/web.py:2724
+msgid "Error opening shelf. Shelf does not exist or is not accessible"
+msgstr "Ошибка открытия Полки. Полка не существует или недоступна"
+
+#: cps/web.py:2755
+#, python-format
+msgid "Change order of Shelf: '%(name)s'"
+msgstr "Изменить расположение полки '%(name)s'"
+
+#: cps/web.py:2784 cps/web.py:3207
+msgid "E-mail is not from valid domain"
+msgstr "E-mail не из существующей доменной зоны"
+
+#: cps/web.py:2786 cps/web.py:2829 cps/web.py:2832
+#, python-format
+msgid "%(name)s's profile"
+msgstr "Профиль %(name)s"
+
+#: cps/web.py:2827
+msgid "Found an existing account for this e-mail address."
+msgstr "Этот адрес электронной почты уже зарегистрирован."
+
+#: cps/web.py:2830
+msgid "Profile updated"
+msgstr "Профиль обновлён"
+
+#: cps/web.py:2858
+msgid "Admin page"
+msgstr "Администрирование"
+
+#: cps/web.py:2938 cps/web.py:3112
+msgid "Calibre-Web configuration updated"
+msgstr "Конфигурация Calibre-Web обновлена"
+
+#: cps/templates/admin.html:100 cps/web.py:2951
+msgid "UI Configuration"
+msgstr "Настройка интерфейса"
+
+#: cps/web.py:2969
+msgid "Import of optional Google Drive requirements missing"
+msgstr "Импорт дополнительных требований к Google Диску отсутствует"
+
+#: cps/web.py:2972
+msgid "client_secrets.json is missing or not readable"
+msgstr "client_secrets.json отсутствует или его невозможно прочесть"
+
+#: cps/web.py:2977 cps/web.py:3004
+msgid "client_secrets.json is not configured for web application"
+msgstr "client_secrets.json не настроен для веб-приложения"
+
+#: cps/templates/admin.html:99 cps/web.py:3007 cps/web.py:3033 cps/web.py:3045
+#: cps/web.py:3088 cps/web.py:3103 cps/web.py:3120 cps/web.py:3127
+#: cps/web.py:3142
+msgid "Basic Configuration"
+msgstr "Настройки сервера"
+
+#: cps/web.py:3030
+msgid "Keyfile location is not valid, please enter correct path"
+msgstr "Неверное расположение файла-ключа, введите правильный путь"
+
+#: cps/web.py:3042
+msgid "Certfile location is not valid, please enter correct path"
+msgstr "Неверное расположение сертификата, введите правильный путь"
+
+#: cps/web.py:3085
+msgid "Logfile location is not valid, please enter correct path"
+msgstr "Неверное расположение лог-файла, введите правильный путь"
+
+#: cps/web.py:3124
+msgid "DB location is not valid, please enter correct path"
+msgstr "Неверное расположение базы данных, введите правильный путь"
+
+#: cps/templates/admin.html:33 cps/web.py:3203 cps/web.py:3209 cps/web.py:3225
+msgid "Add new user"
+msgstr "Добавить пользователя"
+
+#: cps/web.py:3215
+#, python-format
+msgid "User '%(user)s' created"
+msgstr "Пользователь '%(user)s' добавлен"
+
+#: cps/web.py:3219
+msgid "Found an existing account for this e-mail address or nickname."
+msgstr "Для этого адреса электронной почты или логина уже есть аккаунт."
+
+#: cps/web.py:3243 cps/web.py:3257
+msgid "E-mail server settings updated"
+msgstr "Настройки E-mail сервера обновлены"
+
+#: cps/web.py:3250
+#, python-format
+msgid "Test e-mail successfully send to %(kindlemail)s"
+msgstr "Тестовое письмо успешно отправлено на %(kindlemail)s"
+
+#: cps/web.py:3253
+#, python-format
+msgid "There was an error sending the Test e-mail: %(res)s"
+msgstr "Произошла ошибка при отправке тестового письма на: %(res)s"
+
+#: cps/web.py:3258
+msgid "Edit e-mail server settings"
+msgstr "Изменить настройки e-mail сервера"
+
+#: cps/web.py:3283
+#, python-format
+msgid "User '%(nick)s' deleted"
+msgstr "Пользователь '%(nick)s' удалён"
+
+#: cps/web.py:3392
+#, python-format
+msgid "User '%(nick)s' updated"
+msgstr "Пользователь '%(nick)s' обновлён"
+
+#: cps/web.py:3395
+msgid "An unknown error occured."
+msgstr "Произошла неизвестная ошибка."
+
+#: cps/web.py:3397
+#, python-format
+msgid "Edit User %(nick)s"
+msgstr "Изменить пользователя %(nick)s"
+
+#: cps/web.py:3414
+#, python-format
+msgid "Password for user %(user)s reset"
+msgstr "Пароль для пользователя %(user)s сброшен"
+
+#: cps/web.py:3428 cps/web.py:3629
+msgid "Error opening eBook. File does not exist or file is not accessible"
+msgstr "Ошибка при открытии eBook. Файл не существует или файл недоступен"
+
+#: cps/web.py:3453 cps/web.py:3912
+msgid "edit metadata"
+msgstr "изменить метаданные"
+
+#: cps/web.py:3546 cps/web.py:3782
+#, python-format
+msgid "File extension '%(ext)s' is not allowed to be uploaded to this server"
+msgstr "Запрещена загрузка файлов с расширением '%(ext)s'"
+
+#: cps/web.py:3550 cps/web.py:3786
+msgid "File to be uploaded must have an extension"
+msgstr "Загружаемый файл должен иметь расширение"
+
+#: cps/web.py:3562 cps/web.py:3806
+#, python-format
+msgid "Failed to create path %(path)s (Permission denied)."
+msgstr "Ошибка при создании пути %(path)s (Доступ запрещён)."
+
+#: cps/web.py:3567
+#, python-format
+msgid "Failed to store file %(file)s."
+msgstr "Не удалось сохранить файл %(file)s."
+
+#: cps/web.py:3583
+#, python-format
+msgid "File format %(ext)s added to %(book)s"
+msgstr "Формат файла %(ext)s добавлен в %(book)s"
+
+#: cps/web.py:3601
+#, python-format
+msgid "Failed to create path for cover %(path)s (Permission denied)."
+msgstr "Не удалось создать путь для обложки %(path)s (Доступ запрещён)."
+
+#: cps/web.py:3608
+#, python-format
+msgid "Failed to store cover-file %(cover)s."
+msgstr "Не удалось сохранить файл обложки %(cover)s."
+
+#: cps/web.py:3611
+msgid "Cover-file is not a valid image file"
+msgstr "Файл обложки не соответствует изображению"
+
+#: cps/web.py:3641 cps/web.py:3650 cps/web.py:3654
+msgid "unknown"
+msgstr "неизвестно"
+
+#: cps/web.py:3673
+msgid "Cover is not a jpg file, can't save"
+msgstr "Обложка не jpg файл, невозможно сохранить"
+
+#: cps/web.py:3721
+#, python-format
+msgid "%(langname)s is not a valid language"
+msgstr "%(langname)s не допустимый язык"
+
+#: cps/web.py:3752
+msgid "Metadata successfully updated"
+msgstr ""
+
+#: cps/web.py:3761
+msgid "Error editing book, please check logfile for details"
+msgstr "Ошибка редактирования книги. Пожалуйста, проверьте лог-файл для дополнительной информации"
+
+#: cps/web.py:3811
+#, python-format
+msgid "Failed to store file %(file)s (Permission denied)."
+msgstr "Ошибка записи файла %(file)s (Доступ запрещён)."
+
+#: cps/web.py:3816
+#, python-format
+msgid "Failed to delete file %(file)s (Permission denied)."
+msgstr "Ошибка удаления файла %(file)s (Доступ запрещён)."
+
+#: cps/web.py:3898
+#, python-format
+msgid "File %(file)s uploaded"
+msgstr "Файл %(file)s загружен"
+
+#: cps/web.py:3928
+msgid "Source or destination format for conversion missing"
+msgstr "Исходный или целевой формат для конвертирования отсутствует"
+
+#: cps/web.py:3938
+#, python-format
+msgid "Book successfully queued for converting to %(book_format)s"
+msgstr "Книга успешно поставлена в очередь для конвертирования в %(book_format)s"
+
+#: cps/web.py:3942
+#, python-format
+msgid "There was an error converting this book: %(res)s"
+msgstr "Произошла ошибка при конвертирования этой книги: %(res)s"
+
+#: cps/worker.py:287
+#, python-format
+msgid "Ebook-converter failed: %(error)s"
+msgstr "Ошибка Ebook-конвертора: %(error)s"
+
+#: cps/worker.py:298
+#, python-format
+msgid "Kindlegen failed with Error %(error)s. Message: %(message)s"
+msgstr "Kindlegen - неудачно, с Ошибкой %(error)s. Сообщение: %(message)s"
+
+#: cps/templates/admin.html:6
+msgid "User list"
+msgstr "Список пользователей"
+
+#: cps/templates/admin.html:9
+msgid "Nickname"
+msgstr "Имя пользователя"
+
+#: cps/templates/admin.html:10
+msgid "E-mail"
+msgstr "Почта"
+
+#: cps/templates/admin.html:11
+msgid "Kindle"
+msgstr "Kindle"
+
+#: cps/templates/admin.html:12
+msgid "DLS"
+msgstr "DLS"
+
+#: cps/templates/admin.html:13 cps/templates/layout.html:74
+msgid "Admin"
+msgstr "Управление"
+
+#: cps/templates/admin.html:14 cps/templates/detail.html:22
+#: cps/templates/detail.html:31
+msgid "Download"
+msgstr "Скачать"
+
+#: cps/templates/admin.html:15 cps/templates/layout.html:64
+msgid "Upload"
+msgstr "Загрузить"
+
+#: cps/templates/admin.html:16
+msgid "Edit"
+msgstr "Редактировать"
+
+#: cps/templates/admin.html:39
+msgid "SMTP e-mail server settings"
+msgstr "Настройки SMTP-сервера"
+
+#: cps/templates/admin.html:42 cps/templates/email_edit.html:11
+msgid "SMTP hostname"
+msgstr "SMTP-сервер"
+
+#: cps/templates/admin.html:43
+msgid "SMTP port"
+msgstr "SMTP-порт"
+
+#: cps/templates/admin.html:44
+msgid "SSL"
+msgstr "SSL"
+
+#: cps/templates/admin.html:45 cps/templates/email_edit.html:27
+msgid "SMTP login"
+msgstr "SMTP-логин"
+
+#: cps/templates/admin.html:46
+msgid "From mail"
+msgstr "Отправитель"
+
+#: cps/templates/admin.html:56
+msgid "Change SMTP settings"
+msgstr "Изменить настройки SMTP"
+
+#: cps/templates/admin.html:62
+msgid "Configuration"
+msgstr "Настройки сервера"
+
+#: cps/templates/admin.html:65
+msgid "Calibre DB dir"
+msgstr "Папка Calibre DB"
+
+#: cps/templates/admin.html:69
+msgid "Log level"
+msgstr "Уровень лога"
+
+#: cps/templates/admin.html:73
+msgid "Port"
+msgstr "Порт"
+
+#: cps/templates/admin.html:79 cps/templates/config_view_edit.html:23
+msgid "Books per page"
+msgstr "Количество книг на странице"
+
+#: cps/templates/admin.html:83
+msgid "Uploading"
+msgstr "Загрузка на сервер"
+
+#: cps/templates/admin.html:87
+msgid "Anonymous browsing"
+msgstr "Анонимный просмотр"
+
+#: cps/templates/admin.html:91
+msgid "Public registration"
+msgstr "Публичная регистрация"
+
+#: cps/templates/admin.html:95 cps/templates/remote_login.html:4
+msgid "Remote login"
+msgstr "Удалённый логин"
+
+#: cps/templates/admin.html:106
+msgid "Administration"
+msgstr "Управление"
+
+#: cps/templates/admin.html:107
+msgid "Reconnect to Calibre DB"
+msgstr "Переподключиться к БД Calibre"
+
+#: cps/templates/admin.html:108
+msgid "Restart Calibre-Web"
+msgstr "Перезагрузить Calibre-Web"
+
+#: cps/templates/admin.html:109
+msgid "Stop Calibre-Web"
+msgstr "Остановить Calibre-Web"
+
+#: cps/templates/admin.html:115
+msgid "Update"
+msgstr "Обновление"
+
+#: cps/templates/admin.html:119
+msgid "Version"
+msgstr "Версия"
+
+#: cps/templates/admin.html:120
+msgid "Details"
+msgstr "Подробности"
+
+#: cps/templates/admin.html:126
+msgid "Current version"
+msgstr "Текущая версия"
+
+#: cps/templates/admin.html:132
+msgid "Check for update"
+msgstr "Проверка обновлений"
+
+#: cps/templates/admin.html:133
+msgid "Perform Update"
+msgstr "Установить обновления"
+
+#: cps/templates/admin.html:145
+msgid "Do you really want to restart Calibre-Web?"
+msgstr "Вы действительно хотите перезагрузить Calibre-Web?"
+
+#: cps/templates/admin.html:150 cps/templates/admin.html:164
+#: cps/templates/admin.html:184 cps/templates/shelf.html:61
+msgid "Ok"
+msgstr "Ok"
+
+#: cps/templates/admin.html:151 cps/templates/admin.html:165
+#: cps/templates/book_edit.html:178 cps/templates/book_edit.html:200
+#: cps/templates/config_edit.html:212 cps/templates/config_view_edit.html:168
+#: cps/templates/email_edit.html:40 cps/templates/email_edit.html:75
+#: cps/templates/shelf.html:62 cps/templates/shelf_edit.html:19
+#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:155
+msgid "Back"
+msgstr "Назад"
+
+#: cps/templates/admin.html:163
+msgid "Do you really want to stop Calibre-Web?"
+msgstr "Вы действительно хотите остановить Calibre-Web?"
+
+#: cps/templates/admin.html:175
+msgid "Updating, please do not reload page"
+msgstr "Установка обновлений, пожалуйста, не обновляйте страницу."
+
+#: cps/templates/author.html:15
+msgid "via"
+msgstr "с помощью"
+
+#: cps/templates/author.html:23
+msgid "In Library"
+msgstr "В библиотеке"
+
+#: cps/templates/author.html:69
+msgid "More by"
+msgstr "Ещё от"
+
+#: cps/templates/book_edit.html:16
+msgid "Delete Book"
+msgstr "Удалить книгу"
+
+#: cps/templates/book_edit.html:19
+msgid "Delete formats:"
+msgstr "Удалить форматы:"
+
+#: cps/templates/book_edit.html:22 cps/templates/book_edit.html:199
+#: cps/templates/email_edit.html:73 cps/templates/email_edit.html:74
+msgid "Delete"
+msgstr "Удалить"
+
+#: cps/templates/book_edit.html:30
+msgid "Convert book format:"
+msgstr "Конвертировать формат книги:"
+
+#: cps/templates/book_edit.html:34
+msgid "Convert from:"
+msgstr "Конвертировать из:"
+
+#: cps/templates/book_edit.html:36 cps/templates/book_edit.html:43
+msgid "select an option"
+msgstr "выбрать вариант"
+
+#: cps/templates/book_edit.html:41
+msgid "Convert to:"
+msgstr "Конвертировать в:"
+
+#: cps/templates/book_edit.html:50
+msgid "Convert book"
+msgstr "Конвертировать книгу"
+
+#: cps/templates/book_edit.html:59 cps/templates/search_form.html:6
+msgid "Book Title"
+msgstr "Название"
+
+#: cps/templates/book_edit.html:63 cps/templates/book_edit.html:259
+#: cps/templates/book_edit.html:277 cps/templates/search_form.html:10
+msgid "Author"
+msgstr "Автор"
+
+#: cps/templates/book_edit.html:67 cps/templates/book_edit.html:264
+#: cps/templates/book_edit.html:279 cps/templates/search_form.html:106
+msgid "Description"
+msgstr "Описание"
+
+#: cps/templates/book_edit.html:71 cps/templates/search_form.html:33
+msgid "Tags"
+msgstr "Теги"
+
+#: cps/templates/book_edit.html:75 cps/templates/layout.html:157
+#: cps/templates/search_form.html:53
+msgid "Series"
+msgstr "Серии"
+
+#: cps/templates/book_edit.html:79
+msgid "Series id"
+msgstr "Серия"
+
+#: cps/templates/book_edit.html:83
+msgid "Rating"
+msgstr "Рейтинг"
+
+#: cps/templates/book_edit.html:87
+msgid "Cover URL (jpg, cover is downloaded and stored in database, field is afterwards empty again)"
+msgstr "URL обложки(jpg, обложка загружается и сохраняется в базе данных, после этого поле снова пустое)"
+
+#: cps/templates/book_edit.html:91
+msgid "Upload Cover from local drive"
+msgstr "Загрузить обложку с диска"
+
+#: cps/templates/book_edit.html:96 cps/templates/detail.html:135
+msgid "Publishing date"
+msgstr "Опубликовано"
+
+#: cps/templates/book_edit.html:103 cps/templates/book_edit.html:261
+#: cps/templates/book_edit.html:278 cps/templates/detail.html:127
+#: cps/templates/search_form.html:14
+msgid "Publisher"
+msgstr "Издатель"
+
+#: cps/templates/book_edit.html:107 cps/templates/user_edit.html:31
+msgid "Language"
+msgstr "Язык"
+
+#: cps/templates/book_edit.html:117 cps/templates/search_form.html:117
+msgid "Yes"
+msgstr "Да"
+
+#: cps/templates/book_edit.html:118 cps/templates/search_form.html:118
+msgid "No"
+msgstr "Нет"
+
+#: cps/templates/book_edit.html:164
+msgid "Upload format"
+msgstr "Загружаемый формат"
+
+#: cps/templates/book_edit.html:173
+msgid "view book after edit"
+msgstr "смотреть книгу после редактирования"
+
+#: cps/templates/book_edit.html:176 cps/templates/book_edit.html:212
+msgid "Get metadata"
+msgstr "Получить метаданные"
+
+#: cps/templates/book_edit.html:177 cps/templates/config_edit.html:210
+#: cps/templates/config_view_edit.html:167 cps/templates/login.html:20
+#: cps/templates/search_form.html:153 cps/templates/shelf_edit.html:17
+#: cps/templates/user_edit.html:153
+msgid "Submit"
+msgstr "Отправить"
+
+#: cps/templates/book_edit.html:191
+msgid "Are you really sure?"
+msgstr "Вы действительно уверены?"
+
+#: cps/templates/book_edit.html:194
+msgid "Book will be deleted from Calibre database"
+msgstr "Книга будет удалена из БД Calibre"
+
+#: cps/templates/book_edit.html:195
+msgid "and from hard disk"
+msgstr "и с диска"
+
+#: cps/templates/book_edit.html:215
+msgid "Keyword"
+msgstr "Ключевое слово"
+
+#: cps/templates/book_edit.html:216
+msgid " Search keyword "
+msgstr " Поиск по ключевому слову "
+
+#: cps/templates/book_edit.html:218 cps/templates/layout.html:46
+msgid "Go!"
+msgstr "Старт!"
+
+#: cps/templates/book_edit.html:222
+msgid "Click the cover to load metadata to the form"
+msgstr "Нажмите на обложку, чтобы получить метаданные"
+
+#: cps/templates/book_edit.html:234 cps/templates/book_edit.html:274
+msgid "Loading..."
+msgstr "Загрузка..."
+
+#: cps/templates/book_edit.html:239 cps/templates/layout.html:224
+msgid "Close"
+msgstr "Закрыть"
+
+#: cps/templates/book_edit.html:266 cps/templates/book_edit.html:280
+msgid "Source"
+msgstr "Источник"
+
+#: cps/templates/book_edit.html:275
+msgid "Search error!"
+msgstr "Ошибка поиска!"
+
+#: cps/templates/book_edit.html:276
+msgid "No Result(s) found! Please try aonther keyword."
+msgstr "Результат(ы) не найдены! Попробуйте другое ключевое слово."
+
+#: cps/templates/config_edit.html:12
+msgid "Library Configuration"
+msgstr "Настройки библотеки"
+
+#: cps/templates/config_edit.html:19
+msgid "Location of Calibre database"
+msgstr "Расположение БД Calibre"
+
+#: cps/templates/config_edit.html:24
+msgid "Use Google Drive?"
+msgstr "Использовать Google Drive?"
+
+#: cps/templates/config_edit.html:30
+msgid "Google Drive config problem"
+msgstr "Проблема с настройкой Google Drive"
+
+#: cps/templates/config_edit.html:36
+msgid "Authenticate Google Drive"
+msgstr "Аутентификация Google Drive"
+
+#: cps/templates/config_edit.html:40
+msgid "Please finish Google Drive setup after login"
+msgstr "Завершите настройку Google Диска после входа в систему"
+
+#: cps/templates/config_edit.html:44
+msgid "Google Drive Calibre folder"
+msgstr "Google Диск Calibre папка"
+
+#: cps/templates/config_edit.html:52
+msgid "Metadata Watch Channel ID"
+msgstr "ID Канала Просмотра Метаданных"
+
+#: cps/templates/config_edit.html:55
+msgid "Revoke"
+msgstr "Отозвано"
+
+#: cps/templates/config_edit.html:73
+msgid "Server Configuration"
+msgstr "Настройки сервера"
+
+#: cps/templates/config_edit.html:80
+msgid "Server Port"
+msgstr "Порт сервера"
+
+#: cps/templates/config_edit.html:84
+msgid "SSL certfile location (leave it empty for non-SSL Servers)"
+msgstr "Расположение SSL сертификата (оставьте его пустым для серверов без SSL)"
+
+#: cps/templates/config_edit.html:88
+msgid "SSL Keyfile location (leave it empty for non-SSL Servers)"
+msgstr "Расположение SSL файла-ключа (оставьте его пустым для серверов без SSL)"
+
+#: cps/templates/config_edit.html:99
+msgid "Logfile Configuration"
+msgstr "Настройки лог-файла"
+
+#: cps/templates/config_edit.html:106
+msgid "Log Level"
+msgstr "Уровень Логирования"
+
+#: cps/templates/config_edit.html:115
+msgid "Location and name of logfile (calibre-web.log for no entry)"
+msgstr "Расположение и имя лог-файла (не вводите calibre-web.log)"
+
+#: cps/templates/config_edit.html:126
+msgid "Feature Configuration"
+msgstr "Дополнительный Настройки"
+
+#: cps/templates/config_edit.html:134
+msgid "Enable uploading"
+msgstr "Разрешить загрузку на сервер"
+
+#: cps/templates/config_edit.html:138
+msgid "Enable anonymous browsing"
+msgstr "Разрешить анонимный просмотр"
+
+#: cps/templates/config_edit.html:142
+msgid "Enable public registration"
+msgstr "Разрешить публичную регистрацию"
+
+#: cps/templates/config_edit.html:146
+msgid "Enable remote login (\"magic link\")"
+msgstr "Включить удаленный логин (\"magic link\")"
+
+#: cps/templates/config_edit.html:151
+msgid "Use"
+msgstr "Использовать"
+
+#: cps/templates/config_edit.html:152
+msgid "Obtain an API Key"
+msgstr "Получить ключ API"
+
+#: cps/templates/config_edit.html:156
+msgid "Goodreads API Key"
+msgstr "Ключ API Goodreads"
+
+#: cps/templates/config_edit.html:160
+msgid "Goodreads API Secret"
+msgstr "Goodreads API Секрет"
+
+#: cps/templates/config_edit.html:173
+msgid "External binaries"
+msgstr "Внешние двоичные файлы"
+
+#: cps/templates/config_edit.html:181
+msgid "No converter"
+msgstr "Нет конвертера"
+
+#: cps/templates/config_edit.html:183
+msgid "Use Kindlegen"
+msgstr "Использовать Kindlegen"
+
+#: cps/templates/config_edit.html:185
+msgid "Use calibre's ebook converter"
+msgstr "Использовать конвертер calibre's ebook"
+
+#: cps/templates/config_edit.html:189
+msgid "E-Book converter settings"
+msgstr "Настройки конвертера E-Book"
+
+#: cps/templates/config_edit.html:193
+msgid "Path to convertertool"
+msgstr "Путь к конвертеру"
+
+#: cps/templates/config_edit.html:199
+msgid "Location of Unrar binary"
+msgstr "Расположение двоичного файла Unrar"
+
+#: cps/templates/config_edit.html:215 cps/templates/layout.html:82
+#: cps/templates/login.html:4
+msgid "Login"
+msgstr "Логин"
+
+#: cps/templates/config_view_edit.html:12
+msgid "View Configuration"
+msgstr "Просмотреть Конфигурацию"
+
+#: cps/templates/config_view_edit.html:19 cps/templates/layout.html:133
+#: cps/templates/layout.html:134 cps/templates/shelf_edit.html:7
+msgid "Title"
+msgstr "Заголовок"
+
+#: cps/templates/config_view_edit.html:27
+msgid "No. of random books to show"
+msgstr "Количество отображаемых случайных книг"
+
+#: cps/templates/config_view_edit.html:31
+msgid "Regular expression for ignoring columns"
+msgstr "Regexp для игнорирования столбцов"
+
+#: cps/templates/config_view_edit.html:35
+msgid "Link read/unread status to Calibre column"
+msgstr "Ссылка на чтение/непрочитанный статус столбца Caliber"
+
+#: cps/templates/config_view_edit.html:44
+msgid "Regular expression for title sorting"
+msgstr "Regexp для сортировки по названию"
+
+#: cps/templates/config_view_edit.html:48
+msgid "Tags for Mature Content"
+msgstr "Теги для Зрелого Контента"
+
+#: cps/templates/config_view_edit.html:62
+msgid "Default settings for new users"
+msgstr "Настройки по умолчанию для новых пользователей"
+
+#: cps/templates/config_view_edit.html:70 cps/templates/user_edit.html:110
+msgid "Admin user"
+msgstr "Управление сервером"
+
+#: cps/templates/config_view_edit.html:74 cps/templates/user_edit.html:119
+msgid "Allow Downloads"
+msgstr "Разрешить скачивание с сервера"
+
+#: cps/templates/config_view_edit.html:78 cps/templates/user_edit.html:123
+msgid "Allow Uploads"
+msgstr "Разрешить загрузку на сервер"
+
+#: cps/templates/config_view_edit.html:82 cps/templates/user_edit.html:127
+msgid "Allow Edit"
+msgstr "Разрешить редактирование книг"
+
+#: cps/templates/config_view_edit.html:86 cps/templates/user_edit.html:131
+msgid "Allow Delete books"
+msgstr "Разрешить удаление книг"
+
+#: cps/templates/config_view_edit.html:90 cps/templates/user_edit.html:136
+msgid "Allow Changing Password"
+msgstr "Разрешить смену пароля"
+
+#: cps/templates/config_view_edit.html:94 cps/templates/user_edit.html:140
+msgid "Allow Editing Public Shelfs"
+msgstr "Разрешить редактирование публичных книжных полок"
+
+#: cps/templates/config_view_edit.html:104
+msgid "Default visibilities for new users"
+msgstr "Видимость для новых пользователей(по умолчанию)"
+
+#: cps/templates/config_view_edit.html:112 cps/templates/user_edit.html:58
+msgid "Show random books"
+msgstr "Показывать случайные книги"
+
+#: cps/templates/config_view_edit.html:116 cps/templates/user_edit.html:62
+msgid "Show recent books"
+msgstr "Показывать недавние книги"
+
+#: cps/templates/config_view_edit.html:120 cps/templates/user_edit.html:66
+msgid "Show sorted books"
+msgstr "Показывать отсортированные книги"
+
+#: cps/templates/config_view_edit.html:124 cps/templates/user_edit.html:70
+msgid "Show hot books"
+msgstr "Показывать популярные книги"
+
+#: cps/templates/config_view_edit.html:128 cps/templates/user_edit.html:74
+msgid "Show best rated books"
+msgstr "Показывать книги с наивысшим рейтингом"
+
+#: cps/templates/config_view_edit.html:132 cps/templates/user_edit.html:78
+msgid "Show language selection"
+msgstr "Показывать выбор языка"
+
+#: cps/templates/config_view_edit.html:136 cps/templates/user_edit.html:82
+msgid "Show series selection"
+msgstr "Показывать выбор серии"
+
+#: cps/templates/config_view_edit.html:140 cps/templates/user_edit.html:86
+msgid "Show category selection"
+msgstr "Показывать выбор категории"
+
+#: cps/templates/config_view_edit.html:144 cps/templates/user_edit.html:90
+msgid "Show author selection"
+msgstr "Показывать выбор автора"
+
+#: cps/templates/config_view_edit.html:148 cps/templates/user_edit.html:94
+msgid "Show publisher selection"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:152 cps/templates/user_edit.html:98
+msgid "Show read and unread"
+msgstr "Показывать прочитанные и непрочитанные"
+
+#: cps/templates/config_view_edit.html:156 cps/templates/user_edit.html:102
+msgid "Show random books in detail view"
+msgstr "Показывать случайные книги при просмотре деталей"
+
+#: cps/templates/config_view_edit.html:160 cps/templates/user_edit.html:115
+msgid "Show mature content"
+msgstr "Показывать взрослый контент"
+
+#: cps/templates/detail.html:49
+msgid "Read in browser"
+msgstr "Открыть в браузере"
+
+#: cps/templates/detail.html:88
+msgid "Book"
+msgstr "Книга"
+
+#: cps/templates/detail.html:88
+msgid "of"
+msgstr "из"
+
+#: cps/templates/detail.html:94
+msgid "language"
+msgstr "Язык"
+
+#: cps/templates/detail.html:172
+msgid "Read"
+msgstr "Прочитано"
+
+#: cps/templates/detail.html:182
+msgid "Description:"
+msgstr "Описание:"
+
+#: cps/templates/detail.html:195 cps/templates/search.html:14
+msgid "Add to shelf"
+msgstr "Добавить на книжную полку"
+
+#: cps/templates/detail.html:257
+msgid "Edit metadata"
+msgstr "Редактировать метаданные"
+
+#: cps/templates/email_edit.html:15
+msgid "SMTP port (usually 25 for plain SMTP and 465 for SSL and 587 for STARTTLS)"
+msgstr "SMTP-порт (обычно 25 для SMTP, 465 для SSL и 587 для STARTTLS)"
+
+#: cps/templates/email_edit.html:19
+msgid "Encryption"
+msgstr "Шифрование"
+
+#: cps/templates/email_edit.html:21
+msgid "None"
+msgstr "Нет"
+
+#: cps/templates/email_edit.html:22
+msgid "STARTTLS"
+msgstr "STARTTLS"
+
+#: cps/templates/email_edit.html:23
+msgid "SSL/TLS"
+msgstr "SSL/TLS"
+
+#: cps/templates/email_edit.html:31
+msgid "SMTP password"
+msgstr "Пароль SMTP"
+
+#: cps/templates/email_edit.html:35
+msgid "From e-mail"
+msgstr "Адрес отправителя"
+
+#: cps/templates/email_edit.html:38
+msgid "Save settings"
+msgstr "Сохранить настройки"
+
+#: cps/templates/email_edit.html:39
+msgid "Save settings and send Test E-Mail"
+msgstr "Сохранить настройки и отправить тестовое письмо"
+
+#: cps/templates/email_edit.html:43
+msgid "Allowed domains for registering"
+msgstr "Допустимые домены для регистрации"
+
+#: cps/templates/email_edit.html:47
+msgid "Enter domainname"
+msgstr "Введите доменное имя"
+
+#: cps/templates/email_edit.html:55
+msgid "Add Domain"
+msgstr "Добавить Домен"
+
+#: cps/templates/email_edit.html:58
+msgid "Add"
+msgstr "Добавить"
+
+#: cps/templates/email_edit.html:72
+msgid "Do you really want to delete this domain rule?"
+msgstr "Вы действительно желаете удалить это правило домена?"
+
+#: cps/templates/feed.xml:21 cps/templates/layout.html:208
+msgid "Next"
+msgstr "Дальше"
+
+#: cps/templates/feed.xml:33 cps/templates/index.xml:11
+#: cps/templates/layout.html:43 cps/templates/layout.html:44
+msgid "Search"
+msgstr "Поиск"
+
+#: cps/templates/index.html:5
+msgid "Discover (Random Books)"
+msgstr "Обзор (Случайные Книги)"
+
+#: cps/templates/index.xml:6
+msgid "Start"
+msgstr "Старт"
+
+#: cps/templates/index.xml:18 cps/templates/layout.html:139
+msgid "Hot Books"
+msgstr "Популярные Книги"
+
+#: cps/templates/index.xml:22
+msgid "Popular publications from this catalog based on Downloads."
+msgstr "Популярные книги в этом каталоге, на основе количества Скачиваний"
+
+#: cps/templates/index.xml:25 cps/templates/layout.html:142
+msgid "Best rated Books"
+msgstr "Книги с наилучшим рейтингом"
+
+#: cps/templates/index.xml:29
+msgid "Popular publications from this catalog based on Rating."
+msgstr "Популярные книги из этого каталога на основании Рейтинга"
+
+#: cps/templates/index.xml:32
+msgid "New Books"
+msgstr "Новые Книги"
+
+#: cps/templates/index.xml:36
+msgid "The latest Books"
+msgstr "Последние Книги"
+
+#: cps/templates/index.xml:43
+msgid "Show Random Books"
+msgstr "Показывать Случайные Сниги"
+
+#: cps/templates/index.xml:62 cps/templates/layout.html:160
+msgid "Authors"
+msgstr "Авторы"
+
+#: cps/templates/index.xml:66
+msgid "Books ordered by Author"
+msgstr "Книги, отсортированные по Автору"
+
+#: cps/templates/index.xml:69 cps/templates/layout.html:163
+msgid "Publishers"
+msgstr ""
+
+#: cps/templates/index.xml:73
+msgid "Books ordered by publisher"
+msgstr ""
+
+#: cps/templates/index.xml:80
+msgid "Books ordered by category"
+msgstr "Книги, отсортированные по категории"
+
+#: cps/templates/index.xml:87
+msgid "Books ordered by series"
+msgstr "Книги, отсортированные по серии"
+
+#: cps/templates/index.xml:90 cps/templates/layout.html:169
+msgid "Public Shelves"
+msgstr "Общие полки"
+
+#: cps/templates/index.xml:94
+msgid "Books organized in public shelfs, visible to everyone"
+msgstr "Книги размещены на полках, и доступны всем"
+
+#: cps/templates/index.xml:98 cps/templates/layout.html:173
+msgid "Your Shelves"
+msgstr "Ваши полки"
+
+#: cps/templates/index.xml:102
+msgid "User's own shelfs, only visible to the current user himself"
+msgstr "Пользовательские полки, видимые только самому пользователю"
+
+#: cps/templates/layout.html:33
+msgid "Toggle navigation"
+msgstr "Включить навигацию"
+
+#: cps/templates/layout.html:54
+msgid "Advanced Search"
+msgstr "Расширенный поиск"
+
+#: cps/templates/layout.html:78
+msgid "Logout"
+msgstr "Выход"
+
+#: cps/templates/layout.html:83 cps/templates/register.html:14
+msgid "Register"
+msgstr "Зарегистрироваться"
+
+#: cps/templates/layout.html:108
+msgid "Uploading..."
+msgstr "Загружается..."
+
+#: cps/templates/layout.html:109
+msgid "please don't refresh the page"
+msgstr "пожалуйста не обновляйте страницу"
+
+#: cps/templates/layout.html:120
+msgid "Browse"
+msgstr "Просмотр"
+
+#: cps/templates/layout.html:122
+msgid "Recently Added"
+msgstr "Недавно Добавленные"
+
+#: cps/templates/layout.html:127
+msgid "Sorted Books"
+msgstr "Сортировка Книг"
+
+#: cps/templates/layout.html:131 cps/templates/layout.html:132
+#: cps/templates/layout.html:133 cps/templates/layout.html:134
+msgid "Sort By"
+msgstr "Отсортировано по"
+
+#: cps/templates/layout.html:131
+msgid "Newest"
+msgstr "Новинки"
+
+#: cps/templates/layout.html:132
+msgid "Oldest"
+msgstr "Старое"
+
+#: cps/templates/layout.html:133
+msgid "Ascending"
+msgstr "По возрастанию"
+
+#: cps/templates/layout.html:134
+msgid "Descending"
+msgstr "По убыванию"
+
+#: cps/templates/layout.html:151
+msgid "Discover"
+msgstr "Обзор"
+
+#: cps/templates/layout.html:154
+msgid "Categories"
+msgstr "Категории"
+
+#: cps/templates/layout.html:166 cps/templates/search_form.html:74
+msgid "Languages"
+msgstr "Языки"
+
+#: cps/templates/layout.html:178
+msgid "Create a Shelf"
+msgstr "Создать книжную полку"
+
+#: cps/templates/layout.html:179 cps/templates/stats.html:3
+msgid "About"
+msgstr "О программе"
+
+#: cps/templates/layout.html:193
+msgid "Previous"
+msgstr "Предыдущий"
+
+#: cps/templates/layout.html:220
+msgid "Book Details"
+msgstr "Подробнее о книге"
+
+#: cps/templates/login.html:8 cps/templates/login.html:9
+#: cps/templates/register.html:7 cps/templates/user_edit.html:8
+msgid "Username"
+msgstr "Имя пользователя"
+
+#: cps/templates/login.html:12 cps/templates/login.html:13
+#: cps/templates/user_edit.html:21
+msgid "Password"
+msgstr "Пароль"
+
+#: cps/templates/login.html:17
+msgid "Remember me"
+msgstr "Запомнить меня"
+
+#: cps/templates/login.html:22
+msgid "Log in with magic link"
+msgstr "Войти через магическую ссылку"
+
+#: cps/templates/osd.xml:5
+msgid "Calibre-Web ebook catalog"
+msgstr "Каталог электронных книг Caliber-Web"
+
+#: cps/templates/read.html:69 cps/templates/readcbr.html:79
+#: cps/templates/readcbr.html:103
+msgid "Settings"
+msgstr "Настройки"
+
+#: cps/templates/read.html:72
+msgid "Reflow text when sidebars are open."
+msgstr "Обновить размещение текста при открытии боковой панели"
+
+#: cps/templates/readcbr.html:84
+msgid "Keyboard Shortcuts"
+msgstr "Горячие клавиши"
+
+#: cps/templates/readcbr.html:87
+msgid "Previous Page"
+msgstr "Предыдущая страница"
+
+#: cps/templates/readcbr.html:88
+msgid "Next Page"
+msgstr "Следующая страница"
+
+#: cps/templates/readcbr.html:89
+msgid "Scale to Best"
+msgstr "Масштабировать до лучшего"
+
+#: cps/templates/readcbr.html:90
+msgid "Scale to Width"
+msgstr "Масштабироваать по ширине"
+
+#: cps/templates/readcbr.html:91
+msgid "Scale to Height"
+msgstr "Масштабировать по высоте"
+
+#: cps/templates/readcbr.html:92
+msgid "Scale to Native"
+msgstr "Масштабировать до оригинала"
+
+#: cps/templates/readcbr.html:93
+msgid "Rotate Right"
+msgstr "Повернуть Вправо"
+
+#: cps/templates/readcbr.html:94
+msgid "Rotate Left"
+msgstr "Повернуть Влево"
+
+#: cps/templates/readcbr.html:95
+msgid "Flip Image"
+msgstr "Перевернуть изображение"
+
+#: cps/templates/readcbr.html:108 cps/templates/user_edit.html:39
+msgid "Theme"
+msgstr "Тема"
+
+#: cps/templates/readcbr.html:111
+msgid "Light"
+msgstr "Светлая"
+
+#: cps/templates/readcbr.html:112
+msgid "Dark"
+msgstr "Темная"
+
+#: cps/templates/readcbr.html:117
+msgid "Scale"
+msgstr "Масштаб"
+
+#: cps/templates/readcbr.html:120
+msgid "Best"
+msgstr "Лучшее"
+
+#: cps/templates/readcbr.html:121
+msgid "Width"
+msgstr "Ширина"
+
+#: cps/templates/readcbr.html:122
+msgid "Height"
+msgstr "Длина"
+
+#: cps/templates/readcbr.html:123
+msgid "Native"
+msgstr "Оригинальный"
+
+#: cps/templates/readcbr.html:128
+msgid "Rotate"
+msgstr "Повернуть"
+
+#: cps/templates/readcbr.html:139
+msgid "Flip"
+msgstr "Перевернуть"
+
+#: cps/templates/readcbr.html:142
+msgid "Horizontal"
+msgstr "Горизонтально"
+
+#: cps/templates/readcbr.html:143
+msgid "Vertical"
+msgstr "Вертикально"
+
+#: cps/templates/readpdf.html:29
+msgid "PDF.js viewer"
+msgstr "Просмотровщик PDF.js"
+
+#: cps/templates/readtxt.html:6
+msgid "Basic txt Reader"
+msgstr "Средство для чтения txt-файлов"
+
+#: cps/templates/register.html:4
+msgid "Register a new account"
+msgstr "Зарегистрировать новую учётную запись"
+
+#: cps/templates/register.html:8
+msgid "Choose a username"
+msgstr "Выберите имя пользователя"
+
+#: cps/templates/register.html:11 cps/templates/user_edit.html:13
+msgid "E-mail address"
+msgstr "E-mail адрес"
+
+#: cps/templates/register.html:12
+msgid "Your email address"
+msgstr "Ваш email-адрес"
+
+#: cps/templates/remote_login.html:6
+msgid "Using your another device, visit"
+msgstr "Используйте другое устройство, посетите"
+
+#: cps/templates/remote_login.html:6
+msgid "and log in"
+msgstr "и войти"
+
+#: cps/templates/remote_login.html:9
+msgid "Once you do so, you will automatically get logged in on this device."
+msgstr "После этого вы автоматически войдете в систему на этом устройстве."
+
+#: cps/templates/search.html:5
+msgid "No Results for:"
+msgstr "Ничего не найдено по запросу:"
+
+#: cps/templates/search.html:6
+msgid "Please try a different search"
+msgstr "Попробуйте изменить критерии поиск"
+
+#: cps/templates/search.html:8
+msgid "Results for:"
+msgstr "Результаты для:"
+
+#: cps/templates/search_form.html:19
+msgid "Publishing date from"
+msgstr "Опубликовано от"
+
+#: cps/templates/search_form.html:26
+msgid "Publishing date to"
+msgstr "Опубликовано до"
+
+#: cps/templates/search_form.html:43
+msgid "Exclude Tags"
+msgstr "Исключить теги"
+
+#: cps/templates/search_form.html:63
+msgid "Exclude Series"
+msgstr "Исключить серии"
+
+#: cps/templates/search_form.html:84
+msgid "Exclude Languages"
+msgstr "Исключить языки"
+
+#: cps/templates/search_form.html:97
+msgid "Rating bigger than"
+msgstr "Рейтинг больше чем"
+
+#: cps/templates/search_form.html:101
+msgid "Rating less than"
+msgstr "Рейтинг меньше чем"
+
+#: cps/templates/shelf.html:7
+msgid "Delete this Shelf"
+msgstr "Удалить эту книжную полку"
+
+#: cps/templates/shelf.html:8
+msgid "Edit Shelf"
+msgstr "Изменить Полку"
+
+#: cps/templates/shelf.html:9 cps/templates/shelf_order.html:11
+msgid "Change order"
+msgstr "Изменить порядок"
+
+#: cps/templates/shelf.html:56
+msgid "Do you really want to delete the shelf?"
+msgstr "Вы действительно хотите удалить эту книжную полку?"
+
+#: cps/templates/shelf.html:59
+msgid "Shelf will be lost for everybody and forever!"
+msgstr "Книжная полка будет безвозвратно удалена для всех"
+
+#: cps/templates/shelf_edit.html:13
+msgid "should the shelf be public?"
+msgstr "сделать книжную полку доступной для всех?"
+
+#: cps/templates/shelf_order.html:5
+msgid "Drag 'n drop to rearrange order"
+msgstr "Перетащите для изменения порядка"
+
+#: cps/templates/stats.html:7
+msgid "Calibre library statistics"
+msgstr "Статистика библиотеки Calibre"
+
+#: cps/templates/stats.html:12
+msgid "Books in this Library"
+msgstr "Книг в этой Библиотеке"
+
+#: cps/templates/stats.html:16
+msgid "Authors in this Library"
+msgstr "Авторов в этой Библиотеке"
+
+#: cps/templates/stats.html:20
+msgid "Categories in this Library"
+msgstr "Категорий в этой Библиотеке"
+
+#: cps/templates/stats.html:24
+msgid "Series in this Library"
+msgstr "Серий в этой Библиотеке"
+
+#: cps/templates/stats.html:28
+msgid "Linked libraries"
+msgstr "Установленное ПО"
+
+#: cps/templates/stats.html:32
+msgid "Program library"
+msgstr "Название"
+
+#: cps/templates/stats.html:33
+msgid "Installed Version"
+msgstr "Установленная версия"
+
+#: cps/templates/tasks.html:7
+msgid "Tasks list"
+msgstr "Список задач"
+
+#: cps/templates/tasks.html:12
+msgid "User"
+msgstr "Пользователь"
+
+#: cps/templates/tasks.html:14
+msgid "Task"
+msgstr "Задача"
+
+#: cps/templates/tasks.html:15
+msgid "Status"
+msgstr "Статус"
+
+#: cps/templates/tasks.html:16
+msgid "Progress"
+msgstr "Прогресс"
+
+#: cps/templates/tasks.html:17
+msgid "Runtime"
+msgstr "Время выполнения"
+
+#: cps/templates/tasks.html:18
+msgid "Starttime"
+msgstr "Время начала"
+
+#: cps/templates/tasks.html:24
+msgid "Delete finished tasks"
+msgstr "Удалить законченные задачи"
+
+#: cps/templates/tasks.html:25
+msgid "Hide all tasks"
+msgstr "Скрыть все задачи"
+
+#: cps/templates/user_edit.html:18
+msgid "Reset user Password"
+msgstr "Сбросить пароль пользователя"
+
+#: cps/templates/user_edit.html:27
+msgid "Kindle E-Mail"
+msgstr "Адрес почты Kindle"
+
+#: cps/templates/user_edit.html:41
+msgid "Standard Theme"
+msgstr "Стандартная тема"
+
+#: cps/templates/user_edit.html:42
+msgid "caliBlur! Dark Theme (Beta)"
+msgstr "caliBlur! Темная тема (Бета)"
+
+#: cps/templates/user_edit.html:47
+msgid "Show books with language"
+msgstr "Показать книги на языках"
+
+#: cps/templates/user_edit.html:49
+msgid "Show all"
+msgstr "Показать все"
+
+#: cps/templates/user_edit.html:147
+msgid "Delete this user"
+msgstr "Удалить этого пользователя"
+
+#: cps/templates/user_edit.html:162
+msgid "Recent Downloads"
+msgstr "Недавние скачивания"
+
+#~ msgid "E-mail: %s"
+#~ msgstr "Почта: %s"
+
+#~ msgid "Current commit timestamp"
+#~ msgstr "Текущая метка фиксации"
+
+#~ msgid "Newest commit timestamp"
+#~ msgstr "Новая метка фиксации"
+
+#~ msgid "Choose a password"
+#~ msgstr "Выберите пароль"
+
+#~ msgid "Convert: %(book)s"
+#~ msgstr "Конвертировать: %(book)s"
+
+#~ msgid "Convert to %(format)s: %(book)s"
+#~ msgstr "Конвертировать в %(format)s: %(book)s"
+
+#~ msgid "Files are replaced"
+#~ msgstr "Файлы заменены"
+
+#~ msgid "Server is stopped"
+#~ msgstr "Сервер остановлен"
+
+#~ msgid "Convertertool %(converter)s not found"
+#~ msgstr "Инструмент конвертирования %(converter)s не найден"
+
diff --git a/src/cps/translations/sv/LC_MESSAGES/messages.mo b/src/cps/translations/sv/LC_MESSAGES/messages.mo
new file mode 100644
index 0000000..0e52fbc
Binary files /dev/null and b/src/cps/translations/sv/LC_MESSAGES/messages.mo differ
diff --git a/src/cps/translations/sv/LC_MESSAGES/messages.po b/src/cps/translations/sv/LC_MESSAGES/messages.po
new file mode 100644
index 0000000..019728f
--- /dev/null
+++ b/src/cps/translations/sv/LC_MESSAGES/messages.po
@@ -0,0 +1,1964 @@
+# German translations for Calibre-Web.
+# Copyright (C) 2016 Ozzie Isaacs
+# This file is distributed under the same license as the Calibre-Web
+# project.
+# FIRST AUTHOR OzzieIsaacs, 2016.
+msgid ""
+msgstr ""
+"Project-Id-Version: Calibre-Web\n"
+"Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n"
+"POT-Creation-Date: 2018-11-24 20:45+0100\n"
+"PO-Revision-Date: 2018-11-23 02:57+0100\n"
+"Last-Translator: Jonatan Nyberg \n"
+"Language: sv\n"
+"Language-Team: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.6.0\n"
+
+#: cps/book_formats.py:129 cps/book_formats.py:130 cps/book_formats.py:134
+#: cps/book_formats.py:138 cps/converter.py:11 cps/converter.py:27
+msgid "not installed"
+msgstr "inte installerad"
+
+#: cps/converter.py:22 cps/converter.py:38
+msgid "Excecution permissions missing"
+msgstr "Utförande behörighet saknas"
+
+#: cps/converter.py:48
+msgid "not configured"
+msgstr "inte konfigurerad"
+
+#: cps/helper.py:59
+#, python-format
+msgid "%(format)s format not found for book id: %(book)d"
+msgstr "%(format)s formatet hittades inte för bok-id: %(book)d"
+
+#: cps/helper.py:71
+#, python-format
+msgid "%(format)s not found on Google Drive: %(fn)s"
+msgstr "%(format)s hittades inte på Google Drive: %(fn)s"
+
+#: cps/helper.py:78 cps/helper.py:174 cps/templates/detail.html:49
+msgid "Send to Kindle"
+msgstr "Skicka till Kindle"
+
+#: cps/helper.py:79 cps/helper.py:97 cps/helper.py:176
+msgid "This e-mail has been sent via Calibre-Web."
+msgstr "Detta e-postmeddelande har skickats via Calibre-Web."
+
+#: cps/helper.py:90
+#, python-format
+msgid "%(format)s not found: %(fn)s"
+msgstr "%(format)s hittades inte: %(fn)s"
+
+#: cps/helper.py:95
+msgid "Calibre-Web test e-mail"
+msgstr "Calibre-Web test e-post"
+
+#: cps/helper.py:96
+msgid "Test e-mail"
+msgstr "Test e-post"
+
+#: cps/helper.py:112
+msgid "Get Started with Calibre-Web"
+msgstr "Kom igång med Calibre-Web"
+
+#: cps/helper.py:113
+#, python-format
+msgid "Registration e-mail for user: %(name)s"
+msgstr "Registrera e-post för användare: %(name)s"
+
+#: cps/helper.py:126 cps/helper.py:128 cps/helper.py:130 cps/helper.py:138
+#: cps/helper.py:140 cps/helper.py:142 cps/helper.py:144
+#, python-format
+msgid "Send %(format)s to Kindle"
+msgstr ""
+
+#: cps/helper.py:132
+#, python-format
+msgid "Send %(format)s to Kkindle"
+msgstr ""
+
+#: cps/helper.py:148 cps/helper.py:152
+#, python-format
+msgid "Convert %(orig)s to %(format)s and send to Kindle"
+msgstr ""
+
+#: cps/helper.py:175
+#, python-format
+msgid "E-mail: %(book)s"
+msgstr "E-post: %(book)s"
+
+#: cps/helper.py:178
+msgid "The requested file could not be read. Maybe wrong permissions?"
+msgstr "Den begärda filen kunde inte läsas. Kanske fel behörigheter?"
+
+#: cps/helper.py:278
+#, python-format
+msgid "Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgstr "Byt namn på titel från: \"%(src)s\" till \"%(dest)s\" misslyckades med fel: %(error)s"
+
+#: cps/helper.py:287
+#, python-format
+msgid "Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgstr "Byt namn på författare från: \"%(src)s\" till \"%(dest)s\" misslyckades med fel: %(error)s"
+
+#: cps/helper.py:309 cps/helper.py:318
+#, python-format
+msgid "File %(file)s not found on Google Drive"
+msgstr "Filen %(file)s hittades inte på Google Drive"
+
+#: cps/helper.py:336
+#, python-format
+msgid "Book path %(path)s not found on Google Drive"
+msgstr "Boksökvägen %(path)s hittades inte på Google Drive"
+
+#: cps/helper.py:597
+msgid "Error excecuting UnRar"
+msgstr "Fel vid körning av UnRar"
+
+#: cps/helper.py:599
+msgid "Unrar binary file not found"
+msgstr "Unrar binärfil hittades inte"
+
+#: cps/helper.py:648
+msgid "Waiting"
+msgstr "Väntar"
+
+#: cps/helper.py:650
+msgid "Failed"
+msgstr "Misslyckades"
+
+#: cps/helper.py:652
+msgid "Started"
+msgstr "Startad"
+
+#: cps/helper.py:654
+msgid "Finished"
+msgstr "Klar"
+
+#: cps/helper.py:656
+msgid "Unknown Status"
+msgstr "Okänd status"
+
+#: cps/helper.py:661
+msgid "E-mail: "
+msgstr "E-post: "
+
+#: cps/helper.py:663 cps/helper.py:667
+msgid "Convert: "
+msgstr "Konvertera: "
+
+#: cps/helper.py:665
+msgid "Upload: "
+msgstr "Överför: "
+
+#: cps/helper.py:669
+msgid "Unknown Task: "
+msgstr "Okänd uppgift: "
+
+#: cps/web.py:1155 cps/web.py:2860
+msgid "Unknown"
+msgstr "Okänd"
+
+#: cps/web.py:1164 cps/web.py:1195 cps/web.py:1280
+msgid "HTTP Error"
+msgstr "HTTP-fel"
+
+#: cps/web.py:1166 cps/web.py:1197 cps/web.py:1281
+msgid "Connection error"
+msgstr "Anslutningsfel"
+
+#: cps/web.py:1168 cps/web.py:1199 cps/web.py:1282
+msgid "Timeout while establishing connection"
+msgstr "Tiden ute när du etablerade anslutning"
+
+#: cps/web.py:1170 cps/web.py:1201 cps/web.py:1283
+msgid "General error"
+msgstr "Allmänt fel"
+
+#: cps/web.py:1176
+msgid "Unexpected data while reading update information"
+msgstr "Oväntade data vid läsning av uppdateringsinformation"
+
+#: cps/web.py:1183
+msgid "No update available. You already have the latest version installed"
+msgstr "Ingen uppdatering tillgänglig. Du har redan den senaste versionen installerad"
+
+#: cps/web.py:1208
+msgid "A new update is available. Click on the button below to update to the latest version."
+msgstr "En ny uppdatering är tillgänglig. Klicka på knappen nedan för att uppdatera till den senaste versionen."
+
+#: cps/web.py:1258
+msgid "Could not fetch update information"
+msgstr "Kunde inte hämta uppdateringsinformation"
+
+#: cps/web.py:1273
+msgid "Requesting update package"
+msgstr "Begär uppdateringspaketet"
+
+#: cps/web.py:1274
+msgid "Downloading update package"
+msgstr "Hämtar uppdateringspaketet"
+
+#: cps/web.py:1275
+msgid "Unzipping update package"
+msgstr "Packar upp uppdateringspaketet"
+
+#: cps/web.py:1276
+msgid "Replacing files"
+msgstr "Ersätta filer"
+
+#: cps/web.py:1277
+msgid "Database connections are closed"
+msgstr "Databasanslutningarna är stängda"
+
+#: cps/web.py:1278
+msgid "Stopping server"
+msgstr "Stoppar server"
+
+#: cps/web.py:1279
+msgid "Update finished, please press okay and reload page"
+msgstr "Uppdatering klar, tryck på okej och uppdatera sidan"
+
+#: cps/web.py:1280 cps/web.py:1281 cps/web.py:1282 cps/web.py:1283
+msgid "Update failed:"
+msgstr "Uppdateringen misslyckades:"
+
+#: cps/web.py:1306
+msgid "Recently Added Books"
+msgstr "Nyligen tillagda böcker"
+
+#: cps/web.py:1316
+msgid "Newest Books"
+msgstr "Nyaste böcker"
+
+#: cps/web.py:1328
+msgid "Oldest Books"
+msgstr "Äldsta böcker"
+
+#: cps/web.py:1340
+msgid "Books (A-Z)"
+msgstr "Böcker (A-Ö)"
+
+#: cps/web.py:1351
+msgid "Books (Z-A)"
+msgstr "Böcker (Ö-A)"
+
+#: cps/web.py:1380
+msgid "Hot Books (most downloaded)"
+msgstr "Heta böcker (mest hämtade)"
+
+#: cps/web.py:1393
+msgid "Best rated books"
+msgstr "Bäst rankade böcker"
+
+#: cps/templates/index.xml:39 cps/web.py:1406
+msgid "Random Books"
+msgstr "Slumpmässiga böcker"
+
+#: cps/web.py:1421
+msgid "Author list"
+msgstr "Författarlista"
+
+#: cps/web.py:1433 cps/web.py:1524 cps/web.py:1688 cps/web.py:2231
+msgid "Error opening eBook. File does not exist or file is not accessible:"
+msgstr "Fel vid öppnande av e-bok. Filen finns inte eller filen är inte tillgänglig:"
+
+#: cps/web.py:1461
+msgid "Publisher list"
+msgstr "Lista över förlag"
+
+#: cps/web.py:1475
+#, python-format
+msgid "Publisher: %(name)s"
+msgstr "Förlag: %(name)s"
+
+#: cps/templates/index.xml:83 cps/web.py:1507
+msgid "Series list"
+msgstr "Serielista"
+
+#: cps/web.py:1522
+#, python-format
+msgid "Series: %(serie)s"
+msgstr "Serier: %(serie)s"
+
+#: cps/web.py:1551
+msgid "Available languages"
+msgstr "Tillgängliga språk"
+
+#: cps/web.py:1571
+#, python-format
+msgid "Language: %(name)s"
+msgstr "Språk: %(name)s"
+
+#: cps/templates/index.xml:76 cps/web.py:1582
+msgid "Category list"
+msgstr "Kategorilista"
+
+#: cps/web.py:1596
+#, python-format
+msgid "Category: %(name)s"
+msgstr "Kategori: %(name)s"
+
+#: cps/templates/layout.html:71 cps/web.py:1724
+msgid "Tasks"
+msgstr "Uppgifter"
+
+#: cps/web.py:1758
+msgid "Statistics"
+msgstr "Statistik"
+
+#: cps/web.py:1865
+msgid "Callback domain is not verified, please follow steps to verify domain in google developer console"
+msgstr "Återuppringningsdomänen är inte verifierad, följ stegen för att verifiera domänen i Google utvecklarkonsol"
+
+#: cps/web.py:1940
+msgid "Server restarted, please reload page"
+msgstr "Server startas om, vänligen uppdatera sidan"
+
+#: cps/web.py:1943
+msgid "Performing shutdown of server, please close window"
+msgstr "Stänger servern, vänligen stäng fönstret"
+
+#: cps/web.py:1962
+msgid "Update done"
+msgstr "Uppdatering klar"
+
+#: cps/web.py:2032
+msgid "Published after "
+msgstr "Publicerad efter "
+
+#: cps/web.py:2039
+msgid "Published before "
+msgstr "Publicerad före "
+
+#: cps/web.py:2053
+#, python-format
+msgid "Rating <= %(rating)s"
+msgstr "Betyg <= %(rating)s"
+
+#: cps/web.py:2055
+#, python-format
+msgid "Rating >= %(rating)s"
+msgstr "Betyg >= %(rating)s"
+
+#: cps/web.py:2114 cps/web.py:2123
+msgid "search"
+msgstr "sök"
+
+#: cps/templates/index.xml:47 cps/templates/index.xml:51
+#: cps/templates/layout.html:146 cps/web.py:2190
+msgid "Read Books"
+msgstr "Lästa böcker"
+
+#: cps/templates/index.xml:55 cps/templates/index.xml:59
+#: cps/templates/layout.html:148 cps/web.py:2193
+msgid "Unread Books"
+msgstr "Olästa böcker"
+
+#: cps/web.py:2241 cps/web.py:2243 cps/web.py:2245 cps/web.py:2257
+msgid "Read a Book"
+msgstr "Läs en bok"
+
+#: cps/web.py:2316 cps/web.py:3219
+msgid "Please fill out all fields!"
+msgstr "Fyll i alla fält!"
+
+#: cps/web.py:2317 cps/web.py:2338 cps/web.py:2342 cps/web.py:2347
+#: cps/web.py:2349
+msgid "register"
+msgstr "registrera"
+
+#: cps/web.py:2337 cps/web.py:3435
+msgid "An unknown error occurred. Please try again later."
+msgstr "Ett okänt fel uppstod. Försök igen senare."
+
+#: cps/web.py:2340
+msgid "Your e-mail is not allowed to register"
+msgstr "Din e-post är inte tillåten att registrera"
+
+#: cps/web.py:2343
+msgid "Confirmation e-mail was send to your e-mail account."
+msgstr "Bekräftelsemail skickades till ditt e-postkonto."
+
+#: cps/web.py:2346
+msgid "This username or e-mail address is already in use."
+msgstr "Det här användarnamnet eller e-postadressen är redan i bruk."
+
+#: cps/web.py:2363 cps/web.py:2459
+#, python-format
+msgid "you are now logged in as: '%(nickname)s'"
+msgstr "du är nu inloggad som: \"%(nickname)s\""
+
+#: cps/web.py:2368
+msgid "Wrong Username or Password"
+msgstr "Fel användarnamn eller lösenord"
+
+#: cps/web.py:2374 cps/web.py:2395
+msgid "login"
+msgstr "logga in"
+
+#: cps/web.py:2407 cps/web.py:2438
+msgid "Token not found"
+msgstr "Token hittades inte"
+
+#: cps/web.py:2415 cps/web.py:2446
+msgid "Token has expired"
+msgstr "Token har löpt ut"
+
+#: cps/web.py:2423
+msgid "Success! Please return to your device"
+msgstr "Lyckades! Vänligen återvänd till din enhet"
+
+#: cps/web.py:2473
+msgid "Please configure the SMTP mail settings first..."
+msgstr "Konfigurera SMTP-postinställningarna först..."
+
+#: cps/web.py:2477
+#, python-format
+msgid "Book successfully queued for sending to %(kindlemail)s"
+msgstr "Boken är i kö för att skicka till %(kindlemail)s"
+
+#: cps/web.py:2481
+#, python-format
+msgid "There was an error sending this book: %(res)s"
+msgstr "Det gick inte att skicka den här boken: %(res)s"
+
+#: cps/web.py:2483 cps/web.py:3273
+msgid "Please configure your kindle e-mail address first..."
+msgstr "Konfigurera din kindle-e-postadress först..."
+
+#: cps/web.py:2494 cps/web.py:2546
+msgid "Invalid shelf specified"
+msgstr "Ogiltig hylla specificerad"
+
+#: cps/web.py:2501
+#, python-format
+msgid "Sorry you are not allowed to add a book to the the shelf: %(shelfname)s"
+msgstr ""
+
+#: cps/web.py:2509
+msgid "You are not allowed to edit public shelves"
+msgstr "Du får inte redigera offentliga hyllor"
+
+#: cps/web.py:2518
+#, python-format
+msgid "Book is already part of the shelf: %(shelfname)s"
+msgstr "Boken är redan en del av hyllan: %(shelfname)s"
+
+#: cps/web.py:2532
+#, python-format
+msgid "Book has been added to shelf: %(sname)s"
+msgstr "Boken har lagts till i hyllan: %(sname)s"
+
+#: cps/web.py:2551
+#, python-format
+msgid "You are not allowed to add a book to the the shelf: %(name)s"
+msgstr "Du får inte lägga till en bok i hyllan: %(name)s"
+
+#: cps/web.py:2556
+msgid "User is not allowed to edit public shelves"
+msgstr "Användaren får inte redigera publika hyllor"
+
+#: cps/web.py:2574
+#, python-format
+msgid "Books are already part of the shelf: %(name)s"
+msgstr "Böcker är redan en del av hyllan: %(name)s"
+
+#: cps/web.py:2588
+#, python-format
+msgid "Books have been added to shelf: %(sname)s"
+msgstr "Böcker har lagts till hyllan: %(sname)s"
+
+#: cps/web.py:2590
+#, python-format
+msgid "Could not add books to shelf: %(sname)s"
+msgstr "Kunde inte lägga till böcker till hyllan: %(sname)s"
+
+#: cps/web.py:2627
+#, python-format
+msgid "Book has been removed from shelf: %(sname)s"
+msgstr "Boken har tagits bort från hyllan: %(sname)s"
+
+#: cps/web.py:2633
+#, python-format
+msgid "Sorry you are not allowed to remove a book from this shelf: %(sname)s"
+msgstr "Tyvärr har du inte rätt att ta bort en bok från den här hyllan: %(sname)s"
+
+#: cps/web.py:2653 cps/web.py:2677
+#, python-format
+msgid "A shelf with the name '%(title)s' already exists."
+msgstr "En hylla med namnet '%(title)s' finns redan."
+
+#: cps/web.py:2658
+#, python-format
+msgid "Shelf %(title)s created"
+msgstr "Hyllan %(title)s skapad"
+
+#: cps/web.py:2660 cps/web.py:2688
+msgid "There was an error"
+msgstr "Det fanns ett fel"
+
+#: cps/web.py:2661 cps/web.py:2663
+msgid "create a shelf"
+msgstr "skapa en hylla"
+
+#: cps/web.py:2686
+#, python-format
+msgid "Shelf %(title)s changed"
+msgstr "Hyllan %(title)s ändrad"
+
+#: cps/web.py:2689 cps/web.py:2691
+msgid "Edit a shelf"
+msgstr "Redigera en hylla"
+
+#: cps/web.py:2712
+#, python-format
+msgid "successfully deleted shelf %(name)s"
+msgstr "tog bort hyllan %(name)s"
+
+#: cps/web.py:2739
+#, python-format
+msgid "Shelf: '%(name)s'"
+msgstr "Hylla: '%(name)s'"
+
+#: cps/web.py:2742
+msgid "Error opening shelf. Shelf does not exist or is not accessible"
+msgstr "Fel vid öppning av hyllan. Hylla finns inte eller är inte tillgänglig"
+
+#: cps/web.py:2773
+#, python-format
+msgid "Change order of Shelf: '%(name)s'"
+msgstr "Ändra ordning på hyllan: '%(name)s'"
+
+#: cps/web.py:2802 cps/web.py:3225
+msgid "E-mail is not from valid domain"
+msgstr "E-posten är inte från giltig domän"
+
+#: cps/web.py:2804 cps/web.py:2847 cps/web.py:2850
+#, python-format
+msgid "%(name)s's profile"
+msgstr "%(name)ss profil"
+
+#: cps/web.py:2845
+msgid "Found an existing account for this e-mail address."
+msgstr "Hittade ett befintligt konto för den här e-postadressen."
+
+#: cps/web.py:2848
+msgid "Profile updated"
+msgstr "Profilen uppdaterad"
+
+#: cps/web.py:2876
+msgid "Admin page"
+msgstr "Administrationssida"
+
+#: cps/web.py:2956 cps/web.py:3130
+msgid "Calibre-Web configuration updated"
+msgstr "Calibre-Web konfiguration uppdaterad"
+
+#: cps/templates/admin.html:100 cps/web.py:2969
+msgid "UI Configuration"
+msgstr "Användargränssnitt konfiguration"
+
+#: cps/web.py:2987
+msgid "Import of optional Google Drive requirements missing"
+msgstr "Import av valfri Google Drive krav saknas"
+
+#: cps/web.py:2990
+msgid "client_secrets.json is missing or not readable"
+msgstr "client_secrets.json saknas eller inte kan läsas"
+
+#: cps/web.py:2995 cps/web.py:3022
+msgid "client_secrets.json is not configured for web application"
+msgstr "client_secrets.json är inte konfigurerad för webbapplikation"
+
+#: cps/templates/admin.html:99 cps/web.py:3025 cps/web.py:3051 cps/web.py:3063
+#: cps/web.py:3106 cps/web.py:3121 cps/web.py:3138 cps/web.py:3145
+#: cps/web.py:3160
+msgid "Basic Configuration"
+msgstr "Grundläggande konfiguration"
+
+#: cps/web.py:3048
+msgid "Keyfile location is not valid, please enter correct path"
+msgstr "Platsen för Keyfile är inte giltig, ange rätt sökväg"
+
+#: cps/web.py:3060
+msgid "Certfile location is not valid, please enter correct path"
+msgstr "Platsen för Certfile är inte giltig, ange rätt sökväg"
+
+#: cps/web.py:3103
+msgid "Logfile location is not valid, please enter correct path"
+msgstr "Platsen för Logfile platsen är inte giltig, ange rätt sökväg"
+
+#: cps/web.py:3142
+msgid "DB location is not valid, please enter correct path"
+msgstr "Platsen för DB är inte giltig, ange rätt sökväg"
+
+#: cps/templates/admin.html:33 cps/web.py:3221 cps/web.py:3227 cps/web.py:3243
+msgid "Add new user"
+msgstr "Lägg till ny användare"
+
+#: cps/web.py:3233
+#, python-format
+msgid "User '%(user)s' created"
+msgstr "Användaren '%(user)s' skapad"
+
+#: cps/web.py:3237
+msgid "Found an existing account for this e-mail address or nickname."
+msgstr "Hittade ett befintligt konto för den här e-postadressen eller smeknamnet."
+
+#: cps/web.py:3261 cps/web.py:3275
+msgid "E-mail server settings updated"
+msgstr "E-postserverinställningar uppdaterade"
+
+#: cps/web.py:3268
+#, python-format
+msgid "Test e-mail successfully send to %(kindlemail)s"
+msgstr "Test-e-post skicka till %(kindlemail)s"
+
+#: cps/web.py:3271
+#, python-format
+msgid "There was an error sending the Test e-mail: %(res)s"
+msgstr "Det gick inte att skicka Testmeddelandet: %(res)s"
+
+#: cps/web.py:3276
+msgid "Edit e-mail server settings"
+msgstr "Redigera inställningar för e-postserver"
+
+#: cps/web.py:3301
+#, python-format
+msgid "User '%(nick)s' deleted"
+msgstr "Användaren '%(nick)s' borttagen"
+
+#: cps/web.py:3410
+#, python-format
+msgid "User '%(nick)s' updated"
+msgstr "Användaren '%(nick)s' uppdaterad"
+
+#: cps/web.py:3413
+msgid "An unknown error occured."
+msgstr "Ett okänt fel uppstod."
+
+#: cps/web.py:3415
+#, python-format
+msgid "Edit User %(nick)s"
+msgstr "Redigera användaren %(nick)s"
+
+#: cps/web.py:3432
+#, python-format
+msgid "Password for user %(user)s reset"
+msgstr "Lösenord för användaren %(user)s återställd"
+
+#: cps/web.py:3446 cps/web.py:3647
+msgid "Error opening eBook. File does not exist or file is not accessible"
+msgstr "Det gick inte att öppna e-boken. Filen finns inte eller filen är inte tillgänglig"
+
+#: cps/web.py:3471 cps/web.py:3930
+msgid "edit metadata"
+msgstr "redigera metadata"
+
+#: cps/web.py:3564 cps/web.py:3800
+#, python-format
+msgid "File extension '%(ext)s' is not allowed to be uploaded to this server"
+msgstr "Filändelsen '%(ext)s' får inte laddas upp till den här servern"
+
+#: cps/web.py:3568 cps/web.py:3804
+msgid "File to be uploaded must have an extension"
+msgstr "Filen som ska laddas upp måste ha en ändelse"
+
+#: cps/web.py:3580 cps/web.py:3824
+#, python-format
+msgid "Failed to create path %(path)s (Permission denied)."
+msgstr "Det gick inte att skapa sökväg %(path)s (behörighet nekad)."
+
+#: cps/web.py:3585
+#, python-format
+msgid "Failed to store file %(file)s."
+msgstr "Det gick inte att lagra filen %(file)s."
+
+#: cps/web.py:3601
+#, python-format
+msgid "File format %(ext)s added to %(book)s"
+msgstr "Filformatet %(ext)s lades till %(book)s"
+
+#: cps/web.py:3619
+#, python-format
+msgid "Failed to create path for cover %(path)s (Permission denied)."
+msgstr "Det gick inte att skapa sökväg för omslag %(path)s (behörighet nekad)."
+
+#: cps/web.py:3626
+#, python-format
+msgid "Failed to store cover-file %(cover)s."
+msgstr "Det gick inte att lagra omslagsfilen %(cover)s."
+
+#: cps/web.py:3629
+msgid "Cover-file is not a valid image file"
+msgstr "Omslagsfilen är inte en giltig bildfil"
+
+#: cps/web.py:3659 cps/web.py:3668 cps/web.py:3672
+msgid "unknown"
+msgstr "okänd"
+
+#: cps/web.py:3691
+msgid "Cover is not a jpg file, can't save"
+msgstr "Omslag är inte en jpg-fil, kan inte spara"
+
+#: cps/web.py:3739
+#, python-format
+msgid "%(langname)s is not a valid language"
+msgstr "%(langname)s är inte ett giltigt språk"
+
+#: cps/web.py:3770
+msgid "Metadata successfully updated"
+msgstr "Metadata uppdaterades"
+
+#: cps/web.py:3779
+msgid "Error editing book, please check logfile for details"
+msgstr "Det gick inte att redigera boken, kontrollera loggfilen för mer information"
+
+#: cps/web.py:3829
+#, python-format
+msgid "Failed to store file %(file)s (Permission denied)."
+msgstr "Det gick inte att lagra filen %(file)s (behörighet nekad)."
+
+#: cps/web.py:3834
+#, python-format
+msgid "Failed to delete file %(file)s (Permission denied)."
+msgstr "Det gick inte att ta bort filen %(file)s (behörighet nekad)."
+
+#: cps/web.py:3947
+msgid "Source or destination format for conversion missing"
+msgstr "Källa eller målformat för konvertering saknas"
+
+#: cps/web.py:3957
+#, python-format
+msgid "Book successfully queued for converting to %(book_format)s"
+msgstr "Boken är i kö för konvertering till %(book_format)s"
+
+#: cps/web.py:3961
+#, python-format
+msgid "There was an error converting this book: %(res)s"
+msgstr "Det gick inte att konvertera den här boken: %(res)s"
+
+#: cps/worker.py:287
+#, python-format
+msgid "Ebook-converter failed: %(error)s"
+msgstr "E-bokkonverteraren misslyckades: %(error)s"
+
+#: cps/worker.py:298
+#, python-format
+msgid "Kindlegen failed with Error %(error)s. Message: %(message)s"
+msgstr "Kindlegen misslyckades med fel %(error)s. Meddelande: %(message)s"
+
+#: cps/templates/admin.html:6
+msgid "User list"
+msgstr "Användarlista"
+
+#: cps/templates/admin.html:9
+msgid "Nickname"
+msgstr "Smeknamn"
+
+#: cps/templates/admin.html:10
+msgid "E-mail"
+msgstr "E-post"
+
+#: cps/templates/admin.html:11
+msgid "Kindle"
+msgstr "Kindle"
+
+#: cps/templates/admin.html:12
+msgid "DLS"
+msgstr "DLS"
+
+#: cps/templates/admin.html:13 cps/templates/layout.html:74
+msgid "Admin"
+msgstr "Administratör"
+
+#: cps/templates/admin.html:14 cps/templates/detail.html:22
+#: cps/templates/detail.html:31
+msgid "Download"
+msgstr "Hämta"
+
+#: cps/templates/admin.html:15 cps/templates/layout.html:64
+msgid "Upload"
+msgstr "Ladda upp"
+
+#: cps/templates/admin.html:16
+msgid "Edit"
+msgstr "Redigera"
+
+#: cps/templates/admin.html:39
+msgid "SMTP e-mail server settings"
+msgstr "Inställningar för SMTP-e-postserver"
+
+#: cps/templates/admin.html:42 cps/templates/email_edit.html:11
+msgid "SMTP hostname"
+msgstr "SMTP-värdnamn"
+
+#: cps/templates/admin.html:43
+msgid "SMTP port"
+msgstr "SMTP-port"
+
+#: cps/templates/admin.html:44
+msgid "SSL"
+msgstr "SSL"
+
+#: cps/templates/admin.html:45 cps/templates/email_edit.html:27
+msgid "SMTP login"
+msgstr "SMTP-inloggning"
+
+#: cps/templates/admin.html:46
+msgid "From mail"
+msgstr "Från meddelande"
+
+#: cps/templates/admin.html:56
+msgid "Change SMTP settings"
+msgstr "Ändra SMTP-inställningar"
+
+#: cps/templates/admin.html:62
+msgid "Configuration"
+msgstr "Konfiguration"
+
+#: cps/templates/admin.html:65
+msgid "Calibre DB dir"
+msgstr "Calibre DB dir"
+
+#: cps/templates/admin.html:69
+msgid "Log level"
+msgstr "Loggnivå"
+
+#: cps/templates/admin.html:73
+msgid "Port"
+msgstr "Port"
+
+#: cps/templates/admin.html:79 cps/templates/config_view_edit.html:23
+msgid "Books per page"
+msgstr "Böcker per sida"
+
+#: cps/templates/admin.html:83
+msgid "Uploading"
+msgstr "Laddar upp"
+
+#: cps/templates/admin.html:87
+msgid "Anonymous browsing"
+msgstr "Anonym surfning"
+
+#: cps/templates/admin.html:91
+msgid "Public registration"
+msgstr "Publik registrering"
+
+#: cps/templates/admin.html:95 cps/templates/remote_login.html:4
+msgid "Remote login"
+msgstr "Fjärrinloggning"
+
+#: cps/templates/admin.html:106
+msgid "Administration"
+msgstr "Administration"
+
+#: cps/templates/admin.html:107
+msgid "Reconnect to Calibre DB"
+msgstr "Anslut till Calibre DB igen"
+
+#: cps/templates/admin.html:108
+msgid "Restart Calibre-Web"
+msgstr "Starta om Calibre-Web"
+
+#: cps/templates/admin.html:109
+msgid "Stop Calibre-Web"
+msgstr "Stoppa Calibre-Web"
+
+#: cps/templates/admin.html:115
+msgid "Update"
+msgstr "Uppdatera"
+
+#: cps/templates/admin.html:119
+msgid "Version"
+msgstr "Version"
+
+#: cps/templates/admin.html:120
+msgid "Details"
+msgstr "Detaljer"
+
+#: cps/templates/admin.html:126
+msgid "Current version"
+msgstr "Aktuell version"
+
+#: cps/templates/admin.html:132
+msgid "Check for update"
+msgstr "Sök efter uppdatering"
+
+#: cps/templates/admin.html:133
+msgid "Perform Update"
+msgstr "Utför uppdatering"
+
+#: cps/templates/admin.html:145
+msgid "Do you really want to restart Calibre-Web?"
+msgstr "Är du säker på att du vill starta om Calibre-Web?"
+
+#: cps/templates/admin.html:150 cps/templates/admin.html:164
+#: cps/templates/admin.html:184 cps/templates/shelf.html:63
+msgid "Ok"
+msgstr "Ok"
+
+#: cps/templates/admin.html:151 cps/templates/admin.html:165
+#: cps/templates/book_edit.html:178 cps/templates/book_edit.html:200
+#: cps/templates/config_edit.html:212 cps/templates/config_view_edit.html:168
+#: cps/templates/email_edit.html:40 cps/templates/email_edit.html:75
+#: cps/templates/shelf.html:64 cps/templates/shelf_edit.html:19
+#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:155
+msgid "Back"
+msgstr "Tillbaka"
+
+#: cps/templates/admin.html:163
+msgid "Do you really want to stop Calibre-Web?"
+msgstr "Är du säker på att du vill stoppa Calibre-Web?"
+
+#: cps/templates/admin.html:175
+msgid "Updating, please do not reload page"
+msgstr "Uppdaterar, vänligen uppdatera inte sidan"
+
+#: cps/templates/author.html:15
+msgid "via"
+msgstr "via"
+
+#: cps/templates/author.html:23
+msgid "In Library"
+msgstr "I biblioteket"
+
+#: cps/templates/author.html:71
+msgid "More by"
+msgstr "Mer av"
+
+#: cps/templates/book_edit.html:16
+msgid "Delete Book"
+msgstr "Ta bort boken"
+
+#: cps/templates/book_edit.html:19
+msgid "Delete formats:"
+msgstr "Ta bort format:"
+
+#: cps/templates/book_edit.html:22 cps/templates/book_edit.html:199
+#: cps/templates/email_edit.html:73 cps/templates/email_edit.html:74
+msgid "Delete"
+msgstr "Ta bort"
+
+#: cps/templates/book_edit.html:30
+msgid "Convert book format:"
+msgstr "Konvertera bokformat:"
+
+#: cps/templates/book_edit.html:34
+msgid "Convert from:"
+msgstr "Konvertera från:"
+
+#: cps/templates/book_edit.html:36 cps/templates/book_edit.html:43
+msgid "select an option"
+msgstr "välj ett alternativ"
+
+#: cps/templates/book_edit.html:41
+msgid "Convert to:"
+msgstr "Konvertera till:"
+
+#: cps/templates/book_edit.html:50
+msgid "Convert book"
+msgstr "Konvertera boken"
+
+#: cps/templates/book_edit.html:59 cps/templates/search_form.html:6
+msgid "Book Title"
+msgstr "Boktitel"
+
+#: cps/templates/book_edit.html:63 cps/templates/book_edit.html:259
+#: cps/templates/book_edit.html:277 cps/templates/search_form.html:10
+msgid "Author"
+msgstr "Författare"
+
+#: cps/templates/book_edit.html:67 cps/templates/book_edit.html:264
+#: cps/templates/book_edit.html:279 cps/templates/search_form.html:106
+msgid "Description"
+msgstr "Beskrivning"
+
+#: cps/templates/book_edit.html:71 cps/templates/search_form.html:33
+msgid "Tags"
+msgstr "Taggar"
+
+#: cps/templates/book_edit.html:75 cps/templates/layout.html:157
+#: cps/templates/search_form.html:53
+msgid "Series"
+msgstr "Serier"
+
+#: cps/templates/book_edit.html:79
+msgid "Series id"
+msgstr "Serier-id"
+
+#: cps/templates/book_edit.html:83
+msgid "Rating"
+msgstr "Betyg"
+
+#: cps/templates/book_edit.html:87
+msgid "Cover URL (jpg, cover is downloaded and stored in database, field is afterwards empty again)"
+msgstr "Omslagswebbadress (jpg, omslag hämtas och lagras i databasen, fältet är efteråt tomt igen)"
+
+#: cps/templates/book_edit.html:91
+msgid "Upload Cover from local drive"
+msgstr "Ladda upp omslag från lokal enhet"
+
+#: cps/templates/book_edit.html:96 cps/templates/detail.html:149
+msgid "Publishing date"
+msgstr "Publiceringsdatum"
+
+#: cps/templates/book_edit.html:103 cps/templates/book_edit.html:261
+#: cps/templates/book_edit.html:278 cps/templates/detail.html:141
+#: cps/templates/search_form.html:14
+msgid "Publisher"
+msgstr "Förlag"
+
+#: cps/templates/book_edit.html:107 cps/templates/user_edit.html:31
+msgid "Language"
+msgstr "Språk"
+
+#: cps/templates/book_edit.html:117 cps/templates/search_form.html:117
+msgid "Yes"
+msgstr "Ja"
+
+#: cps/templates/book_edit.html:118 cps/templates/search_form.html:118
+msgid "No"
+msgstr "Nej"
+
+#: cps/templates/book_edit.html:164
+msgid "Upload format"
+msgstr "Ladda upp format"
+
+#: cps/templates/book_edit.html:173
+msgid "view book after edit"
+msgstr "visa bok efter redigering"
+
+#: cps/templates/book_edit.html:176 cps/templates/book_edit.html:212
+msgid "Get metadata"
+msgstr "Hämta metadata"
+
+#: cps/templates/book_edit.html:177 cps/templates/config_edit.html:210
+#: cps/templates/config_view_edit.html:167 cps/templates/login.html:20
+#: cps/templates/search_form.html:153 cps/templates/shelf_edit.html:17
+#: cps/templates/user_edit.html:153
+msgid "Submit"
+msgstr "Skicka"
+
+#: cps/templates/book_edit.html:191
+msgid "Are you really sure?"
+msgstr "Är du verkligen säker?"
+
+#: cps/templates/book_edit.html:194
+msgid "Book will be deleted from Calibre database"
+msgstr "Boken kommer att tas bort från Calibre-databasen"
+
+#: cps/templates/book_edit.html:195
+msgid "and from hard disk"
+msgstr "och från hårddisken"
+
+#: cps/templates/book_edit.html:215
+msgid "Keyword"
+msgstr "Sökord"
+
+#: cps/templates/book_edit.html:216
+msgid " Search keyword "
+msgstr " Sök sökord "
+
+#: cps/templates/book_edit.html:218 cps/templates/layout.html:46
+msgid "Go!"
+msgstr "Kör!"
+
+#: cps/templates/book_edit.html:222
+msgid "Click the cover to load metadata to the form"
+msgstr "Klicka på omslaget för att läsa in metadata till formuläret"
+
+#: cps/templates/book_edit.html:234 cps/templates/book_edit.html:274
+msgid "Loading..."
+msgstr "Läser in..."
+
+#: cps/templates/book_edit.html:239 cps/templates/layout.html:224
+msgid "Close"
+msgstr "Stäng"
+
+#: cps/templates/book_edit.html:266 cps/templates/book_edit.html:280
+msgid "Source"
+msgstr "Källa"
+
+#: cps/templates/book_edit.html:275
+msgid "Search error!"
+msgstr "Sökningsfel!"
+
+#: cps/templates/book_edit.html:276
+msgid "No Result(s) found! Please try aonther keyword."
+msgstr "Inga resultat hittades! Försök med ett annat sökord."
+
+#: cps/templates/config_edit.html:12
+msgid "Library Configuration"
+msgstr "Bibliotekets konfiguration"
+
+#: cps/templates/config_edit.html:19
+msgid "Location of Calibre database"
+msgstr "Plats för Calibre-databasen"
+
+#: cps/templates/config_edit.html:24
+msgid "Use Google Drive?"
+msgstr "Använda Google Drive?"
+
+#: cps/templates/config_edit.html:30
+msgid "Google Drive config problem"
+msgstr "Google Drive-konfigurationsproblem"
+
+#: cps/templates/config_edit.html:36
+msgid "Authenticate Google Drive"
+msgstr "Autentisera Google Drive"
+
+#: cps/templates/config_edit.html:40
+msgid "Please finish Google Drive setup after login"
+msgstr "Vänligen avsluta Google Drive-inställning efter inloggning"
+
+#: cps/templates/config_edit.html:44
+msgid "Google Drive Calibre folder"
+msgstr "Google Drive Calibre-mapp"
+
+#: cps/templates/config_edit.html:52
+msgid "Metadata Watch Channel ID"
+msgstr "Metadata Titta på kanal ID"
+
+#: cps/templates/config_edit.html:55
+msgid "Revoke"
+msgstr "Återkalla"
+
+#: cps/templates/config_edit.html:73
+msgid "Server Configuration"
+msgstr "Serverkonfiguration"
+
+#: cps/templates/config_edit.html:80
+msgid "Server Port"
+msgstr "Serverport"
+
+#: cps/templates/config_edit.html:84
+msgid "SSL certfile location (leave it empty for non-SSL Servers)"
+msgstr "SSL certfile plats (lämna den tom för icke-SSL-servrar)"
+
+#: cps/templates/config_edit.html:88
+msgid "SSL Keyfile location (leave it empty for non-SSL Servers)"
+msgstr "SSL Keyfile plats (lämna den tom för icke-SSL-servrar)"
+
+#: cps/templates/config_edit.html:99
+msgid "Logfile Configuration"
+msgstr "Loggfil konfiguration"
+
+#: cps/templates/config_edit.html:106
+msgid "Log Level"
+msgstr "Loggnivå"
+
+#: cps/templates/config_edit.html:115
+msgid "Location and name of logfile (calibre-web.log for no entry)"
+msgstr "Plats och namn på loggfilen (calibre-web.log för ingen post)"
+
+#: cps/templates/config_edit.html:126
+msgid "Feature Configuration"
+msgstr "Funktion konfiguration"
+
+#: cps/templates/config_edit.html:134
+msgid "Enable uploading"
+msgstr "Aktivera uppladdning"
+
+#: cps/templates/config_edit.html:138
+msgid "Enable anonymous browsing"
+msgstr "Aktivera anonym surfning"
+
+#: cps/templates/config_edit.html:142
+msgid "Enable public registration"
+msgstr "Aktivera offentlig registrering"
+
+#: cps/templates/config_edit.html:146
+msgid "Enable remote login (\"magic link\")"
+msgstr "Aktivera fjärrinloggning (\"magic link\")"
+
+#: cps/templates/config_edit.html:151
+msgid "Use"
+msgstr "Använd"
+
+#: cps/templates/config_edit.html:152
+msgid "Obtain an API Key"
+msgstr "Hämta en API-nyckel"
+
+#: cps/templates/config_edit.html:156
+msgid "Goodreads API Key"
+msgstr "Goodreads API-nyckel"
+
+#: cps/templates/config_edit.html:160
+msgid "Goodreads API Secret"
+msgstr "Goodreads API-hemlighet"
+
+#: cps/templates/config_edit.html:173
+msgid "External binaries"
+msgstr "Externa binärer"
+
+#: cps/templates/config_edit.html:181
+msgid "No converter"
+msgstr "Ingen konverterare"
+
+#: cps/templates/config_edit.html:183
+msgid "Use Kindlegen"
+msgstr "Använd Kindlegen"
+
+#: cps/templates/config_edit.html:185
+msgid "Use calibre's ebook converter"
+msgstr "Använd calibres e-bokkonverterare"
+
+#: cps/templates/config_edit.html:189
+msgid "E-Book converter settings"
+msgstr "Inställningar för e-bokkonverteraren"
+
+#: cps/templates/config_edit.html:193
+msgid "Path to convertertool"
+msgstr "Sökväg till convertertool"
+
+#: cps/templates/config_edit.html:199
+msgid "Location of Unrar binary"
+msgstr "Plats för Unrar-binär"
+
+#: cps/templates/config_edit.html:215 cps/templates/layout.html:82
+#: cps/templates/login.html:4
+msgid "Login"
+msgstr "Logga in"
+
+#: cps/templates/config_view_edit.html:12
+msgid "View Configuration"
+msgstr "Visa konfiguration"
+
+#: cps/templates/config_view_edit.html:19 cps/templates/layout.html:133
+#: cps/templates/layout.html:134 cps/templates/shelf_edit.html:7
+msgid "Title"
+msgstr "Titel"
+
+#: cps/templates/config_view_edit.html:27
+msgid "No. of random books to show"
+msgstr "Antal slumpmässiga böcker att visa"
+
+#: cps/templates/config_view_edit.html:31
+msgid "Regular expression for ignoring columns"
+msgstr "Reguljärt uttryck för att ignorera kolumner"
+
+#: cps/templates/config_view_edit.html:35
+msgid "Link read/unread status to Calibre column"
+msgstr "Länka läst/oläst status till Calibre-kolumn"
+
+#: cps/templates/config_view_edit.html:44
+msgid "Regular expression for title sorting"
+msgstr "Reguljärt uttryck för titelsortering"
+
+#: cps/templates/config_view_edit.html:48
+msgid "Tags for Mature Content"
+msgstr "Taggar för vuxeninnehåll"
+
+#: cps/templates/config_view_edit.html:62
+msgid "Default settings for new users"
+msgstr "Standardinställningar för nya användare"
+
+#: cps/templates/config_view_edit.html:70 cps/templates/user_edit.html:110
+msgid "Admin user"
+msgstr "Adminstratör användare"
+
+#: cps/templates/config_view_edit.html:74 cps/templates/user_edit.html:119
+msgid "Allow Downloads"
+msgstr "Tillåt Hämtningar"
+
+#: cps/templates/config_view_edit.html:78 cps/templates/user_edit.html:123
+msgid "Allow Uploads"
+msgstr "Tillåt Uppladdningar"
+
+#: cps/templates/config_view_edit.html:82 cps/templates/user_edit.html:127
+msgid "Allow Edit"
+msgstr "Tillåt Redigera"
+
+#: cps/templates/config_view_edit.html:86 cps/templates/user_edit.html:131
+msgid "Allow Delete books"
+msgstr "Tillåt Ta bort böcker"
+
+#: cps/templates/config_view_edit.html:90 cps/templates/user_edit.html:136
+msgid "Allow Changing Password"
+msgstr "Tillåt Ändra lösenord"
+
+#: cps/templates/config_view_edit.html:94 cps/templates/user_edit.html:140
+msgid "Allow Editing Public Shelfs"
+msgstr "Tillåt Redigering av offentliga hyllor"
+
+#: cps/templates/config_view_edit.html:104
+msgid "Default visibilities for new users"
+msgstr "Standardvisibiliteter för nya användare"
+
+#: cps/templates/config_view_edit.html:112 cps/templates/user_edit.html:58
+msgid "Show random books"
+msgstr "Visa slumpmässiga böcker"
+
+#: cps/templates/config_view_edit.html:116 cps/templates/user_edit.html:62
+msgid "Show recent books"
+msgstr "Visa senaste böcker"
+
+#: cps/templates/config_view_edit.html:120 cps/templates/user_edit.html:66
+msgid "Show sorted books"
+msgstr "Visa sorterade böcker"
+
+#: cps/templates/config_view_edit.html:124 cps/templates/user_edit.html:70
+msgid "Show hot books"
+msgstr "Visa heta böcker"
+
+#: cps/templates/config_view_edit.html:128 cps/templates/user_edit.html:74
+msgid "Show best rated books"
+msgstr "Visa böcker med bästa betyg"
+
+#: cps/templates/config_view_edit.html:132 cps/templates/user_edit.html:78
+msgid "Show language selection"
+msgstr "Visa språkval"
+
+#: cps/templates/config_view_edit.html:136 cps/templates/user_edit.html:82
+msgid "Show series selection"
+msgstr "Visa serieval"
+
+#: cps/templates/config_view_edit.html:140 cps/templates/user_edit.html:86
+msgid "Show category selection"
+msgstr "Visa kategorival"
+
+#: cps/templates/config_view_edit.html:144 cps/templates/user_edit.html:90
+msgid "Show author selection"
+msgstr "Visa författarval"
+
+#: cps/templates/config_view_edit.html:148 cps/templates/user_edit.html:94
+msgid "Show publisher selection"
+msgstr "Visa urval av förlag"
+
+#: cps/templates/config_view_edit.html:152 cps/templates/user_edit.html:98
+msgid "Show read and unread"
+msgstr "Visa lästa och olästa"
+
+#: cps/templates/config_view_edit.html:156 cps/templates/user_edit.html:102
+msgid "Show random books in detail view"
+msgstr "Visa slumpmässiga böcker i detaljvyn"
+
+#: cps/templates/config_view_edit.html:160 cps/templates/user_edit.html:115
+msgid "Show mature content"
+msgstr "Visa vuxeninnehåll"
+
+#: cps/templates/detail.html:63
+msgid "Read in browser"
+msgstr "Läs i webbläsaren"
+
+#: cps/templates/detail.html:102
+msgid "Book"
+msgstr "Bok"
+
+#: cps/templates/detail.html:102
+msgid "of"
+msgstr "av"
+
+#: cps/templates/detail.html:108
+msgid "language"
+msgstr "språk"
+
+#: cps/templates/detail.html:186
+msgid "Read"
+msgstr "Läst"
+
+#: cps/templates/detail.html:196
+msgid "Description:"
+msgstr "Beskrivning:"
+
+#: cps/templates/detail.html:209 cps/templates/search.html:14
+msgid "Add to shelf"
+msgstr "Lägg till hyllan"
+
+#: cps/templates/detail.html:271
+msgid "Edit metadata"
+msgstr "Redigera metadata"
+
+#: cps/templates/email_edit.html:15
+msgid "SMTP port (usually 25 for plain SMTP and 465 for SSL and 587 for STARTTLS)"
+msgstr "SMTP-port (vanligtvis 25 för vanlig SMTP och 465 för SSL och 587 för STARTTLS)"
+
+#: cps/templates/email_edit.html:19
+msgid "Encryption"
+msgstr "Kryptering"
+
+#: cps/templates/email_edit.html:21
+msgid "None"
+msgstr "Ingen"
+
+#: cps/templates/email_edit.html:22
+msgid "STARTTLS"
+msgstr "STARTTLS"
+
+#: cps/templates/email_edit.html:23
+msgid "SSL/TLS"
+msgstr "SSL/TLS"
+
+#: cps/templates/email_edit.html:31
+msgid "SMTP password"
+msgstr "SMTP-lösenord"
+
+#: cps/templates/email_edit.html:35
+msgid "From e-mail"
+msgstr "Från e-post"
+
+#: cps/templates/email_edit.html:38
+msgid "Save settings"
+msgstr "Spara inställningarna"
+
+#: cps/templates/email_edit.html:39
+msgid "Save settings and send Test E-Mail"
+msgstr "Spara inställningarna och skicka test-e-post"
+
+#: cps/templates/email_edit.html:43
+msgid "Allowed domains for registering"
+msgstr "Tillåtna domäner för registrering"
+
+#: cps/templates/email_edit.html:47
+msgid "Enter domainname"
+msgstr "Ange domännamn"
+
+#: cps/templates/email_edit.html:55
+msgid "Add Domain"
+msgstr "Lägg till domän"
+
+#: cps/templates/email_edit.html:58
+msgid "Add"
+msgstr "Lägg till"
+
+#: cps/templates/email_edit.html:72
+msgid "Do you really want to delete this domain rule?"
+msgstr "Är du säker på att du vill ta bort den här domänregeln?"
+
+#: cps/templates/feed.xml:21 cps/templates/layout.html:208
+msgid "Next"
+msgstr "Nästa"
+
+#: cps/templates/feed.xml:33 cps/templates/index.xml:11
+#: cps/templates/layout.html:43 cps/templates/layout.html:44
+msgid "Search"
+msgstr "Sök"
+
+#: cps/templates/http_error.html:23
+msgid "Back to home"
+msgstr "Tillbaka till hemmet"
+
+#: cps/templates/index.html:5
+msgid "Discover (Random Books)"
+msgstr "Upptäck (slumpmässiga böcker)"
+
+#: cps/templates/index.xml:6
+msgid "Start"
+msgstr "Starta"
+
+#: cps/templates/index.xml:18 cps/templates/layout.html:139
+msgid "Hot Books"
+msgstr "Heta böcker"
+
+#: cps/templates/index.xml:22
+msgid "Popular publications from this catalog based on Downloads."
+msgstr "Populära publikationer från den här katalogen baserad på hämtningar."
+
+#: cps/templates/index.xml:25 cps/templates/layout.html:142
+msgid "Best rated Books"
+msgstr "Bäst rankade böcker"
+
+#: cps/templates/index.xml:29
+msgid "Popular publications from this catalog based on Rating."
+msgstr "Populära publikationer från den här katalogen baserad på betyg."
+
+#: cps/templates/index.xml:32
+msgid "New Books"
+msgstr "Nya böcker"
+
+#: cps/templates/index.xml:36
+msgid "The latest Books"
+msgstr "De senaste böckerna"
+
+#: cps/templates/index.xml:43
+msgid "Show Random Books"
+msgstr "Visa slumpmässiga böcker"
+
+#: cps/templates/index.xml:62 cps/templates/layout.html:160
+msgid "Authors"
+msgstr "Författare"
+
+#: cps/templates/index.xml:66
+msgid "Books ordered by Author"
+msgstr "Böcker ordnade efter författare"
+
+#: cps/templates/index.xml:69 cps/templates/layout.html:163
+msgid "Publishers"
+msgstr "Förlag"
+
+#: cps/templates/index.xml:73
+msgid "Books ordered by publisher"
+msgstr "Böcker ordnade efter förlag"
+
+#: cps/templates/index.xml:80
+msgid "Books ordered by category"
+msgstr "Böcker ordnade efter kategori"
+
+#: cps/templates/index.xml:87
+msgid "Books ordered by series"
+msgstr "Böcker ordnade efter serier"
+
+#: cps/templates/index.xml:90 cps/templates/layout.html:169
+msgid "Public Shelves"
+msgstr "Offentliga hyllor"
+
+#: cps/templates/index.xml:94
+msgid "Books organized in public shelfs, visible to everyone"
+msgstr "Böcker organiserade i offentliga hyllor, synliga för alla"
+
+#: cps/templates/index.xml:98 cps/templates/layout.html:173
+msgid "Your Shelves"
+msgstr "Dina hyllor"
+
+#: cps/templates/index.xml:102
+msgid "User's own shelfs, only visible to the current user himself"
+msgstr "Användarens egna hyllor, endast synliga för den aktuella användaren själv"
+
+#: cps/templates/layout.html:33
+msgid "Toggle navigation"
+msgstr "Växla navigering"
+
+#: cps/templates/layout.html:54
+msgid "Advanced Search"
+msgstr "Avancerad sökning"
+
+#: cps/templates/layout.html:78
+msgid "Logout"
+msgstr "Logga ut"
+
+#: cps/templates/layout.html:83 cps/templates/register.html:14
+msgid "Register"
+msgstr "Registrera"
+
+#: cps/templates/layout.html:108
+msgid "Uploading..."
+msgstr "Laddar upp..."
+
+#: cps/templates/layout.html:109
+msgid "please don't refresh the page"
+msgstr "uppdatera inte sidan"
+
+#: cps/templates/layout.html:120
+msgid "Browse"
+msgstr "Bläddra"
+
+#: cps/templates/layout.html:122
+msgid "Recently Added"
+msgstr "Nyligen tillagda"
+
+#: cps/templates/layout.html:127
+msgid "Sorted Books"
+msgstr "Sorterade böcker"
+
+#: cps/templates/layout.html:131 cps/templates/layout.html:132
+#: cps/templates/layout.html:133 cps/templates/layout.html:134
+msgid "Sort By"
+msgstr "Sortera efter"
+
+#: cps/templates/layout.html:131
+msgid "Newest"
+msgstr "Nyast"
+
+#: cps/templates/layout.html:132
+msgid "Oldest"
+msgstr "Äldst"
+
+#: cps/templates/layout.html:133
+msgid "Ascending"
+msgstr "Stigande"
+
+#: cps/templates/layout.html:134
+msgid "Descending"
+msgstr "Fallande"
+
+#: cps/templates/layout.html:151
+msgid "Discover"
+msgstr "Upptäck"
+
+#: cps/templates/layout.html:154
+msgid "Categories"
+msgstr "Kategorier"
+
+#: cps/templates/layout.html:166 cps/templates/search_form.html:74
+msgid "Languages"
+msgstr "Språk"
+
+#: cps/templates/layout.html:178
+msgid "Create a Shelf"
+msgstr "Skapa en hylla"
+
+#: cps/templates/layout.html:179 cps/templates/stats.html:3
+msgid "About"
+msgstr "Om"
+
+#: cps/templates/layout.html:193
+msgid "Previous"
+msgstr "Föregående"
+
+#: cps/templates/layout.html:220
+msgid "Book Details"
+msgstr "Bokdetaljer"
+
+#: cps/templates/login.html:8 cps/templates/login.html:9
+#: cps/templates/register.html:7 cps/templates/user_edit.html:8
+msgid "Username"
+msgstr "Användarnamn"
+
+#: cps/templates/login.html:12 cps/templates/login.html:13
+#: cps/templates/user_edit.html:21
+msgid "Password"
+msgstr "Lösenord"
+
+#: cps/templates/login.html:17
+msgid "Remember me"
+msgstr "Kom ihåg mig"
+
+#: cps/templates/login.html:22
+msgid "Log in with magic link"
+msgstr "Logga in med magic link"
+
+#: cps/templates/osd.xml:5
+msgid "Calibre-Web ebook catalog"
+msgstr "Calibre-Web e-bokkatalog"
+
+#: cps/templates/read.html:69 cps/templates/readcbr.html:79
+#: cps/templates/readcbr.html:103
+msgid "Settings"
+msgstr "Inställningar"
+
+#: cps/templates/read.html:72
+msgid "Reflow text when sidebars are open."
+msgstr "Fyll i texten igen när sidofält är öppna."
+
+#: cps/templates/readcbr.html:84
+msgid "Keyboard Shortcuts"
+msgstr "Kortkommandon"
+
+#: cps/templates/readcbr.html:87
+msgid "Previous Page"
+msgstr "Föregående sida"
+
+#: cps/templates/readcbr.html:88
+msgid "Next Page"
+msgstr "Nästa sida"
+
+#: cps/templates/readcbr.html:89
+msgid "Scale to Best"
+msgstr "Skala till bäst"
+
+#: cps/templates/readcbr.html:90
+msgid "Scale to Width"
+msgstr "Skala till bredd"
+
+#: cps/templates/readcbr.html:91
+msgid "Scale to Height"
+msgstr "Skala till höjd"
+
+#: cps/templates/readcbr.html:92
+msgid "Scale to Native"
+msgstr "Skala till ursprunglig"
+
+#: cps/templates/readcbr.html:93
+msgid "Rotate Right"
+msgstr "Rotera åt höger"
+
+#: cps/templates/readcbr.html:94
+msgid "Rotate Left"
+msgstr "Rotera åt vänster"
+
+#: cps/templates/readcbr.html:95
+msgid "Flip Image"
+msgstr "Vänd bilden"
+
+#: cps/templates/readcbr.html:108 cps/templates/user_edit.html:39
+msgid "Theme"
+msgstr "Tema"
+
+#: cps/templates/readcbr.html:111
+msgid "Light"
+msgstr "Ljust"
+
+#: cps/templates/readcbr.html:112
+msgid "Dark"
+msgstr "Mörkt"
+
+#: cps/templates/readcbr.html:117
+msgid "Scale"
+msgstr "Skala"
+
+#: cps/templates/readcbr.html:120
+msgid "Best"
+msgstr "Bäst"
+
+#: cps/templates/readcbr.html:121
+msgid "Width"
+msgstr "Bredd"
+
+#: cps/templates/readcbr.html:122
+msgid "Height"
+msgstr "Höjd"
+
+#: cps/templates/readcbr.html:123
+msgid "Native"
+msgstr "Ursprunglig"
+
+#: cps/templates/readcbr.html:128
+msgid "Rotate"
+msgstr "Rotera"
+
+#: cps/templates/readcbr.html:139
+msgid "Flip"
+msgstr "Vänd"
+
+#: cps/templates/readcbr.html:142
+msgid "Horizontal"
+msgstr "Horisontell"
+
+#: cps/templates/readcbr.html:143
+msgid "Vertical"
+msgstr "Vertikal"
+
+#: cps/templates/readpdf.html:29
+msgid "PDF.js viewer"
+msgstr "PDF.js visare"
+
+#: cps/templates/readtxt.html:6
+msgid "Basic txt Reader"
+msgstr "Grundläggande txt-läsare"
+
+#: cps/templates/register.html:4
+msgid "Register a new account"
+msgstr "Registrera ett nytt konto"
+
+#: cps/templates/register.html:8
+msgid "Choose a username"
+msgstr "Välj ett användarnamn"
+
+#: cps/templates/register.html:11 cps/templates/user_edit.html:13
+msgid "E-mail address"
+msgstr "E-postadress"
+
+#: cps/templates/register.html:12
+msgid "Your email address"
+msgstr "Din e-postadress"
+
+#: cps/templates/remote_login.html:6
+msgid "Using your another device, visit"
+msgstr "Använda en annan enhet, besök"
+
+#: cps/templates/remote_login.html:6
+msgid "and log in"
+msgstr "och logga in"
+
+#: cps/templates/remote_login.html:9
+msgid "Once you do so, you will automatically get logged in on this device."
+msgstr "När du gör det kommer du automatiskt att logga in på den här enheten."
+
+#: cps/templates/search.html:5
+msgid "No Results for:"
+msgstr "Inga resultat för:"
+
+#: cps/templates/search.html:6
+msgid "Please try a different search"
+msgstr "Försök en annan sökning"
+
+#: cps/templates/search.html:8
+msgid "Results for:"
+msgstr "Resultat för:"
+
+#: cps/templates/search_form.html:19
+msgid "Publishing date from"
+msgstr "Publiceringsdatum från"
+
+#: cps/templates/search_form.html:26
+msgid "Publishing date to"
+msgstr "Publiceringsdatum till"
+
+#: cps/templates/search_form.html:43
+msgid "Exclude Tags"
+msgstr "Uteslut taggar"
+
+#: cps/templates/search_form.html:63
+msgid "Exclude Series"
+msgstr "Uteslut serier"
+
+#: cps/templates/search_form.html:84
+msgid "Exclude Languages"
+msgstr "Uteslut språk"
+
+#: cps/templates/search_form.html:97
+msgid "Rating bigger than"
+msgstr "Betyg större än"
+
+#: cps/templates/search_form.html:101
+msgid "Rating less than"
+msgstr "Betyg mindre än"
+
+#: cps/templates/shelf.html:7
+msgid "Delete this Shelf"
+msgstr "Ta bort den här hyllan"
+
+#: cps/templates/shelf.html:8
+msgid "Edit Shelf"
+msgstr "Redigera hyllan"
+
+#: cps/templates/shelf.html:9 cps/templates/shelf_order.html:11
+msgid "Change order"
+msgstr "Ändra ordningen"
+
+#: cps/templates/shelf.html:58
+msgid "Do you really want to delete the shelf?"
+msgstr "Är du säker på att du vill ta bort hyllan?"
+
+#: cps/templates/shelf.html:61
+msgid "Shelf will be lost for everybody and forever!"
+msgstr "Hylla kommer att gå förlorad för alla och för alltid!"
+
+#: cps/templates/shelf_edit.html:13
+msgid "should the shelf be public?"
+msgstr "ska hyllan vara offentlig?"
+
+#: cps/templates/shelf_order.html:5
+msgid "Drag 'n drop to rearrange order"
+msgstr "Drag och släpp för att ändra ordning"
+
+#: cps/templates/stats.html:7
+msgid "Calibre library statistics"
+msgstr "Calibre-biblioteksstatistik"
+
+#: cps/templates/stats.html:12
+msgid "Books in this Library"
+msgstr "Böcker i det här biblioteket"
+
+#: cps/templates/stats.html:16
+msgid "Authors in this Library"
+msgstr "Författare i det här biblioteket"
+
+#: cps/templates/stats.html:20
+msgid "Categories in this Library"
+msgstr "Kategorier i det här biblioteket"
+
+#: cps/templates/stats.html:24
+msgid "Series in this Library"
+msgstr "Serier i detta bibliotek"
+
+#: cps/templates/stats.html:28
+msgid "Linked libraries"
+msgstr "Länkade bibliotek"
+
+#: cps/templates/stats.html:32
+msgid "Program library"
+msgstr "Programbibliotek"
+
+#: cps/templates/stats.html:33
+msgid "Installed Version"
+msgstr "Installerad version"
+
+#: cps/templates/tasks.html:7
+msgid "Tasks list"
+msgstr "Uppgiftslista"
+
+#: cps/templates/tasks.html:12
+msgid "User"
+msgstr "Användare"
+
+#: cps/templates/tasks.html:14
+msgid "Task"
+msgstr "Uppgift"
+
+#: cps/templates/tasks.html:15
+msgid "Status"
+msgstr "Status"
+
+#: cps/templates/tasks.html:16
+msgid "Progress"
+msgstr "Förlopp"
+
+#: cps/templates/tasks.html:17
+msgid "Runtime"
+msgstr "Drifttid"
+
+#: cps/templates/tasks.html:18
+msgid "Starttime"
+msgstr "Starttid"
+
+#: cps/templates/tasks.html:24
+msgid "Delete finished tasks"
+msgstr "Ta bort färdiga uppgifter"
+
+#: cps/templates/tasks.html:25
+msgid "Hide all tasks"
+msgstr "Dölj alla uppgifter"
+
+#: cps/templates/user_edit.html:18
+msgid "Reset user Password"
+msgstr "Återställ användarlösenordet"
+
+#: cps/templates/user_edit.html:27
+msgid "Kindle E-Mail"
+msgstr "Kindle e-post"
+
+#: cps/templates/user_edit.html:41
+msgid "Standard Theme"
+msgstr "Standard tema"
+
+#: cps/templates/user_edit.html:42
+msgid "caliBlur! Dark Theme (Beta)"
+msgstr "caliBlur! Mörkt tema (beta)"
+
+#: cps/templates/user_edit.html:47
+msgid "Show books with language"
+msgstr "Visa böcker med språk"
+
+#: cps/templates/user_edit.html:49
+msgid "Show all"
+msgstr "Visa alla"
+
+#: cps/templates/user_edit.html:147
+msgid "Delete this user"
+msgstr "Ta bort den här användaren"
+
+#: cps/templates/user_edit.html:162
+msgid "Recent Downloads"
+msgstr "Senaste hämtningar"
+
+#~ msgid "Current commit timestamp"
+#~ msgstr "Aktuelles Commit Datum"
+
+#~ msgid "Newest commit timestamp"
+#~ msgstr "Neuestes Commit Datum"
+
+#~ msgid "Convert: %(book)s"
+#~ msgstr "Konvertera: %(book)s"
+
+#~ msgid "Convert to %(format)s: %(book)s"
+#~ msgstr "Konvertera till %(format)s: %(book)s"
+
+#~ msgid "Files are replaced"
+#~ msgstr "Filer ersätts"
+
+#~ msgid "Server is stopped"
+#~ msgstr "Servern stoppas"
+
+#~ msgid "Convertertool %(converter)s not found"
+#~ msgstr "Convertertool %(converter)s hittades inte"
+
+#~ msgid "Choose a password"
+#~ msgstr "Välj ett lösenord"
+
+#~ msgid "Could not find any formats suitable for sending by e-mail"
+#~ msgstr "Det gick inte att hitta några format som är lämpliga för att skicka via e-post"
+
+#~ msgid "Sorry you are not allowed to add a book to the shelf: %(shelfname)s"
+#~ msgstr "Tyvärr får du inte lägga till en bok på hyllan: %(shelfname)s"
+
+#~ msgid "File %(file)s uploaded"
+#~ msgstr "Filen %(file)s uppladdad"
+
diff --git a/src/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo b/src/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo
new file mode 100644
index 0000000..86bd49c
Binary files /dev/null and b/src/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo differ
diff --git a/src/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po b/src/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po
new file mode 100644
index 0000000..ee728a9
--- /dev/null
+++ b/src/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po
@@ -0,0 +1,1980 @@
+# Chinese (Simplified, China) translations for Calibre-Web.
+# Copyright (C) 2017 Calibre-Web
+# This file is distributed under the same license as the Calibre-Web
+# project.
+# FIRST AUTHOR dalin , 2017.
+msgid ""
+msgstr ""
+"Project-Id-Version: Calibre-Web\n"
+"Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n"
+"POT-Creation-Date: 2018-11-03 14:03+0100\n"
+"PO-Revision-Date: 2017-01-06 17:00+0000\n"
+"Last-Translator: dalin \n"
+"Language: zh_Hans_CN\n"
+"Language-Team: zh_Hans_CN \n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.6.0\n"
+
+#: cps/book_formats.py:129 cps/book_formats.py:130 cps/book_formats.py:134
+#: cps/book_formats.py:138 cps/converter.py:11 cps/converter.py:27
+msgid "not installed"
+msgstr "未安装"
+
+#: cps/converter.py:22 cps/converter.py:38
+msgid "Excecution permissions missing"
+msgstr "可执行权限缺失"
+
+#: cps/converter.py:48
+msgid "not configured"
+msgstr "未配置"
+
+#: cps/helper.py:58
+#, python-format
+msgid "%(format)s format not found for book id: %(book)d"
+msgstr "找不到id为 %(book)d 的书的 %(format)s 格式"
+
+#: cps/helper.py:70
+#, python-format
+msgid "%(format)s not found on Google Drive: %(fn)s"
+msgstr "Google Drive %(fn)s 上找不到 %(format)s"
+
+#: cps/helper.py:77 cps/helper.py:147 cps/templates/detail.html:44
+msgid "Send to Kindle"
+msgstr "发送到Kindle"
+
+#: cps/helper.py:78 cps/helper.py:96
+msgid "This e-mail has been sent via Calibre-Web."
+msgstr "此邮件已经通过Calibre-Web发送"
+
+#: cps/helper.py:89
+#, python-format
+msgid "%(format)s not found: %(fn)s"
+msgstr "找不到 %(format)s: %(fn)s"
+
+#: cps/helper.py:94
+msgid "Calibre-Web test e-mail"
+msgstr "Calibre-Web测试邮件"
+
+#: cps/helper.py:95
+msgid "Test e-mail"
+msgstr "测试邮件"
+
+#: cps/helper.py:111
+msgid "Get Started with Calibre-Web"
+msgstr "开启Calibre-Web之旅"
+
+#: cps/helper.py:112
+#, python-format
+msgid "Registration e-mail for user: %(name)s"
+msgstr "用户 %(name)s 的注册邮箱"
+
+#: cps/helper.py:135 cps/helper.py:145
+msgid "Could not find any formats suitable for sending by e-mail"
+msgstr "找不到任何适合邮件发送的格式"
+
+#: cps/helper.py:148
+#, python-format
+msgid "E-mail: %(book)s"
+msgstr ""
+
+#: cps/helper.py:150
+msgid "The requested file could not be read. Maybe wrong permissions?"
+msgstr "无法读取请求的文件。 可能有错误的权限设置?"
+
+#: cps/helper.py:250
+#, python-format
+msgid "Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgstr "将标题从'%(src)s'改为'%(dest)s'时失败,出错信息: %(error)s"
+
+#: cps/helper.py:259
+#, python-format
+msgid "Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgstr "将作者从'%(src)s'改为'%(dest)s'时失败,出错信息: %(error)s"
+
+#: cps/helper.py:281 cps/helper.py:290
+#, python-format
+msgid "File %(file)s not found on Google Drive"
+msgstr "Google Drive上找不到文件 %(file)s"
+
+#: cps/helper.py:308
+#, python-format
+msgid "Book path %(path)s not found on Google Drive"
+msgstr "Google Drive上找不到书籍路径 %(path)s"
+
+#: cps/helper.py:565
+msgid "Error excecuting UnRar"
+msgstr "执行UnRar时出错"
+
+#: cps/helper.py:567
+msgid "Unrar binary file not found"
+msgstr "找不到Unrar二进制文件"
+
+#: cps/helper.py:609
+msgid "Waiting"
+msgstr "等待中"
+
+#: cps/helper.py:611
+msgid "Failed"
+msgstr "失败"
+
+#: cps/helper.py:613
+msgid "Started"
+msgstr "已开始"
+
+#: cps/helper.py:615
+msgid "Finished"
+msgstr "已完成"
+
+#: cps/helper.py:617
+msgid "Unknown Status"
+msgstr "未知状态"
+
+#: cps/helper.py:622
+msgid "E-mail: "
+msgstr ""
+
+#: cps/helper.py:624 cps/helper.py:628
+msgid "Convert: "
+msgstr "转换:"
+
+#: cps/helper.py:626
+msgid "Upload: "
+msgstr "上传:"
+
+#: cps/helper.py:630
+msgid "Unknown Task: "
+msgstr "未知任务:"
+
+#: cps/web.py:1132 cps/web.py:2842
+msgid "Unknown"
+msgstr "未知"
+
+#: cps/web.py:1141 cps/web.py:1172 cps/web.py:1257
+msgid "HTTP Error"
+msgstr "HTTP错误"
+
+#: cps/web.py:1143 cps/web.py:1174 cps/web.py:1258
+msgid "Connection error"
+msgstr "连接错误"
+
+#: cps/web.py:1145 cps/web.py:1176 cps/web.py:1259
+msgid "Timeout while establishing connection"
+msgstr "建立连接超时"
+
+#: cps/web.py:1147 cps/web.py:1178 cps/web.py:1260
+msgid "General error"
+msgstr "一般错误"
+
+#: cps/web.py:1153
+msgid "Unexpected data while reading update information"
+msgstr "读取更新信息时出现异常数据"
+
+#: cps/web.py:1160
+msgid "No update available. You already have the latest version installed"
+msgstr "没有可用更新。您已经安装了最新版本"
+
+#: cps/web.py:1185
+msgid "A new update is available. Click on the button below to update to the latest version."
+msgstr "有一个更新可用。点击正文按钮更新到最新版本。"
+
+#: cps/web.py:1235
+msgid "Could not fetch update information"
+msgstr "无法获取更新信息"
+
+#: cps/web.py:1250
+msgid "Requesting update package"
+msgstr "正在请求更新包"
+
+#: cps/web.py:1251
+msgid "Downloading update package"
+msgstr "正在下载更新包"
+
+#: cps/web.py:1252
+msgid "Unzipping update package"
+msgstr "正在解压更新包"
+
+#: cps/web.py:1253
+msgid "Replacing files"
+msgstr "正在替换文件"
+
+#: cps/web.py:1254
+msgid "Database connections are closed"
+msgstr "数据库连接已关闭"
+
+#: cps/web.py:1255
+msgid "Stopping server"
+msgstr "正在停止服务器"
+
+#: cps/web.py:1256
+msgid "Update finished, please press okay and reload page"
+msgstr "更新完成,请按确定并刷新页面"
+
+#: cps/web.py:1257 cps/web.py:1258 cps/web.py:1259 cps/web.py:1260
+msgid "Update failed:"
+msgstr "更新失败:"
+
+#: cps/web.py:1283
+msgid "Recently Added Books"
+msgstr "最近添加的书籍"
+
+#: cps/web.py:1293
+msgid "Newest Books"
+msgstr "最新书籍"
+
+#: cps/web.py:1305
+msgid "Oldest Books"
+msgstr "最旧书籍"
+
+#: cps/web.py:1317
+msgid "Books (A-Z)"
+msgstr "书籍 (A-Z)"
+
+#: cps/web.py:1328
+msgid "Books (Z-A)"
+msgstr "书籍 (Z-A)"
+
+#: cps/web.py:1357
+msgid "Hot Books (most downloaded)"
+msgstr "热门书籍(最多下载)"
+
+#: cps/web.py:1370
+msgid "Best rated books"
+msgstr "最高评分书籍"
+
+#: cps/templates/index.xml:39 cps/web.py:1383
+msgid "Random Books"
+msgstr "随机书籍"
+
+#: cps/web.py:1398
+msgid "Author list"
+msgstr "作者列表"
+
+#: cps/web.py:1410 cps/web.py:1501 cps/web.py:1663 cps/web.py:2206
+msgid "Error opening eBook. File does not exist or file is not accessible:"
+msgstr "无法打开电子书。 文件不存在或者文件不可访问:"
+
+#: cps/web.py:1438
+msgid "Publisher list"
+msgstr "出版社列表"
+
+#: cps/web.py:1452
+#, python-format
+msgid "Publisher: %(name)s"
+msgstr "出版社: %(name)s"
+
+#: cps/templates/index.xml:83 cps/web.py:1484
+msgid "Series list"
+msgstr "丛书列表"
+
+#: cps/web.py:1499
+#, python-format
+msgid "Series: %(serie)s"
+msgstr "丛书: %(serie)s"
+
+#: cps/web.py:1528
+msgid "Available languages"
+msgstr "可用语言"
+
+#: cps/web.py:1548
+#, python-format
+msgid "Language: %(name)s"
+msgstr "语言: %(name)s"
+
+#: cps/templates/index.xml:76 cps/web.py:1559
+msgid "Category list"
+msgstr "分类列表"
+
+#: cps/web.py:1573
+#, python-format
+msgid "Category: %(name)s"
+msgstr "分类: %(name)s"
+
+#: cps/templates/layout.html:71 cps/web.py:1699
+msgid "Tasks"
+msgstr "任务"
+
+#: cps/web.py:1733
+msgid "Statistics"
+msgstr "统计"
+
+#: cps/web.py:1840
+msgid "Callback domain is not verified, please follow steps to verify domain in google developer console"
+msgstr "回调域名尚未被校验,请在google开发者控制台按步骤校验域名"
+
+#: cps/web.py:1915
+msgid "Server restarted, please reload page"
+msgstr "服务器已重启,请刷新页面"
+
+#: cps/web.py:1918
+msgid "Performing shutdown of server, please close window"
+msgstr "正在关闭服务器,请关闭窗口"
+
+#: cps/web.py:1937
+msgid "Update done"
+msgstr "更新完成"
+
+#: cps/web.py:2007
+msgid "Published after "
+msgstr "出版时晚于 "
+
+#: cps/web.py:2014
+msgid "Published before "
+msgstr "出版时早于 "
+
+#: cps/web.py:2028
+#, python-format
+msgid "Rating <= %(rating)s"
+msgstr "评分 <= %(rating)s"
+
+#: cps/web.py:2030
+#, python-format
+msgid "Rating >= %(rating)s"
+msgstr "评分 >= %(rating)s"
+
+#: cps/web.py:2089 cps/web.py:2098
+msgid "search"
+msgstr "搜索"
+
+#: cps/templates/index.xml:47 cps/templates/index.xml:51
+#: cps/templates/layout.html:146 cps/web.py:2165
+msgid "Read Books"
+msgstr "已读书籍"
+
+#: cps/templates/index.xml:55 cps/templates/index.xml:59
+#: cps/templates/layout.html:148 cps/web.py:2168
+msgid "Unread Books"
+msgstr "未读书籍"
+
+#: cps/web.py:2216 cps/web.py:2218 cps/web.py:2220 cps/web.py:2232
+msgid "Read a Book"
+msgstr "阅读一本书"
+
+#: cps/web.py:2298 cps/web.py:3201
+msgid "Please fill out all fields!"
+msgstr "请填写所有字段"
+
+#: cps/web.py:2299 cps/web.py:2320 cps/web.py:2324 cps/web.py:2329
+#: cps/web.py:2331
+msgid "register"
+msgstr "注册"
+
+#: cps/web.py:2319 cps/web.py:3417
+msgid "An unknown error occurred. Please try again later."
+msgstr "发生一个未知错误,请稍后再试。"
+
+#: cps/web.py:2322
+msgid "Your e-mail is not allowed to register"
+msgstr "您的邮箱不能用来注册"
+
+#: cps/web.py:2325
+msgid "Confirmation e-mail was send to your e-mail account."
+msgstr "确认邮件已经发送到您的邮箱。"
+
+#: cps/web.py:2328
+msgid "This username or e-mail address is already in use."
+msgstr "这个用户名或者邮箱已经被使用。"
+
+#: cps/web.py:2345 cps/web.py:2441
+#, python-format
+msgid "you are now logged in as: '%(nickname)s'"
+msgstr "您现在已以'%(nickname)s'身份登录"
+
+#: cps/web.py:2350
+msgid "Wrong Username or Password"
+msgstr "用户名或密码错误"
+
+#: cps/web.py:2356 cps/web.py:2377
+msgid "login"
+msgstr "登录"
+
+#: cps/web.py:2389 cps/web.py:2420
+msgid "Token not found"
+msgstr "找不到Token"
+
+#: cps/web.py:2397 cps/web.py:2428
+msgid "Token has expired"
+msgstr "Token已过期"
+
+#: cps/web.py:2405
+msgid "Success! Please return to your device"
+msgstr "成功!请返回您的设备"
+
+#: cps/web.py:2455
+msgid "Please configure the SMTP mail settings first..."
+msgstr "请先配置SMTP邮箱..."
+
+#: cps/web.py:2459
+#, python-format
+msgid "Book successfully queued for sending to %(kindlemail)s"
+msgstr "书籍已经被成功加入 %(kindlemail)s 的发送队列"
+
+#: cps/web.py:2463
+#, python-format
+msgid "There was an error sending this book: %(res)s"
+msgstr "发送这本书的时候出现错误: %(res)s"
+
+#: cps/web.py:2465 cps/web.py:3255
+msgid "Please configure your kindle e-mail address first..."
+msgstr "请先配置您的kindle邮箱..."
+
+#: cps/web.py:2476 cps/web.py:2528
+msgid "Invalid shelf specified"
+msgstr "指定的书架无效"
+
+#: cps/web.py:2483
+#, python-format
+msgid "Sorry you are not allowed to add a book to the the shelf: %(shelfname)s"
+msgstr "对不起,您没有添加书籍到书架 %(shelfname)s 的权限"
+
+#: cps/web.py:2491
+msgid "You are not allowed to edit public shelves"
+msgstr "您没有编辑书架的权限"
+
+#: cps/web.py:2500
+#, python-format
+msgid "Book is already part of the shelf: %(shelfname)s"
+msgstr "此书已经是书架 %(shelfname)s 的一部分"
+
+#: cps/web.py:2514
+#, python-format
+msgid "Book has been added to shelf: %(sname)s"
+msgstr "此书已被添加到书架: %(sname)s"
+
+#: cps/web.py:2533
+#, python-format
+msgid "You are not allowed to add a book to the the shelf: %(name)s"
+msgstr "您没有添加书籍到书架 %(name)s 的权限"
+
+#: cps/web.py:2538
+msgid "User is not allowed to edit public shelves"
+msgstr "用户没有编辑公开书架的权限"
+
+#: cps/web.py:2556
+#, python-format
+msgid "Books are already part of the shelf: %(name)s"
+msgstr "书籍已经在书架 %(name)s 中了"
+
+#: cps/web.py:2570
+#, python-format
+msgid "Books have been added to shelf: %(sname)s"
+msgstr "书籍已经被添加到书架 %(sname)s 中'"
+
+#: cps/web.py:2572
+#, python-format
+msgid "Could not add books to shelf: %(sname)s"
+msgstr "无法添加书籍到书架: %(sname)s"
+
+#: cps/web.py:2609
+#, python-format
+msgid "Book has been removed from shelf: %(sname)s"
+msgstr "此书已从书架 %(sname)s 中删除"
+
+#: cps/web.py:2615
+#, python-format
+msgid "Sorry you are not allowed to remove a book from this shelf: %(sname)s"
+msgstr "对不起,您没有从书架 %(sname)s 中删除书籍的权限"
+
+#: cps/web.py:2635 cps/web.py:2659
+#, python-format
+msgid "A shelf with the name '%(title)s' already exists."
+msgstr "已存在书架 '%(title)s'。"
+
+#: cps/web.py:2640
+#, python-format
+msgid "Shelf %(title)s created"
+msgstr "书架 %(title)s 已被创建"
+
+#: cps/web.py:2642 cps/web.py:2670
+msgid "There was an error"
+msgstr "发生错误"
+
+#: cps/web.py:2643 cps/web.py:2645
+msgid "create a shelf"
+msgstr "创建书架"
+
+#: cps/web.py:2668
+#, python-format
+msgid "Shelf %(title)s changed"
+msgstr "书架 %(title)s 已被修改"
+
+#: cps/web.py:2671 cps/web.py:2673
+msgid "Edit a shelf"
+msgstr "编辑书架"
+
+#: cps/web.py:2694
+#, python-format
+msgid "successfully deleted shelf %(name)s"
+msgstr "成功删除书架 %(name)s"
+
+#: cps/web.py:2721
+#, python-format
+msgid "Shelf: '%(name)s'"
+msgstr "书架: '%(name)s'"
+
+#: cps/web.py:2724
+msgid "Error opening shelf. Shelf does not exist or is not accessible"
+msgstr "打开书架出错。书架不存在或不可访问"
+
+#: cps/web.py:2755
+#, python-format
+msgid "Change order of Shelf: '%(name)s'"
+msgstr "修改书架 '%(name)s' 顺序"
+
+#: cps/web.py:2784 cps/web.py:3207
+msgid "E-mail is not from valid domain"
+msgstr "邮箱不在有效域中'"
+
+#: cps/web.py:2786 cps/web.py:2829 cps/web.py:2832
+#, python-format
+msgid "%(name)s's profile"
+msgstr "%(name)s 的资料"
+
+#: cps/web.py:2827
+msgid "Found an existing account for this e-mail address."
+msgstr "找到一个已有账号使用这个邮箱。"
+
+#: cps/web.py:2830
+msgid "Profile updated"
+msgstr "资料已更新"
+
+#: cps/web.py:2858
+msgid "Admin page"
+msgstr "管理页"
+
+#: cps/web.py:2938 cps/web.py:3112
+msgid "Calibre-Web configuration updated"
+msgstr "Calibre-Web配置已更新"
+
+#: cps/templates/admin.html:100 cps/web.py:2951
+msgid "UI Configuration"
+msgstr "UI配置"
+
+#: cps/web.py:2969
+msgid "Import of optional Google Drive requirements missing"
+msgstr "可选的Google Drive依赖导入缺失"
+
+#: cps/web.py:2972
+msgid "client_secrets.json is missing or not readable"
+msgstr "client_secrets.json文件缺失或不可读"
+
+#: cps/web.py:2977 cps/web.py:3004
+msgid "client_secrets.json is not configured for web application"
+msgstr "没有为web应用配置client_secrets.json"
+
+#: cps/templates/admin.html:99 cps/web.py:3007 cps/web.py:3033 cps/web.py:3045
+#: cps/web.py:3088 cps/web.py:3103 cps/web.py:3120 cps/web.py:3127
+#: cps/web.py:3142
+msgid "Basic Configuration"
+msgstr "基本配置"
+
+#: cps/web.py:3030
+msgid "Keyfile location is not valid, please enter correct path"
+msgstr "key文件位置无效,请输入正确路径"
+
+#: cps/web.py:3042
+msgid "Certfile location is not valid, please enter correct path"
+msgstr "证书文件位置无效,请输入正确路径"
+
+#: cps/web.py:3085
+msgid "Logfile location is not valid, please enter correct path"
+msgstr "日志文件位置无效,请输入正确路径"
+
+#: cps/web.py:3124
+msgid "DB location is not valid, please enter correct path"
+msgstr "DB位置无效,请输入正确路径"
+
+#: cps/templates/admin.html:33 cps/web.py:3203 cps/web.py:3209 cps/web.py:3225
+msgid "Add new user"
+msgstr "添加新用户"
+
+#: cps/web.py:3215
+#, python-format
+msgid "User '%(user)s' created"
+msgstr "用户 '%(user)s' 已被创建"
+
+#: cps/web.py:3219
+msgid "Found an existing account for this e-mail address or nickname."
+msgstr "此邮箱或昵称的账号已经存在。"
+
+#: cps/web.py:3243 cps/web.py:3257
+msgid "E-mail server settings updated"
+msgstr "已更新邮件服务器设置"
+
+#: cps/web.py:3250
+#, python-format
+msgid "Test e-mail successfully send to %(kindlemail)s"
+msgstr "测试邮件已经被成功发到 %(kindlemail)s"
+
+#: cps/web.py:3253
+#, python-format
+msgid "There was an error sending the Test e-mail: %(res)s"
+msgstr "发送测试邮件出错了: %(res)s"
+
+#: cps/web.py:3258
+msgid "Edit e-mail server settings"
+msgstr "编辑邮箱服务器设置"
+
+#: cps/web.py:3283
+#, python-format
+msgid "User '%(nick)s' deleted"
+msgstr "用户 '%(nick)s' 已被删除"
+
+#: cps/web.py:3392
+#, python-format
+msgid "User '%(nick)s' updated"
+msgstr "用户 '%(nick)s' 已被更新"
+
+#: cps/web.py:3395
+msgid "An unknown error occured."
+msgstr "发生未知错误。"
+
+#: cps/web.py:3397
+#, python-format
+msgid "Edit User %(nick)s"
+msgstr "编辑用户 %(nick)s"
+
+#: cps/web.py:3414
+#, python-format
+msgid "Password for user %(user)s reset"
+msgstr "用户 %(user)s 的密码已重置"
+
+#: cps/web.py:3428 cps/web.py:3629
+msgid "Error opening eBook. File does not exist or file is not accessible"
+msgstr "打开电子书出错。文件不存在或不可访问"
+
+#: cps/web.py:3453 cps/web.py:3912
+msgid "edit metadata"
+msgstr "编辑元数据"
+
+#: cps/web.py:3546 cps/web.py:3782
+#, python-format
+msgid "File extension '%(ext)s' is not allowed to be uploaded to this server"
+msgstr "不能上传后缀为 '%(ext)s' 的文件到此服务器"
+
+#: cps/web.py:3550 cps/web.py:3786
+msgid "File to be uploaded must have an extension"
+msgstr "要上传的文件必须有一个后缀"
+
+#: cps/web.py:3562 cps/web.py:3806
+#, python-format
+msgid "Failed to create path %(path)s (Permission denied)."
+msgstr "创建路径 %(path)s 失败(权限拒绝)。"
+
+#: cps/web.py:3567
+#, python-format
+msgid "Failed to store file %(file)s."
+msgstr "保存文件 %(file)s 失败。"
+
+#: cps/web.py:3583
+#, python-format
+msgid "File format %(ext)s added to %(book)s"
+msgstr "已添加 %(ext)s 格式到 %(book)s"
+
+#: cps/web.py:3601
+#, python-format
+msgid "Failed to create path for cover %(path)s (Permission denied)."
+msgstr "为封面 %(path)s 创建路径失败(权限拒绝)。"
+
+#: cps/web.py:3608
+#, python-format
+msgid "Failed to store cover-file %(cover)s."
+msgstr "保存封面文件 %(cover)s 失败。"
+
+#: cps/web.py:3611
+msgid "Cover-file is not a valid image file"
+msgstr "封面文件不是一个有效的图片文件"
+
+#: cps/web.py:3641 cps/web.py:3650 cps/web.py:3654
+msgid "unknown"
+msgstr "未知"
+
+#: cps/web.py:3673
+msgid "Cover is not a jpg file, can't save"
+msgstr "封面不是一个jpg文件,无法保存"
+
+#: cps/web.py:3721
+#, python-format
+msgid "%(langname)s is not a valid language"
+msgstr "%(langname)s 不是一种有效语言"
+
+#: cps/web.py:3752
+msgid "Metadata successfully updated"
+msgstr "已成功更新元数据"
+
+#: cps/web.py:3761
+msgid "Error editing book, please check logfile for details"
+msgstr "编辑书籍出错,详情请检查日志文件"
+
+#: cps/web.py:3811
+#, python-format
+msgid "Failed to store file %(file)s (Permission denied)."
+msgstr "存储文件 %(file)s 失败(权限拒绝)。"
+
+#: cps/web.py:3816
+#, python-format
+msgid "Failed to delete file %(file)s (Permission denied)."
+msgstr "删除文件 %(file)s 失败(权限拒绝)。"
+
+#: cps/web.py:3898
+#, python-format
+msgid "File %(file)s uploaded"
+msgstr "文件 %(file)s 已上传"
+
+#: cps/web.py:3928
+msgid "Source or destination format for conversion missing"
+msgstr "转换的源或目的格式缺失"
+
+#: cps/web.py:3938
+#, python-format
+msgid "Book successfully queued for converting to %(book_format)s"
+msgstr "书籍已经被成功加入 %(book_format)s 的转换队列"
+
+#: cps/web.py:3942
+#, python-format
+msgid "There was an error converting this book: %(res)s"
+msgstr "转换此书时出现错误: %(res)s"
+
+#: cps/worker.py:287
+#, python-format
+msgid "Ebook-converter failed: %(error)s"
+msgstr "电子书转换器失败: %(error)s"
+
+#: cps/worker.py:298
+#, python-format
+msgid "Kindlegen failed with Error %(error)s. Message: %(message)s"
+msgstr "Kindlegen 因为错误 %(error)s 失败。消息: %(message)s"
+
+#: cps/templates/admin.html:6
+msgid "User list"
+msgstr "用户列表"
+
+#: cps/templates/admin.html:9
+msgid "Nickname"
+msgstr "昵称"
+
+#: cps/templates/admin.html:10
+msgid "E-mail"
+msgstr ""
+
+#: cps/templates/admin.html:11
+msgid "Kindle"
+msgstr ""
+
+#: cps/templates/admin.html:12
+msgid "DLS"
+msgstr ""
+
+#: cps/templates/admin.html:13 cps/templates/layout.html:74
+msgid "Admin"
+msgstr "管理"
+
+#: cps/templates/admin.html:14 cps/templates/detail.html:22
+#: cps/templates/detail.html:31
+msgid "Download"
+msgstr "下载"
+
+#: cps/templates/admin.html:15 cps/templates/layout.html:64
+msgid "Upload"
+msgstr "上传"
+
+#: cps/templates/admin.html:16
+msgid "Edit"
+msgstr "编辑"
+
+#: cps/templates/admin.html:39
+msgid "SMTP e-mail server settings"
+msgstr "SMTP邮件服务器设置"
+
+#: cps/templates/admin.html:42 cps/templates/email_edit.html:11
+msgid "SMTP hostname"
+msgstr "SMTP地址"
+
+#: cps/templates/admin.html:43
+msgid "SMTP port"
+msgstr "SMTP端口"
+
+#: cps/templates/admin.html:44
+msgid "SSL"
+msgstr ""
+
+#: cps/templates/admin.html:45 cps/templates/email_edit.html:27
+msgid "SMTP login"
+msgstr "SMTP用户名"
+
+#: cps/templates/admin.html:46
+msgid "From mail"
+msgstr "来自邮箱"
+
+#: cps/templates/admin.html:56
+msgid "Change SMTP settings"
+msgstr "修改SMTP设置"
+
+#: cps/templates/admin.html:62
+msgid "Configuration"
+msgstr "配置"
+
+#: cps/templates/admin.html:65
+msgid "Calibre DB dir"
+msgstr "Calibre DB目录"
+
+#: cps/templates/admin.html:69
+msgid "Log level"
+msgstr "日志级别"
+
+#: cps/templates/admin.html:73
+msgid "Port"
+msgstr "端口"
+
+#: cps/templates/admin.html:79 cps/templates/config_view_edit.html:23
+msgid "Books per page"
+msgstr "每页书籍数"
+
+#: cps/templates/admin.html:83
+msgid "Uploading"
+msgstr "上传"
+
+#: cps/templates/admin.html:87
+msgid "Anonymous browsing"
+msgstr "匿名浏览"
+
+#: cps/templates/admin.html:91
+msgid "Public registration"
+msgstr "开放注册"
+
+#: cps/templates/admin.html:95 cps/templates/remote_login.html:4
+msgid "Remote login"
+msgstr "远程登录"
+
+#: cps/templates/admin.html:106
+msgid "Administration"
+msgstr "管理"
+
+#: cps/templates/admin.html:107
+msgid "Reconnect to Calibre DB"
+msgstr "重新连接到Calibre数据库"
+
+#: cps/templates/admin.html:108
+msgid "Restart Calibre-Web"
+msgstr "重启 Calibre-Web"
+
+#: cps/templates/admin.html:109
+msgid "Stop Calibre-Web"
+msgstr "停止 Calibre-Web"
+
+#: cps/templates/admin.html:115
+msgid "Update"
+msgstr "更新"
+
+#: cps/templates/admin.html:119
+msgid "Version"
+msgstr "版本"
+
+#: cps/templates/admin.html:120
+msgid "Details"
+msgstr "详情"
+
+#: cps/templates/admin.html:126
+msgid "Current version"
+msgstr "当前版本"
+
+#: cps/templates/admin.html:132
+msgid "Check for update"
+msgstr "检查更新"
+
+#: cps/templates/admin.html:133
+msgid "Perform Update"
+msgstr "执行更新"
+
+#: cps/templates/admin.html:145
+msgid "Do you really want to restart Calibre-Web?"
+msgstr "您确定要重启 Calibre-Web 吗?"
+
+#: cps/templates/admin.html:150 cps/templates/admin.html:164
+#: cps/templates/admin.html:184 cps/templates/shelf.html:61
+msgid "Ok"
+msgstr "确定"
+
+#: cps/templates/admin.html:151 cps/templates/admin.html:165
+#: cps/templates/book_edit.html:178 cps/templates/book_edit.html:200
+#: cps/templates/config_edit.html:212 cps/templates/config_view_edit.html:168
+#: cps/templates/email_edit.html:40 cps/templates/email_edit.html:75
+#: cps/templates/shelf.html:62 cps/templates/shelf_edit.html:19
+#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:155
+msgid "Back"
+msgstr "后退"
+
+#: cps/templates/admin.html:163
+msgid "Do you really want to stop Calibre-Web?"
+msgstr "您确定要关闭 Calibre-Web 吗?"
+
+#: cps/templates/admin.html:175
+msgid "Updating, please do not reload page"
+msgstr "正在更新,请不要刷新页面"
+
+#: cps/templates/author.html:15
+msgid "via"
+msgstr ""
+
+#: cps/templates/author.html:23
+msgid "In Library"
+msgstr ""
+
+#: cps/templates/author.html:69
+msgid "More by"
+msgstr ""
+
+#: cps/templates/book_edit.html:16
+msgid "Delete Book"
+msgstr "删除书籍"
+
+#: cps/templates/book_edit.html:19
+msgid "Delete formats:"
+msgstr "删除格式:"
+
+#: cps/templates/book_edit.html:22 cps/templates/book_edit.html:199
+#: cps/templates/email_edit.html:73 cps/templates/email_edit.html:74
+msgid "Delete"
+msgstr "删除"
+
+#: cps/templates/book_edit.html:30
+msgid "Convert book format:"
+msgstr "转换书籍格式:"
+
+#: cps/templates/book_edit.html:34
+msgid "Convert from:"
+msgstr "从格式转换:"
+
+#: cps/templates/book_edit.html:36 cps/templates/book_edit.html:43
+msgid "select an option"
+msgstr "选择一个选项"
+
+#: cps/templates/book_edit.html:41
+msgid "Convert to:"
+msgstr "转换到:"
+
+#: cps/templates/book_edit.html:50
+msgid "Convert book"
+msgstr "转换书籍"
+
+#: cps/templates/book_edit.html:59 cps/templates/search_form.html:6
+msgid "Book Title"
+msgstr "书名"
+
+#: cps/templates/book_edit.html:63 cps/templates/book_edit.html:259
+#: cps/templates/book_edit.html:277 cps/templates/search_form.html:10
+msgid "Author"
+msgstr "作者"
+
+#: cps/templates/book_edit.html:67 cps/templates/book_edit.html:264
+#: cps/templates/book_edit.html:279 cps/templates/search_form.html:106
+msgid "Description"
+msgstr "简介"
+
+#: cps/templates/book_edit.html:71 cps/templates/search_form.html:33
+msgid "Tags"
+msgstr "标签"
+
+#: cps/templates/book_edit.html:75 cps/templates/layout.html:157
+#: cps/templates/search_form.html:53
+msgid "Series"
+msgstr "丛书"
+
+#: cps/templates/book_edit.html:79
+msgid "Series id"
+msgstr "丛书ID"
+
+#: cps/templates/book_edit.html:83
+msgid "Rating"
+msgstr "评分"
+
+#: cps/templates/book_edit.html:87
+msgid "Cover URL (jpg, cover is downloaded and stored in database, field is afterwards empty again)"
+msgstr "封面URL(jpg,封面会被下载被保存在数据库中,然后字段会被重新清空)"
+
+#: cps/templates/book_edit.html:91
+msgid "Upload Cover from local drive"
+msgstr "从本地磁盘上传封面"
+
+#: cps/templates/book_edit.html:96 cps/templates/detail.html:135
+msgid "Publishing date"
+msgstr "出版日期"
+
+#: cps/templates/book_edit.html:103 cps/templates/book_edit.html:261
+#: cps/templates/book_edit.html:278 cps/templates/detail.html:127
+#: cps/templates/search_form.html:14
+msgid "Publisher"
+msgstr "出版社"
+
+#: cps/templates/book_edit.html:107 cps/templates/user_edit.html:31
+msgid "Language"
+msgstr "语言"
+
+#: cps/templates/book_edit.html:117 cps/templates/search_form.html:117
+msgid "Yes"
+msgstr "确认"
+
+#: cps/templates/book_edit.html:118 cps/templates/search_form.html:118
+msgid "No"
+msgstr ""
+
+#: cps/templates/book_edit.html:164
+msgid "Upload format"
+msgstr "上传格式"
+
+#: cps/templates/book_edit.html:173
+msgid "view book after edit"
+msgstr "编辑后查看书籍"
+
+#: cps/templates/book_edit.html:176 cps/templates/book_edit.html:212
+msgid "Get metadata"
+msgstr "获取元数据"
+
+#: cps/templates/book_edit.html:177 cps/templates/config_edit.html:210
+#: cps/templates/config_view_edit.html:167 cps/templates/login.html:20
+#: cps/templates/search_form.html:153 cps/templates/shelf_edit.html:17
+#: cps/templates/user_edit.html:153
+msgid "Submit"
+msgstr "提交"
+
+#: cps/templates/book_edit.html:191
+msgid "Are you really sure?"
+msgstr "您真的确认?"
+
+#: cps/templates/book_edit.html:194
+msgid "Book will be deleted from Calibre database"
+msgstr "书籍会被从Calibre数据库和硬盘中删除"
+
+#: cps/templates/book_edit.html:195
+msgid "and from hard disk"
+msgstr ""
+
+#: cps/templates/book_edit.html:215
+msgid "Keyword"
+msgstr "关键字"
+
+#: cps/templates/book_edit.html:216
+msgid " Search keyword "
+msgstr "搜索关键字"
+
+#: cps/templates/book_edit.html:218 cps/templates/layout.html:46
+msgid "Go!"
+msgstr "走起!"
+
+#: cps/templates/book_edit.html:222
+msgid "Click the cover to load metadata to the form"
+msgstr "点击封面加载元数据到表单"
+
+#: cps/templates/book_edit.html:234 cps/templates/book_edit.html:274
+msgid "Loading..."
+msgstr "加载中..."
+
+#: cps/templates/book_edit.html:239 cps/templates/layout.html:224
+msgid "Close"
+msgstr "关闭"
+
+#: cps/templates/book_edit.html:266 cps/templates/book_edit.html:280
+msgid "Source"
+msgstr "来源"
+
+#: cps/templates/book_edit.html:275
+msgid "Search error!"
+msgstr "搜索错误"
+
+#: cps/templates/book_edit.html:276
+msgid "No Result(s) found! Please try aonther keyword."
+msgstr "找不到结果。请尝试另一个关键字"
+
+#: cps/templates/config_edit.html:12
+msgid "Library Configuration"
+msgstr "书库配置"
+
+#: cps/templates/config_edit.html:19
+msgid "Location of Calibre database"
+msgstr "Calibre 数据库位置"
+
+#: cps/templates/config_edit.html:24
+msgid "Use Google Drive?"
+msgstr "是否使用Google Drive?"
+
+#: cps/templates/config_edit.html:30
+msgid "Google Drive config problem"
+msgstr "Google Drive 配置问题"
+
+#: cps/templates/config_edit.html:36
+msgid "Authenticate Google Drive"
+msgstr "认证 Google Drive"
+
+#: cps/templates/config_edit.html:40
+msgid "Please finish Google Drive setup after login"
+msgstr "登录后请完成Google Drive设置"
+
+#: cps/templates/config_edit.html:44
+msgid "Google Drive Calibre folder"
+msgstr "Google Drive Calibre 目录"
+
+#: cps/templates/config_edit.html:52
+msgid "Metadata Watch Channel ID"
+msgstr "元数据监视频道ID"
+
+#: cps/templates/config_edit.html:55
+msgid "Revoke"
+msgstr "撤回"
+
+#: cps/templates/config_edit.html:73
+msgid "Server Configuration"
+msgstr "服务器配置"
+
+#: cps/templates/config_edit.html:80
+msgid "Server Port"
+msgstr "服务器端口"
+
+#: cps/templates/config_edit.html:84
+msgid "SSL certfile location (leave it empty for non-SSL Servers)"
+msgstr "SSL 证书文件位置(非SSL服务器请留空)"
+
+#: cps/templates/config_edit.html:88
+msgid "SSL Keyfile location (leave it empty for non-SSL Servers)"
+msgstr "SSL Key文件位置(非SSL服务器请留空)"
+
+#: cps/templates/config_edit.html:99
+msgid "Logfile Configuration"
+msgstr "日志文件配置"
+
+#: cps/templates/config_edit.html:106
+msgid "Log Level"
+msgstr "日志级别"
+
+#: cps/templates/config_edit.html:115
+msgid "Location and name of logfile (calibre-web.log for no entry)"
+msgstr "日志文件位置和名称(默认为calibre-web.log)"
+
+#: cps/templates/config_edit.html:126
+msgid "Feature Configuration"
+msgstr "特性配置"
+
+#: cps/templates/config_edit.html:134
+msgid "Enable uploading"
+msgstr "启用上传"
+
+#: cps/templates/config_edit.html:138
+msgid "Enable anonymous browsing"
+msgstr "启用匿名浏览"
+
+#: cps/templates/config_edit.html:142
+msgid "Enable public registration"
+msgstr "启用注册"
+
+#: cps/templates/config_edit.html:146
+msgid "Enable remote login (\"magic link\")"
+msgstr "启用远程登录 ('魔法链接')"
+
+#: cps/templates/config_edit.html:151
+msgid "Use"
+msgstr "使用"
+
+#: cps/templates/config_edit.html:152
+msgid "Obtain an API Key"
+msgstr "获取API Key"
+
+#: cps/templates/config_edit.html:156
+msgid "Goodreads API Key"
+msgstr ""
+
+#: cps/templates/config_edit.html:160
+msgid "Goodreads API Secret"
+msgstr ""
+
+#: cps/templates/config_edit.html:173
+msgid "External binaries"
+msgstr "外部二进制"
+
+#: cps/templates/config_edit.html:181
+msgid "No converter"
+msgstr "没有转换器"
+
+#: cps/templates/config_edit.html:183
+msgid "Use Kindlegen"
+msgstr "使用Kindlegen"
+
+#: cps/templates/config_edit.html:185
+msgid "Use calibre's ebook converter"
+msgstr "使用calibre的电子书转换器"
+
+#: cps/templates/config_edit.html:189
+msgid "E-Book converter settings"
+msgstr "电子书转换设置"
+
+#: cps/templates/config_edit.html:193
+msgid "Path to convertertool"
+msgstr "转换工具路径"
+
+#: cps/templates/config_edit.html:199
+msgid "Location of Unrar binary"
+msgstr "Unrar二进制位置"
+
+#: cps/templates/config_edit.html:215 cps/templates/layout.html:82
+#: cps/templates/login.html:4
+msgid "Login"
+msgstr "登录"
+
+#: cps/templates/config_view_edit.html:12
+msgid "View Configuration"
+msgstr "查看配置"
+
+#: cps/templates/config_view_edit.html:19 cps/templates/layout.html:133
+#: cps/templates/layout.html:134 cps/templates/shelf_edit.html:7
+msgid "Title"
+msgstr "标题"
+
+#: cps/templates/config_view_edit.html:27
+msgid "No. of random books to show"
+msgstr "随机书籍显示数量"
+
+#: cps/templates/config_view_edit.html:31
+msgid "Regular expression for ignoring columns"
+msgstr "忽略列的正则表达式"
+
+#: cps/templates/config_view_edit.html:35
+msgid "Link read/unread status to Calibre column"
+msgstr "链接 已读/未读 状态到Calibre栏"
+
+#: cps/templates/config_view_edit.html:44
+msgid "Regular expression for title sorting"
+msgstr "标题排序的正则表达式"
+
+#: cps/templates/config_view_edit.html:48
+msgid "Tags for Mature Content"
+msgstr "成人内容标签"
+
+#: cps/templates/config_view_edit.html:62
+msgid "Default settings for new users"
+msgstr "新用户默认设置"
+
+#: cps/templates/config_view_edit.html:70 cps/templates/user_edit.html:110
+msgid "Admin user"
+msgstr "管理用户"
+
+#: cps/templates/config_view_edit.html:74 cps/templates/user_edit.html:119
+msgid "Allow Downloads"
+msgstr "允许下载"
+
+#: cps/templates/config_view_edit.html:78 cps/templates/user_edit.html:123
+msgid "Allow Uploads"
+msgstr "允许上传"
+
+#: cps/templates/config_view_edit.html:82 cps/templates/user_edit.html:127
+msgid "Allow Edit"
+msgstr "允许编辑"
+
+#: cps/templates/config_view_edit.html:86 cps/templates/user_edit.html:131
+msgid "Allow Delete books"
+msgstr "允许删除书籍"
+
+#: cps/templates/config_view_edit.html:90 cps/templates/user_edit.html:136
+msgid "Allow Changing Password"
+msgstr "允许修改密码"
+
+#: cps/templates/config_view_edit.html:94 cps/templates/user_edit.html:140
+msgid "Allow Editing Public Shelfs"
+msgstr "允许编辑公共书架"
+
+#: cps/templates/config_view_edit.html:104
+msgid "Default visibilities for new users"
+msgstr "新用户的默认显示权限"
+
+#: cps/templates/config_view_edit.html:112 cps/templates/user_edit.html:58
+msgid "Show random books"
+msgstr "显示随机书籍"
+
+#: cps/templates/config_view_edit.html:116 cps/templates/user_edit.html:62
+msgid "Show recent books"
+msgstr "显示最近书籍"
+
+#: cps/templates/config_view_edit.html:120 cps/templates/user_edit.html:66
+msgid "Show sorted books"
+msgstr "显示已排序书籍"
+
+#: cps/templates/config_view_edit.html:124 cps/templates/user_edit.html:70
+msgid "Show hot books"
+msgstr "显示热门书籍"
+
+#: cps/templates/config_view_edit.html:128 cps/templates/user_edit.html:74
+msgid "Show best rated books"
+msgstr "显示最高评分书籍"
+
+#: cps/templates/config_view_edit.html:132 cps/templates/user_edit.html:78
+msgid "Show language selection"
+msgstr "显示语言选择"
+
+#: cps/templates/config_view_edit.html:136 cps/templates/user_edit.html:82
+msgid "Show series selection"
+msgstr "显示丛书选择"
+
+#: cps/templates/config_view_edit.html:140 cps/templates/user_edit.html:86
+msgid "Show category selection"
+msgstr "显示分类选择"
+
+#: cps/templates/config_view_edit.html:144 cps/templates/user_edit.html:90
+msgid "Show author selection"
+msgstr "显示作者选择"
+
+#: cps/templates/config_view_edit.html:148 cps/templates/user_edit.html:94
+msgid "Show publisher selection"
+msgstr "显示出版社选择"
+
+#: cps/templates/config_view_edit.html:152 cps/templates/user_edit.html:98
+msgid "Show read and unread"
+msgstr "显示已读和未读"
+
+#: cps/templates/config_view_edit.html:156 cps/templates/user_edit.html:102
+msgid "Show random books in detail view"
+msgstr "在详情页显示随机书籍"
+
+#: cps/templates/config_view_edit.html:160 cps/templates/user_edit.html:115
+msgid "Show mature content"
+msgstr "显示成人内容"
+
+#: cps/templates/detail.html:49
+msgid "Read in browser"
+msgstr "在浏览器中阅读"
+
+#: cps/templates/detail.html:88
+msgid "Book"
+msgstr ""
+
+#: cps/templates/detail.html:88
+msgid "of"
+msgstr ""
+
+#: cps/templates/detail.html:94
+msgid "language"
+msgstr "语言"
+
+#: cps/templates/detail.html:172
+msgid "Read"
+msgstr ""
+
+#: cps/templates/detail.html:182
+msgid "Description:"
+msgstr "简介:"
+
+#: cps/templates/detail.html:195 cps/templates/search.html:14
+msgid "Add to shelf"
+msgstr "添加到书架"
+
+#: cps/templates/detail.html:257
+msgid "Edit metadata"
+msgstr "编辑元数据"
+
+#: cps/templates/email_edit.html:15
+msgid "SMTP port (usually 25 for plain SMTP and 465 for SSL and 587 for STARTTLS)"
+msgstr "SMTP端口(无加密SMTP通常是25, SSL加密是465, STARTTLS加密是587)"
+
+#: cps/templates/email_edit.html:19
+msgid "Encryption"
+msgstr "加密方式"
+
+#: cps/templates/email_edit.html:21
+msgid "None"
+msgstr "无"
+
+#: cps/templates/email_edit.html:22
+msgid "STARTTLS"
+msgstr ""
+
+#: cps/templates/email_edit.html:23
+msgid "SSL/TLS"
+msgstr ""
+
+#: cps/templates/email_edit.html:31
+msgid "SMTP password"
+msgstr "SMTP密码"
+
+#: cps/templates/email_edit.html:35
+msgid "From e-mail"
+msgstr "来自邮箱"
+
+#: cps/templates/email_edit.html:38
+msgid "Save settings"
+msgstr "保存设置"
+
+#: cps/templates/email_edit.html:39
+msgid "Save settings and send Test E-Mail"
+msgstr "保存设置并发送测试邮件"
+
+#: cps/templates/email_edit.html:43
+msgid "Allowed domains for registering"
+msgstr "允许注册的域名"
+
+#: cps/templates/email_edit.html:47
+msgid "Enter domainname"
+msgstr "输入域名"
+
+#: cps/templates/email_edit.html:55
+msgid "Add Domain"
+msgstr "添加域名"
+
+#: cps/templates/email_edit.html:58
+msgid "Add"
+msgstr "添加"
+
+#: cps/templates/email_edit.html:72
+msgid "Do you really want to delete this domain rule?"
+msgstr "您确定要删除这条域名规则吗?"
+
+#: cps/templates/feed.xml:21 cps/templates/layout.html:208
+msgid "Next"
+msgstr "下一个"
+
+#: cps/templates/feed.xml:33 cps/templates/index.xml:11
+#: cps/templates/layout.html:43 cps/templates/layout.html:44
+msgid "Search"
+msgstr "搜索"
+
+#: cps/templates/index.html:5
+msgid "Discover (Random Books)"
+msgstr "发现(随机书籍)"
+
+#: cps/templates/index.xml:6
+msgid "Start"
+msgstr "开始"
+
+#: cps/templates/index.xml:18 cps/templates/layout.html:139
+msgid "Hot Books"
+msgstr "热门书籍"
+
+#: cps/templates/index.xml:22
+msgid "Popular publications from this catalog based on Downloads."
+msgstr "基于下载数的热门书籍"
+
+#: cps/templates/index.xml:25 cps/templates/layout.html:142
+msgid "Best rated Books"
+msgstr "最高评分书籍"
+
+#: cps/templates/index.xml:29
+msgid "Popular publications from this catalog based on Rating."
+msgstr "基于评分的热门书籍"
+
+#: cps/templates/index.xml:32
+msgid "New Books"
+msgstr "新书"
+
+#: cps/templates/index.xml:36
+msgid "The latest Books"
+msgstr "最新书籍"
+
+#: cps/templates/index.xml:43
+msgid "Show Random Books"
+msgstr "显示随机书籍"
+
+#: cps/templates/index.xml:62 cps/templates/layout.html:160
+msgid "Authors"
+msgstr "作者"
+
+#: cps/templates/index.xml:66
+msgid "Books ordered by Author"
+msgstr "书籍按作者排序"
+
+#: cps/templates/index.xml:69 cps/templates/layout.html:163
+msgid "Publishers"
+msgstr "出版社"
+
+#: cps/templates/index.xml:73
+msgid "Books ordered by publisher"
+msgstr "书籍按出版社排版"
+
+#: cps/templates/index.xml:80
+msgid "Books ordered by category"
+msgstr "书籍按分类排序"
+
+#: cps/templates/index.xml:87
+msgid "Books ordered by series"
+msgstr "书籍按丛书排序"
+
+#: cps/templates/index.xml:90 cps/templates/layout.html:169
+msgid "Public Shelves"
+msgstr "公开书架"
+
+#: cps/templates/index.xml:94
+msgid "Books organized in public shelfs, visible to everyone"
+msgstr "公开书架中的书籍,对所有人都可见"
+
+#: cps/templates/index.xml:98 cps/templates/layout.html:173
+msgid "Your Shelves"
+msgstr "您的书架"
+
+#: cps/templates/index.xml:102
+msgid "User's own shelfs, only visible to the current user himself"
+msgstr "用户私有书架,只对当前用户本身可见"
+
+#: cps/templates/layout.html:33
+msgid "Toggle navigation"
+msgstr "切换导航"
+
+#: cps/templates/layout.html:54
+msgid "Advanced Search"
+msgstr "高级搜索"
+
+#: cps/templates/layout.html:78
+msgid "Logout"
+msgstr "注销"
+
+#: cps/templates/layout.html:83 cps/templates/register.html:14
+msgid "Register"
+msgstr "注册"
+
+#: cps/templates/layout.html:108
+msgid "Uploading..."
+msgstr "上传中..."
+
+#: cps/templates/layout.html:109
+msgid "please don't refresh the page"
+msgstr "请不要刷新页面"
+
+#: cps/templates/layout.html:120
+msgid "Browse"
+msgstr "浏览"
+
+#: cps/templates/layout.html:122
+msgid "Recently Added"
+msgstr "最近添加"
+
+#: cps/templates/layout.html:127
+msgid "Sorted Books"
+msgstr "已排序书籍"
+
+#: cps/templates/layout.html:131 cps/templates/layout.html:132
+#: cps/templates/layout.html:133 cps/templates/layout.html:134
+msgid "Sort By"
+msgstr "排序"
+
+#: cps/templates/layout.html:131
+msgid "Newest"
+msgstr "最新"
+
+#: cps/templates/layout.html:132
+msgid "Oldest"
+msgstr "最旧"
+
+#: cps/templates/layout.html:133
+msgid "Ascending"
+msgstr "升序"
+
+#: cps/templates/layout.html:134
+msgid "Descending"
+msgstr "降序"
+
+#: cps/templates/layout.html:151
+msgid "Discover"
+msgstr "发现"
+
+#: cps/templates/layout.html:154
+msgid "Categories"
+msgstr "分类"
+
+#: cps/templates/layout.html:166 cps/templates/search_form.html:74
+msgid "Languages"
+msgstr "语言"
+
+#: cps/templates/layout.html:178
+msgid "Create a Shelf"
+msgstr "创建书架"
+
+#: cps/templates/layout.html:179 cps/templates/stats.html:3
+msgid "About"
+msgstr "关于"
+
+#: cps/templates/layout.html:193
+msgid "Previous"
+msgstr "上一个"
+
+#: cps/templates/layout.html:220
+msgid "Book Details"
+msgstr "书籍详情"
+
+#: cps/templates/login.html:8 cps/templates/login.html:9
+#: cps/templates/register.html:7 cps/templates/user_edit.html:8
+msgid "Username"
+msgstr "用户名"
+
+#: cps/templates/login.html:12 cps/templates/login.html:13
+#: cps/templates/user_edit.html:21
+msgid "Password"
+msgstr "密码"
+
+#: cps/templates/login.html:17
+msgid "Remember me"
+msgstr "记住我"
+
+#: cps/templates/login.html:22
+msgid "Log in with magic link"
+msgstr "通过魔法链接登录"
+
+#: cps/templates/osd.xml:5
+msgid "Calibre-Web ebook catalog"
+msgstr "Caliebre-Web电子书目录"
+
+#: cps/templates/read.html:69 cps/templates/readcbr.html:79
+#: cps/templates/readcbr.html:103
+msgid "Settings"
+msgstr "设置"
+
+#: cps/templates/read.html:72
+msgid "Reflow text when sidebars are open."
+msgstr ""
+
+#: cps/templates/readcbr.html:84
+msgid "Keyboard Shortcuts"
+msgstr "快捷键"
+
+#: cps/templates/readcbr.html:87
+msgid "Previous Page"
+msgstr "上一页"
+
+#: cps/templates/readcbr.html:88
+msgid "Next Page"
+msgstr "下一页"
+
+#: cps/templates/readcbr.html:89
+msgid "Scale to Best"
+msgstr "缩放到最佳"
+
+#: cps/templates/readcbr.html:90
+msgid "Scale to Width"
+msgstr "按宽度缩放"
+
+#: cps/templates/readcbr.html:91
+msgid "Scale to Height"
+msgstr "按高度缩放"
+
+#: cps/templates/readcbr.html:92
+msgid "Scale to Native"
+msgstr ""
+
+#: cps/templates/readcbr.html:93
+msgid "Rotate Right"
+msgstr "向右旋转"
+
+#: cps/templates/readcbr.html:94
+msgid "Rotate Left"
+msgstr "向左旋转"
+
+#: cps/templates/readcbr.html:95
+msgid "Flip Image"
+msgstr "翻转图片"
+
+#: cps/templates/readcbr.html:108 cps/templates/user_edit.html:39
+msgid "Theme"
+msgstr "主题"
+
+#: cps/templates/readcbr.html:111
+msgid "Light"
+msgstr "浅色"
+
+#: cps/templates/readcbr.html:112
+msgid "Dark"
+msgstr "深色"
+
+#: cps/templates/readcbr.html:117
+msgid "Scale"
+msgstr "缩放"
+
+#: cps/templates/readcbr.html:120
+msgid "Best"
+msgstr "最佳"
+
+#: cps/templates/readcbr.html:121
+msgid "Width"
+msgstr "宽度"
+
+#: cps/templates/readcbr.html:122
+msgid "Height"
+msgstr "高度"
+
+#: cps/templates/readcbr.html:123
+msgid "Native"
+msgstr ""
+
+#: cps/templates/readcbr.html:128
+msgid "Rotate"
+msgstr "旋转"
+
+#: cps/templates/readcbr.html:139
+msgid "Flip"
+msgstr "翻转"
+
+#: cps/templates/readcbr.html:142
+msgid "Horizontal"
+msgstr "水平"
+
+#: cps/templates/readcbr.html:143
+msgid "Vertical"
+msgstr "垂直"
+
+#: cps/templates/readpdf.html:29
+msgid "PDF.js viewer"
+msgstr "PDF.js 查看器"
+
+#: cps/templates/readtxt.html:6
+msgid "Basic txt Reader"
+msgstr "简单txt阅读器"
+
+#: cps/templates/register.html:4
+msgid "Register a new account"
+msgstr "注册新用户"
+
+#: cps/templates/register.html:8
+msgid "Choose a username"
+msgstr "选择一个用户名"
+
+#: cps/templates/register.html:11 cps/templates/user_edit.html:13
+msgid "E-mail address"
+msgstr "邮箱地址"
+
+#: cps/templates/register.html:12
+msgid "Your email address"
+msgstr "您的邮箱地址"
+
+#: cps/templates/remote_login.html:6
+msgid "Using your another device, visit"
+msgstr "使用您的另一个设备访问"
+
+#: cps/templates/remote_login.html:6
+msgid "and log in"
+msgstr "和登录"
+
+#: cps/templates/remote_login.html:9
+msgid "Once you do so, you will automatically get logged in on this device."
+msgstr "一旦您这样做了,您在这个设备上会自动登录。"
+
+#: cps/templates/search.html:5
+msgid "No Results for:"
+msgstr "找不到结果:"
+
+#: cps/templates/search.html:6
+msgid "Please try a different search"
+msgstr "请尝试别的关键字"
+
+#: cps/templates/search.html:8
+msgid "Results for:"
+msgstr "结果:"
+
+#: cps/templates/search_form.html:19
+msgid "Publishing date from"
+msgstr ""
+
+#: cps/templates/search_form.html:26
+msgid "Publishing date to"
+msgstr ""
+
+#: cps/templates/search_form.html:43
+msgid "Exclude Tags"
+msgstr "排除标签"
+
+#: cps/templates/search_form.html:63
+msgid "Exclude Series"
+msgstr "排除丛书"
+
+#: cps/templates/search_form.html:84
+msgid "Exclude Languages"
+msgstr "排除语言"
+
+#: cps/templates/search_form.html:97
+msgid "Rating bigger than"
+msgstr "评分大于"
+
+#: cps/templates/search_form.html:101
+msgid "Rating less than"
+msgstr "评分小于"
+
+#: cps/templates/shelf.html:7
+msgid "Delete this Shelf"
+msgstr "删除此书架"
+
+#: cps/templates/shelf.html:8
+msgid "Edit Shelf"
+msgstr "编辑书架"
+
+#: cps/templates/shelf.html:9 cps/templates/shelf_order.html:11
+msgid "Change order"
+msgstr "修改顺序"
+
+#: cps/templates/shelf.html:56
+msgid "Do you really want to delete the shelf?"
+msgstr "您真的想要删除这个书架吗?"
+
+#: cps/templates/shelf.html:59
+msgid "Shelf will be lost for everybody and forever!"
+msgstr "书架将会永远丢失!"
+
+#: cps/templates/shelf_edit.html:13
+msgid "should the shelf be public?"
+msgstr "要公开此书架吗?"
+
+#: cps/templates/shelf_order.html:5
+msgid "Drag 'n drop to rearrange order"
+msgstr "拖拽以重新排序"
+
+#: cps/templates/stats.html:7
+msgid "Calibre library statistics"
+msgstr "Calibre书库统计"
+
+#: cps/templates/stats.html:12
+msgid "Books in this Library"
+msgstr "本书在此书库"
+
+#: cps/templates/stats.html:16
+msgid "Authors in this Library"
+msgstr "个作者在此书库"
+
+#: cps/templates/stats.html:20
+msgid "Categories in this Library"
+msgstr "个分类在此书库"
+
+#: cps/templates/stats.html:24
+msgid "Series in this Library"
+msgstr "个丛书在此书库"
+
+#: cps/templates/stats.html:28
+msgid "Linked libraries"
+msgstr "链接库"
+
+#: cps/templates/stats.html:32
+msgid "Program library"
+msgstr "程序库"
+
+#: cps/templates/stats.html:33
+msgid "Installed Version"
+msgstr "已安装版本"
+
+#: cps/templates/tasks.html:7
+msgid "Tasks list"
+msgstr "任务列表"
+
+#: cps/templates/tasks.html:12
+msgid "User"
+msgstr "用户"
+
+#: cps/templates/tasks.html:14
+msgid "Task"
+msgstr "任务"
+
+#: cps/templates/tasks.html:15
+msgid "Status"
+msgstr "状态"
+
+#: cps/templates/tasks.html:16
+msgid "Progress"
+msgstr "进展"
+
+#: cps/templates/tasks.html:17
+msgid "Runtime"
+msgstr "运行时间"
+
+#: cps/templates/tasks.html:18
+msgid "Starttime"
+msgstr "开始时间"
+
+#: cps/templates/tasks.html:24
+msgid "Delete finished tasks"
+msgstr "删除已完成任务"
+
+#: cps/templates/tasks.html:25
+msgid "Hide all tasks"
+msgstr "隐藏所有任务"
+
+#: cps/templates/user_edit.html:18
+msgid "Reset user Password"
+msgstr "重置用户密码"
+
+#: cps/templates/user_edit.html:27
+msgid "Kindle E-Mail"
+msgstr ""
+
+#: cps/templates/user_edit.html:41
+msgid "Standard Theme"
+msgstr "标准主题"
+
+#: cps/templates/user_edit.html:42
+msgid "caliBlur! Dark Theme (Beta)"
+msgstr ""
+
+#: cps/templates/user_edit.html:47
+msgid "Show books with language"
+msgstr "按语言显示书籍"
+
+#: cps/templates/user_edit.html:49
+msgid "Show all"
+msgstr "显示全部"
+
+#: cps/templates/user_edit.html:147
+msgid "Delete this user"
+msgstr "删除此用户"
+
+#: cps/templates/user_edit.html:162
+msgid "Recent Downloads"
+msgstr "最近下载"
+
+#~ msgid "Published after %s"
+#~ msgstr "出版时晚于 %s"
+
+#~ msgid "%s: %s"
+#~ msgstr ""
+
+#~ msgid "E-Mail: %s"
+#~ msgstr ""
+
+#~ msgid "Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+#~ msgstr "将标题从'%(src)s'改为'%(dest)s'时失败,出错信息: %(error)s"
+
+#~ msgid "Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+#~ msgstr "将作者从'%(src)s'改为'%(dest)s'时失败,出错信息: %(error)s"
+
+#~ msgid "Password for user %(user)s reset"
+#~ msgstr "用户 %(user)s 的密码已重置"
+
+#~ msgid "Password for user %s reset"
+#~ msgstr ""
+
+#~ msgid "Rename title from: '%(src)s' to '%(src)s' failed with error: %(error)s"
+#~ msgstr ""
+
+#~ msgid "Rename author from: '%(src)s' to '%(src)s' failed with error: %(error)s"
+#~ msgstr ""
+
+#~ msgid "Failed to create path for cover %(cover)s (Permission denied)."
+#~ msgstr "为封面 %(cover)s 创建路径失败(权限拒绝)。"
+
+#~ msgid "File extension '%s' is not allowed to be uploaded to this server"
+#~ msgstr ""
+
+#~ msgid "File extension \"%(ext)s\" is not allowed to be uploaded to this server"
+#~ msgstr "不能上传后缀为 \"%(ext)s\" 的文件到此服务器"
+
+#~ msgid "Current commit timestamp"
+#~ msgstr "当前提交时间戳"
+
+#~ msgid "Newest commit timestamp"
+#~ msgstr "最新提交时间戳"
+
+#~ msgid "Convert: %(book)s"
+#~ msgstr "转换: %(book)s"
+
+#~ msgid "Convert to %(format)s: %(book)s"
+#~ msgstr "转换到 %(format)s: %(book)s"
+
+#~ msgid "Files are replaced"
+#~ msgstr "文件已替换"
+
+#~ msgid "Server is stopped"
+#~ msgstr "服务器已停止"
+
+#~ msgid "Convertertool %(converter)s not found"
+#~ msgstr "找不到转换工具 $(converter)s"
+
+#~ msgid "Choose a password"
+#~ msgstr "选择一个密码"
+
diff --git a/src/cps/ub.py b/src/cps/ub.py
new file mode 100644
index 0000000..57dbde6
--- /dev/null
+++ b/src/cps/ub.py
@@ -0,0 +1,783 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from sqlalchemy import *
+from sqlalchemy import exc
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import *
+from flask_login import AnonymousUserMixin
+import sys
+import os
+import logging
+from werkzeug.security import generate_password_hash
+import json
+import datetime
+from binascii import hexlify
+import cli
+
+engine = create_engine('sqlite:///{0}'.format(cli.settingspath), echo=False)
+Base = declarative_base()
+
+ROLE_USER = 0
+ROLE_ADMIN = 1
+ROLE_DOWNLOAD = 2
+ROLE_UPLOAD = 4
+ROLE_EDIT = 8
+ROLE_PASSWD = 16
+ROLE_ANONYMOUS = 32
+ROLE_EDIT_SHELFS = 64
+ROLE_DELETE_BOOKS = 128
+
+
+DETAIL_RANDOM = 1
+SIDEBAR_LANGUAGE = 2
+SIDEBAR_SERIES = 4
+SIDEBAR_CATEGORY = 8
+SIDEBAR_HOT = 16
+SIDEBAR_RANDOM = 32
+SIDEBAR_AUTHOR = 64
+SIDEBAR_BEST_RATED = 128
+SIDEBAR_READ_AND_UNREAD = 256
+SIDEBAR_RECENT = 512
+SIDEBAR_SORTED = 1024
+MATURE_CONTENT = 2048
+SIDEBAR_PUBLISHER = 4096
+
+DEFAULT_PASS = "admin123"
+DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083))
+
+
+class UserBase:
+
+ @property
+ def is_authenticated(self):
+ return True
+
+ def role_admin(self):
+ if self.role is not None:
+ return True if self.role & ROLE_ADMIN == ROLE_ADMIN else False
+ else:
+ return False
+
+ def role_download(self):
+ if self.role is not None:
+ return True if self.role & ROLE_DOWNLOAD == ROLE_DOWNLOAD else False
+ else:
+ return False
+
+ def role_upload(self):
+ return bool((self.role is not None)and(self.role & ROLE_UPLOAD == ROLE_UPLOAD))
+
+ def role_edit(self):
+ if self.role is not None:
+ return True if self.role & ROLE_EDIT == ROLE_EDIT else False
+ else:
+ return False
+
+ def role_passwd(self):
+ if self.role is not None:
+ return True if self.role & ROLE_PASSWD == ROLE_PASSWD else False
+ else:
+ return False
+
+ def role_anonymous(self):
+ if self.role is not None:
+ return True if self.role & ROLE_ANONYMOUS == ROLE_ANONYMOUS else False
+ else:
+ return False
+
+ def role_edit_shelfs(self):
+ if self.role is not None:
+ return True if self.role & ROLE_EDIT_SHELFS == ROLE_EDIT_SHELFS else False
+ else:
+ return False
+
+ def role_delete_books(self):
+ return bool((self.role is not None)and(self.role & ROLE_DELETE_BOOKS == ROLE_DELETE_BOOKS))
+
+ @property
+ def is_active(self):
+ return True
+
+ @property
+ def is_anonymous(self):
+ return False
+
+ @property
+ def get_theme(self):
+ return self.theme
+
+ def get_id(self):
+ return str(self.id)
+
+ def filter_language(self):
+ return self.default_language
+
+ def show_random_books(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_RANDOM == SIDEBAR_RANDOM))
+
+ def show_language(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_LANGUAGE == SIDEBAR_LANGUAGE))
+
+ def show_hot_books(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_HOT == SIDEBAR_HOT))
+
+ def show_recent(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_RECENT == SIDEBAR_RECENT))
+
+ def show_sorted(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_SORTED == SIDEBAR_SORTED))
+
+ def show_series(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_SERIES == SIDEBAR_SERIES))
+
+ def show_category(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_CATEGORY == SIDEBAR_CATEGORY))
+
+ def show_author(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR))
+
+ def show_publisher(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_PUBLISHER == SIDEBAR_PUBLISHER))
+
+ def show_best_rated_books(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED))
+
+ def show_read_and_unread(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_READ_AND_UNREAD == SIDEBAR_READ_AND_UNREAD))
+
+ def show_detail_random(self):
+ return bool((self.sidebar_view is not None)and(self.sidebar_view & DETAIL_RANDOM == DETAIL_RANDOM))
+
+ def __repr__(self):
+ return '' % self.nickname
+
+
+# Baseclass for Users in Calibre-Web, settings which are depending on certain users are stored here. It is derived from
+# User Base (all access methods are declared there)
+class User(UserBase, Base):
+ __tablename__ = 'user'
+
+ id = Column(Integer, primary_key=True)
+ nickname = Column(String(64), unique=True)
+ email = Column(String(120), unique=True, default="")
+ role = Column(SmallInteger, default=ROLE_USER)
+ password = Column(String)
+ kindle_mail = Column(String(120), default="")
+ shelf = relationship('Shelf', backref='user', lazy='dynamic', order_by='Shelf.name')
+ downloads = relationship('Downloads', backref='user', lazy='dynamic')
+ locale = Column(String(2), default="en")
+ sidebar_view = Column(Integer, default=1)
+ default_language = Column(String(3), default="all")
+ mature_content = Column(Boolean, default=True)
+ theme = Column(Integer, default=0)
+
+
+# Class for anonymous user is derived from User base and completly overrides methods and properties for the
+# anonymous user
+class Anonymous(AnonymousUserMixin, UserBase):
+ def __init__(self):
+ self.loadSettings()
+
+ def loadSettings(self):
+ data = session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() # type: User
+ settings = session.query(Settings).first()
+ self.nickname = data.nickname
+ self.role = data.role
+ self.id=data.id
+ self.sidebar_view = data.sidebar_view
+ self.default_language = data.default_language
+ self.locale = data.locale
+ self.mature_content = data.mature_content
+ self.anon_browse = settings.config_anonbrowse
+
+ def role_admin(self):
+ return False
+
+ @property
+ def is_active(self):
+ return False
+
+ @property
+ def is_anonymous(self):
+ return self.anon_browse
+
+ @property
+ def is_authenticated(self):
+ return False
+
+
+# Baseclass representing Shelfs in calibre-web in app.db
+class Shelf(Base):
+ __tablename__ = 'shelf'
+
+ id = Column(Integer, primary_key=True)
+ name = Column(String)
+ is_public = Column(Integer, default=0)
+ user_id = Column(Integer, ForeignKey('user.id'))
+
+ def __repr__(self):
+ return '' % self.name
+
+
+# Baseclass representing Relationship between books and Shelfs in Calibre-Web in app.db (N:M)
+class BookShelf(Base):
+ __tablename__ = 'book_shelf_link'
+
+ id = Column(Integer, primary_key=True)
+ book_id = Column(Integer)
+ order = Column(Integer)
+ shelf = Column(Integer, ForeignKey('shelf.id'))
+
+ def __repr__(self):
+ return '' % self.id
+
+
+class ReadBook(Base):
+ __tablename__ = 'book_read_link'
+
+ id = Column(Integer, primary_key=True)
+ book_id = Column(Integer, unique=False)
+ user_id = Column(Integer, ForeignKey('user.id'), unique=False)
+ is_read = Column(Boolean, unique=False)
+
+
+class Bookmark(Base):
+ __tablename__ = 'bookmark'
+
+ id = Column(Integer, primary_key=True)
+ user_id = Column(Integer, ForeignKey('user.id'))
+ book_id = Column(Integer)
+ format = Column(String(collation='NOCASE'))
+ bookmark_key = Column(String)
+
+
+# Baseclass representing Downloads from calibre-web in app.db
+class Downloads(Base):
+ __tablename__ = 'downloads'
+
+ id = Column(Integer, primary_key=True)
+ book_id = Column(Integer)
+ user_id = Column(Integer, ForeignKey('user.id'))
+
+ def __repr__(self):
+ return '".format(self.domain)
+
+
+# Baseclass for representing settings in app.db with email server settings and Calibre database settings
+# (application settings)
+class Settings(Base):
+ __tablename__ = 'settings'
+
+ id = Column(Integer, primary_key=True)
+ mail_server = Column(String)
+ mail_port = Column(Integer, default=25)
+ mail_use_ssl = Column(SmallInteger, default=0)
+ mail_login = Column(String)
+ mail_password = Column(String)
+ mail_from = Column(String)
+ config_calibre_dir = Column(String)
+ config_port = Column(Integer, default=DEFAULT_PORT)
+ config_certfile = Column(String)
+ config_keyfile = Column(String)
+ config_calibre_web_title = Column(String, default=u'Calibre-Web')
+ config_books_per_page = Column(Integer, default=60)
+ config_random_books = Column(Integer, default=4)
+ config_read_column = Column(Integer, default=0)
+ config_title_regex = Column(String, default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
+ config_log_level = Column(SmallInteger, default=logging.INFO)
+ config_uploading = Column(SmallInteger, default=0)
+ config_anonbrowse = Column(SmallInteger, default=0)
+ config_public_reg = Column(SmallInteger, default=0)
+ config_default_role = Column(SmallInteger, default=0)
+ config_default_show = Column(SmallInteger, default=6143)
+ config_columns_to_ignore = Column(String)
+ config_use_google_drive = Column(Boolean)
+ config_google_drive_folder = Column(String)
+ config_google_drive_watch_changes_response = Column(String)
+ config_remote_login = Column(Boolean)
+ config_use_goodreads = Column(Boolean)
+ config_goodreads_api_key = Column(String)
+ config_goodreads_api_secret = Column(String)
+ config_mature_content_tags = Column(String)
+ config_logfile = Column(String)
+ config_ebookconverter = Column(Integer, default=0)
+ config_converterpath = Column(String)
+ config_calibre = Column(String)
+ config_rarfile_location = Column(String)
+
+ def __repr__(self):
+ pass
+
+
+class RemoteAuthToken(Base):
+ __tablename__ = 'remote_auth_token'
+
+ id = Column(Integer, primary_key=True)
+ auth_token = Column(String(8), unique=True)
+ user_id = Column(Integer, ForeignKey('user.id'))
+ verified = Column(Boolean, default=False)
+ expiration = Column(DateTime)
+
+ def __init__(self):
+ self.auth_token = hexlify(os.urandom(4))
+ self.expiration = datetime.datetime.now() + datetime.timedelta(minutes=10) # 10 min from now
+
+ def __repr__(self):
+ return '' % self.id
+
+
+# Class holds all application specific settings in calibre-web
+class Config:
+ def __init__(self):
+ self.config_main_dir = os.path.join(os.path.normpath(os.path.dirname(
+ os.path.realpath(__file__)) + os.sep + ".." + os.sep))
+ self.db_configured = None
+ self.config_logfile = None
+ self.loadSettings()
+
+ def loadSettings(self):
+ data = session.query(Settings).first() # type: Settings
+
+ self.config_calibre_dir = data.config_calibre_dir
+ self.config_port = data.config_port
+ self.config_certfile = data.config_certfile
+ self.config_keyfile = data.config_keyfile
+ self.config_calibre_web_title = data.config_calibre_web_title
+ self.config_books_per_page = data.config_books_per_page
+ self.config_random_books = data.config_random_books
+ self.config_title_regex = data.config_title_regex
+ self.config_read_column = data.config_read_column
+ self.config_log_level = data.config_log_level
+ self.config_uploading = data.config_uploading
+ self.config_anonbrowse = data.config_anonbrowse
+ self.config_public_reg = data.config_public_reg
+ self.config_default_role = data.config_default_role
+ self.config_default_show = data.config_default_show
+ self.config_columns_to_ignore = data.config_columns_to_ignore
+ self.config_use_google_drive = data.config_use_google_drive
+ self.config_google_drive_folder = data.config_google_drive_folder
+ self.config_ebookconverter = data.config_ebookconverter
+ self.config_converterpath = data.config_converterpath
+ self.config_calibre = data.config_calibre
+ if data.config_google_drive_watch_changes_response:
+ self.config_google_drive_watch_changes_response = json.loads(data.config_google_drive_watch_changes_response)
+ else:
+ self.config_google_drive_watch_changes_response=None
+ self.config_columns_to_ignore = data.config_columns_to_ignore
+ self.db_configured = bool(self.config_calibre_dir is not None and
+ (not self.config_use_google_drive or os.path.exists(self.config_calibre_dir + '/metadata.db')))
+ self.config_remote_login = data.config_remote_login
+ self.config_use_goodreads = data.config_use_goodreads
+ self.config_goodreads_api_key = data.config_goodreads_api_key
+ self.config_goodreads_api_secret = data.config_goodreads_api_secret
+ if data.config_mature_content_tags:
+ self.config_mature_content_tags = data.config_mature_content_tags
+ else:
+ self.config_mature_content_tags = u''
+ if data.config_logfile:
+ self.config_logfile = data.config_logfile
+ self.config_rarfile_location = data.config_rarfile_location
+
+ @property
+ def get_main_dir(self):
+ return self.config_main_dir
+
+ def get_config_certfile(self):
+ if cli.certfilepath:
+ return cli.certfilepath
+ else:
+ if cli.certfilepath is "":
+ return None
+ else:
+ return self.config_certfile
+
+ def get_config_keyfile(self):
+ if cli.keyfilepath:
+ return cli.keyfilepath
+ else:
+ if cli.certfilepath is "":
+ return None
+ else:
+ return self.config_keyfile
+
+ def get_config_logfile(self):
+ if not self.config_logfile:
+ return os.path.join(self.get_main_dir, "calibre-web.log")
+ else:
+ if os.path.dirname(self.config_logfile):
+ return self.config_logfile
+ else:
+ return os.path.join(self.get_main_dir, self.config_logfile)
+
+ def role_admin(self):
+ if self.config_default_role is not None:
+ return True if self.config_default_role & ROLE_ADMIN == ROLE_ADMIN else False
+ else:
+ return False
+
+ def role_download(self):
+ if self.config_default_role is not None:
+ return True if self.config_default_role & ROLE_DOWNLOAD == ROLE_DOWNLOAD else False
+ else:
+ return False
+
+ def role_upload(self):
+ if self.config_default_role is not None:
+ return True if self.config_default_role & ROLE_UPLOAD == ROLE_UPLOAD else False
+ else:
+ return False
+
+ def role_edit(self):
+ if self.config_default_role is not None:
+ return True if self.config_default_role & ROLE_EDIT == ROLE_EDIT else False
+ else:
+ return False
+
+ def role_passwd(self):
+ if self.config_default_role is not None:
+ return True if self.config_default_role & ROLE_PASSWD == ROLE_PASSWD else False
+ else:
+ return False
+
+ def role_edit_shelfs(self):
+ if self.config_default_role is not None:
+ return True if self.config_default_role & ROLE_EDIT_SHELFS == ROLE_EDIT_SHELFS else False
+ else:
+ return False
+
+ def role_delete_books(self):
+ return bool((self.config_default_role is not None) and
+ (self.config_default_role & ROLE_DELETE_BOOKS == ROLE_DELETE_BOOKS))
+
+ def show_detail_random(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & DETAIL_RANDOM == DETAIL_RANDOM))
+
+ def show_language(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & SIDEBAR_LANGUAGE == SIDEBAR_LANGUAGE))
+
+ def show_series(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & SIDEBAR_SERIES == SIDEBAR_SERIES))
+
+ def show_category(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & SIDEBAR_CATEGORY == SIDEBAR_CATEGORY))
+
+ def show_hot_books(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & SIDEBAR_HOT == SIDEBAR_HOT))
+
+ def show_random_books(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & SIDEBAR_RANDOM == SIDEBAR_RANDOM))
+
+ def show_author(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR))
+
+ def show_publisher(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & SIDEBAR_PUBLISHER == SIDEBAR_PUBLISHER))
+
+ def show_best_rated_books(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED))
+
+ def show_read_and_unread(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & SIDEBAR_READ_AND_UNREAD == SIDEBAR_READ_AND_UNREAD))
+
+ def show_recent(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & SIDEBAR_RECENT == SIDEBAR_RECENT))
+
+ def show_sorted(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & SIDEBAR_SORTED == SIDEBAR_SORTED))
+
+ def show_mature_content(self):
+ return bool((self.config_default_show is not None) and
+ (self.config_default_show & MATURE_CONTENT == MATURE_CONTENT))
+
+ def mature_content_tags(self):
+ if sys.version_info > (3, 0): # Python3 str, Python2 unicode
+ lstrip = str.lstrip
+ else:
+ lstrip = unicode.lstrip
+ return list(map(lstrip, self.config_mature_content_tags.split(",")))
+
+ def get_Log_Level(self):
+ ret_value = ""
+ if self.config_log_level == logging.INFO:
+ ret_value = 'INFO'
+ elif self.config_log_level == logging.DEBUG:
+ ret_value = 'DEBUG'
+ elif self.config_log_level == logging.WARNING:
+ ret_value = 'WARNING'
+ elif self.config_log_level == logging.ERROR:
+ ret_value = 'ERROR'
+ return ret_value
+
+
+# Migrate database to current version, has to be updated after every database change. Currently migration from
+# everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding
+# rows with SQL commands
+def migrate_Database():
+ if not engine.dialect.has_table(engine.connect(), "bookmark"):
+ Bookmark.__table__.create(bind=engine)
+ if not engine.dialect.has_table(engine.connect(), "registration"):
+ ReadBook.__table__.create(bind=engine)
+ conn = engine.connect()
+ conn.execute("insert into registration (domain) values('%.%')")
+ session.commit()
+ # Handle table exists, but no content
+ cnt = session.query(Registration).count()
+ if not cnt:
+ conn = engine.connect()
+ conn.execute("insert into registration (domain) values('%.%')")
+ session.commit()
+ try:
+ session.query(exists().where(Settings.config_use_google_drive)).scalar()
+ except exc.OperationalError:
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_use_google_drive` INTEGER DEFAULT 0")
+ conn.execute("ALTER TABLE Settings ADD column `config_google_drive_folder` String DEFAULT ''")
+ conn.execute("ALTER TABLE Settings ADD column `config_google_drive_watch_changes_response` String DEFAULT ''")
+ session.commit()
+ try:
+ session.query(exists().where(Settings.config_columns_to_ignore)).scalar()
+ except exc.OperationalError:
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_columns_to_ignore` String DEFAULT ''")
+ session.commit()
+ try:
+ session.query(exists().where(Settings.config_default_role)).scalar()
+ except exc.OperationalError: # Database is not compatible, some rows are missing
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_default_role` SmallInteger DEFAULT 0")
+ session.commit()
+ try:
+ session.query(exists().where(BookShelf.order)).scalar()
+ except exc.OperationalError: # Database is not compatible, some rows are missing
+ conn = engine.connect()
+ conn.execute("ALTER TABLE book_shelf_link ADD column 'order' INTEGER DEFAULT 1")
+ session.commit()
+ try:
+ session.query(exists().where(Settings.config_rarfile_location)).scalar()
+ session.commit()
+ except exc.OperationalError: # Database is not compatible, some rows are missing
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_rarfile_location` String DEFAULT ''")
+ session.commit()
+ try:
+ create = False
+ session.query(exists().where(User.sidebar_view)).scalar()
+ except exc.OperationalError: # Database is not compatible, some rows are missing
+ conn = engine.connect()
+ conn.execute("ALTER TABLE user ADD column `sidebar_view` Integer DEFAULT 1")
+ session.commit()
+ create = True
+ try:
+ if create:
+ conn = engine.connect()
+ conn.execute("SELECT language_books FROM user")
+ session.commit()
+ except exc.OperationalError:
+ conn = engine.connect()
+ conn.execute("UPDATE user SET 'sidebar_view' = (random_books* :side_random + language_books * :side_lang "
+ "+ series_books * :side_series + category_books * :side_category + hot_books * "
+ ":side_hot + :side_autor + :detail_random)"
+ ,{'side_random': SIDEBAR_RANDOM, 'side_lang': SIDEBAR_LANGUAGE, 'side_series': SIDEBAR_SERIES,
+ 'side_category': SIDEBAR_CATEGORY, 'side_hot': SIDEBAR_HOT, 'side_autor': SIDEBAR_AUTHOR,
+ 'detail_random': DETAIL_RANDOM})
+ session.commit()
+ try:
+ session.query(exists().where(User.mature_content)).scalar()
+ except exc.OperationalError:
+ conn = engine.connect()
+ conn.execute("ALTER TABLE user ADD column `mature_content` INTEGER DEFAULT 1")
+ try:
+ session.query(exists().where(User.theme)).scalar()
+ except exc.OperationalError:
+ conn = engine.connect()
+ conn.execute("ALTER TABLE user ADD column `theme` INTEGER DEFAULT 0")
+ session.commit()
+ if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None:
+ create_anonymous_user()
+ try:
+ session.query(exists().where(Settings.config_remote_login)).scalar()
+ except exc.OperationalError:
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_remote_login` INTEGER DEFAULT 0")
+ try:
+ session.query(exists().where(Settings.config_use_goodreads)).scalar()
+ except exc.OperationalError:
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_use_goodreads` INTEGER DEFAULT 0")
+ conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_key` String DEFAULT ''")
+ conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_secret` String DEFAULT ''")
+ try:
+ session.query(exists().where(Settings.config_mature_content_tags)).scalar()
+ except exc.OperationalError:
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_mature_content_tags` String DEFAULT ''")
+ try:
+ session.query(exists().where(Settings.config_default_show)).scalar()
+ except exc.OperationalError: # Database is not compatible, some rows are missing
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_default_show` SmallInteger DEFAULT 2047")
+ session.commit()
+ try:
+ session.query(exists().where(Settings.config_logfile)).scalar()
+ except exc.OperationalError: # Database is not compatible, some rows are missing
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_logfile` String DEFAULT ''")
+ session.commit()
+ try:
+ session.query(exists().where(Settings.config_certfile)).scalar()
+ except exc.OperationalError: # Database is not compatible, some rows are missing
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_certfile` String DEFAULT ''")
+ conn.execute("ALTER TABLE Settings ADD column `config_keyfile` String DEFAULT ''")
+ session.commit()
+ try:
+ session.query(exists().where(Settings.config_read_column)).scalar()
+ except exc.OperationalError: # Database is not compatible, some rows are missing
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_read_column` INTEGER DEFAULT 0")
+ session.commit()
+ try:
+ session.query(exists().where(Settings.config_ebookconverter)).scalar()
+ except exc.OperationalError: # Database is not compatible, some rows are missing
+ conn = engine.connect()
+ conn.execute("ALTER TABLE Settings ADD column `config_ebookconverter` INTEGER DEFAULT 0")
+ conn.execute("ALTER TABLE Settings ADD column `config_converterpath` String DEFAULT ''")
+ conn.execute("ALTER TABLE Settings ADD column `config_calibre` String DEFAULT ''")
+ session.commit()
+
+ # Remove login capability of user Guest
+ conn = engine.connect()
+ conn.execute("UPDATE user SET password='' where nickname = 'Guest' and password !=''")
+ session.commit()
+
+
+def clean_database():
+ # Remove expired remote login tokens
+ now = datetime.datetime.now()
+ session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).delete()
+
+
+def create_default_config():
+ settings = Settings()
+ settings.mail_server = "mail.example.com"
+ settings.mail_port = 25
+ settings.mail_use_ssl = 0
+ settings.mail_login = "mail@example.com"
+ settings.mail_password = "mypassword"
+ settings.mail_from = "automailer "
+
+ session.add(settings)
+ session.commit()
+
+
+def get_mail_settings():
+ settings = session.query(Settings).first()
+
+ if not settings:
+ return {}
+
+ data = {
+ 'mail_server': settings.mail_server,
+ 'mail_port': settings.mail_port,
+ 'mail_use_ssl': settings.mail_use_ssl,
+ 'mail_login': settings.mail_login,
+ 'mail_password': settings.mail_password,
+ 'mail_from': settings.mail_from
+ }
+
+ return data
+
+# Save downloaded books per user in calibre-web's own database
+def update_download(book_id, user_id):
+ check = session.query(Downloads).filter(Downloads.user_id == user_id).filter(Downloads.book_id ==
+ book_id).first()
+
+ if not check:
+ new_download = Downloads(user_id=user_id, book_id=book_id)
+ session.add(new_download)
+ session.commit()
+
+# Delete non exisiting downloaded books in calibre-web's own database
+def delete_download(book_id):
+ session.query(Downloads).filter(book_id == Downloads.book_id).delete()
+ session.commit()
+
+# Generate user Guest (translated text), as anoymous user, no rights
+def create_anonymous_user():
+ user = User()
+ user.nickname = "Guest"
+ user.email = 'no@email'
+ user.role = ROLE_ANONYMOUS
+ user.password = ''
+
+ session.add(user)
+ try:
+ session.commit()
+ except Exception:
+ session.rollback()
+
+
+# Generate User admin with admin123 password, and access to everything
+def create_admin_user():
+ user = User()
+ user.nickname = "admin"
+ user.role = ROLE_USER + ROLE_ADMIN + ROLE_DOWNLOAD + ROLE_UPLOAD + ROLE_EDIT + ROLE_DELETE_BOOKS + ROLE_PASSWD
+ user.sidebar_view = DETAIL_RANDOM + SIDEBAR_LANGUAGE + SIDEBAR_SERIES + SIDEBAR_CATEGORY + SIDEBAR_HOT + \
+ SIDEBAR_RANDOM + SIDEBAR_AUTHOR + SIDEBAR_BEST_RATED + SIDEBAR_READ_AND_UNREAD + SIDEBAR_RECENT + \
+ SIDEBAR_SORTED + MATURE_CONTENT + SIDEBAR_PUBLISHER
+
+ user.password = generate_password_hash(DEFAULT_PASS)
+
+ session.add(user)
+ try:
+ session.commit()
+ except Exception:
+ session.rollback()
+
+
+# Open session for database connection
+Session = sessionmaker()
+Session.configure(bind=engine)
+session = Session()
+
+# generate database and admin and guest user, if no database is existing
+if not os.path.exists(cli.settingspath):
+ try:
+ Base.metadata.create_all(engine)
+ create_default_config()
+ create_admin_user()
+ create_anonymous_user()
+ except Exception:
+ raise
+else:
+ Base.metadata.create_all(engine)
+ migrate_Database()
+ clean_database()
+
+# Generate global Settings Object accessible from every file
+config = Config()
+searched_ids = {}
diff --git a/src/cps/uploader.py b/src/cps/uploader.py
new file mode 100644
index 0000000..40741bf
--- /dev/null
+++ b/src/cps/uploader.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+from tempfile import gettempdir
+import hashlib
+from collections import namedtuple
+import book_formats
+
+BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, series_id, languages')
+
+"""
+ :rtype: BookMeta
+"""
+
+
+def upload(uploadfile):
+ tmp_dir = os.path.join(gettempdir(), 'calibre_web')
+
+ if not os.path.isdir(tmp_dir):
+ os.mkdir(tmp_dir)
+
+ filename = uploadfile.filename
+ filename_root, file_extension = os.path.splitext(filename)
+ md5 = hashlib.md5()
+ md5.update(filename.encode('utf-8'))
+ tmp_file_path = os.path.join(tmp_dir, md5.hexdigest())
+ uploadfile.save(tmp_file_path)
+ meta = book_formats.process(tmp_file_path, filename_root, file_extension)
+ return meta
diff --git a/src/cps/web.py b/src/cps/web.py
new file mode 100644
index 0000000..e5e4b9d
--- /dev/null
+++ b/src/cps/web.py
@@ -0,0 +1,3947 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import mimetypes
+import logging
+from logging.handlers import RotatingFileHandler
+from flask import (Flask, render_template, request, Response, redirect,
+ url_for, send_from_directory, make_response, g, flash,
+ abort, Markup)
+from flask import __version__ as flaskVersion
+from werkzeug import __version__ as werkzeugVersion
+from werkzeug.exceptions import default_exceptions
+
+from jinja2 import __version__ as jinja2Version
+import cache_buster
+import ub
+from ub import config
+import helper
+import os
+from sqlalchemy.sql.expression import func
+from sqlalchemy.sql.expression import false
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy import __version__ as sqlalchemyVersion
+from math import ceil
+from flask_login import (LoginManager, login_user, logout_user,
+ login_required, current_user)
+from flask_principal import Principal
+from flask_principal import __version__ as flask_principalVersion
+from flask_babel import Babel
+from flask_babel import gettext as _
+import requests
+from werkzeug.security import generate_password_hash, check_password_hash
+from werkzeug.datastructures import Headers
+from babel import Locale as LC
+from babel import negotiate_locale
+from babel import __version__ as babelVersion
+from babel.dates import format_date, format_datetime
+from babel.core import UnknownLocaleError
+from functools import wraps
+import base64
+from sqlalchemy.sql import *
+import json
+import datetime
+from iso639 import languages as isoLanguages
+from iso639 import __version__ as iso639Version
+from pytz import __version__ as pytzVersion
+from uuid import uuid4
+import os.path
+import sys
+import re
+import db
+from shutil import move, copyfile
+import gdriveutils
+import converter
+import tempfile
+from redirect import redirect_back
+import time
+import server
+from reverseproxy import ReverseProxied
+
+try:
+ from googleapiclient.errors import HttpError
+except ImportError:
+ pass
+
+try:
+ from goodreads.client import GoodreadsClient
+ goodreads_support = True
+except ImportError:
+ goodreads_support = False
+
+try:
+ import Levenshtein
+ levenshtein_support = True
+except ImportError:
+ levenshtein_support = False
+
+try:
+ from functools import reduce
+except ImportError:
+ pass # We're not using Python 3
+
+try:
+ import rarfile
+ rar_support=True
+except ImportError:
+ rar_support=False
+
+try:
+ from natsort import natsorted as sort
+except ImportError:
+ sort=sorted # Just use regular sort then
+ # may cause issues with badly named pages in cbz/cbr files
+try:
+ import cPickle
+except ImportError:
+ import pickle as cPickle
+
+try:
+ from urllib.parse import quote
+ from imp import reload
+except ImportError:
+ from urllib import quote
+
+try:
+ from flask_login import __version__ as flask_loginVersion
+except ImportError:
+ from flask_login.__about__ import __version__ as flask_loginVersion
+
+
+# Global variables
+current_milli_time = lambda: int(round(time.time() * 1000))
+gdrive_watch_callback_token = 'target=calibreweb-watch_files'
+EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx',
+ 'fb2', 'html', 'rtf', 'odt'}
+EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'html', 'rtf', 'odt'}
+
+
+# Main code
+mimetypes.init()
+mimetypes.add_type('application/xhtml+xml', '.xhtml')
+mimetypes.add_type('application/epub+zip', '.epub')
+mimetypes.add_type('application/fb2+zip', '.fb2')
+mimetypes.add_type('application/x-mobipocket-ebook', '.mobi')
+mimetypes.add_type('application/x-mobipocket-ebook', '.prc')
+mimetypes.add_type('application/vnd.amazon.ebook', '.azw')
+mimetypes.add_type('application/x-cbr', '.cbr')
+mimetypes.add_type('application/x-cbz', '.cbz')
+mimetypes.add_type('application/x-cbt', '.cbt')
+mimetypes.add_type('image/vnd.djvu', '.djvu')
+
+app = (Flask(__name__))
+
+# custom error page
+def error_http(error):
+ return render_template('http_error.html',
+ error_code=error.code,
+ error_name=error.name,
+ instance=config.config_calibre_web_title
+ ), error.code
+
+# http error handling
+for ex in default_exceptions:
+ # new routine for all client errors, server errors stay
+ if ex < 500:
+ app.register_error_handler(ex, error_http)
+
+app.wsgi_app = ReverseProxied(app.wsgi_app)
+cache_buster.init_cache_busting(app)
+
+formatter = logging.Formatter(
+ "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s")
+try:
+ file_handler = RotatingFileHandler(config.get_config_logfile(), maxBytes=50000, backupCount=2)
+except IOError:
+ file_handler = RotatingFileHandler(os.path.join(config.get_main_dir, "calibre-web.log"),
+ maxBytes=50000, backupCount=2)
+ # ToDo: reset logfile value in config class
+file_handler.setFormatter(formatter)
+app.logger.addHandler(file_handler)
+app.logger.setLevel(config.config_log_level)
+
+app.logger.info('Starting Calibre Web...')
+logging.getLogger("book_formats").addHandler(file_handler)
+logging.getLogger("book_formats").setLevel(config.config_log_level)
+
+Principal(app)
+babel = Babel(app)
+
+import uploader
+
+lm = LoginManager(app)
+lm.init_app(app)
+lm.login_view = 'login'
+lm.anonymous_user = ub.Anonymous
+app.secret_key = os.getenv('SECRET_KEY', 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT')
+db.setup_db()
+
+with open(os.path.join(config.get_main_dir, 'cps/translations/iso639.pickle'), 'rb') as f:
+ language_table = cPickle.load(f)
+
+
+def is_gdrive_ready():
+ return os.path.exists(os.path.join(config.get_main_dir, 'settings.yaml')) and \
+ os.path.exists(os.path.join(config.get_main_dir, 'gdrive_credentials'))
+
+
+
+@babel.localeselector
+def get_locale():
+ # if a user is logged in, use the locale from the user settings
+ user = getattr(g, 'user', None)
+ if user is not None and hasattr(user, "locale"):
+ if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings
+ return user.locale
+ translations = [str(item) for item in babel.list_translations()] + ['en']
+ preferred = [str(LC.parse(x.replace('-','_'))) for x in request.accept_languages.values()]
+ return negotiate_locale(preferred, translations)
+
+
+@babel.timezoneselector
+def get_timezone():
+ user = getattr(g, 'user', None)
+ if user is not None:
+ return user.timezone
+
+
+@lm.user_loader
+def load_user(user_id):
+ return ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
+
+
+@lm.header_loader
+def load_user_from_header(header_val):
+ if header_val.startswith('Basic '):
+ header_val = header_val.replace('Basic ', '', 1)
+ basic_username = basic_password = ''
+ try:
+ header_val = base64.b64decode(header_val)
+ basic_username = header_val.split(':')[0]
+ basic_password = header_val.split(':')[1]
+ except TypeError:
+ pass
+ user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == basic_username.lower()).first()
+ if user and check_password_hash(user.password, basic_password):
+ return user
+ return
+
+
+def check_auth(username, password):
+ user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first()
+ return bool(user and check_password_hash(user.password, password))
+
+
+def authenticate():
+ return Response(
+ 'Could not verify your access level for that URL.\n'
+ 'You have to login with proper credentials', 401,
+ {'WWW-Authenticate': 'Basic realm="Login Required"'})
+
+
+def requires_basic_auth_if_no_ano(f):
+ @wraps(f)
+ def decorated(*args, **kwargs):
+ auth = request.authorization
+ if config.config_anonbrowse != 1:
+ if not auth or not check_auth(auth.username, auth.password):
+ return authenticate()
+ return f(*args, **kwargs)
+
+ return decorated
+
+
+# simple pagination for the feed
+class Pagination(object):
+ def __init__(self, page, per_page, total_count):
+ self.page = int(page)
+ self.per_page = int(per_page)
+ self.total_count = int(total_count)
+
+ @property
+ def next_offset(self):
+ return int(self.page * self.per_page)
+
+ @property
+ def previous_offset(self):
+ return int((self.page - 2) * self.per_page)
+
+ @property
+ def last_offset(self):
+ last = int(self.total_count) - int(self.per_page)
+ if last < 0:
+ last = 0
+ return int(last)
+
+ @property
+ def pages(self):
+ return int(ceil(self.total_count / float(self.per_page)))
+
+ @property
+ def has_prev(self):
+ return self.page > 1
+
+ @property
+ def has_next(self):
+ return self.page < self.pages
+
+ # right_edge: last right_edges count of all pages are shown as number, means, if 10 pages are paginated -> 9,10 shwn
+ # left_edge: first left_edges count of all pages are shown as number -> 1,2 shwn
+ # left_current: left_current count below current page are shown as number, means if current page 5 -> 3,4 shwn
+ # left_current: right_current count above current page are shown as number, means if current page 5 -> 6,7 shwn
+ def iter_pages(self, left_edge=2, left_current=2,
+ right_current=4, right_edge=2):
+ last = 0
+ left_current = self.page - left_current - 1
+ right_current = self.page + right_current + 1
+ right_edge = self.pages - right_edge
+ for num in range(1, (self.pages + 1)):
+ if num <= left_edge or (left_current < num < right_current) or num > right_edge:
+ if last + 1 != num:
+ yield None
+ yield num
+ last = num
+
+
+def login_required_if_no_ano(func):
+ @wraps(func)
+ def decorated_view(*args, **kwargs):
+ if config.config_anonbrowse == 1:
+ return func(*args, **kwargs)
+ return login_required(func)(*args, **kwargs)
+ return decorated_view
+
+
+def remote_login_required(f):
+ @wraps(f)
+ def inner(*args, **kwargs):
+ if config.config_remote_login:
+ return f(*args, **kwargs)
+ if request.is_xhr:
+ data = {'status': 'error', 'message': 'Forbidden'}
+ response = make_response(json.dumps(data, ensure_ascii=False))
+ response.headers["Content-Type"] = "application/json; charset=utf-8"
+ return response, 403
+ abort(403)
+
+ return inner
+
+
+# custom jinja filters
+
+# pagination links in jinja
+@app.template_filter('url_for_other_page')
+def url_for_other_page(page):
+ args = request.view_args.copy()
+ args['page'] = page
+ return url_for(request.endpoint, **args)
+
+
+# shortentitles to at longest nchar, shorten longer words if necessary
+@app.template_filter('shortentitle')
+def shortentitle_filter(s, nchar=20):
+ text = s.split()
+ res = "" # result
+ suml = 0 # overall length
+ for line in text:
+ if suml >= 60:
+ res += '...'
+ break
+ # if word longer than 20 chars truncate line and append '...', otherwise add whole word to result
+ # string, and summarize total length to stop at chars given by nchar
+ if len(line) > nchar:
+ res += line[:(nchar-3)] + '[..] '
+ suml += nchar+3
+ else:
+ res += line + ' '
+ suml += len(line) + 1
+ return res.strip()
+
+
+@app.template_filter('mimetype')
+def mimetype_filter(val):
+ try:
+ s = mimetypes.types_map['.' + val]
+ except Exception:
+ s = 'application/octet-stream'
+ return s
+
+
+@app.template_filter('formatdate')
+def formatdate_filter(val):
+ conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val)
+ formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
+ return format_date(formatdate, format='medium', locale=get_locale())
+
+
+@app.template_filter('formatdateinput')
+def format_date_input(val):
+ conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val)
+ date_obj = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
+ input_date = date_obj.isoformat().split('T', 1)[0] # Hack to support dates <1900
+ return '' if input_date == "0101-01-01" else input_date
+
+
+@app.template_filter('strftime')
+def timestamptodate(date, fmt=None):
+ date = datetime.datetime.fromtimestamp(
+ int(date)/1000
+ )
+ native = date.replace(tzinfo=None)
+ if fmt:
+ time_format = fmt
+ else:
+ time_format = '%d %m %Y - %H:%S'
+ return native.strftime(time_format)
+
+
+@app.template_filter('yesno')
+def yesno(value, yes, no):
+ return yes if value else no
+
+
+'''@app.template_filter('canread')
+def canread(ext):
+ if isinstance(ext, db.Data):
+ ext = ext.format
+ return ext.lower() in EXTENSIONS_READER'''
+
+
+def admin_required(f):
+ """
+ Checks if current_user.role == 1
+ """
+
+ @wraps(f)
+ def inner(*args, **kwargs):
+ if current_user.role_admin():
+ return f(*args, **kwargs)
+ abort(403)
+
+ return inner
+
+
+def unconfigured(f):
+ """
+ Checks if current_user.role == 1
+ """
+
+ @wraps(f)
+ def inner(*args, **kwargs):
+ if not config.db_configured:
+ return f(*args, **kwargs)
+ abort(403)
+
+ return inner
+
+
+def download_required(f):
+ @wraps(f)
+ def inner(*args, **kwargs):
+ if current_user.role_download() or current_user.role_admin():
+ return f(*args, **kwargs)
+ abort(403)
+
+ return inner
+
+
+def upload_required(f):
+ @wraps(f)
+ def inner(*args, **kwargs):
+ if current_user.role_upload() or current_user.role_admin():
+ return f(*args, **kwargs)
+ abort(403)
+
+ return inner
+
+
+def edit_required(f):
+ @wraps(f)
+ def inner(*args, **kwargs):
+ if current_user.role_edit() or current_user.role_admin():
+ return f(*args, **kwargs)
+ abort(403)
+
+ return inner
+
+
+# Language and content filters for displaying in the UI
+def common_filters():
+ if current_user.filter_language() != "all":
+ lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
+ else:
+ lang_filter = true()
+ content_rating_filter = false() if current_user.mature_content else \
+ db.Books.tags.any(db.Tags.name.in_(config.mature_content_tags()))
+ return and_(lang_filter, ~content_rating_filter)
+
+
+# Creates for all stored languages a translated speaking name in the array for the UI
+def speaking_language(languages=None):
+ if not languages:
+ languages = db.session.query(db.Languages).all()
+ for lang in languages:
+ try:
+ cur_l = LC.parse(lang.lang_code)
+ lang.name = cur_l.get_language_name(get_locale())
+ except UnknownLocaleError:
+ lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
+ return languages
+
+
+# Fill indexpage with all requested data from database
+def fill_indexpage(page, database, db_filter, order, *join):
+ if current_user.show_detail_random():
+ randm = db.session.query(db.Books).filter(common_filters())\
+ .order_by(func.random()).limit(config.config_random_books)
+ else:
+ randm = false()
+ off = int(int(config.config_books_per_page) * (page - 1))
+ pagination = Pagination(page, config.config_books_per_page,
+ len(db.session.query(database)
+ .filter(db_filter).filter(common_filters()).all()))
+ entries = db.session.query(database).join(*join,isouter=True).filter(db_filter)\
+ .filter(common_filters()).order_by(*order).offset(off).limit(config.config_books_per_page).all()
+ return entries, randm, pagination
+
+
+# Modifies different Database objects, first check if elements have to be added to database, than check
+# if elements have to be deleted, because they are no longer used
+def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type):
+ # passing input_elements not as a list may lead to undesired results
+ if not isinstance(input_elements, list):
+ raise TypeError(str(input_elements) + " should be passed as a list")
+
+ input_elements = [x for x in input_elements if x != '']
+ # we have all input element (authors, series, tags) names now
+ # 1. search for elements to remove
+ del_elements = []
+ for c_elements in db_book_object:
+ found = False
+ if db_type == 'languages':
+ type_elements = c_elements.lang_code
+ elif db_type == 'custom':
+ type_elements = c_elements.value
+ else:
+ type_elements = c_elements.name
+ for inp_element in input_elements:
+ if inp_element.lower() == type_elements.lower():
+ found = True
+ break
+ # if the element was not found in the new list, add it to remove list
+ if not found:
+ del_elements.append(c_elements)
+ # 2. search for elements that need to be added
+ add_elements = []
+ for inp_element in input_elements:
+ found = False
+ for c_elements in db_book_object:
+ if db_type == 'languages':
+ type_elements = c_elements.lang_code
+ elif db_type == 'custom':
+ type_elements = c_elements.value
+ else:
+ type_elements = c_elements.name
+ if inp_element == type_elements:
+ found = True
+ break
+ if not found:
+ add_elements.append(inp_element)
+ # if there are elements to remove, we remove them now
+ if len(del_elements) > 0:
+ for del_element in del_elements:
+ db_book_object.remove(del_element)
+ if len(del_element.books) == 0:
+ db_session.delete(del_element)
+ # if there are elements to add, we add them now!
+ if len(add_elements) > 0:
+ if db_type == 'languages':
+ db_filter = db_object.lang_code
+ elif db_type == 'custom':
+ db_filter = db_object.value
+ else:
+ db_filter = db_object.name
+ for add_element in add_elements:
+ # check if a element with that name exists
+ db_element = db_session.query(db_object).filter(db_filter == add_element).first()
+ # if no element is found add it
+ # if new_element is None:
+ if db_type == 'author':
+ new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')), "")
+ elif db_type == 'series':
+ new_element = db_object(add_element, add_element)
+ elif db_type == 'custom':
+ new_element = db_object(value=add_element)
+ elif db_type == 'publisher':
+ new_element = db_object(add_element, None)
+ else: # db_type should be tag or language
+ new_element = db_object(add_element)
+ if db_element is None:
+ db_session.add(new_element)
+ db_book_object.append(new_element)
+ else:
+ if db_type == 'custom' and db_element.value != add_element:
+ new_element.value = add_element
+ # new_element = db_element
+ elif db_type == 'languages' and db_element.lang_code != add_element:
+ db_element.lang_code = add_element
+ # new_element = db_element
+ elif db_type == 'series' and db_element.name != add_element:
+ db_element.name = add_element # = add_element # new_element = db_object(add_element, add_element)
+ db_element.sort = add_element
+ # new_element = db_element
+ elif db_type == 'author' and db_element.name != add_element:
+ db_element.name = add_element
+ db_element.sort = add_element.replace('|', ',')
+ # new_element = db_element
+ if db_type == 'publisher' and db_element.name != add_element:
+ db_element.name = add_element
+ db_element.sort = None
+ # new_element = db_element
+ elif db_element.name != add_element:
+ db_element.name = add_element
+ # new_element = db_element
+ # add element to book
+ db_book_object.append(db_element)
+
+
+# read search results from calibre-database and return it (function is used for feed and simple search
+def get_search_results(term):
+ q = list()
+ authorterms = re.split("[, ]+", term)
+ for authorterm in authorterms:
+ q.append(db.Books.authors.any(db.Authors.name.ilike("%" + authorterm + "%")))
+ db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
+ db.Books.authors.any(db.Authors.name.ilike("%" + term + "%"))
+
+ return db.session.query(db.Books).filter(common_filters()).filter(
+ db.or_(db.Books.tags.any(db.Tags.name.ilike("%" + term + "%")),
+ db.Books.series.any(db.Series.name.ilike("%" + term + "%")),
+ db.Books.authors.any(and_(*q)),
+ db.Books.publishers.any(db.Publishers.name.ilike("%" + term + "%")),
+ db.Books.title.ilike("%" + term + "%"))).all()
+
+
+def feed_search(term):
+ if term:
+ term = term.strip().lower()
+ entries = get_search_results( term)
+ entriescount = len(entries) if len(entries) > 0 else 1
+ pagination = Pagination(1, entriescount, entriescount)
+ return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)
+ else:
+ return render_xml_template('feed.xml', searchterm="")
+
+
+def render_xml_template(*args, **kwargs):
+ #ToDo: return time in current timezone similar to %z
+ currtime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00")
+ xml = render_template(current_time=currtime, *args, **kwargs)
+ response = make_response(xml)
+ response.headers["Content-Type"] = "application/atom+xml; charset=utf-8"
+ return response
+
+
+# Returns the template for redering and includes the instance name
+def render_title_template(*args, **kwargs):
+ return render_template(instance=config.config_calibre_web_title, *args, **kwargs)
+
+
+@app.before_request
+def before_request():
+ g.user = current_user
+ g.allow_registration = config.config_public_reg
+ g.allow_upload = config.config_uploading
+ g.public_shelfes = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1).order_by(ub.Shelf.name).all()
+ if not config.db_configured and request.endpoint not in ('basic_configuration', 'login') and '/static/' not in request.path:
+ return redirect(url_for('basic_configuration'))
+
+
+# Routing functions
+
+@app.route("/opds")
+@requires_basic_auth_if_no_ano
+def feed_index():
+ return render_xml_template('index.xml')
+
+
+@app.route("/opds/osd")
+@requires_basic_auth_if_no_ano
+def feed_osd():
+ return render_xml_template('osd.xml', lang='en-EN')
+
+
+@app.route("/opds/search/")
+@requires_basic_auth_if_no_ano
+def feed_cc_search(query):
+ return feed_search(query.strip())
+
+
+@app.route("/opds/search", methods=["GET"])
+@requires_basic_auth_if_no_ano
+def feed_normal_search():
+ return feed_search(request.args.get("query").strip())
+
+
+@app.route("/opds/new")
+@requires_basic_auth_if_no_ano
+def feed_new():
+ off = request.args.get("offset") or 0
+ entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
+ db.Books, True, [db.Books.timestamp.desc()])
+ return render_xml_template('feed.xml', entries=entries, pagination=pagination)
+
+
+@app.route("/opds/discover")
+@requires_basic_auth_if_no_ano
+def feed_discover():
+ entries = db.session.query(db.Books).filter(common_filters()).order_by(func.random())\
+ .limit(config.config_books_per_page)
+ pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page))
+ return render_xml_template('feed.xml', entries=entries, pagination=pagination)
+
+
+@app.route("/opds/rated")
+@requires_basic_auth_if_no_ano
+def feed_best_rated():
+ off = request.args.get("offset") or 0
+ entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
+ db.Books, db.Books.ratings.any(db.Ratings.rating > 9), [db.Books.timestamp.desc()])
+ return render_xml_template('feed.xml', entries=entries, pagination=pagination)
+
+
+@app.route("/opds/hot")
+@requires_basic_auth_if_no_ano
+def feed_hot():
+ off = request.args.get("offset") or 0
+ all_books = ub.session.query(ub.Downloads, ub.func.count(ub.Downloads.book_id)).order_by(
+ ub.func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id)
+ hot_books = all_books.offset(off).limit(config.config_books_per_page)
+ entries = list()
+ for book in hot_books:
+ downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first()
+ if downloadBook:
+ entries.append(
+ db.session.query(db.Books).filter(common_filters())
+ .filter(db.Books.id == book.Downloads.book_id).first()
+ )
+ else:
+ ub.delete_download(book.Downloads.book_id)
+ # ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
+ # ub.session.commit()
+ numBooks = entries.__len__()
+ pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1),
+ config.config_books_per_page, numBooks)
+ return render_xml_template('feed.xml', entries=entries, pagination=pagination)
+
+
+@app.route("/opds/author")
+@requires_basic_auth_if_no_ano
+def feed_authorindex():
+ off = request.args.get("offset") or 0
+ entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(common_filters())\
+ .group_by('books_authors_link.author').order_by(db.Authors.sort).limit(config.config_books_per_page).offset(off)
+ pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
+ len(db.session.query(db.Authors).all()))
+ return render_xml_template('feed.xml', listelements=entries, folder='feed_author', pagination=pagination)
+
+
+@app.route("/opds/author/")
+@requires_basic_auth_if_no_ano
+def feed_author(book_id):
+ off = request.args.get("offset") or 0
+ entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
+ db.Books, db.Books.authors.any(db.Authors.id == book_id), [db.Books.timestamp.desc()])
+ return render_xml_template('feed.xml', entries=entries, pagination=pagination)
+
+
+@app.route("/opds/publisher")
+@requires_basic_auth_if_no_ano
+def feed_publisherindex():
+ off = request.args.get("offset") or 0
+ entries = db.session.query(db.Publishers).join(db.books_publishers_link).join(db.Books).filter(common_filters())\
+ .group_by('books_publishers_link.publisher').order_by(db.Publishers.sort).limit(config.config_books_per_page).offset(off)
+ pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
+ len(db.session.query(db.Publishers).all()))
+ return render_xml_template('feed.xml', listelements=entries, folder='feed_publisher', pagination=pagination)
+
+
+@app.route("/opds/publisher/")
+@requires_basic_auth_if_no_ano
+def feed_publisher(book_id):
+ off = request.args.get("offset") or 0
+ entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
+ db.Books, db.Books.publishers.any(db.Publishers.id == book_id),
+ [db.Books.timestamp.desc()])
+ return render_xml_template('feed.xml', entries=entries, pagination=pagination)
+
+
+@app.route("/opds/category")
+@requires_basic_auth_if_no_ano
+def feed_categoryindex():
+ off = request.args.get("offset") or 0
+ entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(common_filters())\
+ .group_by('books_tags_link.tag').order_by(db.Tags.name).offset(off).limit(config.config_books_per_page)
+ pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
+ len(db.session.query(db.Tags).all()))
+ return render_xml_template('feed.xml', listelements=entries, folder='feed_category', pagination=pagination)
+
+
+@app.route("/opds/category/")
+@requires_basic_auth_if_no_ano
+def feed_category(book_id):
+ off = request.args.get("offset") or 0
+ entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
+ db.Books, db.Books.tags.any(db.Tags.id == book_id), [db.Books.timestamp.desc()])
+ return render_xml_template('feed.xml', entries=entries, pagination=pagination)
+
+
+@app.route("/opds/series")
+@requires_basic_auth_if_no_ano
+def feed_seriesindex():
+ off = request.args.get("offset") or 0
+ entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(common_filters())\
+ .group_by('books_series_link.series').order_by(db.Series.sort).offset(off).all()
+ pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
+ len(db.session.query(db.Series).all()))
+ return render_xml_template('feed.xml', listelements=entries, folder='feed_series', pagination=pagination)
+
+
+@app.route("/opds/series/")
+@requires_basic_auth_if_no_ano
+def feed_series(book_id):
+ off = request.args.get("offset") or 0
+ entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
+ db.Books, db.Books.series.any(db.Series.id == book_id), [db.Books.series_index])
+ return render_xml_template('feed.xml', entries=entries, pagination=pagination)
+
+
+@app.route("/opds/shelfindex/", defaults={'public': 0})
+@app.route("/opds/shelfindex/")
+@requires_basic_auth_if_no_ano
+def feed_shelfindex(public):
+ off = request.args.get("offset") or 0
+ if public is not 0:
+ shelf = g.public_shelfes
+ number = len(shelf)
+ else:
+ shelf = g.user.shelf
+ number = shelf.count()
+ pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
+ number)
+ return render_xml_template('feed.xml', listelements=shelf, folder='feed_shelf', pagination=pagination)
+
+
+@app.route("/opds/shelf/")
+@requires_basic_auth_if_no_ano
+def feed_shelf(book_id):
+ off = request.args.get("offset") or 0
+ if current_user.is_anonymous:
+ shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == book_id).first()
+ else:
+ shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id),
+ ub.Shelf.id == book_id),
+ ub.and_(ub.Shelf.is_public == 1,
+ ub.Shelf.id == book_id))).first()
+ result = list()
+ # user is allowed to access shelf
+ if shelf:
+ books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == book_id).order_by(
+ ub.BookShelf.order.asc()).all()
+ for book in books_in_shelf:
+ cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
+ result.append(cur_book)
+ pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
+ len(result))
+ return render_xml_template('feed.xml', entries=result, pagination=pagination)
+
+
+@app.route("/opds/download///")
+@requires_basic_auth_if_no_ano
+@download_required
+def get_opds_download_link(book_id, book_format):
+ book_format = book_format.split(".")[0]
+ book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
+ data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format.upper()).first()
+ app.logger.info(data.name)
+ if current_user.is_authenticated:
+ ub.update_download(book_id, int(current_user.id))
+ file_name = book.title
+ if len(book.authors) > 0:
+ file_name = book.authors[0].name + '_' + file_name
+ file_name = helper.get_valid_filename(file_name)
+ headers = Headers()
+ headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (quote(file_name.encode('utf8')),
+ book_format)
+ try:
+ headers["Content-Type"] = mimetypes.types_map['.' + book_format]
+ except KeyError:
+ headers["Content-Type"] = "application/octet-stream"
+ return helper.do_download_file(book, book_format, data, headers)
+
+
+@app.route("/ajax/book/")
+@requires_basic_auth_if_no_ano
+def get_metadata_calibre_companion(uuid):
+ entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first()
+ if entry is not None:
+ js = render_template('json.txt', entry=entry)
+ response = make_response(js)
+ response.headers["Content-Type"] = "application/json; charset=utf-8"
+ return response
+ else:
+ return ""
+
+@app.route("/ajax/emailstat")
+@login_required
+def get_email_status_json():
+ tasks=helper.global_WorkerThread.get_taskstatus()
+ answer = helper.render_task_status(tasks)
+ js=json.dumps(answer, default=helper.json_serial)
+ response = make_response(js)
+ response.headers["Content-Type"] = "application/json; charset=utf-8"
+ return response
+
+
+# checks if domain is in database (including wildcards)
+# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name;
+# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/
+def check_valid_domain(domain_text):
+ # result = session.query(Notification).from_statement(text(sql)).params(id=5).all()
+ #ToDo: check possible SQL injection
+ domain_text = domain_text.split('@',1)[-1].lower()
+ sql = "SELECT * FROM registration WHERE '%s' LIKE domain;" % domain_text
+ result = ub.session.query(ub.Registration).from_statement(text(sql)).all()
+ return len(result)
+
+
+''' POST /post
+ name: 'username', //name of field (column in db)
+ pk: 1 //primary key (record id)
+ value: 'superuser!' //new value'''
+@app.route("/ajax/editdomain", methods=['POST'])
+@login_required
+@admin_required
+def edit_domain():
+ vals = request.form.to_dict()
+ answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first()
+ # domain_name = request.args.get('domain')
+ answer.domain = vals['value'].replace('*','%').replace('?','_').lower()
+ ub.session.commit()
+ return ""
+
+
+@app.route("/ajax/adddomain", methods=['POST'])
+@login_required
+@admin_required
+def add_domain():
+ domain_name = request.form.to_dict()['domainname'].replace('*','%').replace('?','_').lower()
+ check = ub.session.query(ub.Registration).filter(ub.Registration.domain == domain_name).first()
+ if not check:
+ new_domain = ub.Registration(domain=domain_name)
+ ub.session.add(new_domain)
+ ub.session.commit()
+ return ""
+
+
+@app.route("/ajax/deletedomain", methods=['POST'])
+@login_required
+@admin_required
+def delete_domain():
+ domain_id = request.form.to_dict()['domainid'].replace('*','%').replace('?','_').lower()
+ ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete()
+ ub.session.commit()
+ # If last domain was deleted, add all domains by default
+ if not ub.session.query(ub.Registration).count():
+ new_domain = ub.Registration(domain="%.%")
+ ub.session.add(new_domain)
+ ub.session.commit()
+ return ""
+
+
+@app.route("/ajax/domainlist")
+@login_required
+@admin_required
+def list_domain():
+ answer = ub.session.query(ub.Registration).all()
+ json_dumps = json.dumps([{"domain":r.domain.replace('%','*').replace('_','?'),"id":r.id} for r in answer])
+ js=json.dumps(json_dumps.replace('"', "'")).lstrip('"').strip('"')
+ response = make_response(js.replace("'",'"'))
+ response.headers["Content-Type"] = "application/json; charset=utf-8"
+ return response
+
+
+'''
+@app.route("/ajax/getcomic///")
+@login_required
+def get_comic_book(book_id, book_format, page):
+ book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
+ if not book:
+ return "", 204
+ else:
+ for bookformat in book.data:
+ if bookformat.format.lower() == book_format.lower():
+ cbr_file = os.path.join(config.config_calibre_dir, book.path, bookformat.name) + "." + book_format
+ if book_format in ("cbr", "rar"):
+ if rar_support == True:
+ rarfile.UNRAR_TOOL = config.config_rarfile_location
+ try:
+ rf = rarfile.RarFile(cbr_file)
+ names = sort(rf.namelist())
+ extract = lambda page: rf.read(names[page])
+ except:
+ # rarfile not valid
+ app.logger.error('Unrar binary not found, or unable to decompress file ' + cbr_file)
+ return "", 204
+ else:
+ app.logger.info('Unrar is not supported please install python rarfile extension')
+ # no support means return nothing
+ return "", 204
+ elif book_format in ("cbz", "zip"):
+ zf = zipfile.ZipFile(cbr_file)
+ names=sort(zf.namelist())
+ extract = lambda page: zf.read(names[page])
+ elif book_format in ("cbt", "tar"):
+ tf = tarfile.TarFile(cbr_file)
+ names=sort(tf.getnames())
+ extract = lambda page: tf.extractfile(names[page]).read()
+ else:
+ app.logger.error('unsupported comic format')
+ return "", 204
+
+ if sys.version_info.major >= 3:
+ b64 = codecs.encode(extract(page), 'base64').decode()
+ else:
+ b64 = extract(page).encode('base64')
+ ext = names[page].rpartition('.')[-1]
+ if ext not in ('png', 'gif', 'jpg', 'jpeg'):
+ ext = 'png'
+ extractedfile="data:image/" + ext + ";base64," + b64
+ fileData={"name": names[page], "page":page, "last":len(names)-1, "content": extractedfile}
+ return make_response(json.dumps(fileData))
+ return "", 204
+'''
+
+
+@app.route("/get_authors_json", methods=['GET', 'POST'])
+@login_required_if_no_ano
+def get_authors_json():
+ if request.method == "GET":
+ query = request.args.get('q')
+ entries = db.session.query(db.Authors).filter(db.Authors.name.ilike("%" + query + "%")).all()
+ json_dumps = json.dumps([dict(name=r.name.replace('|',',')) for r in entries])
+ return json_dumps
+
+
+@app.route("/get_publishers_json", methods=['GET', 'POST'])
+@login_required_if_no_ano
+def get_publishers_json():
+ if request.method == "GET":
+ query = request.args.get('q')
+ entries = db.session.query(db.Publishers).filter(db.Publishers.name.ilike("%" + query + "%")).all()
+ json_dumps = json.dumps([dict(name=r.name.replace('|',',')) for r in entries])
+ return json_dumps
+
+
+@app.route("/get_tags_json", methods=['GET', 'POST'])
+@login_required_if_no_ano
+def get_tags_json():
+ if request.method == "GET":
+ query = request.args.get('q')
+ entries = db.session.query(db.Tags).filter(db.Tags.name.ilike("%" + query + "%")).all()
+ json_dumps = json.dumps([dict(name=r.name) for r in entries])
+ return json_dumps
+
+
+@app.route("/get_languages_json", methods=['GET', 'POST'])
+@login_required_if_no_ano
+def get_languages_json():
+ if request.method == "GET":
+ query = request.args.get('q').lower()
+ # languages = speaking_language()
+ languages = language_table[get_locale()]
+ entries_start = [s for key, s in languages.items() if s.lower().startswith(query.lower())]
+ if len(entries_start) < 5:
+ entries = [s for key,s in languages.items() if query in s.lower()]
+ entries_start.extend(entries[0:(5-len(entries_start))])
+ entries_start = list(set(entries_start))
+ json_dumps = json.dumps([dict(name=r) for r in entries_start[0:5]])
+ return json_dumps
+
+
+@app.route("/get_series_json", methods=['GET', 'POST'])
+@login_required_if_no_ano
+def get_series_json():
+ if request.method == "GET":
+ query = request.args.get('q')
+ entries = db.session.query(db.Series).filter(db.Series.name.ilike("%" + query + "%")).all()
+ # entries = db.session.execute("select name from series where name like '%" + query + "%'")
+ json_dumps = json.dumps([dict(name=r.name) for r in entries])
+ return json_dumps
+
+
+@app.route("/get_matching_tags", methods=['GET', 'POST'])
+@login_required_if_no_ano
+def get_matching_tags():
+ tag_dict = {'tags': []}
+ if request.method == "GET":
+ q = db.session.query(db.Books)
+ author_input = request.args.get('author_name')
+ title_input = request.args.get('book_title')
+ include_tag_inputs = request.args.getlist('include_tag')
+ exclude_tag_inputs = request.args.getlist('exclude_tag')
+ q = q.filter(db.Books.authors.any(db.Authors.name.ilike("%" + author_input + "%")),
+ db.Books.title.ilike("%" + title_input + "%"))
+ if len(include_tag_inputs) > 0:
+ for tag in include_tag_inputs:
+ q = q.filter(db.Books.tags.any(db.Tags.id == tag))
+ if len(exclude_tag_inputs) > 0:
+ for tag in exclude_tag_inputs:
+ q = q.filter(not_(db.Books.tags.any(db.Tags.id == tag)))
+ for book in q:
+ for tag in book.tags:
+ if tag.id not in tag_dict['tags']:
+ tag_dict['tags'].append(tag.id)
+ json_dumps = json.dumps(tag_dict)
+ return json_dumps
+
+
+@app.route("/get_update_status", methods=['GET'])
+@login_required_if_no_ano
+def get_update_status():
+ status = {
+ 'update': False,
+ 'success': False,
+ 'message': '',
+ 'current_commit_hash': ''
+ }
+ parents = []
+
+ repository_url = 'https://api.github.com/repos/janeczku/calibre-web'
+ tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
+
+ if request.method == "GET":
+ version = helper.get_current_version_info()
+ if version is False:
+ status['current_commit_hash'] = _(u'Unknown')
+ else:
+ status['current_commit_hash'] = version['hash']
+
+ try:
+ r = requests.get(repository_url + '/git/refs/heads/master')
+ r.raise_for_status()
+ commit = r.json()
+ except requests.exceptions.HTTPError as ex:
+ status['message'] = _(u'HTTP Error') + ' ' + str(ex)
+ except requests.exceptions.ConnectionError:
+ status['message'] = _(u'Connection error')
+ except requests.exceptions.Timeout:
+ status['message'] = _(u'Timeout while establishing connection')
+ except requests.exceptions.RequestException:
+ status['message'] = _(u'General error')
+
+ if status['message'] != '':
+ return json.dumps(status)
+
+ if 'object' not in commit:
+ status['message'] = _(u'Unexpected data while reading update information')
+ return json.dumps(status)
+
+ if commit['object']['sha'] == status['current_commit_hash']:
+ status.update({
+ 'update': False,
+ 'success': True,
+ 'message': _(u'No update available. You already have the latest version installed')
+ })
+ return json.dumps(status)
+
+ # a new update is available
+ status['update'] = True
+
+ try:
+ r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'])
+ r.raise_for_status()
+ update_data = r.json()
+ except requests.exceptions.HTTPError as ex:
+ status['error'] = _(u'HTTP Error') + ' ' + str(ex)
+ except requests.exceptions.ConnectionError:
+ status['error'] = _(u'Connection error')
+ except requests.exceptions.Timeout:
+ status['error'] = _(u'Timeout while establishing connection')
+ except requests.exceptions.RequestException:
+ status['error'] = _(u'General error')
+
+ if status['message'] != '':
+ return json.dumps(status)
+
+ if 'committer' in update_data and 'message' in update_data:
+ status['success'] = True
+ status['message'] = _(u'A new update is available. Click on the button below to update to the latest version.')
+
+ new_commit_date = datetime.datetime.strptime(
+ update_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
+ parents.append(
+ [
+ format_datetime(new_commit_date, format='short', locale=get_locale()),
+ update_data['message'],
+ update_data['sha']
+ ]
+ )
+
+ # it only makes sense to analyze the parents if we know the current commit hash
+ if status['current_commit_hash'] != '':
+ try:
+ parent_commit = update_data['parents'][0]
+ # limit the maximum search depth
+ remaining_parents_cnt = 10
+ except IndexError:
+ remaining_parents_cnt = None
+
+ if remaining_parents_cnt is not None:
+ while True:
+ if remaining_parents_cnt == 0:
+ break
+
+ # check if we are more than one update behind if so, go up the tree
+ if parent_commit['sha'] != status['current_commit_hash']:
+ try:
+ r = requests.get(parent_commit['url'])
+ r.raise_for_status()
+ parent_data = r.json()
+
+ parent_commit_date = datetime.datetime.strptime(
+ parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
+ parent_commit_date = format_datetime(
+ parent_commit_date, format='short', locale=get_locale())
+
+ parents.append([parent_commit_date, parent_data['message'], parent_data['sha']])
+ parent_commit = parent_data['parents'][0]
+ remaining_parents_cnt -= 1
+ except Exception:
+ # it isn't crucial if we can't get information about the parent
+ break
+ else:
+ # parent is our current version
+ break
+
+ else:
+ status['success'] = False
+ status['message'] = _(u'Could not fetch update information')
+
+ status['history'] = parents
+ return json.dumps(status)
+
+
+@app.route("/get_updater_status", methods=['GET', 'POST'])
+@login_required
+@admin_required
+def get_updater_status():
+ status = {}
+ if request.method == "POST":
+ commit = request.form.to_dict()
+ if "start" in commit and commit['start'] == 'True':
+ text = {
+ "1": _(u'Requesting update package'),
+ "2": _(u'Downloading update package'),
+ "3": _(u'Unzipping update package'),
+ "4": _(u'Replacing files'),
+ "5": _(u'Database connections are closed'),
+ "6": _(u'Stopping server'),
+ "7": _(u'Update finished, please press okay and reload page'),
+ "8": _(u'Update failed:') + u' ' + _(u'HTTP Error'),
+ "9": _(u'Update failed:') + u' ' + _(u'Connection error'),
+ "10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'),
+ "11": _(u'Update failed:') + u' ' + _(u'General error')
+ }
+ status['text'] = text
+ helper.updater_thread = helper.Updater()
+ helper.updater_thread.start()
+ status['status'] = helper.updater_thread.get_update_status()
+ elif request.method == "GET":
+ try:
+ status['status'] = helper.updater_thread.get_update_status()
+ except AttributeError:
+ # thread is not active, occours after restart on update
+ status['status'] = 7
+ except Exception:
+ status['status'] = 11
+ return json.dumps(status)
+
+
+@app.route("/", defaults={'page': 1})
+@app.route('/page/')
+@login_required_if_no_ano
+def index(page):
+ entries, random, pagination = fill_indexpage(page, db.Books, True, [db.Books.timestamp.desc()])
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
+ title=_(u"Recently Added Books"), page="root")
+
+
+@app.route('/books/newest', defaults={'page': 1})
+@app.route('/books/newest/page/')
+@login_required_if_no_ano
+def newest_books(page):
+ if current_user.show_sorted():
+ entries, random, pagination = fill_indexpage(page, db.Books, True, [db.Books.pubdate.desc()])
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
+ title=_(u"Newest Books"), page="newest")
+ else:
+ abort(404)
+
+
+@app.route('/books/oldest', defaults={'page': 1})
+@app.route('/books/oldest/page/')
+@login_required_if_no_ano
+def oldest_books(page):
+ if current_user.show_sorted():
+ entries, random, pagination = fill_indexpage(page, db.Books, True, [db.Books.pubdate])
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
+ title=_(u"Oldest Books"), page="oldest")
+ else:
+ abort(404)
+
+
+@app.route('/books/a-z', defaults={'page': 1})
+@app.route('/books/a-z/page/')
+@login_required_if_no_ano
+def titles_ascending(page):
+ if current_user.show_sorted():
+ entries, random, pagination = fill_indexpage(page, db.Books, True, [db.Books.sort])
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
+ title=_(u"Books (A-Z)"), page="a-z")
+ else:
+ abort(404)
+
+
+@app.route('/books/z-a', defaults={'page': 1})
+@app.route('/books/z-a/page/')
+@login_required_if_no_ano
+def titles_descending(page):
+ entries, random, pagination = fill_indexpage(page, db.Books, True, [db.Books.sort.desc()])
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
+ title=_(u"Books (Z-A)"), page="z-a")
+
+
+@app.route("/hot", defaults={'page': 1})
+@app.route('/hot/page/')
+@login_required_if_no_ano
+def hot_books(page):
+ if current_user.show_hot_books():
+ if current_user.show_detail_random():
+ random = db.session.query(db.Books).filter(common_filters())\
+ .order_by(func.random()).limit(config.config_random_books)
+ else:
+ random = false()
+ off = int(int(config.config_books_per_page) * (page - 1))
+ all_books = ub.session.query(ub.Downloads, ub.func.count(ub.Downloads.book_id)).order_by(
+ ub.func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id)
+ hot_books = all_books.offset(off).limit(config.config_books_per_page)
+ entries = list()
+ for book in hot_books:
+ downloadBook = db.session.query(db.Books).filter(common_filters()).filter(db.Books.id == book.Downloads.book_id).first()
+ if downloadBook:
+ entries.append(downloadBook)
+ else:
+ ub.delete_download(book.Downloads.book_id)
+ # ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
+ # ub.session.commit()
+ numBooks = entries.__len__()
+ pagination = Pagination(page, config.config_books_per_page, numBooks)
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
+ title=_(u"Hot Books (most downloaded)"), page="hot")
+ else:
+ abort(404)
+
+
+@app.route("/rated", defaults={'page': 1})
+@app.route('/rated/page/')
+@login_required_if_no_ano
+def best_rated_books(page):
+ if current_user.show_best_rated_books():
+ entries, random, pagination = fill_indexpage(page, db.Books, db.Books.ratings.any(db.Ratings.rating > 9),
+ [db.Books.timestamp.desc()])
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
+ title=_(u"Best rated books"), page="rated")
+ else:
+ abort(404)
+
+
+@app.route("/discover", defaults={'page': 1})
+@app.route('/discover/page/')
+@login_required_if_no_ano
+def discover(page):
+ if current_user.show_random_books():
+ entries, __, pagination = fill_indexpage(page, db.Books, True, [func.randomblob(2)])
+ pagination = Pagination(1, config.config_books_per_page, config.config_books_per_page)
+ return render_title_template('discover.html', entries=entries, pagination=pagination,
+ title=_(u"Random Books"), page="discover")
+ else:
+ abort(404)
+
+
+@app.route("/author")
+@login_required_if_no_ano
+def author_list():
+ if current_user.show_author():
+ entries = db.session.query(db.Authors, func.count('books_authors_link.book').label('count'))\
+ .join(db.books_authors_link).join(db.Books).filter(common_filters())\
+ .group_by('books_authors_link.author').order_by(db.Authors.sort).all()
+ for entry in entries:
+ entry.Authors.name = entry.Authors.name.replace('|', ',')
+ return render_title_template('list.html', entries=entries, folder='author',
+ title=_(u"Author list"), page="authorlist")
+ else:
+ abort(404)
+
+
+@app.route("/author/", defaults={'page': 1})
+@app.route("/author//'")
+@login_required_if_no_ano
+def author(book_id, page):
+ entries, __, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == book_id),
+ [db.Series.name, db.Books.series_index],db.books_series_link, db.Series)
+ if entries is None:
+ flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error")
+ return redirect(url_for("index"))
+
+ name = (db.session.query(db.Authors).filter(db.Authors.id == book_id).first().name).replace('|', ',')
+
+ author_info = None
+ other_books = []
+ if goodreads_support and config.config_use_goodreads:
+ try:
+ gc = GoodreadsClient(config.config_goodreads_api_key, config.config_goodreads_api_secret)
+ author_info = gc.find_author(author_name=name)
+ other_books = get_unique_other_books(entries.all(), author_info.books)
+ except Exception:
+ # Skip goodreads, if site is down/inaccessible
+ app.logger.error('Goodreads website is down/inaccessible')
+
+ return render_title_template('author.html', entries=entries, pagination=pagination,
+ title=name, author=author_info, other_books=other_books, page="author")
+
+
+@app.route("/publisher")
+@login_required_if_no_ano
+def publisher_list():
+ if current_user.show_publisher():
+ entries = db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count'))\
+ .join(db.books_publishers_link).join(db.Books).filter(common_filters())\
+ .group_by('books_publishers_link.publisher').order_by(db.Publishers.sort).all()
+ return render_title_template('list.html', entries=entries, folder='publisher',
+ title=_(u"Publisher list"), page="publisherlist")
+ else:
+ abort(404)
+
+
+@app.route("/publisher/", defaults={'page': 1})
+@app.route('/publisher//')
+@login_required_if_no_ano
+def publisher(book_id, page):
+ publisher = db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first()
+ if publisher:
+ entries, random, pagination = fill_indexpage(page, db.Books,
+ db.Books.publishers.any(db.Publishers.id == book_id),
+ (db.Series.name, db.Books.series_index), db.books_series_link, db.Series)
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
+ title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher")
+ else:
+ abort(404)
+
+
+def get_unique_other_books(library_books, author_books):
+ # Get all identifiers (ISBN, Goodreads, etc) and filter author's books by that list so we show fewer duplicates
+ # Note: Not all images will be shown, even though they're available on Goodreads.com.
+ # See https://www.goodreads.com/topic/show/18213769-goodreads-book-images
+ identifiers = reduce(lambda acc, book: acc + map(lambda identifier: identifier.val, book.identifiers), library_books, [])
+ other_books = filter(lambda book: book.isbn not in identifiers and book.gid["#text"] not in identifiers, author_books)
+
+ # Fuzzy match book titles
+ if levenshtein_support:
+ library_titles = reduce(lambda acc, book: acc + [book.title], library_books, [])
+ other_books = filter(lambda author_book: not filter(
+ lambda library_book:
+ Levenshtein.ratio(re.sub(r"\(.*\)", "", author_book.title), library_book) > 0.7, # Remove items in parentheses before comparing
+ library_titles
+ ), other_books)
+
+ return other_books
+
+
+@app.route("/series")
+@login_required_if_no_ano
+def series_list():
+ if current_user.show_series():
+ entries = db.session.query(db.Series, func.count('books_series_link.book').label('count'))\
+ .join(db.books_series_link).join(db.Books).filter(common_filters())\
+ .group_by('books_series_link.series').order_by(db.Series.sort).all()
+ return render_title_template('list.html', entries=entries, folder='series',
+ title=_(u"Series list"), page="serieslist")
+ else:
+ abort(404)
+
+
+@app.route("/series//", defaults={'page': 1})
+@app.route("/series//")
+@login_required_if_no_ano
+def series(book_id, page):
+ name = db.session.query(db.Series).filter(db.Series.id == book_id).first()
+ if name:
+ entries, random, pagination = fill_indexpage(page, db.Books, db.Books.series.any(db.Series.id == book_id),
+ [db.Books.series_index])
+ return render_title_template('index.html', random=random, pagination=pagination, entries=entries,
+ title=_(u"Series: %(serie)s", serie=name.name), page="series")
+ else:
+ abort(404)
+
+
+@app.route("/language")
+@login_required_if_no_ano
+def language_overview():
+ if current_user.show_language():
+ if current_user.filter_language() == u"all":
+ languages = speaking_language()
+ else:
+ try:
+ cur_l = LC.parse(current_user.filter_language())
+ except UnknownLocaleError:
+ cur_l = None
+ languages = db.session.query(db.Languages).filter(
+ db.Languages.lang_code == current_user.filter_language()).all()
+ if cur_l:
+ languages[0].name = cur_l.get_language_name(get_locale())
+ else:
+ languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name)
+ lang_counter = db.session.query(db.books_languages_link,
+ func.count('books_languages_link.book').label('bookcount')).group_by(
+ 'books_languages_link.lang_code').all()
+ return render_title_template('languages.html', languages=languages, lang_counter=lang_counter,
+ title=_(u"Available languages"), page="langlist")
+ else:
+ abort(404)
+
+
+@app.route("/language/", defaults={'page': 1})
+@app.route('/language//page/')
+@login_required_if_no_ano
+def language(name, page):
+ try:
+ cur_l = LC.parse(name)
+ lang_name = cur_l.get_language_name(get_locale())
+ except UnknownLocaleError:
+ try:
+ lang_name = _(isoLanguages.get(part3=name).name)
+ except KeyError:
+ abort(404)
+ entries, random, pagination = fill_indexpage(page, db.Books, db.Books.languages.any(db.Languages.lang_code == name),
+ [db.Books.timestamp.desc()])
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
+ title=_(u"Language: %(name)s", name=lang_name), page="language")
+
+
+@app.route("/category")
+@login_required_if_no_ano
+def category_list():
+ if current_user.show_category():
+ entries = db.session.query(db.Tags, func.count('books_tags_link.book').label('count'))\
+ .join(db.books_tags_link).join(db.Books).order_by(db.Tags.name).filter(common_filters())\
+ .group_by('books_tags_link.tag').all()
+ return render_title_template('list.html', entries=entries, folder='category',
+ title=_(u"Category list"), page="catlist")
+ else:
+ abort(404)
+
+
+@app.route("/category/", defaults={'page': 1})
+@app.route('/category//')
+@login_required_if_no_ano
+def category(book_id, page):
+ name = db.session.query(db.Tags).filter(db.Tags.id == book_id).first()
+ if name:
+ entries, random, pagination = fill_indexpage(page, db.Books, db.Books.tags.any(db.Tags.id == book_id),
+ (db.Series.name, db.Books.series_index),db.books_series_link,db.Series)
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
+ title=_(u"Category: %(name)s", name=name.name), page="category")
+ else:
+ abort(404)
+
+
+@app.route("/ajax/toggleread/", methods=['POST'])
+@login_required
+def toggle_read(book_id):
+ if not config.config_read_column:
+ book = ub.session.query(ub.ReadBook).filter(ub.and_(ub.ReadBook.user_id == int(current_user.id),
+ ub.ReadBook.book_id == book_id)).first()
+ if book:
+ book.is_read = not book.is_read
+ else:
+ readBook = ub.ReadBook()
+ readBook.user_id = int(current_user.id)
+ readBook.book_id = book_id
+ readBook.is_read = True
+ book = readBook
+ ub.session.merge(book)
+ ub.session.commit()
+ else:
+ try:
+ db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
+ book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
+ read_status = getattr(book, 'custom_column_' + str(config.config_read_column))
+ if len(read_status):
+ read_status[0].value = not read_status[0].value
+ db.session.commit()
+ else:
+ cc_class = db.cc_classes[config.config_read_column]
+ new_cc = cc_class(value=1, book=book_id)
+ db.session.add(new_cc)
+ db.session.commit()
+ except KeyError:
+ app.logger.error(
+ u"Custom Column No.%d is not exisiting in calibre database" % config.config_read_column)
+ return ""
+
+@app.route("/book/")
+@login_required_if_no_ano
+def show_book(book_id):
+ entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
+ if entries:
+ for index in range(0, len(entries.languages)):
+ try:
+ entries.languages[index].language_name = LC.parse(entries.languages[index].lang_code).get_language_name(
+ get_locale())
+ except UnknownLocaleError:
+ entries.languages[index].language_name = _(
+ isoLanguages.get(part3=entries.languages[index].lang_code).name)
+ tmpcc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
+
+ if config.config_columns_to_ignore:
+ cc = []
+ for col in tmpcc:
+ r = re.compile(config.config_columns_to_ignore)
+ if r.match(col.label):
+ cc.append(col)
+ else:
+ cc = tmpcc
+ book_in_shelfs = []
+ shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).all()
+ for entry in shelfs:
+ book_in_shelfs.append(entry.shelf)
+
+ if not current_user.is_anonymous:
+ if not config.config_read_column:
+ matching_have_read_book = ub.session.query(ub.ReadBook)\
+ .filter(ub.and_(ub.ReadBook.user_id == int(current_user.id),
+ ub.ReadBook.book_id == book_id)).all()
+ have_read = len(matching_have_read_book) > 0 and matching_have_read_book[0].is_read
+ else:
+ try:
+ matching_have_read_book = getattr(entries,'custom_column_'+str(config.config_read_column))
+ have_read = len(matching_have_read_book) > 0 and matching_have_read_book[0].value
+ except KeyError:
+ app.logger.error(
+ u"Custom Column No.%d is not exisiting in calibre database" % config.config_read_column)
+ have_read = None
+
+ else:
+ have_read = None
+
+ entries.tags = sort(entries.tags, key = lambda tag: tag.name)
+
+ kindle_list = helper.check_send_to_kindle(entries)
+ reader_list = helper.check_read_formats(entries)
+
+ return render_title_template('detail.html', entry=entries, cc=cc, is_xhr=request.is_xhr,
+ title=entries.title, books_shelfs=book_in_shelfs,
+ have_read=have_read, kindle_list=kindle_list, reader_list=reader_list, page="book")
+ else:
+ flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error")
+ return redirect(url_for("index"))
+
+
+@app.route("/ajax/bookmark//", methods=['POST'])
+@login_required
+def bookmark(book_id, book_format):
+ bookmark_key = request.form["bookmark"]
+ ub.session.query(ub.Bookmark).filter(ub.and_(ub.Bookmark.user_id == int(current_user.id),
+ ub.Bookmark.book_id == book_id,
+ ub.Bookmark.format == book_format)).delete()
+ if not bookmark_key:
+ ub.session.commit()
+ return "", 204
+
+ lbookmark = ub.Bookmark(user_id=current_user.id,
+ book_id=book_id,
+ format=book_format,
+ bookmark_key=bookmark_key)
+ ub.session.merge(lbookmark)
+ ub.session.commit()
+ return "", 201
+
+
+@app.route("/tasks")
+@login_required
+def get_tasks_status():
+ # if current user admin, show all email, otherwise only own emails
+ answer=list()
+ # UIanswer=list()
+ tasks=helper.global_WorkerThread.get_taskstatus()
+ # answer = tasks
+
+ # UIanswer = copy.deepcopy(answer)
+ answer = helper.render_task_status(tasks)
+ # foreach row format row
+ return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"))
+
+
+@app.route("/admin")
+@login_required
+def admin_forbidden():
+ abort(403)
+
+
+@app.route("/stats")
+@login_required
+def stats():
+ counter = db.session.query(db.Books).count()
+ authors = db.session.query(db.Authors).count()
+ categorys = db.session.query(db.Tags).count()
+ series = db.session.query(db.Series).count()
+ versions = uploader.book_formats.get_versions()
+ versions['Babel'] = 'v' + babelVersion
+ versions['Sqlalchemy'] = 'v' + sqlalchemyVersion
+ versions['Werkzeug'] = 'v' + werkzeugVersion
+ versions['Jinja2'] = 'v' + jinja2Version
+ versions['Flask'] = 'v' + flaskVersion
+ versions['Flask Login'] = 'v' + flask_loginVersion
+ versions['Flask Principal'] = 'v' + flask_principalVersion
+ versions['Iso 639'] = 'v' + iso639Version
+ versions['pytz'] = 'v' + pytzVersion
+
+ versions['Requests'] = 'v' + requests.__version__
+ versions['pySqlite'] = 'v' + db.engine.dialect.dbapi.version
+ versions['Sqlite'] = 'v' + db.engine.dialect.dbapi.sqlite_version
+ versions.update(converter.versioncheck())
+ versions.update(server.Server.getNameVersion())
+ versions['Python'] = sys.version
+ return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=versions,
+ categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat")
+
+
+@app.route("/delete//", defaults={'book_format': ""})
+@app.route("/delete///")
+@login_required
+def delete_book(book_id, book_format):
+ if current_user.role_delete_books():
+ book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
+ if book:
+ helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
+ if not book_format:
+ # delete book from Shelfs, Downloads, Read list
+ ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
+ ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete()
+ ub.delete_download(book_id)
+ ub.session.commit()
+
+ # check if only this book links to:
+ # author, language, series, tags, custom columns
+ modify_database_object([u''], book.authors, db.Authors, db.session, 'author')
+ modify_database_object([u''], book.tags, db.Tags, db.session, 'tags')
+ modify_database_object([u''], book.series, db.Series, db.session, 'series')
+ modify_database_object([u''], book.languages, db.Languages, db.session, 'languages')
+ modify_database_object([u''], book.publishers, db.Publishers, db.session, 'publishers')
+
+ cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
+ for c in cc:
+ cc_string = "custom_column_" + str(c.id)
+ if not c.is_multiple:
+ if len(getattr(book, cc_string)) > 0:
+ if c.datatype == 'bool' or c.datatype == 'integer':
+ del_cc = getattr(book, cc_string)[0]
+ getattr(book, cc_string).remove(del_cc)
+ db.session.delete(del_cc)
+ elif c.datatype == 'rating':
+ del_cc = getattr(book, cc_string)[0]
+ getattr(book, cc_string).remove(del_cc)
+ if len(del_cc.books) == 0:
+ db.session.delete(del_cc)
+ else:
+ del_cc = getattr(book, cc_string)[0]
+ getattr(book, cc_string).remove(del_cc)
+ db.session.delete(del_cc)
+ else:
+ modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id],
+ db.session, 'custom')
+ db.session.query(db.Books).filter(db.Books.id == book_id).delete()
+ else:
+ db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format).delete()
+ db.session.commit()
+ else:
+ # book not found
+ app.logger.info('Book with id "'+str(book_id)+'" could not be deleted')
+ if book_format:
+ return redirect(url_for('edit_book', book_id=book_id))
+ else:
+ return redirect(url_for('index'))
+
+
+
+@app.route("/gdrive/authenticate")
+@login_required
+@admin_required
+def authenticate_google_drive():
+ authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl()
+ return redirect(authUrl)
+
+
+@app.route("/gdrive/callback")
+def google_drive_callback():
+ auth_code = request.args.get('code')
+ if not auth_code:
+ abort(403)
+ try:
+ credentials = gdriveutils.Gauth.Instance().auth.flow.step2_exchange(auth_code)
+ with open(os.path.join(config.get_main_dir,'gdrive_credentials'), 'w') as f:
+ f.write(credentials.to_json())
+ except ValueError as error:
+ app.logger.error(error)
+ return redirect(url_for('configuration'))
+
+
+@app.route("/gdrive/watch/subscribe")
+@login_required
+@admin_required
+def watch_gdrive():
+ if not config.config_google_drive_watch_changes_response:
+ with open(os.path.join(config.get_main_dir,'client_secrets.json'), 'r') as settings:
+ filedata = json.load(settings)
+ if filedata['web']['redirect_uris'][0].endswith('/'):
+ filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))]
+ else:
+ filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-(len('/gdrive/callback'))]
+ address = '%s/gdrive/watch/callback' % filedata['web']['redirect_uris'][0]
+ notification_id = str(uuid4())
+ try:
+ result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
+ 'web_hook', address, gdrive_watch_callback_token, current_milli_time() + 604800*1000)
+ settings = ub.session.query(ub.Settings).first()
+ settings.config_google_drive_watch_changes_response = json.dumps(result)
+ ub.session.merge(settings)
+ ub.session.commit()
+ settings = ub.session.query(ub.Settings).first()
+ config.loadSettings()
+ except HttpError as e:
+ reason=json.loads(e.content)['error']['errors'][0]
+ if reason['reason'] == u'push.webhookUrlUnauthorized':
+ flash(_(u'Callback domain is not verified, please follow steps to verify domain in google developer console'), category="error")
+ else:
+ flash(reason['message'], category="error")
+
+ return redirect(url_for('configuration'))
+
+
+@app.route("/gdrive/watch/revoke")
+@login_required
+@admin_required
+def revoke_watch_gdrive():
+ last_watch_response = config.config_google_drive_watch_changes_response
+ if last_watch_response:
+ try:
+ gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'],
+ last_watch_response['resourceId'])
+ except HttpError:
+ pass
+ settings = ub.session.query(ub.Settings).first()
+ settings.config_google_drive_watch_changes_response = None
+ ub.session.merge(settings)
+ ub.session.commit()
+ config.loadSettings()
+ return redirect(url_for('configuration'))
+
+
+@app.route("/gdrive/watch/callback", methods=['GET', 'POST'])
+def on_received_watch_confirmation():
+ app.logger.debug(request.headers)
+ if request.headers.get('X-Goog-Channel-Token') == gdrive_watch_callback_token \
+ and request.headers.get('X-Goog-Resource-State') == 'change' \
+ and request.data:
+
+ data = request.data
+
+ def updateMetaData():
+ app.logger.info('Change received from gdrive')
+ app.logger.debug(data)
+ try:
+ j = json.loads(data)
+ app.logger.info('Getting change details')
+ response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
+ app.logger.debug(response)
+ if response:
+ dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
+ if not response['deleted'] and response['file']['title'] == 'metadata.db' and response['file']['md5Checksum'] != md5(dbpath):
+ tmpDir = tempfile.gettempdir()
+ app.logger.info('Database file updated')
+ copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time())))
+ app.logger.info('Backing up existing and downloading updated metadata.db')
+ gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmpDir, "tmp_metadata.db"))
+ app.logger.info('Setting up new DB')
+ # prevent error on windows, as os.rename does on exisiting files
+ move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath)
+ db.setup_db()
+ except Exception as e:
+ app.logger.info(e.message)
+ app.logger.exception(e)
+ updateMetaData()
+ return ''
+
+
+@app.route("/shutdown")
+@login_required
+@admin_required
+def shutdown():
+ task = int(request.args.get("parameter").strip())
+ if task == 1 or task == 0: # valid commandos received
+ # close all database connections
+ db.session.close()
+ db.engine.dispose()
+ ub.session.close()
+ ub.engine.dispose()
+
+ showtext = {}
+ if task == 0:
+ showtext['text'] = _(u'Server restarted, please reload page')
+ server.Server.setRestartTyp(True)
+ else:
+ showtext['text'] = _(u'Performing shutdown of server, please close window')
+ server.Server.setRestartTyp(False)
+ # stop gevent/tornado server
+ server.Server.stopServer()
+ return json.dumps(showtext)
+ else:
+ if task == 2:
+ db.session.close()
+ db.engine.dispose()
+ db.setup_db()
+ return json.dumps({})
+ abort(404)
+
+
+@app.route("/update")
+@login_required
+@admin_required
+def update():
+ helper.updater_thread = helper.Updater()
+ flash(_(u"Update done"), category="info")
+ return abort(404)
+
+
+@app.route("/search", methods=["GET"])
+@login_required_if_no_ano
+def search():
+ term = request.args.get("query").strip().lower()
+ if term:
+ entries = get_search_results(term)
+ ids = list()
+ for element in entries:
+ ids.append(element.id)
+ ub.searched_ids[current_user.id] = ids
+ return render_title_template('search.html', searchterm=term, entries=entries, page="search")
+ else:
+ return render_title_template('search.html', searchterm="", page="search")
+
+
+@app.route("/advanced_search", methods=['GET'])
+@login_required_if_no_ano
+def advanced_search():
+ # Build custom columns names
+ tmpcc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
+ if config.config_columns_to_ignore:
+ cc = []
+ for col in tmpcc:
+ r = re.compile(config.config_columns_to_ignore)
+ if r.match(col.label):
+ cc.append(col)
+ else:
+ cc = tmpcc
+
+ db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
+ q = db.session.query(db.Books)
+ # postargs = request.form.to_dict()
+
+ include_tag_inputs = request.args.getlist('include_tag')
+ exclude_tag_inputs = request.args.getlist('exclude_tag')
+ include_series_inputs = request.args.getlist('include_serie')
+ exclude_series_inputs = request.args.getlist('exclude_serie')
+ include_languages_inputs = request.args.getlist('include_language')
+ exclude_languages_inputs = request.args.getlist('exclude_language')
+
+ author_name = request.args.get("author_name")
+ book_title = request.args.get("book_title")
+ publisher = request.args.get("publisher")
+ pub_start = request.args.get("Publishstart")
+ pub_end = request.args.get("Publishend")
+ rating_low = request.args.get("ratinghigh")
+ rating_high = request.args.get("ratinglow")
+ description = request.args.get("comment")
+ if author_name: author_name = author_name.strip().lower().replace(',','|')
+ if book_title: book_title = book_title.strip().lower()
+ if publisher: publisher = publisher.strip().lower()
+
+ searchterm = []
+ cc_present = False
+ for c in cc:
+ if request.args.get('custom_column_' + str(c.id)):
+ searchterm.extend([(u"%s: %s" % (c.name, request.args.get('custom_column_' + str(c.id))))])
+ cc_present = True
+
+ if include_tag_inputs or exclude_tag_inputs or include_series_inputs or exclude_series_inputs or \
+ include_languages_inputs or exclude_languages_inputs or author_name or book_title or \
+ publisher or pub_start or pub_end or rating_low or rating_high or description or cc_present:
+ searchterm = []
+ searchterm.extend((author_name.replace('|',','), book_title, publisher))
+ if pub_start:
+ try:
+ searchterm.extend([_(u"Published after ") +
+ format_date(datetime.datetime.strptime(pub_start,"%Y-%m-%d"),
+ format='medium', locale=get_locale())])
+ except ValueError:
+ pub_start = u""
+ if pub_end:
+ try:
+ searchterm.extend([_(u"Published before ") +
+ format_date(datetime.datetime.strptime(pub_end,"%Y-%m-%d"),
+ format='medium', locale=get_locale())])
+ except ValueError:
+ pub_start = u""
+ tag_names = db.session.query(db.Tags).filter(db.Tags.id.in_(include_tag_inputs)).all()
+ searchterm.extend(tag.name for tag in tag_names)
+ serie_names = db.session.query(db.Series).filter(db.Series.id.in_(include_series_inputs)).all()
+ searchterm.extend(serie.name for serie in serie_names)
+ language_names = db.session.query(db.Languages).filter(db.Languages.id.in_(include_languages_inputs)).all()
+ if language_names:
+ language_names = speaking_language(language_names)
+ searchterm.extend(language.name for language in language_names)
+ if rating_high:
+ searchterm.extend([_(u"Rating <= %(rating)s", rating=rating_high)])
+ if rating_low:
+ searchterm.extend([_(u"Rating >= %(rating)s", rating=rating_low)])
+ # handle custom columns
+ for c in cc:
+ if request.args.get('custom_column_' + str(c.id)):
+ searchterm.extend([(u"%s: %s" % (c.name, request.args.get('custom_column_' + str(c.id))))])
+ searchterm = " + ".join(filter(None, searchterm))
+ q = q.filter()
+ if author_name:
+ q = q.filter(db.Books.authors.any(db.Authors.name.ilike("%" + author_name + "%")))
+ if book_title:
+ q = q.filter(db.Books.title.ilike("%" + book_title + "%"))
+ if pub_start:
+ q = q.filter(db.Books.pubdate >= pub_start)
+ if pub_end:
+ q = q.filter(db.Books.pubdate <= pub_end)
+ if publisher:
+ q = q.filter(db.Books.publishers.any(db.Publishers.name.ilike("%" + publisher + "%")))
+ for tag in include_tag_inputs:
+ q = q.filter(db.Books.tags.any(db.Tags.id == tag))
+ for tag in exclude_tag_inputs:
+ q = q.filter(not_(db.Books.tags.any(db.Tags.id == tag)))
+ for serie in include_series_inputs:
+ q = q.filter(db.Books.series.any(db.Series.id == serie))
+ for serie in exclude_series_inputs:
+ q = q.filter(not_(db.Books.series.any(db.Series.id == serie)))
+ if current_user.filter_language() != "all":
+ q = q.filter(db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()))
+ else:
+ for language in include_languages_inputs:
+ q = q.filter(db.Books.languages.any(db.Languages.id == language))
+ for language in exclude_languages_inputs:
+ q = q.filter(not_(db.Books.series.any(db.Languages.id == language)))
+ if rating_high:
+ rating_high = int(rating_high) * 2
+ q = q.filter(db.Books.ratings.any(db.Ratings.rating <= rating_high))
+ if rating_low:
+ rating_low = int(rating_low) *2
+ q = q.filter(db.Books.ratings.any(db.Ratings.rating >= rating_low))
+ if description:
+ q = q.filter(db.Books.comments.any(db.Comments.text.ilike("%" + description + "%")))
+
+ # search custom culumns
+ for c in cc:
+ custom_query = request.args.get('custom_column_' + str(c.id))
+ if custom_query:
+ if c.datatype == 'bool':
+ getattr(db.Books, 'custom_column_1')
+ q = q.filter(getattr(db.Books, 'custom_column_'+str(c.id)).any(
+ db.cc_classes[c.id].value == (custom_query== "True") ))
+ elif c.datatype == 'int':
+ q = q.filter(getattr(db.Books, 'custom_column_'+str(c.id)).any(
+ db.cc_classes[c.id].value == custom_query ))
+ else:
+ q = q.filter(getattr(db.Books, 'custom_column_'+str(c.id)).any(
+ db.cc_classes[c.id].value.ilike("%" + custom_query + "%")))
+ q = q.all()
+ ids = list()
+ for element in q:
+ ids.append(element.id)
+ ub.searched_ids[current_user.id] = ids
+ return render_title_template('search.html', searchterm=searchterm,
+ entries=q, title=_(u"search"), page="search")
+ # prepare data for search-form
+ tags = db.session.query(db.Tags).order_by(db.Tags.name).all()
+ series = db.session.query(db.Series).order_by(db.Series.name).all()
+ if current_user.filter_language() == u"all":
+ languages = speaking_language()
+ else:
+ languages = None
+ return render_title_template('search_form.html', tags=tags, languages=languages,
+ series=series, title=_(u"search"), cc=cc, page="advsearch")
+
+
+@app.route("/cover/")
+@login_required_if_no_ano
+def get_cover(cover_path):
+ return helper.get_book_cover(cover_path)
+
+
+@app.route("/show//")
+@login_required_if_no_ano
+def serve_book(book_id, book_format):
+ book_format = book_format.split(".")[0]
+ book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
+ data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format.upper()).first()
+ app.logger.info(data.name)
+ if config.config_use_google_drive:
+ headers = Headers()
+ try:
+ headers["Content-Type"] = mimetypes.types_map['.' + book_format]
+ except KeyError:
+ headers["Content-Type"] = "application/octet-stream"
+ df = gdriveutils.getFileFromEbooksFolder(book.path, data.name + "." + book_format)
+ return gdriveutils.do_gdrive_download(df, headers)
+ else:
+ return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format)
+
+
+@app.route("/opds/thumb_240_240/")
+@app.route("/opds/cover_240_240/")
+@app.route("/opds/cover_90_90/")
+@app.route("/opds/cover/")
+@requires_basic_auth_if_no_ano
+def feed_get_cover(book_id):
+ book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
+ return helper.get_book_cover(book.path)
+
+
+def render_read_books(page, are_read, as_xml=False):
+ if not config.config_read_column:
+ readBooks = ub.session.query(ub.ReadBook).filter(ub.ReadBook.user_id == int(current_user.id))\
+ .filter(ub.ReadBook.is_read == True).all()
+ readBookIds = [x.book_id for x in readBooks]
+ else:
+ try:
+ readBooks = db.session.query(db.cc_classes[config.config_read_column])\
+ .filter(db.cc_classes[config.config_read_column].value==True).all()
+ readBookIds = [x.book for x in readBooks]
+ except KeyError:
+ app.logger.error(u"Custom Column No.%d is not existing in calibre database" % config.config_read_column)
+ readBookIds = []
+
+ if are_read:
+ db_filter = db.Books.id.in_(readBookIds)
+ else:
+ db_filter = ~db.Books.id.in_(readBookIds)
+
+ entries, random, pagination = fill_indexpage(page, db.Books,
+ db_filter, [db.Books.timestamp.desc()])
+
+ if as_xml:
+ xml = render_title_template('feed.xml', entries=entries, pagination=pagination)
+ response = make_response(xml)
+ response.headers["Content-Type"] = "application/xml; charset=utf-8"
+ return response
+ else:
+ if are_read:
+ name = _(u'Read Books') + ' (' + str(len(readBookIds)) + ')'
+ else:
+ total_books = db.session.query(func.count(db.Books.id)).scalar()
+ name = _(u'Unread Books') + ' (' + str(total_books - len(readBookIds)) + ')'
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
+ title=_(name, name=name), page="read")
+
+
+@app.route("/opds/readbooks/")
+@login_required_if_no_ano
+def feed_read_books():
+ off = request.args.get("offset") or 0
+ return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
+
+
+@app.route("/readbooks/", defaults={'page': 1})
+@app.route("/readbooks/'")
+@login_required_if_no_ano
+def read_books(page):
+ return render_read_books(page, True)
+
+
+@app.route("/opds/unreadbooks/")
+@login_required_if_no_ano
+def feed_unread_books():
+ off = request.args.get("offset") or 0
+ return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True)
+
+
+@app.route("/unreadbooks/", defaults={'page': 1})
+@app.route("/unreadbooks/'")
+@login_required_if_no_ano
+def unread_books(page):
+ return render_read_books(page, False)
+
+
+@app.route("/read//")
+@login_required_if_no_ano
+def read_book(book_id, book_format):
+ book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
+ if not book:
+ flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error")
+ return redirect(url_for("index"))
+
+ # check if book was downloaded before
+ lbookmark = None
+ if current_user.is_authenticated:
+ lbookmark = ub.session.query(ub.Bookmark).filter(ub.and_(ub.Bookmark.user_id == int(current_user.id),
+ ub.Bookmark.book_id == book_id,
+ ub.Bookmark.format == book_format.upper())).first()
+ if book_format.lower() == "epub":
+ return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book"), bookmark=lbookmark)
+ elif book_format.lower() == "pdf":
+ return render_title_template('readpdf.html', pdffile=book_id, title=_(u"Read a Book"))
+ elif book_format.lower() == "txt":
+ return render_title_template('readtxt.html', txtfile=book_id, title=_(u"Read a Book"))
+ else:
+ book_dir = os.path.join(config.get_main_dir, "cps", "static", str(book_id))
+ if not os.path.exists(book_dir):
+ os.mkdir(book_dir)
+ for fileext in ["cbr", "cbt", "cbz"]:
+ if book_format.lower() == fileext:
+ all_name = str(book_id) # + "/" + book.data[0].name + "." + fileext
+ #tmp_file = os.path.join(book_dir, book.data[0].name) + "." + fileext
+ #if not os.path.exists(all_name):
+ # cbr_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + "." + fileext
+ # copyfile(cbr_file, tmp_file)
+ return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"),
+ extension=fileext)
+ '''if rar_support == True:
+ extensionList = ["cbr","cbt","cbz"]
+ else:
+ extensionList = ["cbt","cbz"]
+ for fileext in extensionList:
+ if book_format.lower() == fileext:
+ return render_title_template('readcbr.html', comicfile=book_id,
+ extension=fileext, title=_(u"Read a Book"), book=book)
+ flash(_(u"Error opening eBook. File does not exist or file is not accessible."), category="error")
+ return redirect(url_for("index"))'''
+
+
+@app.route("/download//")
+@login_required_if_no_ano
+@download_required
+def get_download_link(book_id, book_format):
+ book_format = book_format.split(".")[0]
+ book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
+ data = db.session.query(db.Data).filter(db.Data.book == book.id)\
+ .filter(db.Data.format == book_format.upper()).first()
+ if data:
+ # collect downloaded books only for registered user and not for anonymous user
+ if current_user.is_authenticated:
+ ub.update_download(book_id, int(current_user.id))
+ file_name = book.title
+ if len(book.authors) > 0:
+ file_name = book.authors[0].name + '_' + file_name
+ file_name = helper.get_valid_filename(file_name)
+ headers = Headers()
+ try:
+ headers["Content-Type"] = mimetypes.types_map['.' + book_format]
+ except KeyError:
+ headers["Content-Type"] = "application/octet-stream"
+ headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (quote(file_name.encode('utf-8')),
+ book_format)
+ return helper.do_download_file(book, book_format, data, headers)
+ else:
+ abort(404)
+
+
+@app.route("/download///")
+@login_required_if_no_ano
+@download_required
+def get_download_link_ext(book_id, book_format, anyname):
+ return get_download_link(book_id, book_format)
+
+
+@app.route('/register', methods=['GET', 'POST'])
+def register():
+ if not config.config_public_reg:
+ abort(404)
+ if current_user is not None and current_user.is_authenticated:
+ return redirect(url_for('index'))
+
+ if request.method == "POST":
+ to_save = request.form.to_dict()
+ if not to_save["nickname"] or not to_save["email"]:
+ flash(_(u"Please fill out all fields!"), category="error")
+ return render_title_template('register.html', title=_(u"register"), page="register")
+
+ existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower()).first()
+ existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()).first()
+ if not existing_user and not existing_email:
+ content = ub.User()
+ # content.password = generate_password_hash(to_save["password"])
+ if check_valid_domain(to_save["email"]):
+ content.nickname = to_save["nickname"]
+ content.email = to_save["email"]
+ password = helper.generate_random_password()
+ content.password = generate_password_hash(password)
+ content.role = config.config_default_role
+ content.sidebar_view = config.config_default_show
+ try:
+ ub.session.add(content)
+ ub.session.commit()
+ helper.send_registration_mail(to_save["email"],to_save["nickname"], password)
+ except Exception:
+ ub.session.rollback()
+ flash(_(u"An unknown error occurred. Please try again later."), category="error")
+ return render_title_template('register.html', title=_(u"register"), page="register")
+ else:
+ flash(_(u"Your e-mail is not allowed to register"), category="error")
+ app.logger.info('Registering failed for user "' + to_save['nickname'] + '" e-mail adress: ' + to_save["email"])
+ return render_title_template('register.html', title=_(u"register"), page="register")
+ flash(_(u"Confirmation e-mail was send to your e-mail account."), category="success")
+ return redirect(url_for('login'))
+ else:
+ flash(_(u"This username or e-mail address is already in use."), category="error")
+ return render_title_template('register.html', title=_(u"register"), page="register")
+
+ return render_title_template('register.html', title=_(u"register"), page="register")
+
+
+@app.route('/login', methods=['GET', 'POST'])
+def login():
+ if not config.db_configured:
+ return redirect(url_for('basic_configuration'))
+ if current_user is not None and current_user.is_authenticated:
+ return redirect(url_for('index'))
+ if request.method == "POST":
+ form = request.form.to_dict()
+ user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == form['username'].strip().lower()).first()
+ if user and check_password_hash(user.password, form['password']) and user.nickname is not "Guest":
+ login_user(user, remember=True)
+ flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
+ return redirect_back(url_for("index"))
+ else:
+ ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr)
+ app.logger.info('Login failed for user "' + form['username'] + '" IP-adress: ' + ipAdress)
+ flash(_(u"Wrong Username or Password"), category="error")
+
+ # next_url = request.args.get('next')
+ # if next_url is None or not is_safe_url(next_url):
+ next_url = url_for('index')
+
+ return render_title_template('login.html', title=_(u"login"), next_url=next_url,
+ remote_login=config.config_remote_login, page="login")
+
+
+@app.route('/logout')
+@login_required
+def logout():
+ if current_user is not None and current_user.is_authenticated:
+ logout_user()
+ return redirect(url_for('login'))
+
+
+@app.route('/remote/login')
+@remote_login_required
+def remote_login():
+ auth_token = ub.RemoteAuthToken()
+ ub.session.add(auth_token)
+ ub.session.commit()
+
+ verify_url = url_for('verify_token', token=auth_token.auth_token, _external=true)
+
+ return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token,
+ verify_url=verify_url, page="remotelogin")
+
+
+@app.route('/verify/')
+@remote_login_required
+@login_required
+def verify_token(token):
+ auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
+
+ # Token not found
+ if auth_token is None:
+ flash(_(u"Token not found"), category="error")
+ return redirect(url_for('index'))
+
+ # Token expired
+ if datetime.datetime.now() > auth_token.expiration:
+ ub.session.delete(auth_token)
+ ub.session.commit()
+
+ flash(_(u"Token has expired"), category="error")
+ return redirect(url_for('index'))
+
+ # Update token with user information
+ auth_token.user_id = current_user.id
+ auth_token.verified = True
+ ub.session.commit()
+
+ flash(_(u"Success! Please return to your device"), category="success")
+ return redirect(url_for('index'))
+
+
+@app.route('/ajax/verify_token', methods=['POST'])
+@remote_login_required
+def token_verified():
+ token = request.form['token']
+ auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
+
+ data = {}
+
+ # Token not found
+ if auth_token is None:
+ data['status'] = 'error'
+ data['message'] = _(u"Token not found")
+
+ # Token expired
+ elif datetime.datetime.now() > auth_token.expiration:
+ ub.session.delete(auth_token)
+ ub.session.commit()
+
+ data['status'] = 'error'
+ data['message'] = _(u"Token has expired")
+
+ elif not auth_token.verified:
+ data['status'] = 'not_verified'
+
+ else:
+ user = ub.session.query(ub.User).filter(ub.User.id == auth_token.user_id).first()
+ login_user(user)
+
+ ub.session.delete(auth_token)
+ ub.session.commit()
+
+ data['status'] = 'success'
+ flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
+
+ response = make_response(json.dumps(data, ensure_ascii=False))
+ response.headers["Content-Type"] = "application/json; charset=utf-8"
+
+ return response
+
+
+@app.route('/send///')
+@login_required
+@download_required
+def send_to_kindle(book_id, book_format, convert):
+ settings = ub.get_mail_settings()
+ if settings.get("mail_server", "mail.example.com") == "mail.example.com":
+ flash(_(u"Please configure the SMTP mail settings first..."), category="error")
+ elif current_user.kindle_mail:
+ result = helper.send_mail(book_id, book_format, convert, current_user.kindle_mail, config.config_calibre_dir,
+ current_user.nickname)
+ if result is None:
+ flash(_(u"Book successfully queued for sending to %(kindlemail)s", kindlemail=current_user.kindle_mail),
+ category="success")
+ ub.update_download(book_id, int(current_user.id))
+ else:
+ flash(_(u"There was an error sending this book: %(res)s", res=result), category="error")
+ else:
+ flash(_(u"Please configure your kindle e-mail address first..."), category="error")
+ return redirect(request.environ["HTTP_REFERER"])
+
+
+@app.route("/shelf/add//")
+@login_required
+def add_to_shelf(shelf_id, book_id):
+ shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
+ if shelf is None:
+ app.logger.info("Invalid shelf specified")
+ if not request.is_xhr:
+ flash(_(u"Invalid shelf specified"), category="error")
+ return redirect(url_for('index'))
+ return "Invalid shelf specified", 400
+
+ if not shelf.is_public and not shelf.user_id == int(current_user.id):
+ app.logger.info("Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name)
+ if not request.is_xhr:
+ flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name),
+ category="error")
+ return redirect(url_for('index'))
+ return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403
+
+ if shelf.is_public and not current_user.role_edit_shelfs():
+ app.logger.info("User is not allowed to edit public shelves")
+ if not request.is_xhr:
+ flash(_(u"You are not allowed to edit public shelves"), category="error")
+ return redirect(url_for('index'))
+ return "User is not allowed to edit public shelves", 403
+
+ book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
+ ub.BookShelf.book_id == book_id).first()
+ if book_in_shelf:
+ app.logger.info("Book is already part of the shelf: %s" % shelf.name)
+ if not request.is_xhr:
+ flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
+ return redirect(url_for('index'))
+ return "Book is already part of the shelf: %s" % shelf.name, 400
+
+ maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()
+ if maxOrder[0] is None:
+ maxOrder = 0
+ else:
+ maxOrder = maxOrder[0]
+
+ ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1)
+ ub.session.add(ins)
+ ub.session.commit()
+ if not request.is_xhr:
+ flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
+ if "HTTP_REFERER" in request.environ:
+ return redirect(request.environ["HTTP_REFERER"])
+ else:
+ return redirect(url_for('index'))
+ return "", 204
+
+
+@app.route("/shelf/massadd/")
+@login_required
+def search_to_shelf(shelf_id):
+ shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
+ if shelf is None:
+ app.logger.info("Invalid shelf specified")
+ flash(_(u"Invalid shelf specified"), category="error")
+ return redirect(url_for('index'))
+
+ if not shelf.is_public and not shelf.user_id == int(current_user.id):
+ app.logger.info("You are not allowed to add a book to the the shelf: %s" % shelf.name)
+ flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
+ return redirect(url_for('index'))
+
+ if shelf.is_public and not current_user.role_edit_shelfs():
+ app.logger.info("User is not allowed to edit public shelves")
+ flash(_(u"User is not allowed to edit public shelves"), category="error")
+ return redirect(url_for('index'))
+
+ if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
+ books_for_shelf = list()
+ books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
+ if books_in_shelf:
+ book_ids = list()
+ for book_id in books_in_shelf:
+ book_ids.append(book_id.book_id)
+ for id in ub.searched_ids[current_user.id]:
+ if id not in book_ids:
+ books_for_shelf.append(id)
+ else:
+ books_for_shelf = ub.searched_ids[current_user.id]
+
+ if not books_for_shelf:
+ app.logger.info("Books are already part of the shelf: %s" % shelf.name)
+ flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
+ return redirect(url_for('index'))
+
+ maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()
+ if maxOrder[0] is None:
+ maxOrder = 0
+ else:
+ maxOrder = maxOrder[0]
+
+ for book in books_for_shelf:
+ maxOrder = maxOrder + 1
+ ins = ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder)
+ ub.session.add(ins)
+ ub.session.commit()
+ flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
+ else:
+ flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
+ return redirect(url_for('index'))
+
+
+@app.route("/shelf/remove//")
+@login_required
+def remove_from_shelf(shelf_id, book_id):
+ shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
+ if shelf is None:
+ app.logger.info("Invalid shelf specified")
+ if not request.is_xhr:
+ return redirect(url_for('index'))
+ return "Invalid shelf specified", 400
+
+ # if shelf is public and use is allowed to edit shelfs, or if shelf is private and user is owner
+ # allow editing shelfs
+ # result shelf public user allowed user owner
+ # false 1 0 x
+ # true 1 1 x
+ # true 0 x 1
+ # false 0 x 0
+
+ if (not shelf.is_public and shelf.user_id == int(current_user.id)) \
+ or (shelf.is_public and current_user.role_edit_shelfs()):
+ book_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
+ ub.BookShelf.book_id == book_id).first()
+
+ if book_shelf is None:
+ app.logger.info("Book already removed from shelf")
+ if not request.is_xhr:
+ return redirect(url_for('index'))
+ return "Book already removed from shelf", 410
+
+ ub.session.delete(book_shelf)
+ ub.session.commit()
+
+ if not request.is_xhr:
+ flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
+ return redirect(request.environ["HTTP_REFERER"])
+ return "", 204
+ else:
+ app.logger.info("Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name)
+ if not request.is_xhr:
+ flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
+ category="error")
+ return redirect(url_for('index'))
+ return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403
+
+
+
+@app.route("/shelf/create", methods=["GET", "POST"])
+@login_required
+def create_shelf():
+ shelf = ub.Shelf()
+ if request.method == "POST":
+ to_save = request.form.to_dict()
+ if "is_public" in to_save:
+ shelf.is_public = 1
+ shelf.name = to_save["title"]
+ shelf.user_id = int(current_user.id)
+ existing_shelf = ub.session.query(ub.Shelf).filter(
+ or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1),
+ (ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).first()
+ if existing_shelf:
+ flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error")
+ else:
+ try:
+ ub.session.add(shelf)
+ ub.session.commit()
+ flash(_(u"Shelf %(title)s created", title=to_save["title"]), category="success")
+ except Exception:
+ flash(_(u"There was an error"), category="error")
+ return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"), page="shelfcreate")
+ else:
+ return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"), page="shelfcreate")
+
+
+@app.route("/shelf/edit/", methods=["GET", "POST"])
+@login_required
+def edit_shelf(shelf_id):
+ shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
+ if request.method == "POST":
+ to_save = request.form.to_dict()
+ existing_shelf = ub.session.query(ub.Shelf).filter(
+ or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1),
+ (ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).filter(
+ ub.Shelf.id != shelf_id).first()
+ if existing_shelf:
+ flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error")
+ else:
+ shelf.name = to_save["title"]
+ if "is_public" in to_save:
+ shelf.is_public = 1
+ else:
+ shelf.is_public = 0
+ try:
+ ub.session.commit()
+ flash(_(u"Shelf %(title)s changed", title=to_save["title"]), category="success")
+ except Exception:
+ flash(_(u"There was an error"), category="error")
+ return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit")
+ else:
+ return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit")
+
+
+@app.route("/shelf/delete/")
+@login_required
+def delete_shelf(shelf_id):
+ cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
+ deleted = None
+ if current_user.role_admin():
+ deleted = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).delete()
+ else:
+ if (not cur_shelf.is_public and cur_shelf.user_id == int(current_user.id)) \
+ or (cur_shelf.is_public and current_user.role_edit_shelfs()):
+ deleted = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id),
+ ub.Shelf.id == shelf_id),
+ ub.and_(ub.Shelf.is_public == 1,
+ ub.Shelf.id == shelf_id))).delete()
+
+ if deleted:
+ ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
+ ub.session.commit()
+ app.logger.info(_(u"successfully deleted shelf %(name)s", name=cur_shelf.name, category="success"))
+ return redirect(url_for('index'))
+
+
+@app.route("/shelf/")
+@login_required_if_no_ano
+def show_shelf(shelf_id):
+ if current_user.is_anonymous:
+ shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first()
+ else:
+ shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id),
+ ub.Shelf.id == shelf_id),
+ ub.and_(ub.Shelf.is_public == 1,
+ ub.Shelf.id == shelf_id))).first()
+ result = list()
+ # user is allowed to access shelf
+ if shelf:
+ books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by(
+ ub.BookShelf.order.asc()).all()
+ for book in books_in_shelf:
+ cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
+ if cur_book:
+ result.append(cur_book)
+ else:
+ app.logger.info('Not existing book %s in shelf %s deleted' % (book.book_id, shelf.id))
+ ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete()
+ ub.session.commit()
+ return render_title_template('shelf.html', entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name),
+ shelf=shelf, page="shelf")
+ else:
+ flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
+ return redirect(url_for("index"))
+
+
+@app.route("/shelf/order/", methods=["GET", "POST"])
+@login_required
+def order_shelf(shelf_id):
+ if request.method == "POST":
+ to_save = request.form.to_dict()
+ books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by(
+ ub.BookShelf.order.asc()).all()
+ counter = 0
+ for book in books_in_shelf:
+ setattr(book, 'order', to_save[str(book.book_id)])
+ counter += 1
+ ub.session.commit()
+ if current_user.is_anonymous:
+ shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first()
+ else:
+ shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id),
+ ub.Shelf.id == shelf_id),
+ ub.and_(ub.Shelf.is_public == 1,
+ ub.Shelf.id == shelf_id))).first()
+ result = list()
+ if shelf:
+ books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
+ .order_by(ub.BookShelf.order.asc()).all()
+ for book in books_in_shelf2:
+ cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
+ result.append(cur_book)
+ return render_title_template('shelf_order.html', entries=result,
+ title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
+ shelf=shelf, page="shelforder")
+
+
+@app.route("/me", methods=["GET", "POST"])
+@login_required
+def profile():
+ content = ub.session.query(ub.User).filter(ub.User.id == int(current_user.id)).first()
+ downloads = list()
+ languages = speaking_language()
+ translations = babel.list_translations() + [LC('en')]
+ for book in content.downloads:
+ downloadBook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
+ if downloadBook:
+ downloads.append(db.session.query(db.Books).filter(db.Books.id == book.book_id).first())
+ else:
+ ub.delete_download(book.book_id)
+ # ub.session.query(ub.Downloads).filter(book.book_id == ub.Downloads.book_id).delete()
+ # ub.session.commit()
+ if request.method == "POST":
+ to_save = request.form.to_dict()
+ content.random_books = 0
+ if current_user.role_passwd() or current_user.role_admin():
+ if "password" in to_save and to_save["password"]:
+ content.password = generate_password_hash(to_save["password"])
+ if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail:
+ content.kindle_mail = to_save["kindle_mail"]
+ if to_save["email"] and to_save["email"] != content.email:
+ if config.config_public_reg and not check_valid_domain(to_save["email"]):
+ flash(_(u"E-mail is not from valid domain"), category="error")
+ return render_title_template("user_edit.html", content=content, downloads=downloads,
+ title=_(u"%(name)s's profile", name=current_user.nickname))
+ content.email = to_save["email"]
+ if "show_random" in to_save and to_save["show_random"] == "on":
+ content.random_books = 1
+ if "default_language" in to_save:
+ content.default_language = to_save["default_language"]
+ if "locale" in to_save:
+ content.locale = to_save["locale"]
+ content.sidebar_view = 0
+ if "show_random" in to_save:
+ content.sidebar_view += ub.SIDEBAR_RANDOM
+ if "show_language" in to_save:
+ content.sidebar_view += ub.SIDEBAR_LANGUAGE
+ if "show_series" in to_save:
+ content.sidebar_view += ub.SIDEBAR_SERIES
+ if "show_category" in to_save:
+ content.sidebar_view += ub.SIDEBAR_CATEGORY
+ if "show_recent" in to_save:
+ content.sidebar_view += ub.SIDEBAR_RECENT
+ if "show_sorted" in to_save:
+ content.sidebar_view += ub.SIDEBAR_SORTED
+ if "show_hot" in to_save:
+ content.sidebar_view += ub.SIDEBAR_HOT
+ if "show_best_rated" in to_save:
+ content.sidebar_view += ub.SIDEBAR_BEST_RATED
+ if "show_author" in to_save:
+ content.sidebar_view += ub.SIDEBAR_AUTHOR
+ if "show_publisher" in to_save:
+ content.sidebar_view += ub.SIDEBAR_PUBLISHER
+ if "show_read_and_unread" in to_save:
+ content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD
+ if "show_detail_random" in to_save:
+ content.sidebar_view += ub.DETAIL_RANDOM
+
+ content.mature_content = "show_mature_content" in to_save
+ content.theme = int(to_save["theme"])
+
+ try:
+ ub.session.commit()
+ except IntegrityError:
+ ub.session.rollback()
+ flash(_(u"Found an existing account for this e-mail address."), category="error")
+ return render_title_template("user_edit.html", content=content, downloads=downloads,
+ title=_(u"%(name)s's profile", name=current_user.nickname))
+ flash(_(u"Profile updated"), category="success")
+ return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages,
+ content=content, downloads=downloads, title=_(u"%(name)s's profile",
+ name=current_user.nickname), page="me")
+
+
+@app.route("/admin/view")
+@login_required
+@admin_required
+def admin():
+ version = helper.get_current_version_info()
+ if version is False:
+ commit = _(u'Unknown')
+ else:
+ commit = version['datetime']
+
+ tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
+ form_date = datetime.datetime.strptime(commit[:19], "%Y-%m-%dT%H:%M:%S")
+ if len(commit) > 19: # check if string has timezone
+ if commit[19] == '+':
+ form_date -= datetime.timedelta(hours=int(commit[20:22]), minutes=int(commit[23:]))
+ elif commit[19] == '-':
+ form_date += datetime.timedelta(hours=int(commit[20:22]), minutes=int(commit[23:]))
+ commit = format_datetime(form_date - tz, format='short', locale=get_locale())
+
+ content = ub.session.query(ub.User).all()
+ settings = ub.session.query(ub.Settings).first()
+ return render_title_template("admin.html", content=content, email=settings, config=config, commit=commit,
+ title=_(u"Admin page"), page="admin")
+
+
+@app.route("/admin/config", methods=["GET", "POST"])
+@login_required
+@admin_required
+def configuration():
+ return configuration_helper(0)
+
+
+@app.route("/admin/viewconfig", methods=["GET", "POST"])
+@login_required
+@admin_required
+def view_configuration():
+ reboot_required = False
+ if request.method == "POST":
+ to_save = request.form.to_dict()
+ content = ub.session.query(ub.Settings).first()
+ if "config_calibre_web_title" in to_save:
+ content.config_calibre_web_title = to_save["config_calibre_web_title"]
+ if "config_columns_to_ignore" in to_save:
+ content.config_columns_to_ignore = to_save["config_columns_to_ignore"]
+ if "config_read_column" in to_save:
+ content.config_read_column = int(to_save["config_read_column"])
+ if "config_title_regex" in to_save:
+ if content.config_title_regex != to_save["config_title_regex"]:
+ content.config_title_regex = to_save["config_title_regex"]
+ reboot_required = True
+ if "config_random_books" in to_save:
+ content.config_random_books = int(to_save["config_random_books"])
+ if "config_books_per_page" in to_save:
+ content.config_books_per_page = int(to_save["config_books_per_page"])
+ # Mature Content configuration
+ if "config_mature_content_tags" in to_save:
+ content.config_mature_content_tags = to_save["config_mature_content_tags"].strip()
+
+ # Default user configuration
+ content.config_default_role = 0
+ if "admin_role" in to_save:
+ content.config_default_role = content.config_default_role + ub.ROLE_ADMIN
+ if "download_role" in to_save:
+ content.config_default_role = content.config_default_role + ub.ROLE_DOWNLOAD
+ if "upload_role" in to_save:
+ content.config_default_role = content.config_default_role + ub.ROLE_UPLOAD
+ if "edit_role" in to_save:
+ content.config_default_role = content.config_default_role + ub.ROLE_EDIT
+ if "delete_role" in to_save:
+ content.config_default_role = content.config_default_role + ub.ROLE_DELETE_BOOKS
+ if "passwd_role" in to_save:
+ content.config_default_role = content.config_default_role + ub.ROLE_PASSWD
+ if "passwd_role" in to_save:
+ content.config_default_role = content.config_default_role + ub.ROLE_EDIT_SHELFS
+ content.config_default_show = 0
+ if "show_detail_random" in to_save:
+ content.config_default_show = content.config_default_show + ub.DETAIL_RANDOM
+ if "show_language" in to_save:
+ content.config_default_show = content.config_default_show + ub.SIDEBAR_LANGUAGE
+ if "show_series" in to_save:
+ content.config_default_show = content.config_default_show + ub.SIDEBAR_SERIES
+ if "show_category" in to_save:
+ content.config_default_show = content.config_default_show + ub.SIDEBAR_CATEGORY
+ if "show_hot" in to_save:
+ content.config_default_show = content.config_default_show + ub.SIDEBAR_HOT
+ if "show_random" in to_save:
+ content.config_default_show = content.config_default_show + ub.SIDEBAR_RANDOM
+ if "show_author" in to_save:
+ content.config_default_show = content.config_default_show + ub.SIDEBAR_AUTHOR
+ if "show_publisher" in to_save:
+ content.config_default_show = content.config_default_show + ub.SIDEBAR_PUBLISHER
+ if "show_best_rated" in to_save:
+ content.config_default_show = content.config_default_show + ub.SIDEBAR_BEST_RATED
+ if "show_read_and_unread" in to_save:
+ content.config_default_show = content.config_default_show + ub.SIDEBAR_READ_AND_UNREAD
+ if "show_recent" in to_save:
+ content.config_default_show = content.config_default_show + ub.SIDEBAR_RECENT
+ if "show_sorted" in to_save:
+ content.config_default_show = content.config_default_show + ub.SIDEBAR_SORTED
+ if "show_mature_content" in to_save:
+ content.config_default_show = content.config_default_show + ub.MATURE_CONTENT
+ ub.session.commit()
+ flash(_(u"Calibre-Web configuration updated"), category="success")
+ config.loadSettings()
+ if reboot_required:
+ # db.engine.dispose() # ToDo verify correct
+ # ub.session.close()
+ # ub.engine.dispose()
+ # stop Server
+ server.Server.setRestartTyp(True)
+ server.Server.stopServer()
+ app.logger.info('Reboot required, restarting')
+ readColumn = db.session.query(db.Custom_Columns)\
+ .filter(db.and_(db.Custom_Columns.datatype == 'bool',db.Custom_Columns.mark_for_delete == 0)).all()
+ return render_title_template("config_view_edit.html", content=config, readColumns=readColumn,
+ title=_(u"UI Configuration"), page="uiconfig")
+
+
+
+@app.route("/config", methods=["GET", "POST"])
+@unconfigured
+def basic_configuration():
+ logout_user()
+ return configuration_helper(1)
+
+
+def configuration_helper(origin):
+ reboot_required = False
+ gdriveError=None
+ db_change = False
+ success = False
+ filedata = None
+ if gdriveutils.gdrive_support == False:
+ gdriveError = _('Import of optional Google Drive requirements missing')
+ else:
+ if not os.path.isfile(os.path.join(config.get_main_dir,'client_secrets.json')):
+ gdriveError = _('client_secrets.json is missing or not readable')
+ else:
+ with open(os.path.join(config.get_main_dir,'client_secrets.json'), 'r') as settings:
+ filedata=json.load(settings)
+ if not 'web' in filedata:
+ gdriveError = _('client_secrets.json is not configured for web application')
+ if request.method == "POST":
+ to_save = request.form.to_dict()
+ content = ub.session.query(ub.Settings).first() # type: ub.Settings
+ if "config_calibre_dir" in to_save:
+ if content.config_calibre_dir != to_save["config_calibre_dir"]:
+ content.config_calibre_dir = to_save["config_calibre_dir"]
+ db_change = True
+ # Google drive setup
+ if "config_use_google_drive" in to_save and not content.config_use_google_drive and not gdriveError:
+ if filedata:
+ if filedata['web']['redirect_uris'][0].endswith('/'):
+ filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-1]
+ with open(os.path.join(config.get_main_dir,'settings.yaml'), 'w') as f:
+ yaml = "client_config_backend: settings\nclient_config_file: %(client_file)s\n" \
+ "client_config:\n" \
+ " client_id: %(client_id)s\n client_secret: %(client_secret)s\n" \
+ " redirect_uri: %(redirect_uri)s\n\nsave_credentials: True\n" \
+ "save_credentials_backend: file\nsave_credentials_file: %(credential)s\n\n" \
+ "get_refresh_token: True\n\noauth_scope:\n" \
+ " - https://www.googleapis.com/auth/drive\n"
+ f.write(yaml % {'client_file': os.path.join(config.get_main_dir,'client_secrets.json'),
+ 'client_id': filedata['web']['client_id'],
+ 'client_secret': filedata['web']['client_secret'],
+ 'redirect_uri': filedata['web']['redirect_uris'][0],
+ 'credential': os.path.join(config.get_main_dir,'gdrive_credentials')})
+ else:
+ flash(_(u'client_secrets.json is not configured for web application'), category="error")
+ return render_title_template("config_edit.html", content=config, origin=origin,
+ gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError,
+ goodreads=goodreads_support, title=_(u"Basic Configuration"),
+ page="config")
+ # always show google drive settings, but in case of error deny support
+ if "config_use_google_drive" in to_save and not gdriveError:
+ content.config_use_google_drive = "config_use_google_drive" in to_save
+ else:
+ content.config_use_google_drive = 0
+ if "config_google_drive_folder" in to_save:
+ if content.config_google_drive_folder != to_save["config_google_drive_folder"]:
+ content.config_google_drive_folder = to_save["config_google_drive_folder"]
+ gdriveutils.deleteDatabaseOnChange()
+
+ if "config_port" in to_save:
+ if content.config_port != int(to_save["config_port"]):
+ content.config_port = int(to_save["config_port"])
+ reboot_required = True
+ if "config_keyfile" in to_save:
+ if content.config_keyfile != to_save["config_keyfile"]:
+ if os.path.isfile(to_save["config_keyfile"]) or to_save["config_keyfile"] is u"":
+ content.config_keyfile = to_save["config_keyfile"]
+ reboot_required = True
+ else:
+ ub.session.commit()
+ flash(_(u'Keyfile location is not valid, please enter correct path'), category="error")
+ return render_title_template("config_edit.html", content=config, origin=origin,
+ gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError,
+ goodreads=goodreads_support, title=_(u"Basic Configuration"),
+ page="config")
+ if "config_certfile" in to_save:
+ if content.config_certfile != to_save["config_certfile"]:
+ if os.path.isfile(to_save["config_certfile"]) or to_save["config_certfile"] is u"":
+ content.config_certfile = to_save["config_certfile"]
+ reboot_required = True
+ else:
+ ub.session.commit()
+ flash(_(u'Certfile location is not valid, please enter correct path'), category="error")
+ return render_title_template("config_edit.html", content=config, origin=origin,
+ gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError,
+ goodreads=goodreads_support, title=_(u"Basic Configuration"),
+ page="config")
+ content.config_uploading = 0
+ content.config_anonbrowse = 0
+ content.config_public_reg = 0
+ if "config_uploading" in to_save and to_save["config_uploading"] == "on":
+ content.config_uploading = 1
+ if "config_anonbrowse" in to_save and to_save["config_anonbrowse"] == "on":
+ content.config_anonbrowse = 1
+ if "config_public_reg" in to_save and to_save["config_public_reg"] == "on":
+ content.config_public_reg = 1
+
+ if "config_converterpath" in to_save:
+ content.config_converterpath = to_save["config_converterpath"].strip()
+ if "config_calibre" in to_save:
+ content.config_calibre = to_save["config_calibre"].strip()
+ if "config_ebookconverter" in to_save:
+ content.config_ebookconverter = int(to_save["config_ebookconverter"])
+
+ # Remote login configuration
+ content.config_remote_login = ("config_remote_login" in to_save and to_save["config_remote_login"] == "on")
+ if not content.config_remote_login:
+ ub.session.query(ub.RemoteAuthToken).delete()
+
+ # Goodreads configuration
+ content.config_use_goodreads = ("config_use_goodreads" in to_save and to_save["config_use_goodreads"] == "on")
+ if "config_goodreads_api_key" in to_save:
+ content.config_goodreads_api_key = to_save["config_goodreads_api_key"]
+ if "config_goodreads_api_secret" in to_save:
+ content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"]
+ if "config_log_level" in to_save:
+ content.config_log_level = int(to_save["config_log_level"])
+ if content.config_logfile != to_save["config_logfile"]:
+ # check valid path, only path or file
+ if os.path.dirname(to_save["config_logfile"]):
+ if os.path.exists(os.path.dirname(to_save["config_logfile"])) and \
+ os.path.basename(to_save["config_logfile"]) and not os.path.isdir(to_save["config_logfile"]):
+ content.config_logfile = to_save["config_logfile"]
+ else:
+ ub.session.commit()
+ flash(_(u'Logfile location is not valid, please enter correct path'), category="error")
+ return render_title_template("config_edit.html", content=config, origin=origin,
+ gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError,
+ goodreads=goodreads_support, title=_(u"Basic Configuration"),
+ page="config")
+ else:
+ content.config_logfile = to_save["config_logfile"]
+ reboot_required = True
+
+ # Rarfile Content configuration
+ if "config_rarfile_location" in to_save and to_save['config_rarfile_location'] is not u"":
+ check = helper.check_unrar(to_save["config_rarfile_location"].strip())
+ if not check[0] :
+ content.config_rarfile_location = to_save["config_rarfile_location"].strip()
+ else:
+ flash(check[1], category="error")
+ return render_title_template("config_edit.html", content=config, origin=origin,
+ gdrive=gdriveutils.gdrive_support, goodreads=goodreads_support,
+ rarfile_support=rar_support, title=_(u"Basic Configuration"))
+ try:
+ if content.config_use_google_drive and is_gdrive_ready() and not os.path.exists(config.config_calibre_dir + "/metadata.db"):
+ gdriveutils.downloadFile(None, "metadata.db", config.config_calibre_dir + "/metadata.db")
+ if db_change:
+ if config.db_configured:
+ db.session.close()
+ db.engine.dispose()
+ ub.session.commit()
+ flash(_(u"Calibre-Web configuration updated"), category="success")
+ config.loadSettings()
+ app.logger.setLevel(config.config_log_level)
+ logging.getLogger("book_formats").setLevel(config.config_log_level)
+ except Exception as e:
+ flash(e, category="error")
+ return render_title_template("config_edit.html", content=config, origin=origin,
+ gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError,
+ goodreads=goodreads_support, rarfile_support=rar_support,
+ title=_(u"Basic Configuration"), page="config")
+ if db_change:
+ reload(db)
+ if not db.setup_db():
+ flash(_(u'DB location is not valid, please enter correct path'), category="error")
+ return render_title_template("config_edit.html", content=config, origin=origin,
+ gdrive=gdriveutils.gdrive_support,gdriveError=gdriveError,
+ goodreads=goodreads_support, rarfile_support=rar_support,
+ title=_(u"Basic Configuration"), page="config")
+ if reboot_required:
+ # stop Server
+ server.Server.setRestartTyp(True)
+ server.Server.stopServer()
+ app.logger.info('Reboot required, restarting')
+ if origin:
+ success = True
+ if is_gdrive_ready() and gdriveutils.gdrive_support == True and config.config_use_google_drive == True:
+ gdrivefolders=gdriveutils.listRootFolders()
+ else:
+ gdrivefolders=list()
+ return render_title_template("config_edit.html", origin=origin, success=success, content=config,
+ show_authenticate_google_drive=not is_gdrive_ready(),
+ gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError,
+ gdrivefolders=gdrivefolders, rarfile_support=rar_support,
+ goodreads=goodreads_support, title=_(u"Basic Configuration"), page="config")
+
+
+@app.route("/admin/user/new", methods=["GET", "POST"])
+@login_required
+@admin_required
+def new_user():
+ content = ub.User()
+ languages = speaking_language()
+ translations = [LC('en')] + babel.list_translations()
+ if request.method == "POST":
+ to_save = request.form.to_dict()
+ content.default_language = to_save["default_language"]
+ content.mature_content = "show_mature_content" in to_save
+ content.theme = int(to_save["theme"])
+ if "locale" in to_save:
+ content.locale = to_save["locale"]
+ content.sidebar_view = 0
+ if "show_random" in to_save:
+ content.sidebar_view += ub.SIDEBAR_RANDOM
+ if "show_language" in to_save:
+ content.sidebar_view += ub.SIDEBAR_LANGUAGE
+ if "show_series" in to_save:
+ content.sidebar_view += ub.SIDEBAR_SERIES
+ if "show_category" in to_save:
+ content.sidebar_view += ub.SIDEBAR_CATEGORY
+ if "show_hot" in to_save:
+ content.sidebar_view += ub.SIDEBAR_HOT
+ if "show_read_and_unread" in to_save:
+ content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD
+ if "show_best_rated" in to_save:
+ content.sidebar_view += ub.SIDEBAR_BEST_RATED
+ if "show_author" in to_save:
+ content.sidebar_view += ub.SIDEBAR_AUTHOR
+ if "show_publisher" in to_save:
+ content.sidebar_view += ub.SIDEBAR_PUBLISHER
+ if "show_detail_random" in to_save:
+ content.sidebar_view += ub.DETAIL_RANDOM
+ if "show_sorted" in to_save:
+ content.sidebar_view += ub.SIDEBAR_SORTED
+ if "show_recent" in to_save:
+ content.sidebar_view += ub.SIDEBAR_RECENT
+
+ content.role = 0
+ if "admin_role" in to_save:
+ content.role = content.role + ub.ROLE_ADMIN
+ if "download_role" in to_save:
+ content.role = content.role + ub.ROLE_DOWNLOAD
+ if "upload_role" in to_save:
+ content.role = content.role + ub.ROLE_UPLOAD
+ if "edit_role" in to_save:
+ content.role = content.role + ub.ROLE_DELETE_BOOKS
+ if "delete_role" in to_save:
+ content.role = content.role + ub.ROLE_EDIT
+ if "passwd_role" in to_save:
+ content.role = content.role + ub.ROLE_PASSWD
+ if "edit_shelf_role" in to_save:
+ content.role = content.role + ub.ROLE_EDIT_SHELFS
+ if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
+ flash(_(u"Please fill out all fields!"), category="error")
+ return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
+ title=_(u"Add new user"))
+ content.password = generate_password_hash(to_save["password"])
+ content.nickname = to_save["nickname"]
+ if config.config_public_reg and not check_valid_domain(to_save["email"]):
+ flash(_(u"E-mail is not from valid domain"), category="error")
+ return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
+ title=_(u"Add new user"))
+ else:
+ content.email = to_save["email"]
+ try:
+ ub.session.add(content)
+ ub.session.commit()
+ flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
+ return redirect(url_for('admin'))
+ except IntegrityError:
+ ub.session.rollback()
+ flash(_(u"Found an existing account for this e-mail address or nickname."), category="error")
+ else:
+ content.role = config.config_default_role
+ content.sidebar_view = config.config_default_show
+ content.mature_content = bool(config.config_default_show & ub.MATURE_CONTENT)
+ return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
+ languages=languages, title=_(u"Add new user"), page="newuser")
+
+
+@app.route("/admin/mailsettings", methods=["GET", "POST"])
+@login_required
+@admin_required
+def edit_mailsettings():
+ content = ub.session.query(ub.Settings).first()
+ if request.method == "POST":
+ to_save = request.form.to_dict()
+ content.mail_server = to_save["mail_server"]
+ content.mail_port = int(to_save["mail_port"])
+ content.mail_login = to_save["mail_login"]
+ content.mail_password = to_save["mail_password"]
+ content.mail_from = to_save["mail_from"]
+ content.mail_use_ssl = int(to_save["mail_use_ssl"])
+ try:
+ ub.session.commit()
+ flash(_(u"E-mail server settings updated"), category="success")
+ except Exception as e:
+ flash(e, category="error")
+ if "test" in to_save and to_save["test"]:
+ if current_user.kindle_mail:
+ result = helper.send_test_mail(current_user.kindle_mail, current_user.nickname)
+ if result is None:
+ flash(_(u"Test e-mail successfully send to %(kindlemail)s", kindlemail=current_user.kindle_mail),
+ category="success")
+ else:
+ flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error")
+ else:
+ flash(_(u"Please configure your kindle e-mail address first..."), category="error")
+ else:
+ flash(_(u"E-mail server settings updated"), category="success")
+ return render_title_template("email_edit.html", content=content, title=_(u"Edit e-mail server settings"),
+ page="mailset")
+
+
+@app.route("/admin/user/", methods=["GET", "POST"])
+@login_required
+@admin_required
+def edit_user(user_id):
+ content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
+ downloads = list()
+ languages = speaking_language()
+ translations = babel.list_translations() + [LC('en')]
+ for book in content.downloads:
+ downloadbook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first()
+ if downloadbook:
+ downloads.append(downloadbook)
+ else:
+ ub.delete_download(book.book_id)
+ # ub.session.query(ub.Downloads).filter(book.book_id == ub.Downloads.book_id).delete()
+ # ub.session.commit()
+ if request.method == "POST":
+ to_save = request.form.to_dict()
+ if "delete" in to_save:
+ ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
+ ub.session.commit()
+ flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success")
+ return redirect(url_for('admin'))
+ else:
+ if "password" in to_save and to_save["password"]:
+ content.password = generate_password_hash(to_save["password"])
+
+ if "admin_role" in to_save and not content.role_admin():
+ content.role = content.role + ub.ROLE_ADMIN
+ elif "admin_role" not in to_save and content.role_admin():
+ content.role = content.role - ub.ROLE_ADMIN
+
+ if "download_role" in to_save and not content.role_download():
+ content.role = content.role + ub.ROLE_DOWNLOAD
+ elif "download_role" not in to_save and content.role_download():
+ content.role = content.role - ub.ROLE_DOWNLOAD
+
+ if "upload_role" in to_save and not content.role_upload():
+ content.role = content.role + ub.ROLE_UPLOAD
+ elif "upload_role" not in to_save and content.role_upload():
+ content.role = content.role - ub.ROLE_UPLOAD
+
+ if "edit_role" in to_save and not content.role_edit():
+ content.role = content.role + ub.ROLE_EDIT
+ elif "edit_role" not in to_save and content.role_edit():
+ content.role = content.role - ub.ROLE_EDIT
+
+ if "delete_role" in to_save and not content.role_delete_books():
+ content.role = content.role + ub.ROLE_DELETE_BOOKS
+ elif "delete_role" not in to_save and content.role_delete_books():
+ content.role = content.role - ub.ROLE_DELETE_BOOKS
+
+ if "passwd_role" in to_save and not content.role_passwd():
+ content.role = content.role + ub.ROLE_PASSWD
+ elif "passwd_role" not in to_save and content.role_passwd():
+ content.role = content.role - ub.ROLE_PASSWD
+
+ if "edit_shelf_role" in to_save and not content.role_edit_shelfs():
+ content.role = content.role + ub.ROLE_EDIT_SHELFS
+ elif "edit_shelf_role" not in to_save and content.role_edit_shelfs():
+ content.role = content.role - ub.ROLE_EDIT_SHELFS
+
+ if "show_random" in to_save and not content.show_random_books():
+ content.sidebar_view += ub.SIDEBAR_RANDOM
+ elif "show_random" not in to_save and content.show_random_books():
+ content.sidebar_view -= ub.SIDEBAR_RANDOM
+
+ if "show_language" in to_save and not content.show_language():
+ content.sidebar_view += ub.SIDEBAR_LANGUAGE
+ elif "show_language" not in to_save and content.show_language():
+ content.sidebar_view -= ub.SIDEBAR_LANGUAGE
+
+ if "show_series" in to_save and not content.show_series():
+ content.sidebar_view += ub.SIDEBAR_SERIES
+ elif "show_series" not in to_save and content.show_series():
+ content.sidebar_view -= ub.SIDEBAR_SERIES
+
+ if "show_category" in to_save and not content.show_category():
+ content.sidebar_view += ub.SIDEBAR_CATEGORY
+ elif "show_category" not in to_save and content.show_category():
+ content.sidebar_view -= ub.SIDEBAR_CATEGORY
+
+ if "show_recent" in to_save and not content.show_recent():
+ content.sidebar_view += ub.SIDEBAR_RECENT
+ elif "show_recent" not in to_save and content.show_recent():
+ content.sidebar_view -= ub.SIDEBAR_RECENT
+
+ if "show_sorted" in to_save and not content.show_sorted():
+ content.sidebar_view += ub.SIDEBAR_SORTED
+ elif "show_sorted" not in to_save and content.show_sorted():
+ content.sidebar_view -= ub.SIDEBAR_SORTED
+
+ if "show_hot" in to_save and not content.show_hot_books():
+ content.sidebar_view += ub.SIDEBAR_HOT
+ elif "show_hot" not in to_save and content.show_hot_books():
+ content.sidebar_view -= ub.SIDEBAR_HOT
+
+ if "show_best_rated" in to_save and not content.show_best_rated_books():
+ content.sidebar_view += ub.SIDEBAR_BEST_RATED
+ elif "show_best_rated" not in to_save and content.show_best_rated_books():
+ content.sidebar_view -= ub.SIDEBAR_BEST_RATED
+
+ if "show_read_and_unread" in to_save and not content.show_read_and_unread():
+ content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD
+ elif "show_read_and_unread" not in to_save and content.show_read_and_unread():
+ content.sidebar_view -= ub.SIDEBAR_READ_AND_UNREAD
+
+ if "show_author" in to_save and not content.show_author():
+ content.sidebar_view += ub.SIDEBAR_AUTHOR
+ elif "show_author" not in to_save and content.show_author():
+ content.sidebar_view -= ub.SIDEBAR_AUTHOR
+
+ if "show_detail_random" in to_save and not content.show_detail_random():
+ content.sidebar_view += ub.DETAIL_RANDOM
+ elif "show_detail_random" not in to_save and content.show_detail_random():
+ content.sidebar_view -= ub.DETAIL_RANDOM
+
+ content.mature_content = "show_mature_content" in to_save
+ content.theme = int(to_save["theme"])
+
+ if "default_language" in to_save:
+ content.default_language = to_save["default_language"]
+ if "locale" in to_save and to_save["locale"]:
+ content.locale = to_save["locale"]
+ if to_save["email"] and to_save["email"] != content.email:
+ content.email = to_save["email"]
+ if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail:
+ content.kindle_mail = to_save["kindle_mail"]
+ try:
+ ub.session.commit()
+ flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success")
+ except IntegrityError:
+ ub.session.rollback()
+ flash(_(u"An unknown error occured."), category="error")
+ return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0,
+ content=content, downloads=downloads, title=_(u"Edit User %(nick)s",
+ nick=content.nickname), page="edituser")
+
+
+@app.route("/admin/resetpassword/")
+@login_required
+@admin_required
+def reset_password(user_id):
+ if not config.config_public_reg:
+ abort(404)
+ if current_user is not None and current_user.is_authenticated:
+ existing_user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
+ password = helper.generate_random_password()
+ existing_user.password = generate_password_hash(password)
+ try:
+ ub.session.commit()
+ helper.send_registration_mail(existing_user.email, existing_user.nickname, password, True)
+ flash(_(u"Password for user %(user)s reset", user=existing_user.nickname), category="success")
+ except Exception:
+ ub.session.rollback()
+ flash(_(u"An unknown error occurred. Please try again later."), category="error")
+ return redirect(url_for('admin'))
+
+
+def render_edit_book(book_id):
+ db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
+ cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
+ book = db.session.query(db.Books)\
+ .filter(db.Books.id == book_id).filter(common_filters()).first()
+
+ if not book:
+ flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
+ return redirect(url_for("index"))
+
+ for indx in range(0, len(book.languages)):
+ book.languages[indx].language_name = language_table[get_locale()][book.languages[indx].lang_code]
+ author_names = []
+ for authr in book.authors:
+ author_names.append(authr.name.replace('|', ','))
+
+ # Option for showing convertbook button
+ valid_source_formats=list()
+ if config.config_ebookconverter == 2:
+ for file in book.data:
+ if file.format.lower() in EXTENSIONS_CONVERT:
+ valid_source_formats.append(file.format.lower())
+
+ # Determine what formats don't already exist
+ allowed_conversion_formats = EXTENSIONS_CONVERT.copy()
+ for file in book.data:
+ try:
+ allowed_conversion_formats.remove(file.format.lower())
+ except Exception:
+ app.logger.warning(file.format.lower() + ' already removed from list.')
+
+ return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc,
+ title=_(u"edit metadata"), page="editbook",
+ conversion_formats=allowed_conversion_formats,
+ source_formats=valid_source_formats)
+
+
+def edit_cc_data(book_id, book, to_save):
+ cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
+ for c in cc:
+ cc_string = "custom_column_" + str(c.id)
+ if not c.is_multiple:
+ if len(getattr(book, cc_string)) > 0:
+ cc_db_value = getattr(book, cc_string)[0].value
+ else:
+ cc_db_value = None
+ if to_save[cc_string].strip():
+ if c.datatype == 'bool':
+ if to_save[cc_string] == 'None':
+ to_save[cc_string] = None
+ else:
+ to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0
+ if to_save[cc_string] != cc_db_value:
+ if cc_db_value is not None:
+ if to_save[cc_string] is not None:
+ setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string])
+ else:
+ del_cc = getattr(book, cc_string)[0]
+ getattr(book, cc_string).remove(del_cc)
+ db.session.delete(del_cc)
+ else:
+ cc_class = db.cc_classes[c.id]
+ new_cc = cc_class(value=to_save[cc_string], book=book_id)
+ db.session.add(new_cc)
+ elif c.datatype == 'int':
+ if to_save[cc_string] == 'None':
+ to_save[cc_string] = None
+ if to_save[cc_string] != cc_db_value:
+ if cc_db_value is not None:
+ if to_save[cc_string] is not None:
+ setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string])
+ else:
+ del_cc = getattr(book, cc_string)[0]
+ getattr(book, cc_string).remove(del_cc)
+ db.session.delete(del_cc)
+ else:
+ cc_class = db.cc_classes[c.id]
+ new_cc = cc_class(value=to_save[cc_string], book=book_id)
+ db.session.add(new_cc)
+
+ else:
+ if c.datatype == 'rating':
+ to_save[cc_string] = str(int(float(to_save[cc_string]) * 2))
+ if to_save[cc_string].strip() != cc_db_value:
+ if cc_db_value is not None:
+ # remove old cc_val
+ del_cc = getattr(book, cc_string)[0]
+ getattr(book, cc_string).remove(del_cc)
+ if len(del_cc.books) == 0:
+ db.session.delete(del_cc)
+ cc_class = db.cc_classes[c.id]
+ new_cc = db.session.query(cc_class).filter(
+ cc_class.value == to_save[cc_string].strip()).first()
+ # if no cc val is found add it
+ if new_cc is None:
+ new_cc = cc_class(value=to_save[cc_string].strip())
+ db.session.add(new_cc)
+ db.session.flush()
+ new_cc = db.session.query(cc_class).filter(
+ cc_class.value == to_save[cc_string].strip()).first()
+ # add cc value to book
+ getattr(book, cc_string).append(new_cc)
+ else:
+ if cc_db_value is not None:
+ # remove old cc_val
+ del_cc = getattr(book, cc_string)[0]
+ getattr(book, cc_string).remove(del_cc)
+ if len(del_cc.books) == 0:
+ db.session.delete(del_cc)
+ else:
+ input_tags = to_save[cc_string].split(',')
+ input_tags = list(map(lambda it: it.strip(), input_tags))
+ modify_database_object(input_tags, getattr(book, cc_string), db.cc_classes[c.id], db.session,
+ 'custom')
+ return cc
+
+def upload_single_file(request, book, book_id):
+ # Check and handle Uploaded file
+ if 'btn-upload-format' in request.files:
+ requested_file = request.files['btn-upload-format']
+ # check for empty request
+ if requested_file.filename != '':
+ if '.' in requested_file.filename:
+ file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
+ if file_ext not in EXTENSIONS_UPLOAD:
+ flash(_("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext),
+ category="error")
+ return redirect(url_for('show_book', book_id=book.id))
+ else:
+ flash(_('File to be uploaded must have an extension'), category="error")
+ return redirect(url_for('show_book', book_id=book.id))
+
+ file_name = book.path.rsplit('/', 1)[-1]
+ filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path))
+ saved_filename = os.path.join(filepath, file_name + '.' + file_ext)
+
+ # check if file path exists, otherwise create it, copy file to calibre path and delete temp file
+ if not os.path.exists(filepath):
+ try:
+ os.makedirs(filepath)
+ except OSError:
+ flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
+ return redirect(url_for('show_book', book_id=book.id))
+ try:
+ requested_file.save(saved_filename)
+ except OSError:
+ flash(_(u"Failed to store file %(file)s.", file=saved_filename), category="error")
+ return redirect(url_for('show_book', book_id=book.id))
+
+ file_size = os.path.getsize(saved_filename)
+ is_format = db.session.query(db.Data).filter(db.Data.book == book_id).\
+ filter(db.Data.format == file_ext.upper()).first()
+
+ # Format entry already exists, no need to update the database
+ if is_format:
+ app.logger.info('Book format already existing')
+ else:
+ db_format = db.Data(book_id, file_ext.upper(), file_size, file_name)
+ db.session.add(db_format)
+ db.session.commit()
+ db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
+
+ # Queue uploader info
+ uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title)
+ helper.global_WorkerThread.add_upload(current_user.nickname,
+ "" + uploadText + "")
+
+def upload_cover(request, book):
+ if 'btn-upload-cover' in request.files:
+ requested_file = request.files['btn-upload-cover']
+ # check for empty request
+ if requested_file.filename != '':
+ file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
+ filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path))
+ saved_filename = os.path.join(filepath, 'cover.' + file_ext)
+
+ # check if file path exists, otherwise create it, copy file to calibre path and delete temp file
+ if not os.path.exists(filepath):
+ try:
+ os.makedirs(filepath)
+ except OSError:
+ flash(_(u"Failed to create path for cover %(path)s (Permission denied).", cover=filepath),
+ category="error")
+ return redirect(url_for('show_book', book_id=book.id))
+ try:
+ requested_file.save(saved_filename)
+ # im=Image.open(saved_filename)
+ book.has_cover = 1
+ except OSError:
+ flash(_(u"Failed to store cover-file %(cover)s.", cover=saved_filename), category="error")
+ return redirect(url_for('show_book', book_id=book.id))
+ except IOError:
+ flash(_(u"Cover-file is not a valid image file" % saved_filename), category="error")
+ return redirect(url_for('show_book', book_id=book.id))
+
+@app.route("/admin/book/", methods=['GET', 'POST'])
+@login_required_if_no_ano
+@edit_required
+def edit_book(book_id):
+ # Show form
+ if request.method != 'POST':
+ return render_edit_book(book_id)
+
+ # create the function for sorting...
+ db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
+ book = db.session.query(db.Books)\
+ .filter(db.Books.id == book_id).filter(common_filters()).first()
+
+ # Book not found
+ if not book:
+ flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
+ return redirect(url_for("index"))
+
+ upload_single_file(request, book, book_id)
+ upload_cover(request, book)
+ try:
+ to_save = request.form.to_dict()
+ # Update book
+ edited_books_id = None
+ #handle book title
+ if book.title != to_save["book_title"].rstrip().strip():
+ if to_save["book_title"] == '':
+ to_save["book_title"] = _(u'unknown')
+ book.title = to_save["book_title"].rstrip().strip()
+ edited_books_id = book.id
+
+ # handle author(s)
+ input_authors = to_save["author_name"].split('&')
+ input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
+ # we have all author names now
+ if input_authors == ['']:
+ input_authors = [_(u'unknown')] # prevent empty Author
+ if book.authors:
+ author0_before_edit = book.authors[0].name
+ else:
+ author0_before_edit = db.Authors(_(u'unknown'), '', 0)
+ modify_database_object(input_authors, book.authors, db.Authors, db.session, 'author')
+ if book.authors:
+ if author0_before_edit != book.authors[0].name:
+ edited_books_id = book.id
+ book.author_sort = helper.get_sorted_author(input_authors[0])
+
+ if config.config_use_google_drive:
+ gdriveutils.updateGdriveCalibreFromLocal()
+
+ error = False
+ if edited_books_id:
+ error = helper.update_dir_stucture(edited_books_id, config.config_calibre_dir)
+
+ if not error:
+ if to_save["cover_url"]:
+ if helper.save_cover(to_save["cover_url"], book.path) is True:
+ book.has_cover = 1
+ else:
+ flash(_(u"Cover is not a jpg file, can't save"), category="error")
+
+ if book.series_index != to_save["series_index"]:
+ book.series_index = to_save["series_index"]
+
+ # Handle book comments/description
+ if len(book.comments):
+ book.comments[0].text = to_save["description"]
+ else:
+ book.comments.append(db.Comments(text=to_save["description"], book=book.id))
+
+ # Handle book tags
+ input_tags = to_save["tags"].split(',')
+ input_tags = list(map(lambda it: it.strip(), input_tags))
+ modify_database_object(input_tags, book.tags, db.Tags, db.session, 'tags')
+
+ # Handle book series
+ input_series = [to_save["series"].strip()]
+ input_series = [x for x in input_series if x != '']
+ modify_database_object(input_series, book.series, db.Series, db.session, 'series')
+
+ if to_save["pubdate"]:
+ try:
+ book.pubdate = datetime.datetime.strptime(to_save["pubdate"], "%Y-%m-%d")
+ except ValueError:
+ book.pubdate = db.Books.DEFAULT_PUBDATE
+ else:
+ book.pubdate = db.Books.DEFAULT_PUBDATE
+
+ if to_save["publisher"]:
+ publisher = to_save["publisher"].rstrip().strip()
+ if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
+ modify_database_object([publisher], book.publishers, db.Publishers, db.session, 'publisher')
+ elif len(book.publishers):
+ modify_database_object([], book.publishers, db.Publishers, db.session, 'publisher')
+
+
+ # handle book languages
+ input_languages = to_save["languages"].split(',')
+ input_languages = [x.strip().lower() for x in input_languages if x != '']
+ input_l = []
+ invers_lang_table = [x.lower() for x in language_table[get_locale()].values()]
+ for lang in input_languages:
+ try:
+ res = list(language_table[get_locale()].keys())[invers_lang_table.index(lang)]
+ input_l.append(res)
+ except ValueError:
+ app.logger.error('%s is not a valid language' % lang)
+ flash(_(u"%(langname)s is not a valid language", langname=lang), category="error")
+ modify_database_object(input_l, book.languages, db.Languages, db.session, 'languages')
+
+ # handle book ratings
+ if to_save["rating"].strip():
+ old_rating = False
+ if len(book.ratings) > 0:
+ old_rating = book.ratings[0].rating
+ ratingx2 = int(float(to_save["rating"]) * 2)
+ if ratingx2 != old_rating:
+ is_rating = db.session.query(db.Ratings).filter(db.Ratings.rating == ratingx2).first()
+ if is_rating:
+ book.ratings.append(is_rating)
+ else:
+ new_rating = db.Ratings(rating=ratingx2)
+ book.ratings.append(new_rating)
+ if old_rating:
+ book.ratings.remove(book.ratings[0])
+ else:
+ if len(book.ratings) > 0:
+ book.ratings.remove(book.ratings[0])
+
+ # handle cc data
+ edit_cc_data(book_id, book, to_save)
+
+ db.session.commit()
+ if config.config_use_google_drive:
+ gdriveutils.updateGdriveCalibreFromLocal()
+ if "detail_view" in to_save:
+ return redirect(url_for('show_book', book_id=book.id))
+ else:
+ flash(_("Metadata successfully updated"), category="success")
+ return render_edit_book(book_id)
+ else:
+ db.session.rollback()
+ flash(error, category="error")
+ return render_edit_book(book_id)
+ except Exception as e:
+ app.logger.exception(e)
+ db.session.rollback()
+ flash(_("Error editing book, please check logfile for details"), category="error")
+ return redirect(url_for('show_book', book_id=book.id))
+
+
+@app.route("/upload", methods=["GET", "POST"])
+@login_required_if_no_ano
+@upload_required
+def upload():
+ if not config.config_uploading:
+ abort(404)
+ if request.method == 'POST' and 'btn-upload' in request.files:
+ for requested_file in request.files.getlist("btn-upload"):
+ # create the function for sorting...
+ db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
+ db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
+
+ # check if file extension is correct
+ if '.' in requested_file.filename:
+ file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
+ if file_ext not in EXTENSIONS_UPLOAD:
+ flash(
+ _("File extension '%(ext)s' is not allowed to be uploaded to this server",
+ ext=file_ext), category="error")
+ return redirect(url_for('index'))
+ else:
+ flash(_('File to be uploaded must have an extension'), category="error")
+ return redirect(url_for('index'))
+
+ # extract metadata from file
+ meta = uploader.upload(requested_file)
+ title = meta.title
+ authr = meta.author
+ tags = meta.tags
+ series = meta.series
+ series_index = meta.series_id
+ title_dir = helper.get_valid_filename(title)
+ author_dir = helper.get_valid_filename(authr)
+ filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir)
+ saved_filename = os.path.join(filepath, title_dir + meta.extension.lower())
+
+ # check if file path exists, otherwise create it, copy file to calibre path and delete temp file
+ if not os.path.exists(filepath):
+ try:
+ os.makedirs(filepath)
+ except OSError:
+ flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
+ return redirect(url_for('index'))
+ try:
+ copyfile(meta.file_path, saved_filename)
+ except OSError:
+ flash(_(u"Failed to store file %(file)s (Permission denied).", file=saved_filename), category="error")
+ return redirect(url_for('index'))
+ try:
+ os.unlink(meta.file_path)
+ except OSError:
+ flash(_(u"Failed to delete file %(file)s (Permission denied).", file= meta.file_path),
+ category="warning")
+
+ if meta.cover is None:
+ has_cover = 0
+ copyfile(os.path.join(config.get_main_dir, "cps/static/generic_cover.jpg"),
+ os.path.join(filepath, "cover.jpg"))
+ else:
+ has_cover = 1
+ move(meta.cover, os.path.join(filepath, "cover.jpg"))
+
+ # handle authors
+ is_author = db.session.query(db.Authors).filter(db.Authors.name == authr).first()
+ if is_author:
+ db_author = is_author
+ else:
+ db_author = db.Authors(authr, helper.get_sorted_author(authr), "")
+ db.session.add(db_author)
+
+ # handle series
+ db_series = None
+ is_series = db.session.query(db.Series).filter(db.Series.name == series).first()
+ if is_series:
+ db_series = is_series
+ elif series != '':
+ db_series = db.Series(series, "")
+ db.session.add(db_series)
+
+ # add language actually one value in list
+ input_language = meta.languages
+ db_language = None
+ if input_language != "":
+ input_language = isoLanguages.get(name=input_language).part3
+ hasLanguage = db.session.query(db.Languages).filter(db.Languages.lang_code == input_language).first()
+ if hasLanguage:
+ db_language = hasLanguage
+ else:
+ db_language = db.Languages(input_language)
+ db.session.add(db_language)
+
+ # combine path and normalize path from windows systems
+ path = os.path.join(author_dir, title_dir).replace('\\', '/')
+ db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1),
+ series_index, datetime.datetime.now(), path, has_cover, db_author, [], db_language)
+ db_book.authors.append(db_author)
+ if db_series:
+ db_book.series.append(db_series)
+ if db_language is not None:
+ db_book.languages.append(db_language)
+ file_size = os.path.getsize(saved_filename)
+ db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir)
+
+ # handle tags
+ input_tags = tags.split(',')
+ input_tags = list(map(lambda it: it.strip(), input_tags))
+ if input_tags[0] !="":
+ modify_database_object(input_tags, db_book.tags, db.Tags, db.session, 'tags')
+
+ # flush content, get db_book.id available
+ db_book.data.append(db_data)
+ db.session.add(db_book)
+ db.session.flush()
+
+ # add comment
+ book_id = db_book.id
+ upload_comment = Markup(meta.description).unescape()
+ if upload_comment != "":
+ db.session.add(db.Comments(upload_comment, book_id))
+
+ # save data to database, reread data
+ db.session.commit()
+ db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
+ book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
+
+ # upload book to gdrive if nesseccary and add "(bookid)" to folder name
+ if config.config_use_google_drive:
+ gdriveutils.updateGdriveCalibreFromLocal()
+ error = helper.update_dir_stucture(book.id, config.config_calibre_dir)
+ db.session.commit()
+ if config.config_use_google_drive:
+ gdriveutils.updateGdriveCalibreFromLocal()
+ if error:
+ flash(error, category="error")
+ uploadText=(u"File %s" % book.title)
+ helper.global_WorkerThread.add_upload(current_user.nickname,
+ "" + uploadText + "")
+
+ # create data for displaying display Full language name instead of iso639.part3language
+ if db_language is not None:
+ book.languages[0].language_name = _(meta.languages)
+ author_names = []
+ for author in db_book.authors:
+ author_names.append(author.name)
+ if len(request.files.getlist("btn-upload")) < 2:
+ cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.
+ datatype.notin_(db.cc_exceptions)).all()
+ if current_user.role_edit() or current_user.role_admin():
+ return render_title_template('book_edit.html', book=book, authors=author_names,
+ cc=cc, title=_(u"edit metadata"), page="upload")
+ book_in_shelfs = []
+ kindle_list = helper.check_send_to_kindle(book)
+ reader_list = helper.check_read_formats(book)
+
+ return render_title_template('detail.html', entry=book, cc=cc,
+ title=book.title, books_shelfs=book_in_shelfs, kindle_list=kindle_list,
+ reader_list=reader_list, page="upload")
+ return redirect(url_for("index"))
+
+
+@app.route("/admin/book/convert/", methods=['POST'])
+@login_required_if_no_ano
+@edit_required
+def convert_bookformat(book_id):
+ # check to see if we have form fields to work with - if not send user back
+ book_format_from = request.form.get('book_format_from', None)
+ book_format_to = request.form.get('book_format_to', None)
+
+ if (book_format_from is None) or (book_format_to is None):
+ flash(_(u"Source or destination format for conversion missing"), category="error")
+ return redirect(request.environ["HTTP_REFERER"])
+
+ app.logger.debug('converting: book id: ' + str(book_id) +
+ ' from: ' + request.form['book_format_from'] +
+ ' to: ' + request.form['book_format_to'])
+ rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(),
+ book_format_to.upper(), current_user.nickname)
+
+ if rtn is None:
+ flash(_(u"Book successfully queued for converting to %(book_format)s",
+ book_format=book_format_to),
+ category="success")
+ else:
+ flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
+ return redirect(request.environ["HTTP_REFERER"])
diff --git a/src/cps/worker.py b/src/cps/worker.py
new file mode 100644
index 0000000..1439807
--- /dev/null
+++ b/src/cps/worker.py
@@ -0,0 +1,508 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import print_function
+import smtplib
+import threading
+from datetime import datetime
+import logging
+import time
+import socket
+import sys
+import os
+from email.generator import Generator
+import web
+from flask_babel import gettext as _
+import re
+import gdriveutils as gd
+import subprocess
+
+try:
+ from StringIO import StringIO
+ from email.MIMEBase import MIMEBase
+ from email.MIMEMultipart import MIMEMultipart
+ from email.MIMEText import MIMEText
+except ImportError:
+ from io import StringIO
+ from email.mime.base import MIMEBase
+ from email.mime.multipart import MIMEMultipart
+ from email.mime.text import MIMEText
+
+from email import encoders
+from email.utils import formatdate
+from email.utils import make_msgid
+
+chunksize = 8192
+# task 'status' consts
+STAT_WAITING = 0
+STAT_FAIL = 1
+STAT_STARTED = 2
+STAT_FINISH_SUCCESS = 3
+#taskType consts
+TASK_EMAIL = 1
+TASK_CONVERT = 2
+TASK_UPLOAD = 3
+TASK_CONVERT_ANY = 4
+
+RET_FAIL = 0
+RET_SUCCESS = 1
+
+
+# For gdrive download book from gdrive to calibredir (temp dir for books), read contents in both cases and append
+# it in MIME Base64 encoded to
+def get_attachment(bookpath, filename):
+ """Get file as MIMEBase message"""
+ calibrepath = web.config.config_calibre_dir
+ if web.ub.config.config_use_google_drive:
+ df = gd.getFileFromEbooksFolder(bookpath, filename)
+ if df:
+ datafile = os.path.join(calibrepath, bookpath, filename)
+ if not os.path.exists(os.path.join(calibrepath, bookpath)):
+ os.makedirs(os.path.join(calibrepath, bookpath))
+ df.GetContentFile(datafile)
+ else:
+ return None
+ file_ = open(datafile, 'rb')
+ data = file_.read()
+ file_.close()
+ os.remove(datafile)
+ else:
+ try:
+ file_ = open(os.path.join(calibrepath, bookpath, filename), 'rb')
+ data = file_.read()
+ file_.close()
+ except IOError as e:
+ web.app.logger.exception(e) # traceback.print_exc()
+ web.app.logger.error(u'The requested file could not be read. Maybe wrong permissions?')
+ return None
+
+ attachment = MIMEBase('application', 'octet-stream')
+ attachment.set_payload(data)
+ encoders.encode_base64(attachment)
+ attachment.add_header('Content-Disposition', 'attachment',
+ filename=filename)
+ return attachment
+
+
+# Class for sending email with ability to get current progress
+class emailbase():
+
+ transferSize = 0
+ progress = 0
+
+ def data(self, msg):
+ self.transferSize = len(msg)
+ (code, resp) = smtplib.SMTP.data(self, msg)
+ self.progress = 0
+ return (code, resp)
+
+ def send(self, strg):
+ """Send `strg' to the server."""
+ if self.debuglevel > 0:
+ print('send:', repr(strg), file=sys.stderr)
+ if hasattr(self, 'sock') and self.sock:
+ try:
+ if self.transferSize:
+ lock=threading.Lock()
+ lock.acquire()
+ self.transferSize = len(strg)
+ lock.release()
+ for i in range(0, self.transferSize, chunksize):
+ if type(strg) == bytes:
+ self.sock.send((strg[i:i+chunksize]))
+ else:
+ self.sock.send((strg[i:i + chunksize]).encode('utf-8'))
+ lock.acquire()
+ self.progress = i
+ lock.release()
+ else:
+ self.sock.sendall(strg.encode('utf-8'))
+ except socket.error:
+ self.close()
+ raise smtplib.SMTPServerDisconnected('Server not connected')
+ else:
+ raise smtplib.SMTPServerDisconnected('please run connect() first')
+
+ def getTransferStatus(self):
+ if self.transferSize:
+ lock2 = threading.Lock()
+ lock2.acquire()
+ value = int((float(self.progress) / float(self.transferSize))*100)
+ lock2.release()
+ return str(value) + ' %'
+ else:
+ return "100 %"
+
+
+# Class for sending email with ability to get current progress, derived from emailbase class
+class email(emailbase, smtplib.SMTP):
+
+ def __init__(self, *args, **kwargs):
+ smtplib.SMTP.__init__(self, *args, **kwargs)
+
+
+# Class for sending ssl encrypted email with ability to get current progress, , derived from emailbase class
+class email_SSL(emailbase, smtplib.SMTP_SSL):
+
+ def __init__(self, *args, **kwargs):
+ smtplib.SMTP_SSL.__init__(self, *args, **kwargs)
+
+
+#Class for all worker tasks in the background
+class WorkerThread(threading.Thread):
+
+ def __init__(self):
+ self._stopevent = threading.Event()
+ threading.Thread.__init__(self)
+ self.status = 0
+ self.current = 0
+ self.last = 0
+ self.queue = list()
+ self.UIqueue = list()
+ self.asyncSMTP=None
+ self.id = 0
+
+ # Main thread loop starting the different tasks
+ def run(self):
+ while not self._stopevent.isSet():
+ doLock = threading.Lock()
+ doLock.acquire()
+ if self.current != self.last:
+ doLock.release()
+ if self.queue[self.current]['taskType'] == TASK_EMAIL:
+ self._send_raw_email()
+ if self.queue[self.current]['taskType'] == TASK_CONVERT:
+ self._convert_any_format()
+ if self.queue[self.current]['taskType'] == TASK_CONVERT_ANY:
+ self._convert_any_format()
+ # TASK_UPLOAD is handled implicitly
+ self.current += 1
+ else:
+ doLock.release()
+ time.sleep(1)
+
+ def stop(self):
+ self._stopevent.set()
+
+ def get_send_status(self):
+ if self.asyncSMTP:
+ return self.asyncSMTP.getTransferStatus()
+ else:
+ return "0 %"
+
+ def _delete_completed_tasks(self):
+ for index, task in reversed(list(enumerate(self.UIqueue))):
+ if task['progress'] == "100 %":
+ # delete tasks
+ self.queue.pop(index)
+ self.UIqueue.pop(index)
+ # if we are deleting entries before the current index, adjust the index
+ self.current -= 1
+ self.last = len(self.queue)
+
+ def get_taskstatus(self):
+ if self.current < len(self.queue):
+ if self.UIqueue[self.current]['stat'] == STAT_STARTED:
+ if self.queue[self.current]['taskType'] == TASK_EMAIL:
+ self.UIqueue[self.current]['progress'] = self.get_send_status()
+ self.UIqueue[self.current]['runtime'] = self._formatRuntime(
+ datetime.now() - self.queue[self.current]['starttime'])
+ return self.UIqueue
+
+ def _convert_any_format(self):
+ # convert book, and upload in case of google drive
+ self.UIqueue[self.current]['stat'] = STAT_STARTED
+ self.queue[self.current]['starttime'] = datetime.now()
+ self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
+ curr_task = self.queue[self.current]['taskType']
+ filename = self._convert_ebook_format()
+ if filename:
+ if web.ub.config.config_use_google_drive:
+ gd.updateGdriveCalibreFromLocal()
+ if curr_task == TASK_CONVERT:
+ self.add_email(self.queue[self.current]['settings']['subject'], self.queue[self.current]['path'],
+ filename, self.queue[self.current]['settings'], self.queue[self.current]['kindle'],
+ self.UIqueue[self.current]['user'], self.queue[self.current]['title'],
+ self.queue[self.current]['settings']['body'])
+
+ def _convert_ebook_format(self):
+ error_message = None
+ file_path = self.queue[self.current]['file_path']
+ bookid = self.queue[self.current]['bookid']
+ format_old_ext = u'.' + self.queue[self.current]['settings']['old_book_format'].lower()
+ format_new_ext = u'.' + self.queue[self.current]['settings']['new_book_format'].lower()
+
+ # check to see if destination format already exists -
+ # if it does - mark the conversion task as complete and return a success
+ # this will allow send to kindle workflow to continue to work
+ if os.path.isfile(file_path + format_new_ext):
+ web.app.logger.info("Book id %d already converted to %s", bookid, format_new_ext)
+ cur_book = web.db.session.query(web.db.Books).filter(web.db.Books.id == bookid).first()
+ self.queue[self.current]['path'] = file_path
+ self.queue[self.current]['title'] = cur_book.title
+ self._handleSuccess()
+ return file_path + format_new_ext
+ else:
+ web.app.logger.info("Book id %d - target format of %s does not exist. Moving forward with convert.", bookid, format_new_ext)
+
+ # check if converter-executable is existing
+ if not os.path.exists(web.ub.config.config_converterpath):
+ # ToDo Text is not translated
+ self._handleError(u"Convertertool %s not found" % web.ub.config.config_converterpath)
+ return
+
+ try:
+ # check which converter to use kindlegen is "1"
+ if format_old_ext == '.epub' and format_new_ext == '.mobi':
+ if web.ub.config.config_ebookconverter == 1:
+ if os.name == 'nt':
+ command = web.ub.config.config_converterpath + u' "' + file_path + u'.epub"'
+ if sys.version_info < (3, 0):
+ command = command.encode(sys.getfilesystemencoding())
+ else:
+ command = [web.ub.config.config_converterpath, file_path + u'.epub']
+ if sys.version_info < (3, 0):
+ command = [x.encode(sys.getfilesystemencoding()) for x in command]
+ if web.ub.config.config_ebookconverter == 2:
+ # Linux py2.7 encode as list without quotes no empty element for parameters
+ # linux py3.x no encode and as list without quotes no empty element for parameters
+ # windows py2.7 encode as string with quotes empty element for parameters is okay
+ # windows py 3.x no encode and as string with quotes empty element for parameters is okay
+ # separate handling for windows and linux
+ if os.name == 'nt':
+ command = web.ub.config.config_converterpath + u' "' + file_path + format_old_ext + u'" "' + \
+ file_path + format_new_ext + u'" ' + web.ub.config.config_calibre
+ if sys.version_info < (3, 0):
+ command = command.encode(sys.getfilesystemencoding())
+ else:
+ command = [web.ub.config.config_converterpath, (file_path + format_old_ext),
+ (file_path + format_new_ext)]
+ if web.ub.config.config_calibre:
+ command.append(web.ub.config.config_calibre)
+ if sys.version_info < (3, 0):
+ command = [x.encode(sys.getfilesystemencoding()) for x in command]
+
+ p = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
+ except OSError as e:
+ self._handleError(_(u"Ebook-converter failed: %(error)s", error=e))
+ return
+
+ if web.ub.config.config_ebookconverter == 1:
+ nextline = p.communicate()[0]
+ # Format of error message (kindlegen translates its output texts):
+ # Error(prcgen):E23006: Language not recognized in metadata.The dc:Language field is mandatory.Aborting.
+ conv_error = re.search(".*\(.*\):(E\d+):\s(.*)", nextline, re.MULTILINE)
+ # If error occoures, store error message for logfile
+ if conv_error:
+ error_message = _(u"Kindlegen failed with Error %(error)s. Message: %(message)s",
+ error=conv_error.group(1), message=conv_error.group(2).strip())
+ web.app.logger.debug("convert_kindlegen: " + nextline)
+ else:
+ while p.poll() is None:
+ nextline = p.stdout.readline()
+ if os.name == 'nt' and sys.version_info < (3, 0):
+ nextline = nextline.decode('windows-1252')
+ web.app.logger.debug(nextline.strip('\r\n'))
+ # parse progress string from calibre-converter
+ progress = re.search("(\d+)%\s.*", nextline)
+ if progress:
+ self.UIqueue[self.current]['progress'] = progress.group(1) + ' %'
+
+ # process returncode
+ check = p.returncode
+
+ # kindlegen returncodes
+ # 0 = Info(prcgen):I1036: Mobi file built successfully
+ # 1 = Info(prcgen):I1037: Mobi file built with WARNINGS!
+ # 2 = Info(prcgen):I1038: MOBI file could not be generated because of errors!
+ if (check < 2 and web.ub.config.config_ebookconverter == 1) or \
+ (check == 0 and web.ub.config.config_ebookconverter == 2):
+ cur_book = web.db.session.query(web.db.Books).filter(web.db.Books.id == bookid).first()
+ if os.path.isfile(file_path + format_new_ext):
+ new_format = web.db.Data(name=cur_book.data[0].name,
+ book_format=self.queue[self.current]['settings']['new_book_format'].upper(),
+ book=bookid, uncompressed_size=os.path.getsize(file_path + format_new_ext))
+ cur_book.data.append(new_format)
+ web.db.session.commit()
+ self.queue[self.current]['path'] = cur_book.path
+ self.queue[self.current]['title'] = cur_book.title
+ if web.ub.config.config_use_google_drive:
+ os.remove(file_path + format_old_ext)
+ self._handleSuccess()
+ return file_path + format_new_ext
+ else:
+ error_message = format_new_ext.upper() + ' format not found on disk'
+ web.app.logger.info("ebook converter failed with error while converting book")
+ if not error_message:
+ error_message = 'Ebook converter failed with unknown error'
+ self._handleError(error_message)
+ return
+
+
+ def add_convert(self, file_path, bookid, user_name, taskMessage, settings, kindle_mail=None):
+ addLock = threading.Lock()
+ addLock.acquire()
+ if self.last >= 20:
+ self._delete_completed_tasks()
+ # progress, runtime, and status = 0
+ self.id += 1
+ task = TASK_CONVERT_ANY
+ if kindle_mail:
+ task = TASK_CONVERT
+ self.queue.append({'file_path':file_path, 'bookid':bookid, 'starttime': 0, 'kindle': kindle_mail,
+ 'taskType': task, 'settings':settings})
+ self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
+ 'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': task } )
+
+ self.last=len(self.queue)
+ addLock.release()
+
+ def add_email(self, subject, filepath, attachment, settings, recipient, user_name, taskMessage,
+ text):
+ # if more than 20 entries in the list, clean the list
+ addLock = threading.Lock()
+ addLock.acquire()
+ if self.last >= 20:
+ self._delete_completed_tasks()
+ # progress, runtime, and status = 0
+ self.id += 1
+ self.queue.append({'subject':subject, 'attachment':attachment, 'filepath':filepath,
+ 'settings':settings, 'recipent':recipient, 'starttime': 0,
+ 'taskType': TASK_EMAIL, 'text':text})
+ self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
+ 'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': TASK_EMAIL })
+ self.last=len(self.queue)
+ addLock.release()
+
+ def add_upload(self, user_name, taskMessage):
+ # if more than 20 entries in the list, clean the list
+ addLock = threading.Lock()
+ addLock.acquire()
+ if self.last >= 20:
+ self._delete_completed_tasks()
+ # progress=100%, runtime=0, and status finished
+ self.id += 1
+ self.queue.append({'starttime': datetime.now(), 'taskType': TASK_UPLOAD})
+ self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': "100 %", 'taskMess': taskMessage,
+ 'runtime': '0 s', 'stat': STAT_FINISH_SUCCESS,'id': self.id, 'taskType': TASK_UPLOAD})
+ self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
+ self.last=len(self.queue)
+ addLock.release()
+
+
+ def _send_raw_email(self):
+ self.queue[self.current]['starttime'] = datetime.now()
+ self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
+ # self.queue[self.current]['status'] = STAT_STARTED
+ self.UIqueue[self.current]['stat'] = STAT_STARTED
+ obj=self.queue[self.current]
+ # create MIME message
+ msg = MIMEMultipart()
+ msg['Subject'] = self.queue[self.current]['subject']
+ msg['Message-Id'] = make_msgid('calibre-web')
+ msg['Date'] = formatdate(localtime=True)
+ text = self.queue[self.current]['text']
+ msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8'))
+ if obj['attachment']:
+ result = get_attachment(obj['filepath'], obj['attachment'])
+ if result:
+ msg.attach(result)
+ else:
+ self._handleError(u"Attachment not found")
+ return
+
+ msg['From'] = obj['settings']["mail_from"]
+ msg['To'] = obj['recipent']
+
+ use_ssl = int(obj['settings'].get('mail_use_ssl', 0))
+ try:
+ # convert MIME message to string
+ fp = StringIO()
+ gen = Generator(fp, mangle_from_=False)
+ gen.flatten(msg)
+ msg = fp.getvalue()
+
+ # send email
+ timeout = 600 # set timeout to 5mins
+
+ org_stderr = sys.stderr
+ sys.stderr = StderrLogger()
+
+ if use_ssl == 2:
+ self.asyncSMTP = email_SSL(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout)
+ else:
+ self.asyncSMTP = email(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout)
+
+ # link to logginglevel
+ if web.ub.config.config_log_level != logging.DEBUG:
+ self.asyncSMTP.set_debuglevel(0)
+ else:
+ self.asyncSMTP.set_debuglevel(1)
+ if use_ssl == 1:
+ self.asyncSMTP.starttls()
+ if obj['settings']["mail_password"]:
+ self.asyncSMTP.login(str(obj['settings']["mail_login"]), str(obj['settings']["mail_password"]))
+ self.asyncSMTP.sendmail(obj['settings']["mail_from"], obj['recipent'], msg)
+ self.asyncSMTP.quit()
+ self._handleSuccess()
+ sys.stderr = org_stderr
+
+ except (MemoryError) as e:
+ self._handleError(u'Error sending email: ' + e.message)
+ return None
+ except (smtplib.SMTPException) as e:
+ if hasattr(e, "smtp_error"):
+ text = e.smtp_error.replace("\n",'. ')
+ else:
+ text = ''
+ self._handleError(u'Error sending email: ' + text)
+ return None
+ except (socket.error) as e:
+ self._handleError(u'Error sending email: ' + e.strerror)
+ return None
+
+ def _formatRuntime(self, runtime):
+ self.UIqueue[self.current]['rt'] = runtime.total_seconds()
+ val = re.split('\:|\.', str(runtime))[0:3]
+ erg = list()
+ for v in val:
+ if int(v) > 0:
+ erg.append(v)
+ retVal = (':'.join(erg)).lstrip('0') + ' s'
+ if retVal == ' s':
+ retVal = '0 s'
+ return retVal
+
+ def _handleError(self, error_message):
+ web.app.logger.error(error_message)
+ # self.queue[self.current]['status'] = STAT_FAIL
+ self.UIqueue[self.current]['stat'] = STAT_FAIL
+ self.UIqueue[self.current]['progress'] = "100 %"
+ self.UIqueue[self.current]['runtime'] = self._formatRuntime(
+ datetime.now() - self.queue[self.current]['starttime'])
+ self.UIqueue[self.current]['message'] = error_message
+
+ def _handleSuccess(self):
+ # self.queue[self.current]['status'] = STAT_FINISH_SUCCESS
+ self.UIqueue[self.current]['stat'] = STAT_FINISH_SUCCESS
+ self.UIqueue[self.current]['progress'] = "100 %"
+ self.UIqueue[self.current]['runtime'] = self._formatRuntime(
+ datetime.now() - self.queue[self.current]['starttime'])
+
+
+# Enable logging of smtp lib debug output
+class StderrLogger(object):
+
+ buffer = ''
+
+ def __init__(self):
+ self.logger = web.app.logger
+
+ def write(self, message):
+ if message == '\n':
+ self.logger.debug(self.buffer)
+ print(self.buffer)
+ self.buffer = ''
+ else:
+ self.buffer += message
+
diff --git a/src/messages.pot b/src/messages.pot
new file mode 100644
index 0000000..e01f9d4
--- /dev/null
+++ b/src/messages.pot
@@ -0,0 +1,1925 @@
+# Translations template for PROJECT.
+# Copyright (C) 2018 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2018.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2018-12-10 19:35+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.6.0\n"
+
+#: cps/book_formats.py:129 cps/book_formats.py:130 cps/book_formats.py:134
+#: cps/book_formats.py:138 cps/converter.py:11 cps/converter.py:27
+msgid "not installed"
+msgstr ""
+
+#: cps/converter.py:22 cps/converter.py:38
+msgid "Excecution permissions missing"
+msgstr ""
+
+#: cps/converter.py:48
+msgid "not configured"
+msgstr ""
+
+#: cps/helper.py:59
+#, python-format
+msgid "%(format)s format not found for book id: %(book)d"
+msgstr ""
+
+#: cps/helper.py:71
+#, python-format
+msgid "%(format)s not found on Google Drive: %(fn)s"
+msgstr ""
+
+#: cps/helper.py:78 cps/helper.py:186 cps/templates/detail.html:49
+msgid "Send to Kindle"
+msgstr ""
+
+#: cps/helper.py:79 cps/helper.py:97 cps/helper.py:188
+msgid "This e-mail has been sent via Calibre-Web."
+msgstr ""
+
+#: cps/helper.py:90
+#, python-format
+msgid "%(format)s not found: %(fn)s"
+msgstr ""
+
+#: cps/helper.py:95
+msgid "Calibre-Web test e-mail"
+msgstr ""
+
+#: cps/helper.py:96
+msgid "Test e-mail"
+msgstr ""
+
+#: cps/helper.py:112
+msgid "Get Started with Calibre-Web"
+msgstr ""
+
+#: cps/helper.py:113
+#, python-format
+msgid "Registration e-mail for user: %(name)s"
+msgstr ""
+
+#: cps/helper.py:126 cps/helper.py:128 cps/helper.py:130 cps/helper.py:132
+#: cps/helper.py:138 cps/helper.py:140 cps/helper.py:142 cps/helper.py:144
+#, python-format
+msgid "Send %(format)s to Kindle"
+msgstr ""
+
+#: cps/helper.py:148 cps/helper.py:152
+#, python-format
+msgid "Convert %(orig)s to %(format)s and send to Kindle"
+msgstr ""
+
+#: cps/helper.py:187
+#, python-format
+msgid "E-mail: %(book)s"
+msgstr ""
+
+#: cps/helper.py:190
+msgid "The requested file could not be read. Maybe wrong permissions?"
+msgstr ""
+
+#: cps/helper.py:290
+#, python-format
+msgid "Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgstr ""
+
+#: cps/helper.py:299
+#, python-format
+msgid "Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgstr ""
+
+#: cps/helper.py:321 cps/helper.py:330
+#, python-format
+msgid "File %(file)s not found on Google Drive"
+msgstr ""
+
+#: cps/helper.py:348
+#, python-format
+msgid "Book path %(path)s not found on Google Drive"
+msgstr ""
+
+#: cps/helper.py:614
+msgid "Error excecuting UnRar"
+msgstr ""
+
+#: cps/helper.py:616
+msgid "Unrar binary file not found"
+msgstr ""
+
+#: cps/helper.py:667
+msgid "Waiting"
+msgstr ""
+
+#: cps/helper.py:669
+msgid "Failed"
+msgstr ""
+
+#: cps/helper.py:671
+msgid "Started"
+msgstr ""
+
+#: cps/helper.py:673
+msgid "Finished"
+msgstr ""
+
+#: cps/helper.py:675
+msgid "Unknown Status"
+msgstr ""
+
+#: cps/helper.py:680
+msgid "E-mail: "
+msgstr ""
+
+#: cps/helper.py:682 cps/helper.py:686
+msgid "Convert: "
+msgstr ""
+
+#: cps/helper.py:684
+msgid "Upload: "
+msgstr ""
+
+#: cps/helper.py:688
+msgid "Unknown Task: "
+msgstr ""
+
+#: cps/web.py:1126 cps/web.py:2835
+msgid "Unknown"
+msgstr ""
+
+#: cps/web.py:1135 cps/web.py:1166 cps/web.py:1251
+msgid "HTTP Error"
+msgstr ""
+
+#: cps/web.py:1137 cps/web.py:1168 cps/web.py:1252
+msgid "Connection error"
+msgstr ""
+
+#: cps/web.py:1139 cps/web.py:1170 cps/web.py:1253
+msgid "Timeout while establishing connection"
+msgstr ""
+
+#: cps/web.py:1141 cps/web.py:1172 cps/web.py:1254
+msgid "General error"
+msgstr ""
+
+#: cps/web.py:1147
+msgid "Unexpected data while reading update information"
+msgstr ""
+
+#: cps/web.py:1154
+msgid "No update available. You already have the latest version installed"
+msgstr ""
+
+#: cps/web.py:1179
+msgid "A new update is available. Click on the button below to update to the latest version."
+msgstr ""
+
+#: cps/web.py:1229
+msgid "Could not fetch update information"
+msgstr ""
+
+#: cps/web.py:1244
+msgid "Requesting update package"
+msgstr ""
+
+#: cps/web.py:1245
+msgid "Downloading update package"
+msgstr ""
+
+#: cps/web.py:1246
+msgid "Unzipping update package"
+msgstr ""
+
+#: cps/web.py:1247
+msgid "Replacing files"
+msgstr ""
+
+#: cps/web.py:1248
+msgid "Database connections are closed"
+msgstr ""
+
+#: cps/web.py:1249
+msgid "Stopping server"
+msgstr ""
+
+#: cps/web.py:1250
+msgid "Update finished, please press okay and reload page"
+msgstr ""
+
+#: cps/web.py:1251 cps/web.py:1252 cps/web.py:1253 cps/web.py:1254
+msgid "Update failed:"
+msgstr ""
+
+#: cps/web.py:1277
+msgid "Recently Added Books"
+msgstr ""
+
+#: cps/web.py:1287
+msgid "Newest Books"
+msgstr ""
+
+#: cps/web.py:1299
+msgid "Oldest Books"
+msgstr ""
+
+#: cps/web.py:1311
+msgid "Books (A-Z)"
+msgstr ""
+
+#: cps/web.py:1322
+msgid "Books (Z-A)"
+msgstr ""
+
+#: cps/web.py:1351
+msgid "Hot Books (most downloaded)"
+msgstr ""
+
+#: cps/web.py:1364
+msgid "Best rated books"
+msgstr ""
+
+#: cps/templates/index.xml:39 cps/web.py:1377
+msgid "Random Books"
+msgstr ""
+
+#: cps/web.py:1392
+msgid "Author list"
+msgstr ""
+
+#: cps/web.py:1404 cps/web.py:1657 cps/web.py:2204
+msgid "Error opening eBook. File does not exist or file is not accessible:"
+msgstr ""
+
+#: cps/web.py:1432
+msgid "Publisher list"
+msgstr ""
+
+#: cps/web.py:1447
+#, python-format
+msgid "Publisher: %(name)s"
+msgstr ""
+
+#: cps/templates/index.xml:83 cps/web.py:1479
+msgid "Series list"
+msgstr ""
+
+#: cps/web.py:1493
+#, python-format
+msgid "Series: %(serie)s"
+msgstr ""
+
+#: cps/web.py:1519
+msgid "Available languages"
+msgstr ""
+
+#: cps/web.py:1539
+#, python-format
+msgid "Language: %(name)s"
+msgstr ""
+
+#: cps/templates/index.xml:76 cps/web.py:1550
+msgid "Category list"
+msgstr ""
+
+#: cps/web.py:1564
+#, python-format
+msgid "Category: %(name)s"
+msgstr ""
+
+#: cps/templates/layout.html:71 cps/web.py:1693
+msgid "Tasks"
+msgstr ""
+
+#: cps/web.py:1727
+msgid "Statistics"
+msgstr ""
+
+#: cps/web.py:1835
+msgid "Callback domain is not verified, please follow steps to verify domain in google developer console"
+msgstr ""
+
+#: cps/web.py:1911
+msgid "Server restarted, please reload page"
+msgstr ""
+
+#: cps/web.py:1914
+msgid "Performing shutdown of server, please close window"
+msgstr ""
+
+#: cps/web.py:1933
+msgid "Update done"
+msgstr ""
+
+#: cps/web.py:2003
+msgid "Published after "
+msgstr ""
+
+#: cps/web.py:2010
+msgid "Published before "
+msgstr ""
+
+#: cps/web.py:2024
+#, python-format
+msgid "Rating <= %(rating)s"
+msgstr ""
+
+#: cps/web.py:2026
+#, python-format
+msgid "Rating >= %(rating)s"
+msgstr ""
+
+#: cps/web.py:2087 cps/web.py:2096
+msgid "search"
+msgstr ""
+
+#: cps/templates/index.xml:47 cps/templates/index.xml:51
+#: cps/templates/layout.html:146 cps/web.py:2163
+msgid "Read Books"
+msgstr ""
+
+#: cps/templates/index.xml:55 cps/templates/index.xml:59
+#: cps/templates/layout.html:148 cps/web.py:2166
+msgid "Unread Books"
+msgstr ""
+
+#: cps/web.py:2214 cps/web.py:2216 cps/web.py:2218 cps/web.py:2230
+msgid "Read a Book"
+msgstr ""
+
+#: cps/web.py:2289 cps/web.py:3197
+msgid "Please fill out all fields!"
+msgstr ""
+
+#: cps/web.py:2290 cps/web.py:2311 cps/web.py:2315 cps/web.py:2320
+#: cps/web.py:2322
+msgid "register"
+msgstr ""
+
+#: cps/web.py:2310 cps/web.py:3413
+msgid "An unknown error occurred. Please try again later."
+msgstr ""
+
+#: cps/web.py:2313
+msgid "Your e-mail is not allowed to register"
+msgstr ""
+
+#: cps/web.py:2316
+msgid "Confirmation e-mail was send to your e-mail account."
+msgstr ""
+
+#: cps/web.py:2319
+msgid "This username or e-mail address is already in use."
+msgstr ""
+
+#: cps/web.py:2336 cps/web.py:2432
+#, python-format
+msgid "you are now logged in as: '%(nickname)s'"
+msgstr ""
+
+#: cps/web.py:2341
+msgid "Wrong Username or Password"
+msgstr ""
+
+#: cps/web.py:2347 cps/web.py:2368
+msgid "login"
+msgstr ""
+
+#: cps/web.py:2380 cps/web.py:2411
+msgid "Token not found"
+msgstr ""
+
+#: cps/web.py:2388 cps/web.py:2419
+msgid "Token has expired"
+msgstr ""
+
+#: cps/web.py:2396
+msgid "Success! Please return to your device"
+msgstr ""
+
+#: cps/web.py:2446
+msgid "Please configure the SMTP mail settings first..."
+msgstr ""
+
+#: cps/web.py:2451
+#, python-format
+msgid "Book successfully queued for sending to %(kindlemail)s"
+msgstr ""
+
+#: cps/web.py:2455
+#, python-format
+msgid "There was an error sending this book: %(res)s"
+msgstr ""
+
+#: cps/web.py:2457 cps/web.py:3251
+msgid "Please configure your kindle e-mail address first..."
+msgstr ""
+
+#: cps/web.py:2468 cps/web.py:2520
+msgid "Invalid shelf specified"
+msgstr ""
+
+#: cps/web.py:2475
+#, python-format
+msgid "Sorry you are not allowed to add a book to the the shelf: %(shelfname)s"
+msgstr ""
+
+#: cps/web.py:2483
+msgid "You are not allowed to edit public shelves"
+msgstr ""
+
+#: cps/web.py:2492
+#, python-format
+msgid "Book is already part of the shelf: %(shelfname)s"
+msgstr ""
+
+#: cps/web.py:2506
+#, python-format
+msgid "Book has been added to shelf: %(sname)s"
+msgstr ""
+
+#: cps/web.py:2525
+#, python-format
+msgid "You are not allowed to add a book to the the shelf: %(name)s"
+msgstr ""
+
+#: cps/web.py:2530
+msgid "User is not allowed to edit public shelves"
+msgstr ""
+
+#: cps/web.py:2548
+#, python-format
+msgid "Books are already part of the shelf: %(name)s"
+msgstr ""
+
+#: cps/web.py:2562
+#, python-format
+msgid "Books have been added to shelf: %(sname)s"
+msgstr ""
+
+#: cps/web.py:2564
+#, python-format
+msgid "Could not add books to shelf: %(sname)s"
+msgstr ""
+
+#: cps/web.py:2601
+#, python-format
+msgid "Book has been removed from shelf: %(sname)s"
+msgstr ""
+
+#: cps/web.py:2607
+#, python-format
+msgid "Sorry you are not allowed to remove a book from this shelf: %(sname)s"
+msgstr ""
+
+#: cps/web.py:2628 cps/web.py:2652
+#, python-format
+msgid "A shelf with the name '%(title)s' already exists."
+msgstr ""
+
+#: cps/web.py:2633
+#, python-format
+msgid "Shelf %(title)s created"
+msgstr ""
+
+#: cps/web.py:2635 cps/web.py:2663
+msgid "There was an error"
+msgstr ""
+
+#: cps/web.py:2636 cps/web.py:2638
+msgid "create a shelf"
+msgstr ""
+
+#: cps/web.py:2661
+#, python-format
+msgid "Shelf %(title)s changed"
+msgstr ""
+
+#: cps/web.py:2664 cps/web.py:2666
+msgid "Edit a shelf"
+msgstr ""
+
+#: cps/web.py:2687
+#, python-format
+msgid "successfully deleted shelf %(name)s"
+msgstr ""
+
+#: cps/web.py:2714
+#, python-format
+msgid "Shelf: '%(name)s'"
+msgstr ""
+
+#: cps/web.py:2717
+msgid "Error opening shelf. Shelf does not exist or is not accessible"
+msgstr ""
+
+#: cps/web.py:2748
+#, python-format
+msgid "Change order of Shelf: '%(name)s'"
+msgstr ""
+
+#: cps/web.py:2777 cps/web.py:3203
+msgid "E-mail is not from valid domain"
+msgstr ""
+
+#: cps/web.py:2779 cps/web.py:2822 cps/web.py:2825
+#, python-format
+msgid "%(name)s's profile"
+msgstr ""
+
+#: cps/web.py:2820
+msgid "Found an existing account for this e-mail address."
+msgstr ""
+
+#: cps/web.py:2823
+msgid "Profile updated"
+msgstr ""
+
+#: cps/web.py:2851
+msgid "Admin page"
+msgstr ""
+
+#: cps/web.py:2931 cps/web.py:3105
+msgid "Calibre-Web configuration updated"
+msgstr ""
+
+#: cps/templates/admin.html:100 cps/web.py:2944
+msgid "UI Configuration"
+msgstr ""
+
+#: cps/web.py:2962
+msgid "Import of optional Google Drive requirements missing"
+msgstr ""
+
+#: cps/web.py:2965
+msgid "client_secrets.json is missing or not readable"
+msgstr ""
+
+#: cps/web.py:2970 cps/web.py:2997
+msgid "client_secrets.json is not configured for web application"
+msgstr ""
+
+#: cps/templates/admin.html:99 cps/web.py:3000 cps/web.py:3026 cps/web.py:3038
+#: cps/web.py:3081 cps/web.py:3096 cps/web.py:3114 cps/web.py:3122
+#: cps/web.py:3138
+msgid "Basic Configuration"
+msgstr ""
+
+#: cps/web.py:3023
+msgid "Keyfile location is not valid, please enter correct path"
+msgstr ""
+
+#: cps/web.py:3035
+msgid "Certfile location is not valid, please enter correct path"
+msgstr ""
+
+#: cps/web.py:3078
+msgid "Logfile location is not valid, please enter correct path"
+msgstr ""
+
+#: cps/web.py:3118
+msgid "DB location is not valid, please enter correct path"
+msgstr ""
+
+#: cps/templates/admin.html:33 cps/web.py:3199 cps/web.py:3205 cps/web.py:3221
+msgid "Add new user"
+msgstr ""
+
+#: cps/web.py:3211
+#, python-format
+msgid "User '%(user)s' created"
+msgstr ""
+
+#: cps/web.py:3215
+msgid "Found an existing account for this e-mail address or nickname."
+msgstr ""
+
+#: cps/web.py:3239 cps/web.py:3253
+msgid "E-mail server settings updated"
+msgstr ""
+
+#: cps/web.py:3246
+#, python-format
+msgid "Test e-mail successfully send to %(kindlemail)s"
+msgstr ""
+
+#: cps/web.py:3249
+#, python-format
+msgid "There was an error sending the Test e-mail: %(res)s"
+msgstr ""
+
+#: cps/web.py:3254
+msgid "Edit e-mail server settings"
+msgstr ""
+
+#: cps/web.py:3279
+#, python-format
+msgid "User '%(nick)s' deleted"
+msgstr ""
+
+#: cps/web.py:3388
+#, python-format
+msgid "User '%(nick)s' updated"
+msgstr ""
+
+#: cps/web.py:3391
+msgid "An unknown error occured."
+msgstr ""
+
+#: cps/web.py:3393
+#, python-format
+msgid "Edit User %(nick)s"
+msgstr ""
+
+#: cps/web.py:3410
+#, python-format
+msgid "Password for user %(user)s reset"
+msgstr ""
+
+#: cps/web.py:3424 cps/web.py:3627
+msgid "Error opening eBook. File does not exist or file is not accessible"
+msgstr ""
+
+#: cps/web.py:3449 cps/web.py:3912
+msgid "edit metadata"
+msgstr ""
+
+#: cps/web.py:3542 cps/web.py:3780
+#, python-format
+msgid "File extension '%(ext)s' is not allowed to be uploaded to this server"
+msgstr ""
+
+#: cps/web.py:3546 cps/web.py:3784
+msgid "File to be uploaded must have an extension"
+msgstr ""
+
+#: cps/web.py:3558 cps/web.py:3804
+#, python-format
+msgid "Failed to create path %(path)s (Permission denied)."
+msgstr ""
+
+#: cps/web.py:3563
+#, python-format
+msgid "Failed to store file %(file)s."
+msgstr ""
+
+#: cps/web.py:3580
+#, python-format
+msgid "File format %(ext)s added to %(book)s"
+msgstr ""
+
+#: cps/web.py:3598
+#, python-format
+msgid "Failed to create path for cover %(path)s (Permission denied)."
+msgstr ""
+
+#: cps/web.py:3606
+#, python-format
+msgid "Failed to store cover-file %(cover)s."
+msgstr ""
+
+#: cps/web.py:3609
+msgid "Cover-file is not a valid image file"
+msgstr ""
+
+#: cps/web.py:3639 cps/web.py:3648 cps/web.py:3652
+msgid "unknown"
+msgstr ""
+
+#: cps/web.py:3671
+msgid "Cover is not a jpg file, can't save"
+msgstr ""
+
+#: cps/web.py:3719
+#, python-format
+msgid "%(langname)s is not a valid language"
+msgstr ""
+
+#: cps/web.py:3750
+msgid "Metadata successfully updated"
+msgstr ""
+
+#: cps/web.py:3759
+msgid "Error editing book, please check logfile for details"
+msgstr ""
+
+#: cps/web.py:3809
+#, python-format
+msgid "Failed to store file %(file)s (Permission denied)."
+msgstr ""
+
+#: cps/web.py:3814
+#, python-format
+msgid "Failed to delete file %(file)s (Permission denied)."
+msgstr ""
+
+#: cps/web.py:3932
+msgid "Source or destination format for conversion missing"
+msgstr ""
+
+#: cps/web.py:3942
+#, python-format
+msgid "Book successfully queued for converting to %(book_format)s"
+msgstr ""
+
+#: cps/web.py:3946
+#, python-format
+msgid "There was an error converting this book: %(res)s"
+msgstr ""
+
+#: cps/worker.py:287
+#, python-format
+msgid "Ebook-converter failed: %(error)s"
+msgstr ""
+
+#: cps/worker.py:298
+#, python-format
+msgid "Kindlegen failed with Error %(error)s. Message: %(message)s"
+msgstr ""
+
+#: cps/templates/admin.html:6
+msgid "User list"
+msgstr ""
+
+#: cps/templates/admin.html:9
+msgid "Nickname"
+msgstr ""
+
+#: cps/templates/admin.html:10
+msgid "E-mail"
+msgstr ""
+
+#: cps/templates/admin.html:11
+msgid "Kindle"
+msgstr ""
+
+#: cps/templates/admin.html:12
+msgid "DLS"
+msgstr ""
+
+#: cps/templates/admin.html:13 cps/templates/layout.html:74
+msgid "Admin"
+msgstr ""
+
+#: cps/templates/admin.html:14 cps/templates/detail.html:22
+#: cps/templates/detail.html:31
+msgid "Download"
+msgstr ""
+
+#: cps/templates/admin.html:15 cps/templates/layout.html:64
+msgid "Upload"
+msgstr ""
+
+#: cps/templates/admin.html:16
+msgid "Edit"
+msgstr ""
+
+#: cps/templates/admin.html:39
+msgid "SMTP e-mail server settings"
+msgstr ""
+
+#: cps/templates/admin.html:42 cps/templates/email_edit.html:11
+msgid "SMTP hostname"
+msgstr ""
+
+#: cps/templates/admin.html:43
+msgid "SMTP port"
+msgstr ""
+
+#: cps/templates/admin.html:44
+msgid "SSL"
+msgstr ""
+
+#: cps/templates/admin.html:45 cps/templates/email_edit.html:27
+msgid "SMTP login"
+msgstr ""
+
+#: cps/templates/admin.html:46
+msgid "From mail"
+msgstr ""
+
+#: cps/templates/admin.html:56
+msgid "Change SMTP settings"
+msgstr ""
+
+#: cps/templates/admin.html:62
+msgid "Configuration"
+msgstr ""
+
+#: cps/templates/admin.html:65
+msgid "Calibre DB dir"
+msgstr ""
+
+#: cps/templates/admin.html:69
+msgid "Log level"
+msgstr ""
+
+#: cps/templates/admin.html:73
+msgid "Port"
+msgstr ""
+
+#: cps/templates/admin.html:79 cps/templates/config_view_edit.html:23
+msgid "Books per page"
+msgstr ""
+
+#: cps/templates/admin.html:83
+msgid "Uploading"
+msgstr ""
+
+#: cps/templates/admin.html:87
+msgid "Anonymous browsing"
+msgstr ""
+
+#: cps/templates/admin.html:91
+msgid "Public registration"
+msgstr ""
+
+#: cps/templates/admin.html:95 cps/templates/remote_login.html:4
+msgid "Remote login"
+msgstr ""
+
+#: cps/templates/admin.html:106
+msgid "Administration"
+msgstr ""
+
+#: cps/templates/admin.html:107
+msgid "Reconnect to Calibre DB"
+msgstr ""
+
+#: cps/templates/admin.html:108
+msgid "Restart Calibre-Web"
+msgstr ""
+
+#: cps/templates/admin.html:109
+msgid "Stop Calibre-Web"
+msgstr ""
+
+#: cps/templates/admin.html:115
+msgid "Update"
+msgstr ""
+
+#: cps/templates/admin.html:119
+msgid "Version"
+msgstr ""
+
+#: cps/templates/admin.html:120
+msgid "Details"
+msgstr ""
+
+#: cps/templates/admin.html:126
+msgid "Current version"
+msgstr ""
+
+#: cps/templates/admin.html:132
+msgid "Check for update"
+msgstr ""
+
+#: cps/templates/admin.html:133
+msgid "Perform Update"
+msgstr ""
+
+#: cps/templates/admin.html:145
+msgid "Do you really want to restart Calibre-Web?"
+msgstr ""
+
+#: cps/templates/admin.html:150 cps/templates/admin.html:164
+#: cps/templates/admin.html:184 cps/templates/shelf.html:63
+msgid "Ok"
+msgstr ""
+
+#: cps/templates/admin.html:151 cps/templates/admin.html:165
+#: cps/templates/book_edit.html:178 cps/templates/book_edit.html:200
+#: cps/templates/config_edit.html:212 cps/templates/config_view_edit.html:168
+#: cps/templates/email_edit.html:40 cps/templates/email_edit.html:75
+#: cps/templates/shelf.html:64 cps/templates/shelf_edit.html:19
+#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:155
+msgid "Back"
+msgstr ""
+
+#: cps/templates/admin.html:163
+msgid "Do you really want to stop Calibre-Web?"
+msgstr ""
+
+#: cps/templates/admin.html:175
+msgid "Updating, please do not reload page"
+msgstr ""
+
+#: cps/templates/author.html:15
+msgid "via"
+msgstr ""
+
+#: cps/templates/author.html:23
+msgid "In Library"
+msgstr ""
+
+#: cps/templates/author.html:71
+msgid "More by"
+msgstr ""
+
+#: cps/templates/book_edit.html:16
+msgid "Delete Book"
+msgstr ""
+
+#: cps/templates/book_edit.html:19
+msgid "Delete formats:"
+msgstr ""
+
+#: cps/templates/book_edit.html:22 cps/templates/book_edit.html:199
+#: cps/templates/email_edit.html:73 cps/templates/email_edit.html:74
+msgid "Delete"
+msgstr ""
+
+#: cps/templates/book_edit.html:30
+msgid "Convert book format:"
+msgstr ""
+
+#: cps/templates/book_edit.html:34
+msgid "Convert from:"
+msgstr ""
+
+#: cps/templates/book_edit.html:36 cps/templates/book_edit.html:43
+msgid "select an option"
+msgstr ""
+
+#: cps/templates/book_edit.html:41
+msgid "Convert to:"
+msgstr ""
+
+#: cps/templates/book_edit.html:50
+msgid "Convert book"
+msgstr ""
+
+#: cps/templates/book_edit.html:59 cps/templates/search_form.html:6
+msgid "Book Title"
+msgstr ""
+
+#: cps/templates/book_edit.html:63 cps/templates/book_edit.html:259
+#: cps/templates/book_edit.html:277 cps/templates/search_form.html:10
+msgid "Author"
+msgstr ""
+
+#: cps/templates/book_edit.html:67 cps/templates/book_edit.html:264
+#: cps/templates/book_edit.html:279 cps/templates/search_form.html:106
+msgid "Description"
+msgstr ""
+
+#: cps/templates/book_edit.html:71 cps/templates/search_form.html:33
+msgid "Tags"
+msgstr ""
+
+#: cps/templates/book_edit.html:75 cps/templates/layout.html:157
+#: cps/templates/search_form.html:53
+msgid "Series"
+msgstr ""
+
+#: cps/templates/book_edit.html:79
+msgid "Series id"
+msgstr ""
+
+#: cps/templates/book_edit.html:83
+msgid "Rating"
+msgstr ""
+
+#: cps/templates/book_edit.html:87
+msgid "Cover URL (jpg, cover is downloaded and stored in database, field is afterwards empty again)"
+msgstr ""
+
+#: cps/templates/book_edit.html:91
+msgid "Upload Cover from local drive"
+msgstr ""
+
+#: cps/templates/book_edit.html:96 cps/templates/detail.html:147
+msgid "Publishing date"
+msgstr ""
+
+#: cps/templates/book_edit.html:103 cps/templates/book_edit.html:261
+#: cps/templates/book_edit.html:278 cps/templates/detail.html:139
+#: cps/templates/search_form.html:14
+msgid "Publisher"
+msgstr ""
+
+#: cps/templates/book_edit.html:107 cps/templates/user_edit.html:31
+msgid "Language"
+msgstr ""
+
+#: cps/templates/book_edit.html:117 cps/templates/search_form.html:117
+msgid "Yes"
+msgstr ""
+
+#: cps/templates/book_edit.html:118 cps/templates/search_form.html:118
+msgid "No"
+msgstr ""
+
+#: cps/templates/book_edit.html:164
+msgid "Upload format"
+msgstr ""
+
+#: cps/templates/book_edit.html:173
+msgid "view book after edit"
+msgstr ""
+
+#: cps/templates/book_edit.html:176 cps/templates/book_edit.html:212
+msgid "Get metadata"
+msgstr ""
+
+#: cps/templates/book_edit.html:177 cps/templates/config_edit.html:210
+#: cps/templates/config_view_edit.html:167 cps/templates/login.html:20
+#: cps/templates/search_form.html:153 cps/templates/shelf_edit.html:17
+#: cps/templates/user_edit.html:153
+msgid "Submit"
+msgstr ""
+
+#: cps/templates/book_edit.html:191
+msgid "Are you really sure?"
+msgstr ""
+
+#: cps/templates/book_edit.html:194
+msgid "Book will be deleted from Calibre database"
+msgstr ""
+
+#: cps/templates/book_edit.html:195
+msgid "and from hard disk"
+msgstr ""
+
+#: cps/templates/book_edit.html:215
+msgid "Keyword"
+msgstr ""
+
+#: cps/templates/book_edit.html:216
+msgid " Search keyword "
+msgstr ""
+
+#: cps/templates/book_edit.html:218 cps/templates/layout.html:46
+msgid "Go!"
+msgstr ""
+
+#: cps/templates/book_edit.html:222
+msgid "Click the cover to load metadata to the form"
+msgstr ""
+
+#: cps/templates/book_edit.html:234 cps/templates/book_edit.html:274
+msgid "Loading..."
+msgstr ""
+
+#: cps/templates/book_edit.html:239 cps/templates/layout.html:224
+msgid "Close"
+msgstr ""
+
+#: cps/templates/book_edit.html:266 cps/templates/book_edit.html:280
+msgid "Source"
+msgstr ""
+
+#: cps/templates/book_edit.html:275
+msgid "Search error!"
+msgstr ""
+
+#: cps/templates/book_edit.html:276
+msgid "No Result(s) found! Please try aonther keyword."
+msgstr ""
+
+#: cps/templates/config_edit.html:12
+msgid "Library Configuration"
+msgstr ""
+
+#: cps/templates/config_edit.html:19
+msgid "Location of Calibre database"
+msgstr ""
+
+#: cps/templates/config_edit.html:24
+msgid "Use Google Drive?"
+msgstr ""
+
+#: cps/templates/config_edit.html:30
+msgid "Google Drive config problem"
+msgstr ""
+
+#: cps/templates/config_edit.html:36
+msgid "Authenticate Google Drive"
+msgstr ""
+
+#: cps/templates/config_edit.html:40
+msgid "Please finish Google Drive setup after login"
+msgstr ""
+
+#: cps/templates/config_edit.html:44
+msgid "Google Drive Calibre folder"
+msgstr ""
+
+#: cps/templates/config_edit.html:52
+msgid "Metadata Watch Channel ID"
+msgstr ""
+
+#: cps/templates/config_edit.html:55
+msgid "Revoke"
+msgstr ""
+
+#: cps/templates/config_edit.html:73
+msgid "Server Configuration"
+msgstr ""
+
+#: cps/templates/config_edit.html:80
+msgid "Server Port"
+msgstr ""
+
+#: cps/templates/config_edit.html:84
+msgid "SSL certfile location (leave it empty for non-SSL Servers)"
+msgstr ""
+
+#: cps/templates/config_edit.html:88
+msgid "SSL Keyfile location (leave it empty for non-SSL Servers)"
+msgstr ""
+
+#: cps/templates/config_edit.html:99
+msgid "Logfile Configuration"
+msgstr ""
+
+#: cps/templates/config_edit.html:106
+msgid "Log Level"
+msgstr ""
+
+#: cps/templates/config_edit.html:115
+msgid "Location and name of logfile (calibre-web.log for no entry)"
+msgstr ""
+
+#: cps/templates/config_edit.html:126
+msgid "Feature Configuration"
+msgstr ""
+
+#: cps/templates/config_edit.html:134
+msgid "Enable uploading"
+msgstr ""
+
+#: cps/templates/config_edit.html:138
+msgid "Enable anonymous browsing"
+msgstr ""
+
+#: cps/templates/config_edit.html:142
+msgid "Enable public registration"
+msgstr ""
+
+#: cps/templates/config_edit.html:146
+msgid "Enable remote login (\"magic link\")"
+msgstr ""
+
+#: cps/templates/config_edit.html:151
+msgid "Use"
+msgstr ""
+
+#: cps/templates/config_edit.html:152
+msgid "Obtain an API Key"
+msgstr ""
+
+#: cps/templates/config_edit.html:156
+msgid "Goodreads API Key"
+msgstr ""
+
+#: cps/templates/config_edit.html:160
+msgid "Goodreads API Secret"
+msgstr ""
+
+#: cps/templates/config_edit.html:173
+msgid "External binaries"
+msgstr ""
+
+#: cps/templates/config_edit.html:181
+msgid "No converter"
+msgstr ""
+
+#: cps/templates/config_edit.html:183
+msgid "Use Kindlegen"
+msgstr ""
+
+#: cps/templates/config_edit.html:185
+msgid "Use calibre's ebook converter"
+msgstr ""
+
+#: cps/templates/config_edit.html:189
+msgid "E-Book converter settings"
+msgstr ""
+
+#: cps/templates/config_edit.html:193
+msgid "Path to convertertool"
+msgstr ""
+
+#: cps/templates/config_edit.html:199
+msgid "Location of Unrar binary"
+msgstr ""
+
+#: cps/templates/config_edit.html:215 cps/templates/layout.html:82
+#: cps/templates/login.html:4
+msgid "Login"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:12
+msgid "View Configuration"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:19 cps/templates/layout.html:133
+#: cps/templates/layout.html:134 cps/templates/shelf_edit.html:7
+msgid "Title"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:27
+msgid "No. of random books to show"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:31
+msgid "Regular expression for ignoring columns"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:35
+msgid "Link read/unread status to Calibre column"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:44
+msgid "Regular expression for title sorting"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:48
+msgid "Tags for Mature Content"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:62
+msgid "Default settings for new users"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:70 cps/templates/user_edit.html:110
+msgid "Admin user"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:74 cps/templates/user_edit.html:119
+msgid "Allow Downloads"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:78 cps/templates/user_edit.html:123
+msgid "Allow Uploads"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:82 cps/templates/user_edit.html:127
+msgid "Allow Edit"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:86 cps/templates/user_edit.html:131
+msgid "Allow Delete books"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:90 cps/templates/user_edit.html:136
+msgid "Allow Changing Password"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:94 cps/templates/user_edit.html:140
+msgid "Allow Editing Public Shelfs"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:104
+msgid "Default visibilities for new users"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:112 cps/templates/user_edit.html:58
+msgid "Show random books"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:116 cps/templates/user_edit.html:62
+msgid "Show recent books"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:120 cps/templates/user_edit.html:66
+msgid "Show sorted books"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:124 cps/templates/user_edit.html:70
+msgid "Show hot books"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:128 cps/templates/user_edit.html:74
+msgid "Show best rated books"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:132 cps/templates/user_edit.html:78
+msgid "Show language selection"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:136 cps/templates/user_edit.html:82
+msgid "Show series selection"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:140 cps/templates/user_edit.html:86
+msgid "Show category selection"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:144 cps/templates/user_edit.html:90
+msgid "Show author selection"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:148 cps/templates/user_edit.html:94
+msgid "Show publisher selection"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:152 cps/templates/user_edit.html:98
+msgid "Show read and unread"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:156 cps/templates/user_edit.html:102
+msgid "Show random books in detail view"
+msgstr ""
+
+#: cps/templates/config_view_edit.html:160 cps/templates/user_edit.html:115
+msgid "Show mature content"
+msgstr ""
+
+#: cps/templates/detail.html:63
+msgid "Read in browser"
+msgstr ""
+
+#: cps/templates/detail.html:100
+msgid "Book"
+msgstr ""
+
+#: cps/templates/detail.html:100
+msgid "of"
+msgstr ""
+
+#: cps/templates/detail.html:106
+msgid "language"
+msgstr ""
+
+#: cps/templates/detail.html:184
+msgid "Read"
+msgstr ""
+
+#: cps/templates/detail.html:194
+msgid "Description:"
+msgstr ""
+
+#: cps/templates/detail.html:207 cps/templates/search.html:14
+msgid "Add to shelf"
+msgstr ""
+
+#: cps/templates/detail.html:269
+msgid "Edit metadata"
+msgstr ""
+
+#: cps/templates/email_edit.html:15
+msgid "SMTP port (usually 25 for plain SMTP and 465 for SSL and 587 for STARTTLS)"
+msgstr ""
+
+#: cps/templates/email_edit.html:19
+msgid "Encryption"
+msgstr ""
+
+#: cps/templates/email_edit.html:21
+msgid "None"
+msgstr ""
+
+#: cps/templates/email_edit.html:22
+msgid "STARTTLS"
+msgstr ""
+
+#: cps/templates/email_edit.html:23
+msgid "SSL/TLS"
+msgstr ""
+
+#: cps/templates/email_edit.html:31
+msgid "SMTP password"
+msgstr ""
+
+#: cps/templates/email_edit.html:35
+msgid "From e-mail"
+msgstr ""
+
+#: cps/templates/email_edit.html:38
+msgid "Save settings"
+msgstr ""
+
+#: cps/templates/email_edit.html:39
+msgid "Save settings and send Test E-Mail"
+msgstr ""
+
+#: cps/templates/email_edit.html:43
+msgid "Allowed domains for registering"
+msgstr ""
+
+#: cps/templates/email_edit.html:47
+msgid "Enter domainname"
+msgstr ""
+
+#: cps/templates/email_edit.html:55
+msgid "Add Domain"
+msgstr ""
+
+#: cps/templates/email_edit.html:58
+msgid "Add"
+msgstr ""
+
+#: cps/templates/email_edit.html:72
+msgid "Do you really want to delete this domain rule?"
+msgstr ""
+
+#: cps/templates/feed.xml:21 cps/templates/layout.html:208
+msgid "Next"
+msgstr ""
+
+#: cps/templates/feed.xml:33 cps/templates/index.xml:11
+#: cps/templates/layout.html:43 cps/templates/layout.html:44
+msgid "Search"
+msgstr ""
+
+#: cps/templates/http_error.html:23
+msgid "Back to home"
+msgstr ""
+
+#: cps/templates/index.html:5
+msgid "Discover (Random Books)"
+msgstr ""
+
+#: cps/templates/index.xml:6
+msgid "Start"
+msgstr ""
+
+#: cps/templates/index.xml:18 cps/templates/layout.html:139
+msgid "Hot Books"
+msgstr ""
+
+#: cps/templates/index.xml:22
+msgid "Popular publications from this catalog based on Downloads."
+msgstr ""
+
+#: cps/templates/index.xml:25 cps/templates/layout.html:142
+msgid "Best rated Books"
+msgstr ""
+
+#: cps/templates/index.xml:29
+msgid "Popular publications from this catalog based on Rating."
+msgstr ""
+
+#: cps/templates/index.xml:32
+msgid "New Books"
+msgstr ""
+
+#: cps/templates/index.xml:36
+msgid "The latest Books"
+msgstr ""
+
+#: cps/templates/index.xml:43
+msgid "Show Random Books"
+msgstr ""
+
+#: cps/templates/index.xml:62 cps/templates/layout.html:160
+msgid "Authors"
+msgstr ""
+
+#: cps/templates/index.xml:66
+msgid "Books ordered by Author"
+msgstr ""
+
+#: cps/templates/index.xml:69 cps/templates/layout.html:163
+msgid "Publishers"
+msgstr ""
+
+#: cps/templates/index.xml:73
+msgid "Books ordered by publisher"
+msgstr ""
+
+#: cps/templates/index.xml:80
+msgid "Books ordered by category"
+msgstr ""
+
+#: cps/templates/index.xml:87
+msgid "Books ordered by series"
+msgstr ""
+
+#: cps/templates/index.xml:90 cps/templates/layout.html:169
+msgid "Public Shelves"
+msgstr ""
+
+#: cps/templates/index.xml:94
+msgid "Books organized in public shelfs, visible to everyone"
+msgstr ""
+
+#: cps/templates/index.xml:98 cps/templates/layout.html:173
+msgid "Your Shelves"
+msgstr ""
+
+#: cps/templates/index.xml:102
+msgid "User's own shelfs, only visible to the current user himself"
+msgstr ""
+
+#: cps/templates/layout.html:33
+msgid "Toggle navigation"
+msgstr ""
+
+#: cps/templates/layout.html:54
+msgid "Advanced Search"
+msgstr ""
+
+#: cps/templates/layout.html:78
+msgid "Logout"
+msgstr ""
+
+#: cps/templates/layout.html:83 cps/templates/register.html:14
+msgid "Register"
+msgstr ""
+
+#: cps/templates/layout.html:108
+msgid "Uploading..."
+msgstr ""
+
+#: cps/templates/layout.html:109
+msgid "please don't refresh the page"
+msgstr ""
+
+#: cps/templates/layout.html:120
+msgid "Browse"
+msgstr ""
+
+#: cps/templates/layout.html:122
+msgid "Recently Added"
+msgstr ""
+
+#: cps/templates/layout.html:127
+msgid "Sorted Books"
+msgstr ""
+
+#: cps/templates/layout.html:131 cps/templates/layout.html:132
+#: cps/templates/layout.html:133 cps/templates/layout.html:134
+msgid "Sort By"
+msgstr ""
+
+#: cps/templates/layout.html:131
+msgid "Newest"
+msgstr ""
+
+#: cps/templates/layout.html:132
+msgid "Oldest"
+msgstr ""
+
+#: cps/templates/layout.html:133
+msgid "Ascending"
+msgstr ""
+
+#: cps/templates/layout.html:134
+msgid "Descending"
+msgstr ""
+
+#: cps/templates/layout.html:151
+msgid "Discover"
+msgstr ""
+
+#: cps/templates/layout.html:154
+msgid "Categories"
+msgstr ""
+
+#: cps/templates/layout.html:166 cps/templates/search_form.html:74
+msgid "Languages"
+msgstr ""
+
+#: cps/templates/layout.html:178
+msgid "Create a Shelf"
+msgstr ""
+
+#: cps/templates/layout.html:179 cps/templates/stats.html:3
+msgid "About"
+msgstr ""
+
+#: cps/templates/layout.html:193
+msgid "Previous"
+msgstr ""
+
+#: cps/templates/layout.html:220
+msgid "Book Details"
+msgstr ""
+
+#: cps/templates/login.html:8 cps/templates/login.html:9
+#: cps/templates/register.html:7 cps/templates/user_edit.html:8
+msgid "Username"
+msgstr ""
+
+#: cps/templates/login.html:12 cps/templates/login.html:13
+#: cps/templates/user_edit.html:21
+msgid "Password"
+msgstr ""
+
+#: cps/templates/login.html:17
+msgid "Remember me"
+msgstr ""
+
+#: cps/templates/login.html:22
+msgid "Log in with magic link"
+msgstr ""
+
+#: cps/templates/osd.xml:5
+msgid "Calibre-Web ebook catalog"
+msgstr ""
+
+#: cps/templates/read.html:69 cps/templates/readcbr.html:79
+#: cps/templates/readcbr.html:103
+msgid "Settings"
+msgstr ""
+
+#: cps/templates/read.html:72
+msgid "Reflow text when sidebars are open."
+msgstr ""
+
+#: cps/templates/readcbr.html:84
+msgid "Keyboard Shortcuts"
+msgstr ""
+
+#: cps/templates/readcbr.html:87
+msgid "Previous Page"
+msgstr ""
+
+#: cps/templates/readcbr.html:88
+msgid "Next Page"
+msgstr ""
+
+#: cps/templates/readcbr.html:89
+msgid "Scale to Best"
+msgstr ""
+
+#: cps/templates/readcbr.html:90
+msgid "Scale to Width"
+msgstr ""
+
+#: cps/templates/readcbr.html:91
+msgid "Scale to Height"
+msgstr ""
+
+#: cps/templates/readcbr.html:92
+msgid "Scale to Native"
+msgstr ""
+
+#: cps/templates/readcbr.html:93
+msgid "Rotate Right"
+msgstr ""
+
+#: cps/templates/readcbr.html:94
+msgid "Rotate Left"
+msgstr ""
+
+#: cps/templates/readcbr.html:95
+msgid "Flip Image"
+msgstr ""
+
+#: cps/templates/readcbr.html:108 cps/templates/user_edit.html:39
+msgid "Theme"
+msgstr ""
+
+#: cps/templates/readcbr.html:111
+msgid "Light"
+msgstr ""
+
+#: cps/templates/readcbr.html:112
+msgid "Dark"
+msgstr ""
+
+#: cps/templates/readcbr.html:117
+msgid "Scale"
+msgstr ""
+
+#: cps/templates/readcbr.html:120
+msgid "Best"
+msgstr ""
+
+#: cps/templates/readcbr.html:121
+msgid "Width"
+msgstr ""
+
+#: cps/templates/readcbr.html:122
+msgid "Height"
+msgstr ""
+
+#: cps/templates/readcbr.html:123
+msgid "Native"
+msgstr ""
+
+#: cps/templates/readcbr.html:128
+msgid "Rotate"
+msgstr ""
+
+#: cps/templates/readcbr.html:139
+msgid "Flip"
+msgstr ""
+
+#: cps/templates/readcbr.html:142
+msgid "Horizontal"
+msgstr ""
+
+#: cps/templates/readcbr.html:143
+msgid "Vertical"
+msgstr ""
+
+#: cps/templates/readpdf.html:29
+msgid "PDF.js viewer"
+msgstr ""
+
+#: cps/templates/readtxt.html:6
+msgid "Basic txt Reader"
+msgstr ""
+
+#: cps/templates/register.html:4
+msgid "Register a new account"
+msgstr ""
+
+#: cps/templates/register.html:8
+msgid "Choose a username"
+msgstr ""
+
+#: cps/templates/register.html:11 cps/templates/user_edit.html:13
+msgid "E-mail address"
+msgstr ""
+
+#: cps/templates/register.html:12
+msgid "Your email address"
+msgstr ""
+
+#: cps/templates/remote_login.html:6
+msgid "Using your another device, visit"
+msgstr ""
+
+#: cps/templates/remote_login.html:6
+msgid "and log in"
+msgstr ""
+
+#: cps/templates/remote_login.html:9
+msgid "Once you do so, you will automatically get logged in on this device."
+msgstr ""
+
+#: cps/templates/search.html:5
+msgid "No Results for:"
+msgstr ""
+
+#: cps/templates/search.html:6
+msgid "Please try a different search"
+msgstr ""
+
+#: cps/templates/search.html:8
+msgid "Results for:"
+msgstr ""
+
+#: cps/templates/search_form.html:19
+msgid "Publishing date from"
+msgstr ""
+
+#: cps/templates/search_form.html:26
+msgid "Publishing date to"
+msgstr ""
+
+#: cps/templates/search_form.html:43
+msgid "Exclude Tags"
+msgstr ""
+
+#: cps/templates/search_form.html:63
+msgid "Exclude Series"
+msgstr ""
+
+#: cps/templates/search_form.html:84
+msgid "Exclude Languages"
+msgstr ""
+
+#: cps/templates/search_form.html:97
+msgid "Rating bigger than"
+msgstr ""
+
+#: cps/templates/search_form.html:101
+msgid "Rating less than"
+msgstr ""
+
+#: cps/templates/shelf.html:7
+msgid "Delete this Shelf"
+msgstr ""
+
+#: cps/templates/shelf.html:8
+msgid "Edit Shelf"
+msgstr ""
+
+#: cps/templates/shelf.html:9 cps/templates/shelf_order.html:11
+msgid "Change order"
+msgstr ""
+
+#: cps/templates/shelf.html:58
+msgid "Do you really want to delete the shelf?"
+msgstr ""
+
+#: cps/templates/shelf.html:61
+msgid "Shelf will be lost for everybody and forever!"
+msgstr ""
+
+#: cps/templates/shelf_edit.html:13
+msgid "should the shelf be public?"
+msgstr ""
+
+#: cps/templates/shelf_order.html:5
+msgid "Drag 'n drop to rearrange order"
+msgstr ""
+
+#: cps/templates/stats.html:7
+msgid "Calibre library statistics"
+msgstr ""
+
+#: cps/templates/stats.html:12
+msgid "Books in this Library"
+msgstr ""
+
+#: cps/templates/stats.html:16
+msgid "Authors in this Library"
+msgstr ""
+
+#: cps/templates/stats.html:20
+msgid "Categories in this Library"
+msgstr ""
+
+#: cps/templates/stats.html:24
+msgid "Series in this Library"
+msgstr ""
+
+#: cps/templates/stats.html:28
+msgid "Linked libraries"
+msgstr ""
+
+#: cps/templates/stats.html:32
+msgid "Program library"
+msgstr ""
+
+#: cps/templates/stats.html:33
+msgid "Installed Version"
+msgstr ""
+
+#: cps/templates/tasks.html:7
+msgid "Tasks list"
+msgstr ""
+
+#: cps/templates/tasks.html:12
+msgid "User"
+msgstr ""
+
+#: cps/templates/tasks.html:14
+msgid "Task"
+msgstr ""
+
+#: cps/templates/tasks.html:15
+msgid "Status"
+msgstr ""
+
+#: cps/templates/tasks.html:16
+msgid "Progress"
+msgstr ""
+
+#: cps/templates/tasks.html:17
+msgid "Runtime"
+msgstr ""
+
+#: cps/templates/tasks.html:18
+msgid "Starttime"
+msgstr ""
+
+#: cps/templates/tasks.html:24
+msgid "Delete finished tasks"
+msgstr ""
+
+#: cps/templates/tasks.html:25
+msgid "Hide all tasks"
+msgstr ""
+
+#: cps/templates/user_edit.html:18
+msgid "Reset user Password"
+msgstr ""
+
+#: cps/templates/user_edit.html:27
+msgid "Kindle E-Mail"
+msgstr ""
+
+#: cps/templates/user_edit.html:41
+msgid "Standard Theme"
+msgstr ""
+
+#: cps/templates/user_edit.html:42
+msgid "caliBlur! Dark Theme (Beta)"
+msgstr ""
+
+#: cps/templates/user_edit.html:47
+msgid "Show books with language"
+msgstr ""
+
+#: cps/templates/user_edit.html:49
+msgid "Show all"
+msgstr ""
+
+#: cps/templates/user_edit.html:147
+msgid "Delete this user"
+msgstr ""
+
+#: cps/templates/user_edit.html:162
+msgid "Recent Downloads"
+msgstr ""
+
diff --git a/src/optional-requirements.txt b/src/optional-requirements.txt
new file mode 100644
index 0000000..154612b
--- /dev/null
+++ b/src/optional-requirements.txt
@@ -0,0 +1,20 @@
+# GDrive Integration
+google-api-python-client==1.6.1
+gevent==1.2.1
+greenlet==0.4.12
+httplib2==0.9.2
+oauth2client==4.0.0
+uritemplate==3.0.0
+pyasn1-modules==0.0.8
+pyasn1==0.1.9
+PyDrive==1.3.1
+PyYAML==3.12
+rsa==3.4.2
+six==1.10.0
+# goodreads
+goodreads>=0.3.2
+python-Levenshtein>=0.12.0
+# other
+lxml>=3.8.0
+rarfile>=2.7
+natsort>=2.2.0
diff --git a/src/readme.md b/src/readme.md
new file mode 100755
index 0000000..4539cd4
--- /dev/null
+++ b/src/readme.md
@@ -0,0 +1,215 @@
+# About
+
+Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database.
+
+*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
+
+
+
+## Features
+
+- Bootstrap 3 HTML5 interface
+- full graphical setup
+- User management with fine grained per-user permissions
+- Admin interface
+- User Interface in dutch, english, french, german, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish
+- OPDS feed for eBook reader apps
+- Filter and search by titles, authors, tags, series and language
+- Create custom book collection (shelves)
+- Support for editing eBook metadata and deleting eBooks from Calibre library
+- Support for converting eBooks from EPUB to Kindle format (mobi/azw)
+- Restrict eBook download to logged-in users
+- Support for public user registration
+- Send eBooks to Kindle devices with the click of a button
+- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz)
+- Upload new books in PDF, epub, fb2 format
+- Support for Calibre custom columns
+- Ability to hide content based on categories for certain users
+- Self update capability
+- "Magic Link" login to make it easy to log on eReaders
+
+## Quick start
+
+1. Install dependencies by running `pip install --target vendor -r requirements.txt`.
+2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window)
+3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
+4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button
+ optionally a google drive can be used to host the calibre library (-> Using Google Drive integration)
+5. Go to Login page
+
+**Default admin login:**
+*Username:* admin
+*Password:* admin123
+
+**Issues with Ubuntu:**
+Please note that running the above install command can fail on some versions of Ubuntu, saying `"can't combine user with prefix"`. This is a [known bug](https://github.com/pypa/pip/issues/3826) and can be remedied by using the command `pip install --system --target vendor -r requirements.txt` instead.
+
+## Runtime Configuration Options
+
+The configuration can be changed as admin in the admin panel under "Configuration"
+
+Server Port:
+Changes the port Calibre-Web is listening, changes take effect after pressing submit button
+
+Enable public registration:
+Tick to enable public user registration.
+
+Enable anonymous browsing:
+Tick to allow not logged in users to browse the catalog, anonymous user permissions can be set as admin ("Guest" user)
+
+Enable uploading:
+Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick library to be installed.
+
+Enable remote login ("magic link"):
+Tick to enable remote login, i.e. a link that allows user to log in via a different device.
+
+## Requirements
+
+Python 2.7+
+
+Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the send-to-kindle feature:
+
+[Download](http://www.amazon.com/gp/feature.html?docId=1000765211) Amazon's KindleGen tool for your platform and place the binary named as `kindlegen` in the `vendor` folder.
+
+## Using Google Drive integration
+
+Calibre Calibre library (metadata.db) can be located on a Google Drive. Additional optional dependencys are necessary to get this work. Please install all optional requirements by executing `pip install --target vendor -r optional-requirements.txt`
+
+To use google drive integration, you have to use the google developer console to create a new app. https://console.developers.google.com
+
+Once a project has been created, we need to create a client ID and a client secret that will be used to enable the OAuth request with google, and enable the Drive API. To do this, follow the steps below: -
+
+1. Open project in developer console
+2. Click Enable API, and enable google drive
+3. Now on the sidebar, click Credentials
+4. Click Create Credentials and OAuth Client ID
+5. Select Web Application and then next
+6. Give the Credentials a name and enter your callback, which will be CALIBRE_WEB_URL/gdrive/callback
+7. Click save
+8. Download json file and place it in `calibre-web` directory, with the name `client_secrets.json`
+
+The Drive API should now be setup and ready to use, so we need to integrate it into Calibre-Web. This is done as below: -
+
+1. Open config page
+2. Enter the location that will be used to store the metadata.db file locally, and to temporary store uploaded books and other temporary files for upload ("Location of Calibre database")
+2. Tick Use Google Drive
+3. Click the "Submit" button
+4. Now select Authenticate Google Drive
+5. This should redirect you to Google. After allowing it to use your Drive, it redirects you back to the config page
+6. Select the folder that is the root of your calibre library on Gdrive ("Google drive Calibre folder")
+7. Click the "Submit" button
+8. Google Drive should now be connected and be used to get images and download Epubs. The metadata.db is stored in the calibre library location
+
+### Optional
+If your Calibre-Web is using https, it is possible to add a "watch" to the drive. This will inform us if the metadata.db file is updated and allow us to update our calibre library accordingly.
+Additionally the public adress your server uses (e.g.https://example.com) has to be verified in the Google developer console. After this is done, please wait a few minutes.
+
+9. Open config page
+10. Click enable watch of metadata.db
+11. Note that this expires after a week, so will need to be manually refresh
+
+## Docker images
+
+Pre-built Docker images based on Alpine Linux are available in these Docker Hub repositories:
+
+**x64**
++ **technosoft2000** at [technosoft2000/calibre-web](https://hub.docker.com/r/technosoft2000/calibre-web/)
++ **linuxserver.io** at [linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web/)
+
+**armhf**
++ **linuxserver.io** at [lsioarmhf/calibre-web](https://hub.docker.com/r/lsioarmhf/calibre-web/)
+
+**aarch64**
++ **linuxserver.io** at [lsioarmhf/calibre-web-aarch64](https://hub.docker.com/r/lsioarmhf/calibre-web-aarch64)
+
+## Reverse Proxy
+
+Reverse proxy configuration examples for apache and nginx to use Calibre-Web:
+
+nginx configuration for a local server listening on port 8080, mapping Calibre-Web to /calibre:
+
+```
+http {
+ upstream calibre {
+ server 127.0.0.1:8083;
+ }
+ server {
+ client_max_body_size 20M;
+ location /calibre {
+ proxy_bind $server_adress;
+ proxy_pass http://127.0.0.1:8083;
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Scheme $scheme;
+ proxy_set_header X-Script-Name /calibre;
+ }
+ }
+}
+```
+*Note: If using SSL in your reverse proxy on a non-standard port (e.g.12345), the following proxy_redirect line may be required:*
+```
+proxy_redirect http://$host/ https://$host:12345/;
+```
+
+Apache 2.4 configuration for a local server listening on port 443, mapping Calibre-Web to /calibre-web:
+
+The following modules have to be activated: headers, proxy, rewrite.
+```
+Listen 443
+
+
+ SSLEngine on
+ SSLProxyEngine on
+ SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
+ SSLCertificateFile "C:\Apache24\conf\ssl\test.crt"
+ SSLCertificateKeyFile "C:\Apache24\conf\ssl\test.key"
+
+
+ RequestHeader set X-SCRIPT-NAME /calibre-web
+ RequestHeader set X-SCHEME https
+ ProxyPass http://localhost:8083/
+ ProxyPassReverse http://localhost:8083/
+
+
+```
+
+## (Optional) SSL Configuration
+
+For configuration of calibre-web as SSL Server go to the Config page in the Admin section. Enter the certfile- and keyfile-location, optionally change port to 443 and press submit.
+Afterwards the server can only be accessed via SSL. In case of a misconfiguration (wrong/invalid files) both files can be overridden via command line options
+-c [certfile location] -k [keyfile location]
+By using "" as file locations the server runs as non SSL server again. The correct file path can than be entered on the Config page. After the next restart without command line options the changed file paths are applied.
+
+
+## Start Calibre-Web as service under Linux
+
+Create a file "cps.service" as root in the folder /etc/systemd/system with the following content:
+
+```[Unit]
+Description=Calibre-Web
+
+[Service]
+Type=simple
+User=[Username]
+ExecStart=[path to python] [/PATH/TO/cps.py]
+WorkingDirectory=[/PATH/TO/cps.py]
+
+[Install]
+WantedBy=multi-user.target
+```
+
+Replace the user and ExecStart with your user and foldernames.
+
+`sudo systemctl enable cps.service`
+
+enables the service.
+
+## Command line options
+
+Starting the script with `-h` lists all supported command line options
+Currently supported are 2 options, which are both useful for running multiple instances of Calibre-Web
+
+`"-p path"` allows to specify the location of the settings database
+`"-g path"` allows to specify the location of the google-drive database
+`"-c path"` allows to specify the location of SSL certfile, works only in combination with keyfile
+`"-k path"` allows to specify the location of SSL keyfile, works only in combination with certfile
diff --git a/src/requirements.txt b/src/requirements.txt
new file mode 100644
index 0000000..3fb23ea
--- /dev/null
+++ b/src/requirements.txt
@@ -0,0 +1,15 @@
+Babel>=1.3
+Flask-Babel>=0.11.1
+Flask-Login>=0.3.2
+Flask-Principal>=0.3.2
+singledispatch>=3.4.0.0
+backports_abc>=0.4
+Flask>=0.11
+iso-639>=0.4.5
+PyPDF2==1.26.0
+pytz>=2016.10
+requests>=2.11.1
+SQLAlchemy>=1.1.0
+tornado>=4.1
+Wand>=0.4.4
+unidecode>=0.04.19
diff --git a/src/test b/src/test
deleted file mode 100644
index c0ae62c..0000000
--- a/src/test
+++ /dev/null
@@ -1 +0,0 @@
-#test
\ No newline at end of file
diff --git a/src/test/Calibre-Web TestSummary.html b/src/test/Calibre-Web TestSummary.html
new file mode 100644
index 0000000..2feb61b
--- /dev/null
+++ b/src/test/Calibre-Web TestSummary.html
@@ -0,0 +1,2232 @@
+
+
+
+
+
+
+ Test Report
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Test Report
+
+
+
+
+
+
+
+
All Calibre-Web tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ test_shelf.test_shelf |
+ 7 |
+ 6 |
+ 1 |
+ 0 |
+ 0 |
+
+ Detail
+ |
+
+
+
+ test_delete_book_of_shelf
+ |
+ PASS |
+
+
+
+ test_private_shelf
+ |
+ PASS |
+
+
+
+ test_public_private_shelf
+ |
+ PASS |
+
+
+
+ test_public_shelf
+ |
+ PASS |
+
+
+
+ test_rename_shelf
+ |
+ PASS |
+
+
+
+ test_shelf_database_change
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_shelf_long_name
+ |
+ PASS |
+
+
+ test_logging.test_logging |
+ 4 |
+ 3 |
+ 1 |
+ 0 |
+ 0 |
+
+ Detail
+ |
+
+
+
+ test_debug_log
+ |
+ PASS |
+
+
+
+ test_failed_login
+ |
+ PASS |
+
+
+
+ test_logfile_change
+ |
+ PASS |
+
+
+
+ test_logfile_recover
+ |
+
+
+
+
+
+
+
+ |
+
+
+ test_visiblilitys.calibre_web_visibilitys |
+ 17 |
+ 17 |
+ 0 |
+ 0 |
+ 0 |
+
+ Detail
+ |
+
+
+
+ test_about
+ |
+ PASS |
+
+
+
+ test_admin_SMTP_Settings
+ |
+ PASS |
+
+
+
+ test_admin_add_user
+ |
+ PASS |
+
+
+
+ test_admin_change_password
+ |
+ PASS |
+
+
+
+ test_admin_change_visibility_authors
+ |
+ PASS |
+
+
+
+ test_admin_change_visibility_category
+ |
+ PASS |
+
+
+
+ test_admin_change_visibility_hot
+ |
+ PASS |
+
+
+
+ test_admin_change_visibility_language
+ |
+ PASS |
+
+
+
+ test_admin_change_visibility_publisher
+ |
+ PASS |
+
+
+
+ test_admin_change_visibility_rated
+ |
+ PASS |
+
+
+
+ test_admin_change_visibility_read
+ |
+ PASS |
+
+
+
+ test_admin_change_visibility_series
+ |
+ PASS |
+
+
+
+ test_admin_change_visibility_sorted
+ |
+ PASS |
+
+
+
+ test_checked_logged_in
+ |
+ PASS |
+
+
+
+ test_random_books_available
+ |
+ PASS |
+
+
+
+ test_user_email_available
+ |
+ PASS |
+
+
+
+ test_user_visibility_sidebar
+ |
+ PASS |
+
+
+ test_user_template.test_user_template |
+ 15 |
+ 12 |
+ 0 |
+ 0 |
+ 3 |
+
+ Detail
+ |
+
+
+
+ test_author_user_template
+ |
+ PASS |
+
+
+
+ test_best_user_template
+ |
+ PASS |
+
+
+
+ test_category_user_template
+ |
+ PASS |
+
+
+
+ test_detail_random_user_template
+ |
+ PASS |
+
+
+
+ test_hot_user_template
+ |
+ PASS |
+
+
+
+ test_language_user_template
+ |
+ PASS |
+
+
+
+ test_limit_book_languages
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_mature_content_settings
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_publisher_user_template
+ |
+ PASS |
+
+
+
+ test_random_user_template
+ |
+ PASS |
+
+
+
+ test_read_user_template
+ |
+ PASS |
+
+
+
+ test_recent_user_template
+ |
+ PASS |
+
+
+
+ test_series_user_template
+ |
+ PASS |
+
+
+
+ test_sorted_user_template
+ |
+ PASS |
+
+
+
+ test_ui_language_settings
+ |
+
+
+
+
+
+
+
+ |
+
+
+ test_anonymous.test_anonymous |
+ 1 |
+ 0 |
+ 0 |
+ 0 |
+ 1 |
+
+ Detail
+ |
+
+
+
+ test_anonymous_user
+ |
+
+
+
+
+
+
+
+ |
+
+
+ test_edit_books.test_edit_books |
+ 22 |
+ 3 |
+ 2 |
+ 0 |
+ 17 |
+
+ Detail
+ |
+
+
+
+ test_database_errors
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_delete_book
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_delete_format
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_author
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_category
+ |
+ PASS |
+
+
+
+ test_edit_comments
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_custom_bool
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_custom_rating
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_custom_single_select
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_custom_text
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_language
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_publisher
+ |
+ PASS |
+
+
+
+ test_edit_publishing_date
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_rating
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_series
+ |
+ PASS |
+
+
+
+ test_edit_title
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_typeahead_author
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_typeahead_language
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_typeahead_publisher
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_typeahead_series
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_typeahead_tag
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_upload_cover_hdd
+ |
+
+
+
+
+
+
+
+ |
+
+
+ test_edit_books_gdrive.test_edit_books_gdrive |
+ 22 |
+ 0 |
+ 0 |
+ 0 |
+ 22 |
+
+ Detail
+ |
+
+
+
+ test_database_errors
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_delete_book
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_delete_format
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_author
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_category
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_comments
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_custom_bool
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_custom_rating
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_custom_single_select
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_custom_text
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_language
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_publisher
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_publishing_date
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_rating
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_series
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_edit_title
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_typeahead_author
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_typeahead_language
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_typeahead_publisher
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_typeahead_series
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_typeahead_tag
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_upload_cover_hdd
+ |
+
+
+
+
+
+
+
+ |
+
+
+ test_ebook_convert.test_ebook_convert |
+ 12 |
+ 1 |
+ 0 |
+ 0 |
+ 11 |
+
+ Detail
+ |
+
+
+
+ test_SSL_smtp_setup_error
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_STARTTLS_smtp_setup_error
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_convert_deactivate
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_convert_email
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_convert_failed_and_email
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_convert_only
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_convert_parameter
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_convert_wrong_excecutable
+ |
+ PASS |
+
+
+
+ test_email_failed
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_email_only
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_kindle_send_not_configured
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_smtp_setup_error
+ |
+
+
+
+
+
+
+
+ |
+
+
+ test_login.test_login |
+ 6 |
+ 5 |
+ 1 |
+ 0 |
+ 0 |
+
+ Detail
+ |
+
+
+
+ test_login_capital_letters_user_unicode_password_passwort
+ |
+ PASS |
+
+
+
+ test_login_delete_admin
+ |
+ PASS |
+
+
+
+ test_login_empty_password
+ |
+ PASS |
+
+
+
+ test_login_protected
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_login_unicode_user_space_end_passwort
+ |
+ PASS |
+
+
+
+ test_login_user_with_space_passwort_end_space
+ |
+ PASS |
+
+
+ test_opds_feed.test_opds_feed |
+ 1 |
+ 0 |
+ 0 |
+ 0 |
+ 1 |
+
+ Detail
+ |
+
+
+
+ test_opds
+ |
+
+
+
+
+
+
+
+ |
+
+
+ test_updater.test_updater |
+ 1 |
+ 0 |
+ 0 |
+ 0 |
+ 1 |
+
+ Detail
+ |
+
+
+
+ test_updater
+ |
+
+
+
+
+
+
+
+ |
+
+
+ test_helper.calibre_helper |
+ 13 |
+ 13 |
+ 0 |
+ 0 |
+ 0 |
+
+ Detail
+ |
+
+
+
+ test_author_sort
+ |
+ PASS |
+
+
+
+ test_author_sort_comma
+ |
+ PASS |
+
+
+
+ test_author_sort_junior
+ |
+ PASS |
+
+
+
+ test_author_sort_oneword
+ |
+ PASS |
+
+
+
+ test_author_sort_roman
+ |
+ PASS |
+
+
+
+ test_check_Limit_Length
+ |
+ PASS |
+
+
+
+ test_check_char_replacement
+ |
+ PASS |
+
+
+
+ test_check_chinese_Characters
+ |
+ PASS |
+
+
+
+ test_check_degEUR_replacement
+ |
+ PASS |
+
+
+
+ test_check_doubleS
+ |
+ PASS |
+
+
+
+ test_check_finish_Dot
+ |
+ PASS |
+
+
+
+ test_check_high23
+ |
+ PASS |
+
+
+
+ test_check_umlauts
+ |
+ PASS |
+
+
+ test_register.test_register |
+ 4 |
+ 0 |
+ 0 |
+ 0 |
+ 4 |
+
+ Detail
+ |
+
+
+
+ test_login_with_password
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_registering_user
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_registering_user_fail
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ test_resend_password
+ |
+
+
+
+
+
+
+
+ |
+
+
+ test_cli.test_cli |
+ 4 |
+ 4 |
+ 0 |
+ 0 |
+ 0 |
+
+ Detail
+ |
+
+
+
+ test_cli_SSL_files
+ |
+ PASS |
+
+
+
+ test_cli_different_folder
+ |
+ PASS |
+
+
+
+ test_cli_different_settings_database
+ |
+ PASS |
+
+
+
+ test_environ_port_setting
+ |
+ PASS |
+
+
+
+ Total |
+ 129 |
+ 64 |
+ 5 |
+ 0 |
+ 60 |
+ |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/css/runner.css b/src/test/css/runner.css
new file mode 100644
index 0000000..440f87e
--- /dev/null
+++ b/src/test/css/runner.css
@@ -0,0 +1,21 @@
+.hiddenRow {
+ display: none;
+}
+
+.bg-grey {
+ background-color: rgba(0, 0, 0, 0.03);
+}
+
+.table-curved {
+ border-radius: 20px;
+}
+
+.buttons, .report-description {
+ margin: 5px;
+ padding: 5px;
+}
+
+.piechart{
+ text-align: center;
+}
+
diff --git a/src/test/js/runner.js b/src/test/js/runner.js
new file mode 100644
index 0000000..2ecf365
--- /dev/null
+++ b/src/test/js/runner.js
@@ -0,0 +1,189 @@
+output_list = Array();
+
+/* Level - 0: Summary; 1: Failed; 2: All; 3: Skipped */
+function showCase(level) {
+ table_rows = document.getElementsByTagName("tr");
+ for (var i = 0; i < table_rows.length; i++) {
+ row = table_rows[i];
+ id = row.id;
+ if (id.substr(0,2) == 'ft') {
+ if (level < 1 || level == 3) {
+ row.classList.add('hiddenRow');
+ }
+ else {
+ row.classList.remove('hiddenRow');
+ }
+ }
+ if (id.substr(0,2) == 'pt') {
+ if (level > 1 && level != 3) {
+ row.classList.remove('hiddenRow');
+ }
+ else {
+ row.classList.add('hiddenRow');
+ }
+ }
+ if (id.substr(0,2) == 'st') {
+ if (level >=2) {
+ row.classList.remove('hiddenRow');
+ }
+ else {
+ row.classList.add('hiddenRow');
+ }
+ }
+
+
+ }
+}
+
+
+function showClassDetail(class_id, count) {
+ var testcases_list = Array(count);
+ var all_hidden = true;
+ for (var i = 0; i < count; i++) {
+ testcase_postfix_id = 't' + class_id.substr(1) + '.' + (i+1);
+ testcase_id = 'f' + testcase_postfix_id;
+ testcase = document.getElementById(testcase_id);
+ if (!testcase) {
+ testcase_id = 'p' + testcase_postfix_id;
+ testcase = document.getElementById(testcase_id);
+ }
+ if (!testcase) {
+ testcase_id = 's' + testcase_postfix_id;
+ testcase = document.getElementById(testcase_id);
+ }
+ testcases_list[i] = testcase;
+ if (testcase.classList.contains('hiddenRow')) {
+ all_hidden = false;
+ }
+ }
+ for (var i = 0; i < count; i++) {
+ testcase = testcases_list[i];
+ if (!all_hidden) {
+ testcase.classList.remove('hiddenRow');
+ }
+ else {
+ testcase.classList.add('hiddenRow');
+ }
+ }
+}
+
+
+function showTestDetail(div_id){
+ var details_div = document.getElementById(div_id)
+ var displayState = details_div.style.display
+ // alert(displayState)
+ if (displayState != 'block' ) {
+ displayState = 'block'
+ details_div.style.display = 'block'
+ }
+ else {
+ details_div.style.display = 'none'
+ }
+}
+
+
+function html_escape(s) {
+ s = s.replace(/&/g,'&');
+ s = s.replace(//g,'>');
+ return s;
+}
+
+/* obsoleted by detail in
+function showOutput(id, name) {
+ var w = window.open("", //url
+ name,
+ "resizable,scrollbars,status,width=800,height=450");
+ d = w.document;
+ d.write("
");
+ d.write(html_escape(output_list[id]));
+ d.write("\n");
+ d.write("close\n");
+ d.write("
\n");
+ d.close();
+}
+*/
+function drawCircle(pass, fail, error, skip){
+ var color = ["#5cb85c","#d9534f","#c00","#f0ad4e"];
+ var data = [pass,fail,error,skip];
+ var text_arr = ["pass", "fail", "error","skip"];
+
+ var canvas = document.getElementById("circle");
+ var ctx = canvas.getContext("2d");
+ var startPoint=0;
+ var width = 20, height = 10;
+ var posX = 112 * 2 + 20, posY = 30;
+ var textX = posX + width + 5, textY = posY + 10;
+ for(var i=0;i
len-1){
+ index=0;
+ clearInterval(start); //运行一轮后停止
+ }
+ changeImg(index++);
+ }
+ imgyuan.style.width= 25*len +"px";
+ //对应圆圈和图片同步
+ function changeImg(index) {
+ var list = obj1.getElementsByTagName('img');
+ var list1 = obj1.getElementsByTagName('font');
+ for (i = 0; i < list.length; i++) {
+ list[i].style.display = 'none';
+ list1[i].style.backgroundColor = 'white';
+ }
+ list[index].style.display = 'block';
+ list1[index].style.backgroundColor = 'blue';
+ }
+
+}
+function hide_img(obj){
+ obj.parentElement.style.display = "none";
+ obj.parentElement.getElementsByClassName('imgyuan')[0].innerHTML = "";
+}
\ No newline at end of file