@@ -23,6 +25,9 @@ export default {
computed: {
message() {
return this.text || this.$strings.MessagePleaseWait
+ },
+ hasSlotContent() {
+ return this.$slots.default && this.$slots.default.length > 0
}
}
}
diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue
index eef05b608..63e133137 100644
--- a/client/pages/upload/index.vue
+++ b/client/pages/upload/index.vue
@@ -297,6 +297,15 @@ export default {
ref.setUploadStatus(status)
}
},
+ updateItemCardProgress(index, progress) {
+ var ref = this.$refs[`itemCard-${index}`]
+ if (ref && ref.length) ref = ref[0]
+ if (!ref) {
+ console.error('Book card ref not found', index, this.$refs)
+ } else {
+ ref.setUploadProgress(progress)
+ }
+ },
async uploadItem(item) {
var form = new FormData()
form.set('title', item.title)
@@ -312,8 +321,20 @@ export default {
form.set(`${index++}`, file)
})
+ const config = {
+ onUploadProgress: (progressEvent) => {
+ if (progressEvent.lengthComputable) {
+ const progress = {
+ loaded: progressEvent.loaded,
+ total: progressEvent.total
+ }
+ this.updateItemCardProgress(item.index, progress)
+ }
+ }
+ }
+
return this.$axios
- .$post('/api/upload', form)
+ .$post('/api/upload', form, config)
.then(() => true)
.catch((error) => {
console.error('Failed to upload item', error)
From 3934461c4666e99c749eaad2a3314b3696c9dcc8 Mon Sep 17 00:00:00 2001
From: advplyr
Date: Tue, 23 Sep 2025 15:37:22 +0200
Subject: [PATCH 002/111] Added translation using Weblate (Persian)
---
client/strings/fa.json | 1 +
1 file changed, 1 insertion(+)
create mode 100644 client/strings/fa.json
diff --git a/client/strings/fa.json b/client/strings/fa.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/client/strings/fa.json
@@ -0,0 +1 @@
+{}
From 2f3036fabae8182c03af3fdc3e96516c9d62d621 Mon Sep 17 00:00:00 2001
From: SmileFate
Date: Tue, 23 Sep 2025 16:24:00 +0200
Subject: [PATCH 003/111] Translated using Weblate (Turkish)
Currently translated at 100.0% (1161 of 1161 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
---
client/strings/tr.json | 1111 +++++++++++++++++++++++++++++++++++-----
1 file changed, 969 insertions(+), 142 deletions(-)
diff --git a/client/strings/tr.json b/client/strings/tr.json
index 556382b9f..be5f5fa7c 100644
--- a/client/strings/tr.json
+++ b/client/strings/tr.json
@@ -11,84 +11,84 @@
"ButtonApplyChapters": "Bölümleri Uygula",
"ButtonAuthors": "Yazarlar",
"ButtonBack": "Geri",
- "ButtonBatchEditPopulateFromExisting": "Mevcut olandan çoğalt",
- "ButtonBatchEditPopulateMapDetails": "Harita detaylarını çoğalt",
- "ButtonBrowseForFolder": "Klasör için göz at",
+ "ButtonBatchEditPopulateFromExisting": "Mevcuttan Doldur",
+ "ButtonBatchEditPopulateMapDetails": "Harita Detaylarını Doldur",
+ "ButtonBrowseForFolder": "Klasöre Göz At",
"ButtonCancel": "İptal",
- "ButtonCancelEncode": "Kodlamayı Durdur",
+ "ButtonCancelEncode": "Kodlamayı İptal Et",
"ButtonChangeRootPassword": "Root Şifresini Değiştir",
- "ButtonCheckAndDownloadNewEpisodes": "Yeni Bölümleri Kontrol Et & İndir",
- "ButtonChooseAFolder": "Klasör seç",
- "ButtonChooseFiles": "Dosya seç",
+ "ButtonCheckAndDownloadNewEpisodes": "Yeni Bölümleri Kontrol Et ve İndir",
+ "ButtonChooseAFolder": "Bir Klasör Seç",
+ "ButtonChooseFiles": "Dosyaları Seç",
"ButtonClearFilter": "Filtreyi Temizle",
"ButtonClose": "Kapat",
"ButtonCloseFeed": "Akışı Kapat",
- "ButtonCloseSession": "Acık Oturumu Kapat",
+ "ButtonCloseSession": "Oturumu Kapat",
"ButtonCollections": "Koleksiyonlar",
- "ButtonConfigureScanner": "Tarayıcı Ayarları",
+ "ButtonConfigureScanner": "Tarayıcıyı Yapılandır",
"ButtonCreate": "Oluştur",
"ButtonCreateBackup": "Yedek Oluştur",
"ButtonDelete": "Sil",
- "ButtonDownloadQueue": "Sıra",
+ "ButtonDownloadQueue": "İndirme Sırası",
"ButtonEdit": "Düzenle",
"ButtonEditChapters": "Bölümleri Düzenle",
- "ButtonEditPodcast": "Podcast Düzenle",
+ "ButtonEditPodcast": "Podcast'i Düzenle",
"ButtonEnable": "Etkinleştir",
- "ButtonFireAndFail": "Gönder ve hata al",
- "ButtonFireOnTest": "onTest Gönder",
- "ButtonForceReScan": "Zorla Yeniden Tara",
- "ButtonFullPath": "Tam Dosya Yolu",
+ "ButtonFireAndFail": "Tetikle ve Hata Ver",
+ "ButtonFireOnTest": "onTest Olayını Tetikle",
+ "ButtonForceReScan": "Yeniden Taramaya Zorla",
+ "ButtonFullPath": "Tam Yol",
"ButtonHide": "Gizle",
- "ButtonHome": "Ana sayfa",
+ "ButtonHome": "Ana Sayfa",
"ButtonIssues": "Sorunlar",
"ButtonJumpBackward": "Geri Sar",
"ButtonJumpForward": "İleri Sar",
- "ButtonLatest": "En yeni",
+ "ButtonLatest": "En Yeniler",
"ButtonLibrary": "Kütüphane",
"ButtonLogout": "Çıkış Yap",
"ButtonLookup": "Sorgula",
"ButtonManageTracks": "Parçaları Yönet",
- "ButtonMapChapterTitles": "Bölüm Başlıklarını Haritalandır",
- "ButtonMatchAllAuthors": "Bütün Yazarlarla Eşleştir",
+ "ButtonMapChapterTitles": "Bölüm Başlıklarını Eşle",
+ "ButtonMatchAllAuthors": "Tüm Yazarları Eşleştir",
"ButtonMatchBooks": "Kitapları Eşleştir",
"ButtonNevermind": "Vazgeç",
- "ButtonNext": "Sonraki",
+ "ButtonNext": "İleri",
"ButtonNextChapter": "Sonraki Bölüm",
- "ButtonNextItemInQueue": "Sıradaki Sonraki Öğe",
+ "ButtonNextItemInQueue": "Sıradaki Öğe",
"ButtonOk": "Tamam",
"ButtonOpenFeed": "Akışı Aç",
"ButtonOpenManager": "Yöneticiyi Aç",
- "ButtonPause": "Durdur",
+ "ButtonPause": "Duraklat",
"ButtonPlay": "Oynat",
- "ButtonPlayAll": "Hepsini Oynat",
+ "ButtonPlayAll": "Tümünü Oynat",
"ButtonPlaying": "Oynatılıyor",
- "ButtonPlaylists": "Oynatma listeleri",
+ "ButtonPlaylists": "Oynatma Listeleri",
"ButtonPrevious": "Önceki",
"ButtonPreviousChapter": "Önceki Bölüm",
- "ButtonProbeAudioFile": "Ses Dosyasını Yokla",
- "ButtonPurgeAllCache": "Bütün Önbelleği Temizle",
- "ButtonPurgeItemsCache": "Öğenin Önbelleğini Temizle",
- "ButtonQueueAddItem": "Sıraya ekle",
- "ButtonQueueRemoveItem": "Sıradan çıkar",
- "ButtonQuickEmbed": "Hızlı Embed",
- "ButtonQuickEmbedMetadata": "Hızlı Embed Üstverisi",
+ "ButtonProbeAudioFile": "Ses Dosyasını Analiz Et",
+ "ButtonPurgeAllCache": "Tüm Önbelleği Temizle",
+ "ButtonPurgeItemsCache": "Öğe Önbelleğini Temizle",
+ "ButtonQueueAddItem": "Sıraya Ekle",
+ "ButtonQueueRemoveItem": "Sıradan Kaldır",
+ "ButtonQuickEmbed": "Hızlı Gömme",
+ "ButtonQuickEmbedMetadata": "Üst Veriyi Hızlıca Göm",
"ButtonQuickMatch": "Hızlı Eşleştirme",
"ButtonReScan": "Yeniden Tara",
"ButtonRead": "Oku",
"ButtonReadLess": "Daha az göster",
- "ButtonReadMore": "Daha fazla göster",
+ "ButtonReadMore": "Devamını oku",
"ButtonRefresh": "Yenile",
"ButtonRemove": "Kaldır",
- "ButtonRemoveAll": "Hepsini Sil",
- "ButtonRemoveAllLibraryItems": "Bütün Kütüphane Öğelerini Sil",
- "ButtonRemoveFromContinueListening": "Dinlemeye Devam Etmekten Çıkar",
- "ButtonRemoveFromContinueReading": "Okumaya Devam Etmekten Çıkar",
- "ButtonRemoveSeriesFromContinueSeries": "Seriye Devam Et'ten Seriyi Çıkar",
+ "ButtonRemoveAll": "Tümünü Kaldır",
+ "ButtonRemoveAllLibraryItems": "Tüm Kütüphane Öğelerini Kaldır",
+ "ButtonRemoveFromContinueListening": "Dinlemeye Devam Ettiklerimden Kaldır",
+ "ButtonRemoveFromContinueReading": "Okumaya Devam Ettiklerimden Kaldır",
+ "ButtonRemoveSeriesFromContinueSeries": "Seriyi 'Seriye Devam Et'ten Kaldır",
"ButtonReset": "Sıfırla",
- "ButtonResetToDefault": "Varsayılana sıfırla",
- "ButtonRestore": "Geri getir",
+ "ButtonResetToDefault": "Varsayılana Sıfırla",
+ "ButtonRestore": "Geri Yükle",
"ButtonSave": "Kaydet",
- "ButtonSaveAndClose": "Kaydet & Kapat",
+ "ButtonSaveAndClose": "Kaydet ve Kapat",
"ButtonSaveTracklist": "Parça Listesini Kaydet",
"ButtonScan": "Tara",
"ButtonScanLibrary": "Kütüphaneyi Tara",
@@ -97,240 +97,1067 @@
"ButtonSearch": "Ara",
"ButtonSelectFolderPath": "Klasör Yolunu Seç",
"ButtonSeries": "Seriler",
- "ButtonSetChaptersFromTracks": "Parçaların bölümlerini ayarla",
+ "ButtonSetChaptersFromTracks": "Bölümleri Parçalardan Ayarla",
"ButtonShare": "Paylaş",
+ "ButtonShiftTimes": "Zamanlamayı Kaydır",
"ButtonShow": "Göster",
"ButtonStartM4BEncode": "M4B Kodlamasını Başlat",
- "ButtonStartMetadataEmbed": "Meta Veri Yerleştirmeyi Başlat",
+ "ButtonStartMetadataEmbed": "Üst Veri Gömme İşlemini Başlat",
"ButtonStats": "İstatistikler",
"ButtonSubmit": "Gönder",
- "ButtonTest": "Dene",
- "ButtonUnlinkOpenId": "OpenID ilişiğini kaldır",
+ "ButtonTest": "Test Et",
+ "ButtonUnlinkOpenId": "OpenID Bağlantısını Kaldır",
"ButtonUpload": "Yükle",
"ButtonUploadBackup": "Yedeği Yükle",
"ButtonUploadCover": "Kapağı Yükle",
"ButtonUploadOPMLFile": "OPML Dosyası Yükle",
- "ButtonUserDelete": "{0} kullanıcısını sil.",
+ "ButtonUserDelete": "{0} kullanıcısını sil",
"ButtonUserEdit": "{0} kullanıcısını düzenle",
"ButtonViewAll": "Tümünü Görüntüle",
"ButtonYes": "Evet",
- "ErrorUploadFetchMetadataAPI": "Üst veriyi almakta hata",
- "ErrorUploadFetchMetadataNoResults": "Üstveri alınamadı - başlık ve(ya) yazarı güncellemeyi deneyin",
- "ErrorUploadLacksTitle": "Başlığa sahip olmalı",
+ "ErrorUploadFetchMetadataAPI": "Üst veri alınırken hata oluştu",
+ "ErrorUploadFetchMetadataNoResults": "Üst veri bulunamadı. Başlığı veya yazarı güncellemeyi deneyin.",
+ "ErrorUploadLacksTitle": "Başlık girilmesi zorunludur",
"HeaderAccount": "Hesap",
- "HeaderAddCustomMetadataProvider": "Özel Üstveri Sağlayıcısı Ekle",
+ "HeaderAddCustomMetadataProvider": "Özel Üst Veri Sağlayıcısı Ekle",
"HeaderAdvanced": "Gelişmiş",
"HeaderApiKeys": "API Anahtarları",
- "HeaderAppriseNotificationSettings": "Bildirim Ayarlarının Haberini Ver",
- "HeaderAudioTracks": "Ses Kanalları",
+ "HeaderAppriseNotificationSettings": "Apprise Bildirim Ayarları",
+ "HeaderAudioTracks": "Ses Parçaları",
"HeaderAudiobookTools": "Sesli Kitap Dosya Yönetim Araçları",
"HeaderAuthentication": "Kimlik Doğrulama",
- "HeaderBackups": "Yedeklemeler",
- "HeaderBulkChapterModal": "Birden Çok Bölüm Ekle",
- "HeaderChangePassword": "Parolayı Değiştir",
+ "HeaderBackups": "Yedekler",
+ "HeaderBulkChapterModal": "Toplu Bölüm Ekle",
+ "HeaderChangePassword": "Şifre Değiştir",
"HeaderChapters": "Bölümler",
- "HeaderChooseAFolder": "Klasör Seç",
+ "HeaderChooseAFolder": "Bir Klasör Seç",
"HeaderCollection": "Koleksiyon",
"HeaderCollectionItems": "Koleksiyon Öğeleri",
"HeaderCover": "Kapak",
- "HeaderCurrentDownloads": "Geçerli İndirmeler",
- "HeaderCustomMessageOnLogin": "Girişteki Kişiselleştirilmiş Mesaj",
- "HeaderCustomMetadataProviders": "Kişiselleştirilmiş Metadata Sağlayıcıları",
+ "HeaderCurrentDownloads": "Mevcut İndirmeler",
+ "HeaderCustomMessageOnLogin": "Giriş Ekranı Özel Mesajı",
+ "HeaderCustomMetadataProviders": "Özel Üst Veri Sağlayıcıları",
"HeaderDetails": "Detaylar",
- "HeaderDownloadQueue": "Kuyruktakileri İndir",
- "HeaderEbookFiles": "Ebook Dosyaları",
- "HeaderEmail": "Email",
- "HeaderEmailSettings": "Email Ayarları",
+ "HeaderDownloadQueue": "İndirme Sırası",
+ "HeaderEbookFiles": "E-kitap Dosyaları",
+ "HeaderEmail": "E-posta",
+ "HeaderEmailSettings": "E-posta Ayarları",
"HeaderEpisodes": "Bölümler",
- "HeaderEreaderDevices": "Ekitap Cihazları",
- "HeaderEreaderSettings": "Ereader Ayarları",
+ "HeaderEreaderDevices": "E-kitap Okuyucu Cihazlar",
+ "HeaderEreaderSettings": "E-kitap Okuyucu Ayarları",
"HeaderFiles": "Dosyalar",
- "HeaderFindChapters": "Bölümleri Bul",
- "HeaderIgnoredFiles": "Görmezden Gelinen Dosyalar",
+ "HeaderFindChapters": "Bölüm Bul",
+ "HeaderIgnoredFiles": "Yoksayılan Dosyalar",
"HeaderItemFiles": "Öğe Dosyaları",
- "HeaderItemMetadataUtils": "Öğe Üstveri Araçları",
+ "HeaderItemMetadataUtils": "Öğe Üst Veri Araçları",
"HeaderLastListeningSession": "Son Dinleme Oturumu",
- "HeaderLatestEpisodes": "En son bölümler",
+ "HeaderLatestEpisodes": "En Son Bölümler",
"HeaderLibraries": "Kütüphaneler",
"HeaderLibraryFiles": "Kütüphane Dosyaları",
"HeaderLibraryStats": "Kütüphane İstatistikleri",
"HeaderListeningSessions": "Dinleme Oturumları",
"HeaderListeningStats": "Dinleme İstatistikleri",
"HeaderLogin": "Giriş Yap",
- "HeaderLogs": "Günlükler",
+ "HeaderLogs": "Kayıtlar",
"HeaderManageGenres": "Türleri Yönet",
"HeaderManageTags": "Etiketleri Yönet",
- "HeaderMapDetails": "Detayları haritalandır",
+ "HeaderMapDetails": "Detayları Eşle",
"HeaderMatch": "Eşleştir",
- "HeaderMetadataOrderOfPrecedence": "Üstveri öncelik sırası",
- "HeaderMetadataToEmbed": "Gömülecek üstveri",
+ "HeaderMetadataOrderOfPrecedence": "Üst Veri Öncelik Sırası",
+ "HeaderMetadataToEmbed": "Gömülecek Üst Veri",
"HeaderNewAccount": "Yeni Hesap",
"HeaderNewApiKey": "Yeni API Anahtarı",
"HeaderNewLibrary": "Yeni Kütüphane",
"HeaderNotificationCreate": "Bildirim Oluştur",
- "HeaderNotificationUpdate": "Güncelleme Bildirimi",
+ "HeaderNotificationUpdate": "Bildirimi Güncelle",
"HeaderNotifications": "Bildirimler",
- "HeaderOpenIDConnectAuthentication": "OpenID Connect Kimlik Doğrulaması",
+ "HeaderOpenIDConnectAuthentication": "OpenID Connect ile Kimlik Doğrulama",
"HeaderOpenListeningSessions": "Açık Dinleme Oturumları",
"HeaderOpenRSSFeed": "RSS Akışını Aç",
"HeaderOtherFiles": "Diğer Dosyalar",
- "HeaderPasswordAuthentication": "Parola Doğrulaması",
+ "HeaderPasswordAuthentication": "Şifre ile Kimlik Doğrulama",
"HeaderPermissions": "İzinler",
- "HeaderPlayerQueue": "Oynatıcı Sırası",
+ "HeaderPlayerQueue": "Oynatma Sırası",
"HeaderPlayerSettings": "Oynatıcı Ayarları",
- "HeaderPlaylist": "Oynatma listesi",
+ "HeaderPlaylist": "Oynatma Listesi",
"HeaderPlaylistItems": "Oynatma Listesi Öğeleri",
- "HeaderPodcastsToAdd": "Eklenecek Podcastler",
- "HeaderPresets": "Ön ayarlar",
+ "HeaderPodcastsToAdd": "Eklenecek Podcast'ler",
+ "HeaderPresets": "Hazır Ayarlar",
"HeaderPreviewCover": "Kapak Önizlemesi",
"HeaderRSSFeedGeneral": "RSS Detayları",
"HeaderRSSFeedIsOpen": "RSS Akışı Açık",
- "HeaderRSSFeeds": "RSS Bültenleri",
+ "HeaderRSSFeeds": "RSS Akışları",
"HeaderRemoveEpisode": "Bölümü Kaldır",
"HeaderRemoveEpisodes": "{0} Bölümü Kaldır",
- "HeaderSavedMediaProgress": "Kaydedilen Medya İlerleyişi",
- "HeaderSchedule": "Program",
+ "HeaderSavedMediaProgress": "Kaydedilmiş Medya İlerlemesi",
+ "HeaderSchedule": "Zamanlama",
+ "HeaderScheduleEpisodeDownloads": "Otomatik Bölüm İndirmeyi Zamanla",
+ "HeaderScheduleLibraryScans": "Otomatik Kütüphane Taramalarını Zamanla",
+ "HeaderSession": "Oturum",
+ "HeaderSetBackupSchedule": "Yedekleme Zamanlaması Ayarla",
"HeaderSettings": "Ayarlar",
+ "HeaderSettingsDisplay": "Görünüm",
+ "HeaderSettingsExperimental": "Deneysel Özellikler",
+ "HeaderSettingsGeneral": "Genel",
+ "HeaderSettingsScanner": "Tarayıcı",
+ "HeaderSettingsSecurity": "Güvenlik",
+ "HeaderSettingsWebClient": "Web İstemcisi",
"HeaderSleepTimer": "Uyku Zamanlayıcısı",
- "HeaderStatsMinutesListeningChart": "Dinlenilen Dakika (son 7 gün)",
- "HeaderStatsRecentSessions": "Geçmiş Oturumlar",
+ "HeaderStatsLargestItems": "En Büyük Öğeler",
+ "HeaderStatsLongestItems": "En Uzun Öğeler (saat)",
+ "HeaderStatsMinutesListeningChart": "Dinlenen Dakika (son 7 gün)",
+ "HeaderStatsRecentSessions": "Son Oturumlar",
+ "HeaderStatsTop10Authors": "En İyi 10 Yazar",
+ "HeaderStatsTop5Genres": "En İyi 5 Tür",
"HeaderTableOfContents": "İçindekiler",
+ "HeaderTools": "Araçlar",
+ "HeaderUpdateAccount": "Hesabı Güncelle",
+ "HeaderUpdateApiKey": "API Anahtarını Güncelle",
"HeaderUpdateAuthor": "Yazarı Güncelle",
"HeaderUpdateDetails": "Detayları Güncelle",
"HeaderUpdateLibrary": "Kütüphaneyi Güncelle",
"HeaderUsers": "Kullanıcılar",
- "HeaderYearReview": "{0} Yılına Bir Bakış",
+ "HeaderYearReview": "{0} Yılına Genel Bakış",
"HeaderYourStats": "İstatistiklerin",
+ "LabelAbridged": "Kısaltılmış",
+ "LabelAbridgedChecked": "Kısaltılmış (işaretli)",
+ "LabelAbridgedUnchecked": "Kısaltılmamış (işaretli değil)",
+ "LabelAccessibleBy": "Erişebilenler",
"LabelAccountType": "Hesap Türü",
"LabelAccountTypeAdmin": "Yönetici",
- "LabelAccountTypeGuest": "Ziyaretçi",
+ "LabelAccountTypeGuest": "Misafir",
"LabelAccountTypeUser": "Kullanıcı",
- "LabelActivities": "Etkinlikler",
- "LabelActivity": "Etkinlik",
+ "LabelActivities": "Aktiviteler",
+ "LabelActivity": "Aktivite",
"LabelAddToCollection": "Koleksiyona Ekle",
- "LabelAddToCollectionBatch": "Koleksiyona {0} Kitap Ekle",
+ "LabelAddToCollectionBatch": "{0} Kitabı Koleksiyona Ekle",
"LabelAddToPlaylist": "Oynatma Listesine Ekle",
- "LabelAddToPlaylistBatch": "Çalma Listesine {0} Öğe Ekle",
+ "LabelAddToPlaylistBatch": "{0} Öğeyi Oynatma Listesine Ekle",
"LabelAddedAt": "Eklenme Zamanı",
- "LabelAddedDate": "Eklendi {0}",
- "LabelAdminUsersOnly": "Yalnızca yönetici kullanıcılar",
- "LabelAll": "Hepsi",
+ "LabelAddedDate": "Eklendi: {0}",
+ "LabelAdminUsersOnly": "Sadece Yöneticiler",
+ "LabelAll": "Tümü",
"LabelAllEpisodesDownloaded": "Tüm bölümler indirildi",
"LabelAllUsers": "Tüm Kullanıcılar",
- "LabelAllUsersExcludingGuests": "Ziyaretçiler dışında tüm kullanıcılar",
- "LabelAllUsersIncludingGuests": "Ziyaretçiler dahil tüm kullanıcılar",
- "LabelAlreadyInYourLibrary": "Zaten kütüphanenizde var",
- "LabelApiToken": "API Token",
+ "LabelAllUsersExcludingGuests": "Misafirler hariç tüm kullanıcılar",
+ "LabelAllUsersIncludingGuests": "Misafirler dahil tüm kullanıcılar",
+ "LabelAlreadyInYourLibrary": "Zaten kütüphanende mevcut",
+ "LabelApiKeyCreated": "API Anahtarı \"{0}\" başarıyla oluşturuldu.",
+ "LabelApiKeyCreatedDescription": "API anahtarını şimdi kopyaladığından emin ol, çünkü bir daha göremeyeceksin.",
+ "LabelApiKeyUser": "Kullanıcı adına hareket et",
+ "LabelApiKeyUserDescription": "Bu API anahtarı, adına hareket ettiği kullanıcıyla aynı izinlere sahip olacaktır. Kayıtlarda, isteği doğrudan kullanıcının kendisi yapmış gibi görünecektir.",
+ "LabelApiToken": "API Jetonu (Token)",
+ "LabelAppend": "Sona Ekle",
+ "LabelAudioBitrate": "Ses Bit Hızı (ör. 128k)",
+ "LabelAudioChannels": "Ses Kanalları (1 veya 2)",
+ "LabelAudioCodec": "Ses Kodlayıcı (Codec)",
"LabelAuthor": "Yazar",
- "LabelAuthorFirstLast": "Yazar (İlk Son)",
- "LabelAuthorLastFirst": "Yazar (Son, İlk)",
+ "LabelAuthorFirstLast": "Yazar (Ad Soyad)",
+ "LabelAuthorLastFirst": "Yazar (Soyad, Ad)",
"LabelAuthors": "Yazarlar",
- "LabelAutoDownloadEpisodes": "Bölümleri Otomatik Olarak İndir",
+ "LabelAutoDownloadEpisodes": "Bölümleri Otomatik İndir",
+ "LabelAutoFetchMetadata": "Üst Veriyi Otomatik Getir",
+ "LabelAutoFetchMetadataHelp": "Yüklemeyi kolaylaştırmak için başlık, yazar ve seri üst verilerini getirir. Ek üst verilerin yüklemeden sonra eşleştirilmesi gerekebilir.",
+ "LabelAutoLaunch": "Otomatik Başlat",
+ "LabelAutoLaunchDescription": "Giriş sayfasına gidildiğinde otomatik olarak kimlik doğrulama sağlayıcısına yönlendir (manuel geçersiz kılma yolu: /login?autoLaunch=0)",
+ "LabelAutoRegister": "Otomatik Kayıt",
+ "LabelAutoRegisterDescription": "Giriş yaptıktan sonra yeni kullanıcıları otomatik olarak oluştur",
+ "LabelBackToUser": "Kullanıcıya Geri Dön",
+ "LabelBackupAudioFiles": "Ses Dosyalarını Yedekle",
+ "LabelBackupLocation": "Yedekleme Konumu",
+ "LabelBackupsEnableAutomaticBackups": "Otomatik yedeklemeler",
+ "LabelBackupsEnableAutomaticBackupsHelp": "Yedekler /metadata/backups klasörüne kaydedilir",
+ "LabelBackupsMaxBackupSize": "Maksimum yedek boyutu (GB) (sınırsız için 0)",
+ "LabelBackupsMaxBackupSizeHelp": "Yanlış yapılandırmaya karşı bir önlem olarak, yedekler yapılandırılan boyutu aşarsa başarısız olur.",
+ "LabelBackupsNumberToKeep": "Saklanacak yedek sayısı",
+ "LabelBackupsNumberToKeepHelp": "Her seferinde sadece 1 yedek kaldırılır, bu yüzden zaten bundan daha fazla yedeğiniz varsa bunları manuel olarak kaldırmalısınız.",
+ "LabelBitrate": "Bit Hızı",
+ "LabelBonus": "Bonus",
"LabelBooks": "Kitaplar",
+ "LabelButtonText": "Buton Metni",
+ "LabelByAuthor": "Yazar: {0}",
"LabelChangePassword": "Şifreyi Değiştir",
+ "LabelChannels": "Kanallar",
+ "LabelChapterCount": "{0} Bölüm",
+ "LabelChapterTitle": "Bölüm Başlığı",
"LabelChapters": "Bölümler",
- "LabelClickForMoreInfo": "Daha fazla bilgi için tıklayın",
+ "LabelChaptersFound": "bölüm bulundu",
+ "LabelClickForMoreInfo": "Daha fazla bilgi için tıkla",
+ "LabelClickToUseCurrentValue": "Mevcut değeri kullanmak için tıkla",
"LabelClosePlayer": "Oynatıcıyı kapat",
+ "LabelCodec": "Kodlayıcı (Codec)",
"LabelCollapseSeries": "Seriyi Daralt",
+ "LabelCollapseSubSeries": "Alt Seriyi Daralt",
+ "LabelCollection": "Koleksiyon",
+ "LabelCollections": "Koleksiyonlar",
"LabelComplete": "Tamamlandı",
+ "LabelConfirmPassword": "Şifreyi Onayla",
"LabelContinueListening": "Dinlemeye Devam Et",
"LabelContinueReading": "Okumaya Devam Et",
"LabelContinueSeries": "Seriye Devam Et",
- "LabelDeleteFromFileSystemCheckbox": "Dosya sisteminden sil (sadece veritabanından silmek için işareti kaldır)",
+ "LabelCorsAllowed": "İzin Verilen CORS Kökenleri",
+ "LabelCover": "Kapak",
+ "LabelCoverImageURL": "Kapak Görseli URL'si",
+ "LabelCoverProvider": "Kapak Sağlayıcı",
+ "LabelCreatedAt": "Oluşturulma Tarihi",
+ "LabelCronExpression": "Cron İfadesi",
+ "LabelCurrent": "Mevcut",
+ "LabelCurrently": "Şu anda:",
+ "LabelCustomCronExpression": "Özel Cron İfadesi:",
+ "LabelDatetime": "Tarih ve Saat",
+ "LabelDays": "Gün",
+ "LabelDeleteFromFileSystemCheckbox": "Dosya sisteminden sil (sadece veritabanından kaldırmak için işareti kaldır)",
"LabelDescription": "Açıklama",
- "LabelDeselectAll": "Tümünün Seçimini Kaldır",
+ "LabelDeselectAll": "Tüm Seçimi Kaldır",
+ "LabelDetectedPattern": "Algılanan desen:",
"LabelDevice": "Cihaz",
"LabelDeviceInfo": "Cihaz Bilgisi",
+ "LabelDeviceIsAvailableTo": "Cihaz şunlar için mevcut...",
"LabelDirectory": "Dizin",
+ "LabelDiscFromFilename": "Dosya Adından Disk",
+ "LabelDiscFromMetadata": "Üst Veriden Disk",
"LabelDiscover": "Keşfet",
"LabelDownload": "İndir",
+ "LabelDownloadNEpisodes": "{0} bölümü indir",
+ "LabelDownloadable": "İndirilebilir",
"LabelDuration": "Süre",
- "LabelEbook": "Ekitap",
- "LabelEbooks": "Ekitaplar",
+ "LabelDurationComparisonExactMatch": "(tam eşleşme)",
+ "LabelDurationComparisonLonger": "({0} daha uzun)",
+ "LabelDurationComparisonShorter": "({0} daha kısa)",
+ "LabelDurationFound": "Bulunan süre:",
+ "LabelEbook": "E-kitap",
+ "LabelEbooks": "E-kitaplar",
+ "LabelEdit": "Düzenle",
+ "LabelEmail": "E-posta",
+ "LabelEmailSettingsFromAddress": "Gönderen Adresi",
+ "LabelEmailSettingsRejectUnauthorized": "Yetkisiz sertifikaları reddet",
+ "LabelEmailSettingsRejectUnauthorizedHelp": "SSL sertifika doğrulamasını devre dışı bırakmak, bağlantınızı ortadaki adam saldırıları gibi güvenlik risklerine maruz bırakabilir. Bu seçeneği yalnızca sonuçlarını anlıyorsanız ve bağlandığınız posta sunucusuna güveniyorsanız devre dışı bırakın.",
+ "LabelEmailSettingsSecure": "Güvenli",
+ "LabelEmailSettingsSecureHelp": "Eğer doğruysa, sunucuya bağlanırken bağlantı TLS kullanacaktır. Eğer yanlışsa, sunucu STARTTLS uzantısını destekliyorsa TLS kullanılır. Çoğu durumda, 465 numaralı bağlantı noktasına bağlanıyorsanız bu değeri doğru olarak ayarlayın. 587 veya 25 numaralı bağlantı noktası için yanlış olarak bırakın. (nodemailer.com/smtp/#authentication adresinden)",
+ "LabelEmailSettingsTestAddress": "Test Adresi",
+ "LabelEmbeddedCover": "Gömülü Kapak",
"LabelEnable": "Etkinleştir",
- "LabelEnd": "Son",
+ "LabelEncodingBackupLocation": "Orijinal ses dosyalarınızın bir yedeği şu konumda saklanacaktır:",
+ "LabelEncodingChaptersNotEmbedded": "Bölümler, çok parçalı sesli kitaplara gömülmez.",
+ "LabelEncodingClearItemCache": "Öğe önbelleğini periyodik olarak temizlediğinizden emin olun.",
+ "LabelEncodingFinishedM4B": "Bitmiş M4B, sesli kitap klasörünüze şu konuma konulacaktır:",
+ "LabelEncodingInfoEmbedded": "Üst veri, sesli kitap klasörünüzdeki ses parçalarının içine gömülecektir.",
+ "LabelEncodingStartedNavigation": "Görev başlatıldığında bu sayfadan ayrılabilirsiniz.",
+ "LabelEncodingTimeWarning": "Kodlama 30 dakikaya kadar sürebilir.",
+ "LabelEncodingWarningAdvancedSettings": "Uyarı: ffmpeg kodlama seçeneklerine aşina değilseniz bu ayarları güncellemeyin.",
+ "LabelEncodingWatcherDisabled": "Eğer izleyiciyi devre dışı bıraktıysanız, daha sonra bu sesli kitabı yeniden taramanız gerekecektir.",
+ "LabelEnd": "Bitiş",
"LabelEndOfChapter": "Bölüm Sonu",
"LabelEpisode": "Bölüm",
- "LabelExplicit": "Belirgin",
- "LabelFeedURL": "Akış URLsi",
+ "LabelEpisodeNotLinkedToRssFeed": "Bölüm RSS akışına bağlı değil",
+ "LabelEpisodeNumber": "Bölüm #{0}",
+ "LabelEpisodeTitle": "Bölüm Başlığı",
+ "LabelEpisodeType": "Bölüm Türü",
+ "LabelEpisodeUrlFromRssFeed": "RSS akışından bölüm URL'si",
+ "LabelEpisodes": "Bölümler",
+ "LabelEpisodic": "Bölümlü",
+ "LabelExample": "Örnek",
+ "LabelExpandSeries": "Seriyi Genişlet",
+ "LabelExpandSubSeries": "Alt Seriyi Genişlet",
+ "LabelExpired": "Süresi Doldu",
+ "LabelExpiresAt": "Bitiş Tarihi",
+ "LabelExpiresInSeconds": "Bitiş Süresi (saniye)",
+ "LabelExpiresNever": "Asla",
+ "LabelExplicit": "Açık İçerik",
+ "LabelExplicitChecked": "Açık İçerik (işaretli)",
+ "LabelExplicitUnchecked": "Açık İçerik Değil (işaretli değil)",
+ "LabelExportOPML": "OPML Olarak Dışa Aktar",
+ "LabelFeedURL": "Akış URL'si",
+ "LabelFetchingMetadata": "Üst Veri Alınıyor",
"LabelFile": "Dosya",
- "LabelFileBirthtime": "Dosya Oluşum Zamanı",
- "LabelFileModified": "Dosya Düzenlendi",
- "LabelFilename": "Dosya İsmi",
- "LabelFinished": "Tamamlandı",
+ "LabelFileBirthtime": "Dosya Oluşturulma Zamanı",
+ "LabelFileBornDate": "Oluşturulma: {0}",
+ "LabelFileModified": "Dosya Değiştirilme Zamanı",
+ "LabelFileModifiedDate": "Değiştirilme: {0}",
+ "LabelFilename": "Dosya Adı",
+ "LabelFilterByUser": "Kullanıcıya Göre Filtrele",
+ "LabelFindEpisodes": "Bölüm Bul",
+ "LabelFinished": "Bitirildi",
+ "LabelFinishedDate": "Bitirildi: {0}",
"LabelFolder": "Klasör",
- "LabelFontBoldness": "Font Kalınlığı",
- "LabelFontScale": "Font büyüklüğü",
+ "LabelFolders": "Klasörler",
+ "LabelFontBold": "Kalın",
+ "LabelFontBoldness": "Yazı Tipi Kalınlığı",
+ "LabelFontFamily": "Yazı Tipi Ailesi",
+ "LabelFontItalic": "İtalik",
+ "LabelFontScale": "Yazı Tipi Ölçeği",
+ "LabelFontStrikethrough": "Üstü Çizili",
+ "LabelFormat": "Biçim",
+ "LabelFull": "Tam",
"LabelGenre": "Tür",
"LabelGenres": "Türler",
- "LabelHasEbook": "Ekitabı var",
- "LabelHasSupplementaryEbook": "İlave ekitabı var",
+ "LabelHardDeleteFile": "Dosyayı kalıcı olarak sil",
+ "LabelHasEbook": "E-kitabı var",
+ "LabelHasSupplementaryEbook": "Ek e-kitabı var",
+ "LabelHideSubtitles": "Altyazıları Gizle",
+ "LabelHighestPriority": "En Yüksek Öncelik",
"LabelHost": "Sunucu",
- "LabelInProgress": "İlerleme Halinde",
+ "LabelHour": "Saat",
+ "LabelHours": "Saat",
+ "LabelIcon": "Simge",
+ "LabelImageURLFromTheWeb": "Web'den resim URL'si",
+ "LabelInProgress": "Devam Ediyor",
+ "LabelIncludeInTracklist": "Parça Listesine Dahil Et",
"LabelIncomplete": "Tamamlanmamış",
+ "LabelInterval": "Aralık",
+ "LabelIntervalCustomDailyWeekly": "Özel günlük/haftalık",
+ "LabelIntervalEvery12Hours": "Her 12 saatte bir",
+ "LabelIntervalEvery15Minutes": "Her 15 dakikada bir",
+ "LabelIntervalEvery2Hours": "Her 2 saatte bir",
+ "LabelIntervalEvery30Minutes": "Her 30 dakikada bir",
+ "LabelIntervalEvery6Hours": "Her 6 saatte bir",
+ "LabelIntervalEveryDay": "Her gün",
+ "LabelIntervalEveryHour": "Her saat",
+ "LabelIntervalEveryMinute": "Her dakika",
+ "LabelInvert": "Ters Çevir",
+ "LabelItem": "Öğe",
+ "LabelJumpBackwardAmount": "Geri sarma miktarı",
+ "LabelJumpForwardAmount": "İleri sarma miktarı",
"LabelLanguage": "Dil",
+ "LabelLanguageDefaultServer": "Varsayılan Sunucu Dili",
+ "LabelLanguages": "Diller",
+ "LabelLastBookAdded": "Son Eklenen Kitap",
+ "LabelLastBookUpdated": "Son Güncellenen Kitap",
+ "LabelLastProgressDate": "Son ilerleme: {0}",
+ "LabelLastSeen": "Son Görülme",
+ "LabelLastTime": "Son Kez",
+ "LabelLastUpdate": "Son Güncelleme",
"LabelLayout": "Düzen",
"LabelLayoutSinglePage": "Tek sayfa",
+ "LabelLayoutSplitPage": "Bölünmüş sayfa",
+ "LabelLess": "Daha Az",
+ "LabelLibrariesAccessibleToUser": "Kullanıcının Erişebildiği Kütüphaneler",
+ "LabelLibrary": "Kütüphane",
+ "LabelLibraryFilterSublistEmpty": "{0} yok",
+ "LabelLibraryItem": "Kütüphane Öğesi",
+ "LabelLibraryName": "Kütüphane Adı",
+ "LabelLibrarySortByProgress": "İlerleme: Son Güncellenme",
+ "LabelLibrarySortByProgressFinished": "İlerleme: Bitirildi",
+ "LabelLibrarySortByProgressStarted": "İlerleme: Başlandı",
+ "LabelLimit": "Sınır",
"LabelLineSpacing": "Satır aralığı",
"LabelListenAgain": "Tekrar Dinle",
+ "LabelLogLevelDebug": "Hata Ayıklama (Debug)",
+ "LabelLogLevelInfo": "Bilgi (Info)",
+ "LabelLogLevelWarn": "Uyarı (Warn)",
+ "LabelLookForNewEpisodesAfterDate": "Bu tarihten sonra yeni bölümleri ara",
+ "LabelLowestPriority": "En Düşük Öncelik",
+ "LabelMatchConfidence": "Güvenilirlik",
+ "LabelMatchExistingUsersBy": "Mevcut kullanıcıları şuna göre eşleştir",
+ "LabelMatchExistingUsersByDescription": "Mevcut kullanıcıları bağlamak için kullanılır. Bağlandıktan sonra, kullanıcılar SSO sağlayıcınızdan gelen benzersiz bir kimlikle eşleştirilir.",
+ "LabelMaxEpisodesToDownload": "İndirilecek maks. bölüm sayısı. Sınırsız için 0 kullanın.",
+ "LabelMaxEpisodesToDownloadPerCheck": "Kontrol başına indirilecek maks. yeni bölüm sayısı",
+ "LabelMaxEpisodesToKeep": "Saklanacak maks. bölüm sayısı",
+ "LabelMaxEpisodesToKeepHelp": "0 değeri maks. sınır belirlemez. Yeni bir bölüm otomatik indirildikten sonra, X'ten fazla bölümünüz varsa en eski bölümü siler. Bu, her yeni indirme için yalnızca 1 bölüm siler.",
+ "LabelMediaPlayer": "Medya Oynatıcı",
"LabelMediaType": "Medya Türü",
- "LabelMissing": "Kayıp",
- "LabelMore": "Daha fazla",
- "LabelMoreInfo": "Daha fazla bilgi",
+ "LabelMetaTag": "Meta Etiketi",
+ "LabelMetaTags": "Meta Etiketleri",
+ "LabelMetadataOrderOfPrecedenceDescription": "Daha yüksek öncelikli üst veri kaynakları, daha düşük öncelikli üst veri kaynaklarının üzerine yazacaktır",
+ "LabelMetadataProvider": "Üst Veri Sağlayıcısı",
+ "LabelMinute": "Dakika",
+ "LabelMinutes": "Dakika",
+ "LabelMissing": "Eksik",
+ "LabelMissingEbook": "E-kitabı yok",
+ "LabelMissingSupplementaryEbook": "Ek e-kitabı yok",
+ "LabelMobileRedirectURIs": "İzin Verilen Mobil Yönlendirme URI'leri",
+ "LabelMobileRedirectURIsDescription": "Bu, mobil uygulamalar için geçerli yönlendirme URI'lerinin beyaz listesidir. Varsayılan olan audiobookshelf://oauth'dır, bunu kaldırabilir veya üçüncü taraf uygulama entegrasyonu için ek URI'lerle tamamlayabilirsiniz. Tek giriş olarak bir yıldız işareti (*) kullanmak herhangi bir URI'ye izin verir.",
+ "LabelMore": "Daha Fazla",
+ "LabelMoreInfo": "Daha Fazla Bilgi",
"LabelName": "İsim",
"LabelNarrator": "Anlatıcı",
"LabelNarrators": "Anlatıcılar",
+ "LabelNew": "Yeni",
+ "LabelNewPassword": "Yeni Şifre",
"LabelNewestAuthors": "En Yeni Yazarlar",
"LabelNewestEpisodes": "En Yeni Bölümler",
- "LabelNotFinished": "Tamamlanmadı",
+ "LabelNextBackupDate": "Sonraki yedekleme tarihi",
+ "LabelNextChapters": "Sonraki bölümler şunlar olacak:",
+ "LabelNextScheduledRun": "Sonraki zamanlanmış çalışma",
+ "LabelNoApiKeys": "API anahtarı yok",
+ "LabelNoCustomMetadataProviders": "Özel üst veri sağlayıcısı yok",
+ "LabelNoEpisodesSelected": "Seçili bölüm yok",
+ "LabelNotFinished": "Bitirilmedi",
"LabelNotStarted": "Başlanmadı",
+ "LabelNotes": "Notlar",
+ "LabelNotificationAppriseURL": "Apprise URL(leri)",
+ "LabelNotificationAvailableVariables": "Mevcut değişkenler",
+ "LabelNotificationBodyTemplate": "Gövde Şablonu",
+ "LabelNotificationEvent": "Bildirim Olayı",
+ "LabelNotificationTitleTemplate": "Başlık Şablonu",
+ "LabelNotificationsMaxFailedAttempts": "Maks. başarısız deneme sayısı",
+ "LabelNotificationsMaxFailedAttemptsHelp": "Bildirimler bu kadar çok kez gönderilemediğinde devre dışı bırakılır",
+ "LabelNotificationsMaxQueueSize": "Bildirim olayları için maks. sıra boyutu",
+ "LabelNotificationsMaxQueueSizeHelp": "Olaylar saniyede 1 kez tetiklenmekle sınırlıdır. Sıra maksimum boyuttaysa olaylar yoksayılır. Bu, bildirim spam'ini önler.",
+ "LabelNumberOfBooks": "Kitap Sayısı",
+ "LabelNumberOfChapters": "Bölüm sayısı:",
"LabelNumberOfEpisodes": "Bölüm Sayısı",
+ "LabelOpenIDAdvancedPermsClaimDescription": "Uygulama içindeki kullanıcı eylemleri için yönetici olmayan rollere uygulanacak gelişmiş izinleri içeren OpenID talebinin adı (yapılandırılmışsa). Talep yanıtta eksikse, ABS'ye erişim reddedilir. Tek bir seçenek eksikse, 'false' olarak kabul edilir. Kimlik sağlayıcısının talebinin beklenen yapıyla eşleştiğinden emin olun:",
+ "LabelOpenIDClaims": "Gelişmiş grup ve izin atamasını devre dışı bırakmak için aşağıdaki seçenekleri boş bırakın, bu durumda otomatik olarak 'Kullanıcı' grubu atanır.",
+ "LabelOpenIDGroupClaimDescription": "Kullanıcının gruplarının bir listesini içeren OpenID talebinin adı. Genellikle 'groups' olarak adlandırılır. (Yapılandırılmışsa), uygulama, bu grupların talepte büyük/küçük harfe duyarsız olarak 'admin', 'user' veya 'guest' olarak adlandırılması koşuluyla, kullanıcının grup üyeliklerine göre rolleri otomatik olarak atayacaktır. Talep bir liste içermelidir ve bir kullanıcı birden fazla gruba aitse, uygulama en yüksek erişim düzeyine karşılık gelen rolü atayacaktır. Hiçbir grup eşleşmezse, erişim reddedilir.",
+ "LabelOpenRSSFeed": "RSS Akışını Aç",
+ "LabelOverwrite": "Üzerine Yaz",
+ "LabelPaginationPageXOfY": "Sayfa {0} / {1}",
"LabelPassword": "Şifre",
"LabelPath": "Yol",
+ "LabelPermanent": "Kalıcı",
+ "LabelPermissionsAccessAllLibraries": "Tüm Kütüphanelere Erişebilir",
+ "LabelPermissionsAccessAllTags": "Tüm Etiketlere Erişebilir",
+ "LabelPermissionsAccessExplicitContent": "Açık İçeriğe Erişebilir",
+ "LabelPermissionsCreateEreader": "E-kitap Okuyucu Oluşturabilir",
+ "LabelPermissionsDelete": "Silebilir",
+ "LabelPermissionsDownload": "İndirebilir",
+ "LabelPermissionsUpdate": "Güncelleyebilir",
+ "LabelPermissionsUpload": "Yükleyebilir",
+ "LabelPersonalYearReview": "Yıl Değerlendirmen ({0})",
+ "LabelPhotoPathURL": "Fotoğraf Yolu/URL'si",
+ "LabelPlayMethod": "Oynatma Yöntemi",
+ "LabelPlaybackRateIncrementDecrement": "Oynatma Hızı Artırma/Azaltma Miktarı",
+ "LabelPlayerChapterNumberMarker": "{0} / {1}",
+ "LabelPlaylists": "Oynatma Listeleri",
"LabelPodcast": "Podcast",
- "LabelPodcasts": "Podcastler",
- "LabelPreventIndexing": "Akışınızın iTunes ve Google podcast dizinleri tarafından dizinlenmesini önleyin",
+ "LabelPodcastSearchRegion": "Podcast arama bölgesi",
+ "LabelPodcastType": "Podcast Türü",
+ "LabelPodcasts": "Podcast'ler",
+ "LabelPort": "Port",
+ "LabelPrefixesToIgnore": "Yoksayılacak Önekler (büyük/küçük harf duyarsız)",
+ "LabelPreventIndexing": "Akışınızın iTunes ve Google tarafından dizine eklenmesini önleyin",
+ "LabelPrimaryEbook": "Birincil e-kitap",
"LabelProgress": "İlerleme",
- "LabelPubDate": "Yay. Tarihi",
- "LabelPublishYear": "Yayım Yılı",
- "LabelPublishedDate": "Yayımlandı {0}",
- "LabelRSSFeedCustomOwnerEmail": "Özelleştirilmiş sahip Emaili",
- "LabelRSSFeedCustomOwnerName": "Özelleştirilmis sahip İsmi",
- "LabelRSSFeedPreventIndexing": "Dizinlemeyi Önle",
+ "LabelProvider": "Sağlayıcı",
+ "LabelProviderAuthorizationValue": "Yetkilendirme Başlık Değeri",
+ "LabelPubDate": "Yayın Tarihi",
+ "LabelPublishYear": "Yayın Yılı",
+ "LabelPublishedDate": "Yayınlandı: {0}",
+ "LabelPublishedDecade": "Yayınlandığı On Yıl",
+ "LabelPublishedDecades": "Yayınlandığı On Yıllar",
+ "LabelPublisher": "Yayıncı",
+ "LabelPublishers": "Yayıncılar",
+ "LabelRSSFeedCustomOwnerEmail": "Özel Sahip E-postası",
+ "LabelRSSFeedCustomOwnerName": "Özel Sahip Adı",
+ "LabelRSSFeedOpen": "RSS Akışı Açık",
+ "LabelRSSFeedPreventIndexing": "Dizine Eklemeyi Önle",
+ "LabelRSSFeedSlug": "RSS Akışı Kısa Adı",
+ "LabelRSSFeedURL": "RSS Akışı URL'si",
"LabelRandomly": "Rastgele",
+ "LabelReAddSeriesToContinueListening": "Seriyi 'Dinlemeye Devam Et'e yeniden ekle",
"LabelRead": "Oku",
"LabelReadAgain": "Tekrar Oku",
- "LabelRecentlyAdded": "Yakınlarda Eklenmiş",
+ "LabelReadEbookWithoutProgress": "İlerlemeyi kaydetmeden e-kitap oku",
+ "LabelRecentSeries": "Son Seriler",
+ "LabelRecentlyAdded": "Son Eklenenler",
+ "LabelRecommended": "Önerilenler",
+ "LabelRedo": "Yinele",
+ "LabelRegion": "Bölge",
+ "LabelReleaseDate": "Çıkış Tarihi",
+ "LabelRemoveAllMetadataAbs": "Tüm metadata.abs dosyalarını kaldır",
+ "LabelRemoveAllMetadataJson": "Tüm metadata.json dosyalarını kaldır",
+ "LabelRemoveAudibleBranding": "Bölümlerden Audible giriş ve çıkışını kaldır",
+ "LabelRemoveCover": "Kapağı kaldır",
+ "LabelRemoveMetadataFile": "Kütüphane öğesi klasörlerindeki üst veri dosyalarını kaldır",
+ "LabelRemoveMetadataFileHelp": "{0} klasörlerinizdeki tüm metadata.json ve metadata.abs dosyalarını kaldırın.",
+ "LabelRowsPerPage": "Sayfa başına satır",
+ "LabelSearchTerm": "Arama Terimi",
+ "LabelSearchTitle": "Başlık Ara",
+ "LabelSearchTitleOrASIN": "Başlık veya ASIN Ara",
"LabelSeason": "Sezon",
+ "LabelSeasonNumber": "Sezon #{0}",
+ "LabelSelectAll": "Tümünü Seç",
+ "LabelSelectAllEpisodes": "Tüm bölümleri seç",
+ "LabelSelectEpisodesShowing": "Gösterilen {0} bölümü seç",
+ "LabelSelectUser": "Kullanıcı seç",
+ "LabelSelectUsers": "Kullanıcıları seç",
+ "LabelSendEbookToDevice": "E-kitabı şuraya gönder...",
+ "LabelSequence": "Sıra",
+ "LabelSerial": "Seri",
"LabelSeries": "Seriler",
+ "LabelSeriesName": "Seri Adı",
+ "LabelSeriesProgress": "Seri İlerlemesi",
+ "LabelServerLogLevel": "Sunucu Kayıt Seviyesi",
+ "LabelServerYearReview": "Sunucu Yıl Değerlendirmesi ({0})",
"LabelSetEbookAsPrimary": "Birincil olarak ayarla",
- "LabelSetEbookAsSupplementary": "Yedek olarak ayarla",
- "LabelShowAll": "Hepsini Göster",
+ "LabelSetEbookAsSupplementary": "Ek olarak ayarla",
+ "LabelSettingsAllowIframe": "Iframe içine gömülmesine izin ver",
+ "LabelSettingsAudiobooksOnly": "Sadece sesli kitaplar",
+ "LabelSettingsAudiobooksOnlyHelp": "Bu ayarın etkinleştirilmesi, bir sesli kitap klasörünün içinde olmadıkları sürece e-kitap dosyalarını yoksayar, bu durumda ek e-kitaplar olarak ayarlanırlar.",
+ "LabelSettingsBookshelfViewHelp": "Ahşap raflı skeuomorfik tasarım",
+ "LabelSettingsChromecastSupport": "Chromecast desteği",
+ "LabelSettingsDateFormat": "Tarih Formatı",
+ "LabelSettingsEnableWatcher": "Değişiklikler için kütüphaneleri otomatik olarak tara",
+ "LabelSettingsEnableWatcherForLibrary": "Değişiklikler için kütüphaneyi otomatik olarak tara",
+ "LabelSettingsEnableWatcherHelp": "Dosya değişiklikleri algılandığında öğelerin otomatik olarak eklenmesini/güncellenmesini sağlar. *Sunucunun yeniden başlatılmasını gerektirir",
+ "LabelSettingsEpubsAllowScriptedContent": "Epub'larda betiklenmiş içeriğe izin ver",
+ "LabelSettingsEpubsAllowScriptedContentHelp": "Epub dosyalarının betik çalıştırmasına izin verin. Epub dosyalarının kaynağına güvenmiyorsanız bu ayarı devre dışı bırakmanız önerilir.",
+ "LabelSettingsExperimentalFeatures": "Deneysel özellikler",
+ "LabelSettingsExperimentalFeaturesHelp": "Geri bildiriminize ve test yardımınıza ihtiyaç duyabilecek geliştirme aşamasındaki özellikler. Github tartışmasını açmak için tıklayın.",
+ "LabelSettingsFindCovers": "Kapak bul",
+ "LabelSettingsFindCoversHelp": "Sesli kitabınızın gömülü bir kapağı veya klasör içinde bir kapak resmi yoksa, tarayıcı bir kapak bulmaya çalışır. Not: Bu, tarama süresini uzatacaktır",
+ "LabelSettingsHideSingleBookSeries": "Tek kitaplık serileri gizle",
+ "LabelSettingsHideSingleBookSeriesHelp": "Tek bir kitaba sahip seriler, seriler sayfasından ve ana sayfa raflarından gizlenir.",
+ "LabelSettingsHomePageBookshelfView": "Ana sayfa kitaplık görünümünü kullan",
+ "LabelSettingsLibraryBookshelfView": "Kütüphane kitaplık görünümünü kullan",
+ "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Tamamlanma yüzdesi şundan büyükse",
+ "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Kalan süre şundan azsa (saniye)",
+ "LabelSettingsLibraryMarkAsFinishedWhen": "Medya öğesini bitti olarak işaretle",
+ "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Seriye Devam Et'te önceki kitapları atla",
+ "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Seriye Devam Et ana sayfa rafı, en az bir kitabı bitmiş ve devam eden kitabı olmayan serilerde başlanmamış ilk kitabı gösterir. Bu ayarı etkinleştirmek, seriyi başlanmamış ilk kitaptan değil, en son tamamlanan kitaptan devam ettirir.",
+ "LabelSettingsParseSubtitles": "Altyazıları ayrıştır",
+ "LabelSettingsParseSubtitlesHelp": "Sesli kitap klasör adlarından altyazıları çıkarın. Altyazı \" - \" ile ayrılmalıdır örn. \"Kitap Başlığı - Bir Altyazı\" altyazısı \"Bir Altyazı\"dır",
+ "LabelSettingsPreferMatchedMetadata": "Eşleşen üst veriyi tercih et",
+ "LabelSettingsPreferMatchedMetadataHelp": "Hızlı Eşleştirme kullanıldığında eşleşen veriler öğe ayrıntılarının üzerine yazılır. Varsayılan olarak Hızlı Eşleştirme yalnızca eksik ayrıntıları doldurur.",
+ "LabelSettingsSkipMatchingBooksWithASIN": "Zaten bir ASIN'e sahip kitapları eşleştirmeyi atla",
+ "LabelSettingsSkipMatchingBooksWithISBN": "Zaten bir ISBN'e sahip kitapları eşleştirmeyi atla",
+ "LabelSettingsSortingIgnorePrefixes": "Sıralarken önekleri yoksay",
+ "LabelSettingsSortingIgnorePrefixesHelp": "örn. \"the\" öneki için \"The Book Title\" kitap başlığı \"Book Title, The\" olarak sıralanır",
+ "LabelSettingsSquareBookCovers": "Kare kitap kapakları kullan",
+ "LabelSettingsSquareBookCoversHelp": "Standart 1.6:1 kitap kapakları yerine kare kapakları tercih et",
+ "LabelSettingsStoreCoversWithItem": "Kapakları öğeyle birlikte sakla",
+ "LabelSettingsStoreCoversWithItemHelp": "Varsayılan olarak kapaklar /metadata/items içinde saklanır, bu ayarı etkinleştirmek kapakları kütüphane öğesi klasörünüzde saklar. Yalnızca \"cover\" adlı bir dosya tutulur",
+ "LabelSettingsStoreMetadataWithItem": "Üst veriyi öğeyle birlikte sakla",
+ "LabelSettingsStoreMetadataWithItemHelp": "Varsayılan olarak üst veri dosyaları /metadata/items içinde saklanır, bu ayarı etkinleştirmek üst veri dosyalarını kütüphane öğesi klasörlerinizde saklar",
+ "LabelSettingsTimeFormat": "Saat Formatı",
+ "LabelShare": "Paylaş",
+ "LabelShareDownloadableHelp": "Paylaşım bağlantısına sahip kullanıcıların kütüphane öğesinin bir zip dosyasını indirmesine olanak tanır.",
+ "LabelShareOpen": "Paylaşım Açık",
+ "LabelShareURL": "Paylaşım URL'si",
+ "LabelShowAll": "Tümünü Göster",
+ "LabelShowSeconds": "Saniyeleri göster",
+ "LabelShowSubtitles": "Altyazıları Göster",
"LabelSize": "Boyut",
"LabelSleepTimer": "Uyku Zamanlayıcısı",
- "LabelStart": "Başla",
+ "LabelSlug": "Kısa Ad (Slug)",
+ "LabelSortAscending": "Artan",
+ "LabelSortDescending": "Azalan",
+ "LabelSortPubDate": "Yayın Tarihine Göre Sırala",
+ "LabelStart": "Başlangıç",
+ "LabelStartTime": "Başlangıç Zamanı",
+ "LabelStarted": "Başlandı",
+ "LabelStartedAt": "Başlama Tarihi",
+ "LabelStartedDate": "Başlandı: {0}",
+ "LabelStatsAudioTracks": "Ses Parçaları",
+ "LabelStatsAuthors": "Yazarlar",
"LabelStatsBestDay": "En İyi Gün",
"LabelStatsDailyAverage": "Günlük Ortalama",
- "LabelStatsDays": "Günler",
- "LabelStatsDaysListened": "Dinlenen Günler",
- "LabelStatsInARow": "art arda",
- "LabelStatsItemsFinished": "Bitirilen Öğeler",
+ "LabelStatsDays": "Gün",
+ "LabelStatsDaysListened": "Dinlenen Gün Sayısı",
+ "LabelStatsHours": "Saat",
+ "LabelStatsInARow": "üst üste",
+ "LabelStatsItemsFinished": "Bitirilen Öğe Sayısı",
+ "LabelStatsItemsInLibrary": "Kütüphanedeki Öğe Sayısı",
"LabelStatsMinutes": "dakika",
"LabelStatsMinutesListening": "Dinlenen Dakika",
+ "LabelStatsOverallDays": "Toplam Gün",
+ "LabelStatsOverallHours": "Toplam Saat",
+ "LabelStatsWeekListening": "Haftalık Dinleme",
+ "LabelSubtitle": "Altyazı",
+ "LabelSupportedFileTypes": "Desteklenen Dosya Türleri",
"LabelTag": "Etiket",
"LabelTags": "Etiketler",
+ "LabelTagsAccessibleToUser": "Kullanıcının Erişebildiği Etiketler",
+ "LabelTagsNotAccessibleToUser": "Kullanıcının Erişemediği Etiketler",
+ "LabelTasks": "Çalışan Görevler",
+ "LabelTextEditorBulletedList": "Madde imli liste",
+ "LabelTextEditorLink": "Bağlantı",
+ "LabelTextEditorNumberedList": "Numaralı liste",
+ "LabelTextEditorUnlink": "Bağlantıyı kaldır",
"LabelTheme": "Tema",
"LabelThemeDark": "Koyu",
"LabelThemeLight": "Açık",
- "LabelTimeRemaining": "{0} kalan",
+ "LabelThemeSepia": "Sepya",
+ "LabelTimeBase": "Zaman Temeli",
+ "LabelTimeDurationXHours": "{0} saat",
+ "LabelTimeDurationXMinutes": "{0} dakika",
+ "LabelTimeDurationXSeconds": "{0} saniye",
+ "LabelTimeInMinutes": "Süre (dakika)",
+ "LabelTimeLeft": "{0} kaldı",
+ "LabelTimeListened": "Dinlenen Süre",
+ "LabelTimeListenedToday": "Bugün Dinlenen Süre",
+ "LabelTimeRemaining": "{0} kaldı",
+ "LabelTimeToShift": "Kaydırılacak süre (saniye)",
"LabelTitle": "Başlık",
+ "LabelToolsEmbedMetadata": "Üst Veri Göm",
+ "LabelToolsEmbedMetadataDescription": "Kapak resmi ve bölümler dahil olmak üzere üst verileri ses dosyalarına gömün.",
+ "LabelToolsM4bEncoder": "M4B Kodlayıcı",
+ "LabelToolsMakeM4b": "M4B Sesli Kitap Dosyası Oluştur",
+ "LabelToolsMakeM4bDescription": "Gömülü üst veri, kapak resmi ve bölümler içeren bir .M4B sesli kitap dosyası oluşturun.",
+ "LabelToolsSplitM4b": "M4B'yi MP3'lere Böl",
+ "LabelToolsSplitM4bDescription": "Gömülü üst veri, kapak resmi ve bölümlerle bir M4B'den bölümlere göre bölünmüş MP3'ler oluşturun.",
+ "LabelTotalDuration": "Toplam Süre",
+ "LabelTotalTimeListened": "Toplam Dinlenen Süre",
+ "LabelTrackFromFilename": "Dosya Adından Parça",
+ "LabelTrackFromMetadata": "Üst Veriden Parça",
"LabelTracks": "Parçalar",
+ "LabelTracksMultiTrack": "Çoklu parça",
+ "LabelTracksNone": "Parça yok",
+ "LabelTracksSingleTrack": "Tek parça",
+ "LabelTrailer": "Fragman",
"LabelType": "Tür",
+ "LabelUnabridged": "Kısaltılmamış",
+ "LabelUndo": "Geri Al",
"LabelUnknown": "Bilinmeyen",
+ "LabelUnknownPublishDate": "Bilinmeyen yayın tarihi",
+ "LabelUpdateCover": "Kapağı Güncelle",
+ "LabelUpdateCoverHelp": "Bir eşleşme bulunduğunda seçilen kitaplar için mevcut kapakların üzerine yazılmasına izin ver",
+ "LabelUpdateDetails": "Detayları Güncelle",
+ "LabelUpdateDetailsHelp": "Bir eşleşme bulunduğunda seçilen kitaplar için mevcut ayrıntıların üzerine yazılmasına izin ver",
+ "LabelUpdatedAt": "Güncellenme Tarihi",
+ "LabelUploaderDragAndDrop": "Dosyaları veya klasörleri sürükleyip bırakın",
+ "LabelUploaderDragAndDropFilesOnly": "Dosyaları sürükleyip bırakın",
+ "LabelUploaderDropFiles": "Dosyaları bırakın",
+ "LabelUploaderItemFetchMetadataHelp": "Başlık, yazar ve seriyi otomatik olarak getir",
+ "LabelUseAdvancedOptions": "Gelişmiş Seçenekleri Kullan",
+ "LabelUseChapterTrack": "Bölüm parçasını kullan",
+ "LabelUseFullTrack": "Tam parçayı kullan",
+ "LabelUseZeroForUnlimited": "Sınırsız için 0 kullanın",
"LabelUser": "Kullanıcı",
"LabelUsername": "Kullanıcı Adı",
- "LabelYearReviewHide": "Yıla Bakışı Sakla",
- "LabelYearReviewShow": "Yıla Bakışı Göster",
- "LabelYourBookmarks": "Yer İşaretleriniz",
- "LabelYourProgress": "Gelişiminiz"
+ "LabelValue": "Değer",
+ "LabelVersion": "Sürüm",
+ "LabelViewBookmarks": "Yer imlerini görüntüle",
+ "LabelViewChapters": "Bölümleri görüntüle",
+ "LabelViewPlayerSettings": "Oynatıcı ayarlarını görüntüle",
+ "LabelViewQueue": "Oynatma sırasını görüntüle",
+ "LabelVolume": "Ses Seviyesi",
+ "LabelWebRedirectURLsDescription": "Giriş yaptıktan sonra web uygulamasına geri yönlendirmeye izin vermek için OAuth sağlayıcınızda bu URL'leri yetkilendirin:",
+ "LabelWebRedirectURLsSubfolder": "Yönlendirme URL'leri için Alt Klasör",
+ "LabelWeekdaysToRun": "Çalıştırılacak hafta günleri",
+ "LabelXBooks": "{0} kitap",
+ "LabelXItems": "{0} öğe",
+ "LabelYearReviewHide": "Yıl Değerlendirmesini Gizle",
+ "LabelYearReviewShow": "Yıl Değerlendirmesini Gör",
+ "LabelYourAudiobookDuration": "Sesli kitabınızın süresi",
+ "LabelYourBookmarks": "Yer İmlerin",
+ "LabelYourPlaylists": "Oynatma Listelerin",
+ "LabelYourProgress": "İlerlemen",
+ "MessageAddToPlayerQueue": "Oynatma sırasına ekle",
+ "MessageAppriseDescription": "Bu özelliği kullanmak için Apprise API'nin bir örneğinin çalışıyor olması veya aynı istekleri işleyecek bir api'nizin olması gerekir. Apprise API Url'si, bildirimi göndermek için tam URL yolu olmalıdır, örn. API örneğiniz http://192.168.1.1:8337 adresinde sunuluyorsa http://192.168.1.1:8337/notify girmelisiniz.",
+ "MessageAsinCheck": "Amazon'dan değil, doğru Audible bölgesinden ASIN kullandığınızdan emin olun.",
+ "MessageAuthenticationLegacyTokenWarning": "Eski API jetonları gelecekte kaldırılacaktır. Bunun yerine API Anahtarları kullanın.",
+ "MessageAuthenticationOIDCChangesRestart": "OIDC değişikliklerini uygulamak için kaydettikten sonra sunucunuzu yeniden başlatın.",
+ "MessageAuthenticationSecurityMessage": "Güvenlik için kimlik doğrulama iyileştirildi. Tüm kullanıcıların yeniden giriş yapması gerekmektedir.",
+ "MessageBackupsDescription": "Yedekler; kullanıcıları, kullanıcı ilerlemesini, kütüphane öğesi ayrıntılarını, sunucu ayarlarını ve /metadata/items & /metadata/authors içinde depolanan görüntüleri içerir. Yedekler, kütüphane klasörlerinizde depolanan herhangi bir dosyayı içermez.",
+ "MessageBackupsLocationEditNote": "Not: Yedekleme konumunu güncellemek, mevcut yedekleri taşımaz veya değiştirmez",
+ "MessageBackupsLocationNoEditNote": "Not: Yedekleme konumu bir ortam değişkeni aracılığıyla ayarlanır ve buradan değiştirilemez.",
+ "MessageBackupsLocationPathEmpty": "Yedekleme konumu yolu boş olamaz",
+ "MessageBatchEditPopulateMapDetailsAllHelp": "Etkin alanları tüm öğelerden gelen verilerle doldurun. Birden çok değere sahip alanlar birleştirilecektir",
+ "MessageBatchEditPopulateMapDetailsItemHelp": "Etkin harita ayrıntıları alanlarını bu öğeden gelen verilerle doldurun",
+ "MessageBatchQuickMatchDescription": "Hızlı Eşleştirme, seçilen öğeler için eksik kapakları ve üst verileri eklemeye çalışacaktır. Hızlı Eşleştirme'nin mevcut kapakların ve/veya üst verilerin üzerine yazmasına izin vermek için aşağıdaki seçenekleri etkinleştirin.",
+ "MessageBookshelfNoCollections": "Henüz hiç koleksiyon oluşturmadın",
+ "MessageBookshelfNoCollectionsHelp": "Koleksiyonlar herkese açıktır. Kütüphaneye erişimi olan tüm kullanıcılar bunları görebilir.",
+ "MessageBookshelfNoRSSFeeds": "Açık RSS akışı yok",
+ "MessageBookshelfNoResultsForFilter": "\"{0}: {1}\" filtresi için sonuç bulunamadı",
+ "MessageBookshelfNoResultsForQuery": "Sorgu için sonuç bulunamadı",
+ "MessageBookshelfNoSeries": "Hiç serin yok",
+ "MessageBulkChapterPattern": "Bu numaralandırma deseniyle kaç bölüm eklemek istersiniz?",
+ "MessageChapterEndIsAfter": "Bölüm sonu, sesli kitabınızın sonundan sonra",
+ "MessageChapterErrorFirstNotZero": "İlk bölüm 0'da başlamalıdır",
+ "MessageChapterErrorStartGteDuration": "Geçersiz başlangıç zamanı, sesli kitap süresinden az olmalıdır",
+ "MessageChapterErrorStartLtPrev": "Geçersiz başlangıç zamanı, önceki bölüm başlangıç zamanından büyük veya ona eşit olmalıdır",
+ "MessageChapterStartIsAfter": "Bölüm başlangıcı, sesli kitabınızın sonundan sonra",
+ "MessageChaptersNotFound": "Bölüm bulunamadı",
+ "MessageCheckingCron": "Cron kontrol ediliyor...",
+ "MessageConfirmCloseFeed": "Bu akışı kapatmak istediğinizden emin misiniz?",
+ "MessageConfirmDeleteApiKey": "\"{0}\" adlı API anahtarını silmek istediğinizden emin misiniz?",
+ "MessageConfirmDeleteBackup": "{0} tarihli yedeği silmek istediğinizden emin misiniz?",
+ "MessageConfirmDeleteDevice": "\"{0}\" adlı e-kitap okuyucu cihazını silmek istediğinizden emin misiniz?",
+ "MessageConfirmDeleteFile": "Bu, dosyayı dosya sisteminizden silecektir. Emin misiniz?",
+ "MessageConfirmDeleteLibrary": "\"{0}\" adlı kütüphaneyi kalıcı olarak silmek istediğinizden emin misiniz?",
+ "MessageConfirmDeleteLibraryItem": "Bu, kütüphane öğesini veritabanından ve dosya sisteminizden silecektir. Emin misiniz?",
+ "MessageConfirmDeleteLibraryItems": "Bu, {0} kütüphane öğesini veritabanından ve dosya sisteminizden silecektir. Emin misiniz?",
+ "MessageConfirmDeleteMetadataProvider": "\"{0}\" adlı özel üst veri sağlayıcısını silmek istediğinizden emin misiniz?",
+ "MessageConfirmDeleteNotification": "Bu bildirimi silmek istediğinizden emin misiniz?",
+ "MessageConfirmDeleteSession": "Bu oturumu silmek istediğinizden emin misiniz?",
+ "MessageConfirmEmbedMetadataInAudioFiles": "{0} ses dosyasına üst veri gömmek istediğinizden emin misiniz?",
+ "MessageConfirmForceReScan": "Yeniden taramaya zorlamak istediğinizden emin misiniz?",
+ "MessageConfirmMarkAllEpisodesFinished": "Tüm bölümleri bitirildi olarak işaretlemek istediğinizden emin misiniz?",
+ "MessageConfirmMarkAllEpisodesNotFinished": "Tüm bölümleri bitirilmedi olarak işaretlemek istediğinizden emin misiniz?",
+ "MessageConfirmMarkItemFinished": "\"{0}\" öğesini bitirildi olarak işaretlemek istediğinizden emin misiniz?",
+ "MessageConfirmMarkItemNotFinished": "\"{0}\" öğesini bitirilmedi olarak işaretlemek istediğinizden emin misiniz?",
+ "MessageConfirmMarkSeriesFinished": "Bu serideki tüm kitapları bitirildi olarak işaretlemek istediğinizden emin misiniz?",
+ "MessageConfirmMarkSeriesNotFinished": "Bu serideki tüm kitapları bitirilmedi olarak işaretlemek istediğinizden emin misiniz?",
+ "MessageConfirmNotificationTestTrigger": "Bu bildirimi test verileriyle tetiklemek istiyor musunuz?",
+ "MessageConfirmPurgeCache": "Önbelleği temizlemek, /metadata/cache konumundaki tüm dizini silecektir.
Önbellek dizinini kaldırmak istediğinizden emin misiniz?",
+ "MessageConfirmPurgeItemsCache": "Öğe önbelleğini temizlemek, /metadata/cache/items konumundaki tüm dizini silecektir. Emin misiniz?",
+ "MessageConfirmQuickEmbed": "Uyarı! Hızlı gömme, ses dosyalarınızı yedeklemez. Ses dosyalarınızın bir yedeğine sahip olduğunuzdan emin olun.
Devam etmek ister misiniz?",
+ "MessageConfirmQuickMatchEpisodes": "Bölümleri hızlı eşleştirmek, bir eşleşme bulunursa ayrıntıların üzerine yazacaktır. Yalnızca eşleşmeyen bölümler güncellenecektir. Emin misiniz?",
+ "MessageConfirmReScanLibraryItems": "{0} öğeyi yeniden taramak istediğinizden emin misiniz?",
+ "MessageConfirmRemoveAllChapters": "Tüm bölümleri kaldırmak istediğinizden emin misiniz?",
+ "MessageConfirmRemoveAuthor": "\"{0}\" yazarını kaldırmak istediğinizden emin misiniz?",
+ "MessageConfirmRemoveCollection": "\"{0}\" koleksiyonunu kaldırmak istediğinizden emin misiniz?",
+ "MessageConfirmRemoveEpisode": "\"{0}\" bölümünü kaldırmak istediğinizden emin misiniz?",
+ "MessageConfirmRemoveEpisodeNote": "Not: Bu, \"Dosyayı kalıcı olarak sil\" seçeneği işaretlenmedikçe ses dosyasını silmez.",
+ "MessageConfirmRemoveEpisodes": "{0} bölümü kaldırmak istediğinizden emin misiniz?",
+ "MessageConfirmRemoveListeningSessions": "{0} dinleme oturumunu kaldırmak istediğinizden emin misiniz?",
+ "MessageConfirmRemoveMetadataFiles": "Kütüphane öğesi klasörlerinizdeki tüm metadata.{0} dosyalarını kaldırmak istediğinizden emin misiniz?",
+ "MessageConfirmRemoveNarrator": "\"{0}\" anlatıcısını kaldırmak istediğinizden emin misiniz?",
+ "MessageConfirmRemovePlaylist": "\"{0}\" adlı oynatma listenizi kaldırmak istediğinizden emin misiniz?",
+ "MessageConfirmRenameGenre": "\"{0}\" türünü tüm öğeler için \"{1}\" olarak yeniden adlandırmak istediğinizden emin misiniz?",
+ "MessageConfirmRenameGenreMergeNote": "Not: Bu tür zaten mevcut, bu yüzden birleştirilecekler.",
+ "MessageConfirmRenameGenreWarning": "Uyarı! Farklı bir büyük/küçük harf kullanımına sahip benzer bir tür zaten mevcut: \"{0}\".",
+ "MessageConfirmRenameTag": "\"{0}\" etiketini tüm öğeler için \"{1}\" olarak yeniden adlandırmak istediğinizden emin misiniz?",
+ "MessageConfirmRenameTagMergeNote": "Not: Bu etiket zaten mevcut, bu yüzden birleştirilecekler.",
+ "MessageConfirmRenameTagWarning": "Uyarı! Farklı bir büyük/küçük harf kullanımına sahip benzer bir etiket zaten mevcut: \"{0}\".",
+ "MessageConfirmResetProgress": "İlerlemenizi sıfırlamak istediğinizden emin misiniz?",
+ "MessageConfirmSendEbookToDevice": "{0} türündeki \"{1}\" adlı e-kitabı \"{2}\" cihazına göndermek istediğinizden emin misiniz?",
+ "MessageConfirmUnlinkOpenId": "Bu kullanıcının OpenID bağlantısını kaldırmak istediğinizden emin misiniz?",
+ "MessageDaysListenedInTheLastYear": "Son bir yılda {0} gün dinlendi",
+ "MessageDownloadingEpisode": "Bölüm indiriliyor",
+ "MessageDragFilesIntoTrackOrder": "Dosyaları doğru parça sırasına sürükleyin",
+ "MessageEmbedFailed": "Gömme Başarısız Oldu!",
+ "MessageEmbedFinished": "Gömme Tamamlandı!",
+ "MessageEmbedQueue": "Üst veri gömme için sıraya alındı (sırada {0} var)",
+ "MessageEpisodesQueuedForDownload": "{0} bölüm indirme için sıraya alındı",
+ "MessageEreaderDevices": "E-kitapların teslimatını sağlamak için, yukarıdaki e-posta adresini aşağıda listelenen her cihaz için geçerli bir gönderen olarak eklemeniz gerekebilir.",
+ "MessageFeedURLWillBe": "Akış URL'si {0} olacak",
+ "MessageFetching": "Alınıyor...",
+ "MessageForceReScanDescription": "tüm dosyaları yeni bir tarama gibi yeniden tarayacaktır. Ses dosyası ID3 etiketleri, OPF dosyaları ve metin dosyaları yeni olarak taranacaktır.",
+ "MessageHeatmapListeningTimeTooltip": "{1} tarihinde {0} dinleme",
+ "MessageHeatmapNoListeningSessions": "{0} tarihinde dinleme oturumu yok",
+ "MessageImportantNotice": "Önemli Duyuru!",
+ "MessageInsertChapterBelow": "Aşağıya bölüm ekle",
+ "MessageInvalidAsin": "Geçersiz ASIN",
+ "MessageItemsSelected": "{0} öğe seçildi",
+ "MessageItemsUpdated": "{0} öğe güncellendi",
+ "MessageJoinUsOn": "Bize katılın:",
+ "MessageLoading": "Yükleniyor...",
+ "MessageLoadingFolders": "Klasörler yükleniyor...",
+ "MessageLogsDescription": "Kayıtlar /metadata/logs içinde JSON dosyaları olarak saklanır. Çökme kayıtları /metadata/logs/crash_logs.txt içinde saklanır.",
+ "MessageM4BFailed": "M4B Başarısız Oldu!",
+ "MessageM4BFinished": "M4B Tamamlandı!",
+ "MessageMapChapterTitles": "Zaman damgalarını ayarlamadan bölüm başlıklarını mevcut sesli kitap bölümlerinizle eşleyin",
+ "MessageMarkAllEpisodesFinished": "Tüm bölümleri bitirildi olarak işaretle",
+ "MessageMarkAllEpisodesNotFinished": "Tüm bölümleri bitirilmedi olarak işaretle",
+ "MessageMarkAsFinished": "Bitirildi Olarak İşaretle",
+ "MessageMarkAsNotFinished": "Bitirilmedi Olarak İşaretle",
+ "MessageMatchBooksDescription": "kütüphanedeki kitapları seçilen arama sağlayıcısından bir kitapla eşleştirmeye ve boş ayrıntıları ve kapak resmini doldurmaya çalışacaktır. Ayrıntıların üzerine yazmaz.",
+ "MessageNoAudioTracks": "Ses parçası yok",
+ "MessageNoAuthors": "Yazar yok",
+ "MessageNoBackups": "Yedek yok",
+ "MessageNoBookmarks": "Yer İmi Yok",
+ "MessageNoChapters": "Bölüm Yok",
+ "MessageNoCollections": "Koleksiyon Yok",
+ "MessageNoCoversFound": "Kapak bulunamadı",
+ "MessageNoDescription": "Açıklama yok",
+ "MessageNoDevices": "Cihaz yok",
+ "MessageNoDownloadsInProgress": "Şu anda devam eden indirme yok",
+ "MessageNoDownloadsQueued": "Sırada bekleyen indirme yok",
+ "MessageNoEpisodeMatchesFound": "Bölüm eşleşmesi bulunamadı",
+ "MessageNoEpisodes": "Bölüm yok",
+ "MessageNoFoldersAvailable": "Kullanılabilir klasör yok",
+ "MessageNoGenres": "Tür yok",
+ "MessageNoIssues": "Sorun yok",
+ "MessageNoItems": "Öğe Yok",
+ "MessageNoItemsFound": "Öğe bulunamadı",
+ "MessageNoListeningSessions": "Dinleme Oturumu Yok",
+ "MessageNoLogs": "Kayıt yok",
+ "MessageNoMediaProgress": "Medya ilerlemesi yok",
+ "MessageNoNotifications": "Bildirim yok",
+ "MessageNoPodcastFeed": "Geçersiz podcast: Akış Yok",
+ "MessageNoPodcastsFound": "Podcast bulunamadı",
+ "MessageNoResults": "Sonuç yok",
+ "MessageNoSearchResultsFor": "\"{0}\" için arama sonucu bulunamadı",
+ "MessageNoSeries": "Seri yok",
+ "MessageNoTags": "Etiket yok",
+ "MessageNoTasksRunning": "Çalışan görev yok",
+ "MessageNoUpdatesWereNecessary": "Güncelleme gerekmedi",
+ "MessageNoUserPlaylists": "Hiç oynatma listeniz yok",
+ "MessageNoUserPlaylistsHelp": "Oynatma listeleri özeldir. Yalnızca oluşturan kullanıcı bunları görebilir.",
+ "MessageNotYetImplemented": "Henüz uygulanmadı",
+ "MessageOpmlPreviewNote": "Not: Bu, ayrıştırılmış OPML dosyasının bir önizlemesidir. Gerçek podcast başlığı RSS akışından alınacaktır.",
+ "MessageOr": "veya",
+ "MessagePauseChapter": "Bölüm oynatımını duraklat",
+ "MessagePlayChapter": "Bölümün başlangıcını dinle",
+ "MessagePlaylistCreateFromCollection": "Koleksiyondan oynatma listesi oluştur",
+ "MessagePleaseWait": "Lütfen bekleyin...",
+ "MessagePodcastHasNoRSSFeedForMatching": "Podcast'in eşleştirme için kullanılabilecek bir RSS akış URL'si yok",
+ "MessagePodcastSearchField": "Arama terimi veya RSS akış URL'si girin",
+ "MessageQuickEmbedInProgress": "Hızlı gömme devam ediyor",
+ "MessageQuickEmbedQueue": "Hızlı gömme için sıraya alındı (sırada {0} var)",
+ "MessageQuickMatchAllEpisodes": "Tüm Bölümleri Hızlı Eşleştir",
+ "MessageQuickMatchDescription": "Boş öğe ayrıntılarını ve kapağı '{0}'dan gelen ilk eşleşme sonucuyla doldurun. 'Eşleşen üst veriyi tercih et' sunucu ayarı etkinleştirilmedikçe ayrıntıların üzerine yazmaz.",
+ "MessageRemoveChapter": "Bölümü kaldır",
+ "MessageRemoveEpisodes": "{0} bölümü kaldır",
+ "MessageRemoveFromPlayerQueue": "Oynatma sırasından kaldır",
+ "MessageRemoveUserWarning": "\"{0}\" kullanıcısını kalıcı olarak silmek istediğinizden emin misiniz?",
+ "MessageReportBugsAndContribute": "Hataları bildirin, özellikler isteyin ve katkıda bulunun:",
+ "MessageResetChaptersConfirm": "Bölümleri sıfırlamak ve yaptığınız değişiklikleri geri almak istediğinizden emin misiniz?",
+ "MessageRestoreBackupConfirm": "Şu tarihte oluşturulan yedeği geri yüklemek istediğinizden emin misiniz:",
+ "MessageRestoreBackupWarning": "Bir yedeği geri yüklemek, /config konumundaki tüm veritabanının ve /metadata/items & /metadata/authors içindeki kapak resimlerinin üzerine yazacaktır.
Yedekler, kütüphane klasörlerinizdeki hiçbir dosyayı değiştirmez. Sunucu ayarlarını kütüphane klasörlerinizde kapak resmi ve üst veri saklamak için etkinleştirdiyseniz, bunlar yedeklenmez veya üzerine yazılmaz.
Sunucunuzu kullanan tüm istemciler otomatik olarak yenilenecektir.",
+ "MessageScheduleLibraryScanNote": "Çoğu kullanıcı için, bu özelliği devre dışı bırakıp klasör izleyici ayarını etkin tutmaları önerilir. Klasör izleyici, kütüphane klasörlerinizdeki değişiklikleri otomatik olarak algılayacaktır. Klasör izleyici her dosya sistemi için (NFS gibi) çalışmaz, bu nedenle bunun yerine zamanlanmış kütüphane taramaları kullanılabilir.",
+ "MessageScheduleRunEveryWeekdayAtTime": "Her {0} günü saat {1}'de çalıştır",
+ "MessageSearchResultsFor": "Arama sonuçları:",
+ "MessageSelected": "{0} seçildi",
+ "MessageSeriesSequenceCannotContainSpaces": "Seri sırası boşluk içeremez",
+ "MessageServerCouldNotBeReached": "Sunucuya ulaşılamadı",
+ "MessageSetChaptersFromTracksDescription": "Her ses dosyasını bir bölüm ve bölüm başlığını ses dosyası adı olarak kullanarak bölümleri ayarlayın",
+ "MessageShareExpirationWillBe": "Son kullanma tarihi {0} olacak",
+ "MessageShareExpiresIn": "{0} içinde sona eriyor",
+ "MessageShareURLWillBe": "Paylaşım URL'si {0} olacak",
+ "MessageStartPlaybackAtTime": "\"{0}\" için oynatmayı {1}'de başlatmak istiyor musunuz?",
+ "MessageTaskAudioFileNotWritable": "\"{0}\" ses dosyası yazılabilir değil",
+ "MessageTaskCanceledByUser": "Görev kullanıcı tarafından iptal edildi",
+ "MessageTaskDownloadingEpisodeDescription": "\"{0}\" bölümü indiriliyor",
+ "MessageTaskEmbeddingMetadata": "Üst veri gömülüyor",
+ "MessageTaskEmbeddingMetadataDescription": "\"{0}\" sesli kitabına üst veri gömülüyor",
+ "MessageTaskEncodingM4b": "M4B kodlanıyor",
+ "MessageTaskEncodingM4bDescription": "\"{0}\" sesli kitabı tek bir m4b dosyasına kodlanıyor",
+ "MessageTaskFailed": "Başarısız oldu",
+ "MessageTaskFailedToBackupAudioFile": "\"{0}\" ses dosyasını yedekleme başarısız oldu",
+ "MessageTaskFailedToCreateCacheDirectory": "Önbellek dizini oluşturma başarısız oldu",
+ "MessageTaskFailedToEmbedMetadataInFile": "\"{0}\" dosyasına üst veri gömme başarısız oldu",
+ "MessageTaskFailedToMergeAudioFiles": "Ses dosyalarını birleştirme başarısız oldu",
+ "MessageTaskFailedToMoveM4bFile": "m4b dosyasını taşıma başarısız oldu",
+ "MessageTaskFailedToWriteMetadataFile": "Üst veri dosyasını yazma başarısız oldu",
+ "MessageTaskMatchingBooksInLibrary": "\"{0}\" kütüphanesindeki kitaplar eşleştiriliyor",
+ "MessageTaskNoFilesToScan": "Taranacak dosya yok",
+ "MessageTaskOpmlImport": "OPML içe aktarma",
+ "MessageTaskOpmlImportDescription": "{0} RSS akışından podcast'ler oluşturuluyor",
+ "MessageTaskOpmlImportFeed": "OPML içe aktarma akışı",
+ "MessageTaskOpmlImportFeedDescription": "\"{0}\" RSS akışı içe aktarılıyor",
+ "MessageTaskOpmlImportFeedFailed": "Podcast akışı alınamadı",
+ "MessageTaskOpmlImportFeedPodcastDescription": "\"{0}\" podcast'i oluşturuluyor",
+ "MessageTaskOpmlImportFeedPodcastExists": "Podcast zaten bu yolda mevcut",
+ "MessageTaskOpmlImportFeedPodcastFailed": "Podcast oluşturulamadı",
+ "MessageTaskOpmlImportFinished": "{0} podcast eklendi",
+ "MessageTaskOpmlParseFailed": "OPML dosyası ayrıştırılamadı",
+ "MessageTaskOpmlParseFastFail": "Geçersiz OPML dosyası, etiketi bulunamadı VEYA bir etiketi bulunamadı",
+ "MessageTaskOpmlParseNoneFound": "OPML dosyasında akış bulunamadı",
+ "MessageTaskScanItemsAdded": "{0} eklendi",
+ "MessageTaskScanItemsMissing": "{0} eksik",
+ "MessageTaskScanItemsUpdated": "{0} güncellendi",
+ "MessageTaskScanNoChangesNeeded": "Değişiklik gerekmiyor",
+ "MessageTaskScanningFileChanges": "\"{0}\" içindeki dosya değişiklikleri taranıyor",
+ "MessageTaskScanningLibrary": "\"{0}\" kütüphanesi taranıyor",
+ "MessageTaskTargetDirectoryNotWritable": "Hedef dizin yazılabilir değil",
+ "MessageThinking": "Düşünüyor...",
+ "MessageUploaderItemFailed": "Yükleme başarısız oldu",
+ "MessageUploaderItemSuccess": "Başarıyla Yüklendi!",
+ "MessageUploading": "Yükleniyor...",
+ "MessageValidCronExpression": "Geçerli cron ifadesi",
+ "MessageWatcherIsDisabledGlobally": "İzleyici genel olarak sunucu ayarlarında devre dışı bırakıldı",
+ "MessageXLibraryIsEmpty": "{0} Kütüphanesi boş!",
+ "MessageYourAudiobookDurationIsLonger": "Sesli kitabınızın süresi bulunan süreden daha uzun",
+ "MessageYourAudiobookDurationIsShorter": "Sesli kitabınızın süresi bulunan süreden daha kısa",
+ "NoteChangeRootPassword": "Root kullanıcısı, boş şifreye sahip olabilen tek kullanıcıdır",
+ "NoteChapterEditorTimes": "Not: İlk bölüm başlangıç zamanı 0:00'da kalmalı ve son bölüm başlangıç zamanı bu sesli kitabın süresini aşamaz.",
+ "NoteFolderPicker": "Not: Zaten eşlenmiş klasörler gösterilmeyecektir",
+ "NoteRSSFeedPodcastAppsHttps": "Uyarı: Çoğu podcast uygulaması, RSS akış URL'sinin HTTPS kullanmasını gerektirir",
+ "NoteRSSFeedPodcastAppsPubDate": "Uyarı: Bölümlerinizden bir veya daha fazlasının Yayın Tarihi yok. Bazı podcast uygulamaları bunu zorunlu kılar.",
+ "NoteUploaderFoldersWithMediaFiles": "Medya dosyaları içeren klasörler ayrı kütüphane öğeleri olarak ele alınacaktır.",
+ "NoteUploaderOnlyAudioFiles": "Yalnızca ses dosyaları yükleniyorsa, her ses dosyası ayrı bir sesli kitap olarak ele alınacaktır.",
+ "NoteUploaderUnsupportedFiles": "Desteklenmeyen dosyalar yoksayılır. Bir klasör seçerken veya bırakırken, bir öğe klasöründe olmayan diğer dosyalar yoksayılır.",
+ "NotificationOnBackupCompletedDescription": "Bir yedekleme tamamlandığında tetiklenir",
+ "NotificationOnBackupFailedDescription": "Bir yedekleme başarısız olduğunda tetiklenir",
+ "NotificationOnEpisodeDownloadedDescription": "Bir podcast bölümü otomatik indirildiğinde tetiklenir",
+ "NotificationOnRSSFeedDisabledDescription": "Çok fazla başarısız deneme nedeniyle otomatik bölüm indirmeleri devre dışı bırakıldığında tetiklenir",
+ "NotificationOnRSSFeedFailedDescription": "Bir otomatik bölüm indirme için RSS akışı isteği başarısız olduğunda tetiklenir",
+ "NotificationOnTestDescription": "Bildirim sistemini test etmek için olay",
+ "PlaceholderBulkChapterInput": "Bölüm başlığı girin veya numaralandırma kullanın (ör. 'Bölüm 1', 'Kısım 10', '1.')",
+ "PlaceholderNewCollection": "Yeni koleksiyon adı",
+ "PlaceholderNewFolderPath": "Yeni klasör yolu",
+ "PlaceholderNewPlaylist": "Yeni oynatma listesi adı",
+ "PlaceholderSearch": "Ara...",
+ "PlaceholderSearchEpisode": "Bölüm ara...",
+ "StatsAuthorsAdded": "yazar eklendi",
+ "StatsBooksAdded": "kitap eklendi",
+ "StatsBooksAdditional": "Eklenenlerden bazıları…",
+ "StatsBooksFinished": "kitap bitirildi",
+ "StatsBooksFinishedThisYear": "Bu yıl bitirilen bazı kitaplar…",
+ "StatsBooksListenedTo": "kitap dinlendi",
+ "StatsCollectionGrewTo": "Kitap koleksiyonun şuna ulaştı…",
+ "StatsSessions": "oturum",
+ "StatsSpentListening": "dinleyerek geçirildi",
+ "StatsTopAuthor": "EN İYİ YAZAR",
+ "StatsTopAuthors": "EN İYİ YAZARLAR",
+ "StatsTopGenre": "EN İYİ TÜR",
+ "StatsTopGenres": "EN İYİ TÜRLER",
+ "StatsTopMonth": "EN İYİ AY",
+ "StatsTopNarrator": "EN İYİ ANLATICI",
+ "StatsTopNarrators": "EN İYİ ANLATICILAR",
+ "StatsTotalDuration": "Toplam süresi…",
+ "StatsYearInReview": "YILA GENEL BAKIŞ",
+ "ToastAccountUpdateSuccess": "Hesap güncellendi",
+ "ToastAppriseUrlRequired": "Bir Apprise URL'si girilmelidir",
+ "ToastAsinRequired": "ASIN gereklidir",
+ "ToastAuthorImageRemoveSuccess": "Yazar resmi kaldırıldı",
+ "ToastAuthorNotFound": "\"{0}\" yazarı bulunamadı",
+ "ToastAuthorRemoveSuccess": "Yazar kaldırıldı",
+ "ToastAuthorSearchNotFound": "Yazar bulunamadı",
+ "ToastAuthorUpdateMerged": "Yazar birleştirildi",
+ "ToastAuthorUpdateSuccess": "Yazar güncellendi",
+ "ToastAuthorUpdateSuccessNoImageFound": "Yazar güncellendi (resim bulunamadı)",
+ "ToastBackupAppliedSuccess": "Yedek uygulandı",
+ "ToastBackupCreateFailed": "Yedek oluşturulamadı",
+ "ToastBackupCreateSuccess": "Yedek oluşturuldu",
+ "ToastBackupDeleteFailed": "Yedek silinemedi",
+ "ToastBackupDeleteSuccess": "Yedek silindi",
+ "ToastBackupInvalidMaxKeep": "Geçersiz saklanacak yedek sayısı",
+ "ToastBackupInvalidMaxSize": "Geçersiz maksimum yedek boyutu",
+ "ToastBackupRestoreFailed": "Yedek geri yüklenemedi",
+ "ToastBackupUploadFailed": "Yedek yüklenemedi",
+ "ToastBackupUploadSuccess": "Yedek yüklendi",
+ "ToastBatchApplyDetailsToItemsSuccess": "Ayrıntılar öğelere uygulandı",
+ "ToastBatchDeleteFailed": "Toplu silme başarısız oldu",
+ "ToastBatchDeleteSuccess": "Toplu silme başarılı oldu",
+ "ToastBatchQuickMatchFailed": "Toplu Hızlı Eşleştirme başarısız oldu!",
+ "ToastBatchQuickMatchStarted": "{0} kitabın Toplu Hızlı Eşleştirmesi başladı!",
+ "ToastBatchUpdateFailed": "Toplu güncelleme başarısız oldu",
+ "ToastBatchUpdateSuccess": "Toplu güncelleme başarılı oldu",
+ "ToastBookmarkCreateFailed": "Yer işareti oluşturulamadı",
+ "ToastBookmarkCreateSuccess": "Yer imi eklendi",
+ "ToastBookmarkRemoveSuccess": "Yer imi kaldırıldı",
+ "ToastBulkChapterInvalidCount": "1 ile 150 arasında bir sayı girin",
+ "ToastCachePurgeFailed": "Önbellek temizlenemedi",
+ "ToastCachePurgeSuccess": "Önbellek başarıyla temizlendi",
+ "ToastChapterLocked": "Bölüm kilitli.",
+ "ToastChapterStartTimeAdjusted": "Bölüm başlangıç zamanı {0} saniye ayarlandı",
+ "ToastChaptersAllLocked": "Tüm bölümler kilitli. Zamanlarını kaydırmak için bazı bölümlerin kilidini açın.",
+ "ToastChaptersHaveErrors": "Bölümlerde hatalar var",
+ "ToastChaptersInvalidShiftAmountLast": "Geçersiz kaydırma miktarı. Son bölüm başlangıç zamanı bu sesli kitabın süresini aşacaktır.",
+ "ToastChaptersInvalidShiftAmountStart": "Geçersiz kaydırma miktarı. İlk bölüm sıfır veya negatif uzunlukta olacak ve ikinci bölüm tarafından üzerine yazılacaktır. İkinci bölümün başlangıç süresini artırın.",
+ "ToastChaptersMustHaveTitles": "Bölümlerin başlıkları olmalıdır",
+ "ToastChaptersRemoved": "Bölümler kaldırıldı",
+ "ToastChaptersUpdated": "Bölümler güncellendi",
+ "ToastCollectionItemsAddFailed": "Öğe(ler) koleksiyona eklenemedi",
+ "ToastCollectionRemoveSuccess": "Koleksiyon kaldırıldı",
+ "ToastCollectionUpdateSuccess": "Koleksiyon güncellendi",
+ "ToastCoverUpdateFailed": "Kapak güncellemesi başarısız oldu",
+ "ToastDateTimeInvalidOrIncomplete": "Tarih ve saat geçersiz veya eksik",
+ "ToastDeleteFileFailed": "Dosya silinemedi",
+ "ToastDeleteFileSuccess": "Dosya silindi",
+ "ToastDeviceAddFailed": "Cihaz eklenemedi",
+ "ToastDeviceNameAlreadyExists": "Bu ada sahip bir e-kitap okuyucu cihazı zaten mevcut",
+ "ToastDeviceTestEmailFailed": "Test e-postası gönderilemedi",
+ "ToastDeviceTestEmailSuccess": "Test e-postası gönderildi",
+ "ToastEmailSettingsUpdateSuccess": "E-posta ayarları güncellendi",
+ "ToastEncodeCancelFailed": "Kodlama iptal edilemedi",
+ "ToastEncodeCancelSucces": "Kodlama iptal edildi",
+ "ToastEpisodeDownloadQueueClearFailed": "Sıra temizlenemedi",
+ "ToastEpisodeDownloadQueueClearSuccess": "Bölüm indirme sırası temizlendi",
+ "ToastEpisodeUpdateSuccess": "{0} bölüm güncellendi",
+ "ToastErrorCannotShare": "Bu cihazda yerel olarak paylaşılamıyor",
+ "ToastFailedToCreate": "Oluşturulamadı",
+ "ToastFailedToDelete": "Silinemedi",
+ "ToastFailedToLoadData": "Veri yüklenemedi",
+ "ToastFailedToMatch": "Eşleştirilemedi",
+ "ToastFailedToShare": "Paylaşılamadı",
+ "ToastFailedToUpdate": "Güncellenemedi",
+ "ToastInvalidImageUrl": "Geçersiz resim URL'si",
+ "ToastInvalidMaxEpisodesToDownload": "Geçersiz maks. indirilecek bölüm sayısı",
+ "ToastInvalidUrl": "Geçersiz URL",
+ "ToastInvalidUrls": "Bir veya daha fazla URL geçersiz",
+ "ToastItemCoverUpdateSuccess": "Öğe kapağı güncellendi",
+ "ToastItemDeletedFailed": "Öğe silinemedi",
+ "ToastItemDeletedSuccess": "Öğe silindi",
+ "ToastItemDetailsUpdateSuccess": "Öğe ayrıntıları güncellendi",
+ "ToastItemMarkedAsFinishedFailed": "Bitirildi olarak işaretlenemedi",
+ "ToastItemMarkedAsFinishedSuccess": "Öğe bitirildi olarak işaretlendi",
+ "ToastItemMarkedAsNotFinishedFailed": "Bitirilmedi olarak işaretlenemedi",
+ "ToastItemMarkedAsNotFinishedSuccess": "Öğe bitirilmedi olarak işaretlendi",
+ "ToastItemUpdateSuccess": "Öğe güncellendi",
+ "ToastLibraryCreateFailed": "Kütüphane oluşturulamadı",
+ "ToastLibraryCreateSuccess": "\"{0}\" kütüphanesi oluşturuldu",
+ "ToastLibraryDeleteFailed": "Kütüphane silinemedi",
+ "ToastLibraryDeleteSuccess": "Kütüphane silindi",
+ "ToastLibraryScanFailedToStart": "Tarama başlatılamadı",
+ "ToastLibraryScanStarted": "Kütüphane taraması başladı",
+ "ToastLibraryUpdateSuccess": "\"{0}\" kütüphanesi güncellendi",
+ "ToastMatchAllAuthorsFailed": "Tüm yazarlar eşleştirilemedi",
+ "ToastMetadataFilesRemovedError": "metadata.{0} dosyaları kaldırılırken hata oluştu",
+ "ToastMetadataFilesRemovedNoneFound": "Kütüphanede metadata.{0} dosyası bulunamadı",
+ "ToastMetadataFilesRemovedNoneRemoved": "Hiçbir metadata.{0} dosyası kaldırılmadı",
+ "ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} dosyası kaldırıldı",
+ "ToastMustHaveAtLeastOnePath": "En az bir yol olmalıdır",
+ "ToastNameEmailRequired": "İsim ve e-posta gereklidir",
+ "ToastNameRequired": "İsim gereklidir",
+ "ToastNewApiKeyUserError": "Bir kullanıcı seçilmelidir",
+ "ToastNewEpisodesFound": "{0} yeni bölüm bulundu",
+ "ToastNewUserCreatedFailed": "Hesap oluşturulamadı: \"{0}\"",
+ "ToastNewUserCreatedSuccess": "Yeni hesap oluşturuldu",
+ "ToastNewUserLibraryError": "En az bir kütüphane seçilmelidir",
+ "ToastNewUserPasswordError": "Bir şifre olmalıdır, yalnızca root kullanıcısının boş şifresi olabilir",
+ "ToastNewUserTagError": "En az bir etiket seçilmelidir",
+ "ToastNewUserUsernameError": "Bir kullanıcı adı girin",
+ "ToastNoNewEpisodesFound": "Yeni bölüm bulunamadı",
+ "ToastNoRSSFeed": "Podcast'in bir RSS Akışı yok",
+ "ToastNoUpdatesNecessary": "Güncelleme gerekmiyor",
+ "ToastNotificationCreateFailed": "Bildirim oluşturulamadı",
+ "ToastNotificationDeleteFailed": "Bildirim silinemedi",
+ "ToastNotificationFailedMaximum": "Maks. başarısız deneme sayısı >= 0 olmalıdır",
+ "ToastNotificationQueueMaximum": "Maks. bildirim sırası >= 0 olmalıdır",
+ "ToastNotificationSettingsUpdateSuccess": "Bildirim ayarları güncellendi",
+ "ToastNotificationTestTriggerFailed": "Test bildirimi tetiklenemedi",
+ "ToastNotificationTestTriggerSuccess": "Test bildirimi tetiklendi",
+ "ToastNotificationUpdateSuccess": "Bildirim güncellendi",
+ "ToastPlaylistCreateFailed": "Oynatma listesi oluşturulamadı",
+ "ToastPlaylistCreateSuccess": "Oynatma listesi oluşturuldu",
+ "ToastPlaylistRemoveSuccess": "Oynatma listesi kaldırıldı",
+ "ToastPlaylistUpdateSuccess": "Oynatma listesi güncellendi",
+ "ToastPodcastCreateFailed": "Podcast oluşturulamadı",
+ "ToastPodcastCreateSuccess": "Podcast başarıyla oluşturuldu",
+ "ToastPodcastEpisodeUpdated": "Bölüm güncellendi",
+ "ToastPodcastGetFeedFailed": "Podcast akışı alınamadı",
+ "ToastPodcastNoEpisodesInFeed": "RSS akışında bölüm bulunamadı",
+ "ToastPodcastNoRssFeed": "Podcast'in bir RSS akışı yok",
+ "ToastProgressIsNotBeingSynced": "İlerleme senkronize edilmiyor, oynatmayı yeniden başlatın",
+ "ToastProviderCreatedFailed": "Sağlayıcı eklenemedi",
+ "ToastProviderCreatedSuccess": "Yeni sağlayıcı eklendi",
+ "ToastProviderNameAndUrlRequired": "İsim ve Url gereklidir",
+ "ToastProviderRemoveSuccess": "Sağlayıcı kaldırıldı",
+ "ToastRSSFeedCloseFailed": "RSS akışı kapatılamadı",
+ "ToastRSSFeedCloseSuccess": "RSS akışı kapatıldı",
+ "ToastRemoveFailed": "Kaldırılamadı",
+ "ToastRemoveItemFromCollectionFailed": "Öğe koleksiyondan kaldırılamadı",
+ "ToastRemoveItemFromCollectionSuccess": "Öğe koleksiyondan kaldırıldı",
+ "ToastRemoveItemsWithIssuesFailed": "Sorunlu kütüphane öğeleri kaldırılamadı",
+ "ToastRemoveItemsWithIssuesSuccess": "Sorunlu kütüphane öğeleri kaldırıldı",
+ "ToastRenameFailed": "Yeniden adlandırılamadı",
+ "ToastRescanFailed": "{0} için Yeniden Tarama Başarısız Oldu",
+ "ToastRescanRemoved": "Yeniden Tarama tamamlandı, öğe kaldırıldı",
+ "ToastRescanUpToDate": "Yeniden Tarama tamamlandı, öğe günceldi",
+ "ToastRescanUpdated": "Yeniden Tarama tamamlandı, öğe güncellendi",
+ "ToastScanFailed": "Kütüphane öğesi taranamadı",
+ "ToastSelectAtLeastOneUser": "En az bir kullanıcı seçin",
+ "ToastSendEbookToDeviceFailed": "E-kitap cihaza gönderilemedi",
+ "ToastSendEbookToDeviceSuccess": "E-kitap \"{0}\" cihazına gönderildi",
+ "ToastSeriesSubmitFailedSameName": "Aynı ada sahip iki seri eklenemez",
+ "ToastSeriesUpdateFailed": "Seri güncellemesi başarısız oldu",
+ "ToastSeriesUpdateSuccess": "Seri güncellemesi başarılı oldu",
+ "ToastServerSettingsUpdateSuccess": "Sunucu ayarları güncellendi",
+ "ToastSessionCloseFailed": "Oturum kapatılamadı",
+ "ToastSessionDeleteFailed": "Oturum silinemedi",
+ "ToastSessionDeleteSuccess": "Oturum silindi",
+ "ToastSleepTimerDone": "Uyku zamanlayıcısı bitti... zZzzZz",
+ "ToastSlugMustChange": "Kısa ad (slug) geçersiz karakterler içeriyor",
+ "ToastSlugRequired": "Kısa ad (slug) gereklidir",
+ "ToastSocketConnected": "Soket bağlandı",
+ "ToastSocketDisconnected": "Soket bağlantısı kesildi",
+ "ToastSocketFailedToConnect": "Soket bağlanamadı",
+ "ToastSortingPrefixesEmptyError": "En az 1 sıralama öneki olmalıdır",
+ "ToastSortingPrefixesUpdateSuccess": "Sıralama önekleri güncellendi ({0} öğe)",
+ "ToastTitleRequired": "Başlık gereklidir",
+ "ToastUnknownError": "Bilinmeyen hata",
+ "ToastUnlinkOpenIdFailed": "Kullanıcının OpenID bağlantısı kaldırılamadı",
+ "ToastUnlinkOpenIdSuccess": "Kullanıcının OpenID bağlantısı kaldırıldı",
+ "ToastUploaderFilepathExistsError": "\"{0}\" dosya yolu sunucuda zaten mevcut",
+ "ToastUploaderItemExistsInSubdirectoryError": "\"{0}\" öğesi, yükleme yolunun bir alt dizinini kullanıyor.",
+ "ToastUserDeleteFailed": "Kullanıcı silinemedi",
+ "ToastUserDeleteSuccess": "Kullanıcı silindi",
+ "ToastUserPasswordChangeSuccess": "Şifre başarıyla değiştirildi",
+ "ToastUserPasswordMismatch": "Şifreler eşleşmiyor",
+ "ToastUserPasswordMustChange": "Yeni şifre eski şifreyle aynı olamaz",
+ "ToastUserRootRequireName": "Bir root kullanıcı adı girilmelidir",
+ "TooltipAddChapters": "Bölüm(ler) ekle",
+ "TooltipAddOneSecond": "1 saniye ekle",
+ "TooltipAdjustChapterStart": "Başlangıç zamanını ayarlamak için tıkla",
+ "TooltipLockAllChapters": "Tüm bölümleri kilitle",
+ "TooltipLockChapter": "Bölümü kilitle (aralık için Shift+tıkla)",
+ "TooltipSubtractOneSecond": "1 saniye çıkar",
+ "TooltipUnlockAllChapters": "Tüm bölümlerin kilidini aç",
+ "TooltipUnlockChapter": "Bölüm kilidini aç (aralık için Shift+tıkla)"
}
From 3c4e80f1c15ef9e45d4ce3f40525fc476053bf51 Mon Sep 17 00:00:00 2001
From: Amirhossein Ghorbanmehr
Date: Tue, 23 Sep 2025 17:04:45 +0200
Subject: [PATCH 004/111] Translated using Weblate (Persian)
Currently translated at 2.2% (26 of 1161 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fa/
---
client/strings/fa.json | 29 ++++++++++++++++++++++++++++-
1 file changed, 28 insertions(+), 1 deletion(-)
diff --git a/client/strings/fa.json b/client/strings/fa.json
index 0967ef424..b0d4ab141 100644
--- a/client/strings/fa.json
+++ b/client/strings/fa.json
@@ -1 +1,28 @@
-{}
+{
+ "ButtonAdd": "افزودن",
+ "ButtonAuthors": "ناشر",
+ "ButtonBack": "بازگشت",
+ "ButtonCancel": "انصراف",
+ "ButtonClearFilter": "حذف صافی",
+ "ButtonCloseFeed": "بستن فید",
+ "ButtonCollections": "مجموعه ها",
+ "ButtonCreate": "ساختن",
+ "ButtonDelete": "حذف",
+ "ButtonHome": "خانه",
+ "ButtonIssues": "مشکلات",
+ "ButtonLatest": "جدیدترین",
+ "ButtonLibrary": "کتابخانه",
+ "ButtonOk": "تایید",
+ "ButtonOpenFeed": "باز کردن فید",
+ "ButtonPause": "توقف",
+ "ButtonPlay": "پخش",
+ "ButtonPlaylists": "لیست پخش",
+ "ButtonRead": "خواندن",
+ "ButtonReadLess": "خواندن کمتر",
+ "ButtonReadMore": "خواندن بیشتر",
+ "ButtonRemove": "حذف",
+ "ButtonSave": "ذخیره",
+ "ButtonSearch": "جستجو",
+ "ButtonSeries": "مجموعه",
+ "ButtonSubmit": "ثبت"
+}
From 3a163e1746cea5beb116cd3258c94f9e4995d227 Mon Sep 17 00:00:00 2001
From: max grakov
Date: Sat, 27 Sep 2025 15:32:30 +0200
Subject: [PATCH 005/111] Translated using Weblate (Russian)
Currently translated at 100.0% (1161 of 1161 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
---
client/strings/ru.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/client/strings/ru.json b/client/strings/ru.json
index 4dd99fd41..14953379e 100644
--- a/client/strings/ru.json
+++ b/client/strings/ru.json
@@ -436,9 +436,9 @@
"LabelLibraryFilterSublistEmpty": "Нет {0}",
"LabelLibraryItem": "Элемент библиотеки",
"LabelLibraryName": "Имя библиотеки",
- "LabelLibrarySortByProgress": "Прогресс обновлён",
- "LabelLibrarySortByProgressFinished": "Дата завершения",
- "LabelLibrarySortByProgressStarted": "Дата начала",
+ "LabelLibrarySortByProgress": "Прогресс: Последнее обновление",
+ "LabelLibrarySortByProgressFinished": "Прогресс: Завершено",
+ "LabelLibrarySortByProgressStarted": "Прогресс: Начато",
"LabelLimit": "Лимит",
"LabelLineSpacing": "Межстрочный интервал",
"LabelListenAgain": "Послушать снова",
From b21046027c89c13aa8e0e4fed3b69420b127098f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Petri=20H=C3=A4m=C3=A4l=C3=A4inen?=
Date: Sat, 27 Sep 2025 18:19:52 +0200
Subject: [PATCH 006/111] Translated using Weblate (Finnish)
Currently translated at 95.5% (1109 of 1161 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
---
client/strings/fi.json | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/client/strings/fi.json b/client/strings/fi.json
index 918b66f59..d29ce7f95 100644
--- a/client/strings/fi.json
+++ b/client/strings/fi.json
@@ -1,5 +1,6 @@
{
"ButtonAdd": "Lisää",
+ "ButtonAddApiKey": "Lisää API avain",
"ButtonAddChapters": "Lisää lukuja",
"ButtonAddDevice": "Lisää laite",
"ButtonAddLibrary": "Lisää kirjasto",
@@ -20,6 +21,7 @@
"ButtonChooseAFolder": "Valitse kansio",
"ButtonChooseFiles": "Valitse tiedostot",
"ButtonClearFilter": "Poista suodatus",
+ "ButtonClose": "Sulje",
"ButtonCloseFeed": "Sulje syöte",
"ButtonCloseSession": "Sulje Avoin Sessio",
"ButtonCollections": "Kokoelmat",
@@ -119,11 +121,13 @@
"HeaderAccount": "Tili",
"HeaderAddCustomMetadataProvider": "Lisää mukautettu metadata tarjoaja",
"HeaderAdvanced": "Edistynyt",
+ "HeaderApiKeys": "API avaimet",
"HeaderAppriseNotificationSettings": "Apprise-ilmoitusasetukset",
"HeaderAudioTracks": "Ääniraidat",
"HeaderAudiobookTools": "Äänikirjojen tiedostonhallintatyökalut",
"HeaderAuthentication": "Todennus",
"HeaderBackups": "Varmuuskopiot",
+ "HeaderBulkChapterModal": "Lisää useita kappaleita",
"HeaderChangePassword": "Vaihda salasana",
"HeaderChapters": "Luvut",
"HeaderChooseAFolder": "Valitse kansio",
@@ -162,6 +166,7 @@
"HeaderMetadataOrderOfPrecedence": "Metadatan tärkeysjärjestys",
"HeaderMetadataToEmbed": "Sisällytettävä metadata",
"HeaderNewAccount": "Uusi tili",
+ "HeaderNewApiKey": "Uusi API avain",
"HeaderNewLibrary": "Uusi kirjasto",
"HeaderNotificationCreate": "Luo ilmoitus",
"HeaderNotificationUpdate": "Päivitä ilmoitus",
@@ -194,6 +199,7 @@
"HeaderSettingsExperimental": "Kokeelliset ominaisuudet",
"HeaderSettingsGeneral": "Yleiset",
"HeaderSettingsScanner": "Skannaaja",
+ "HeaderSettingsSecurity": "Turvallisuus",
"HeaderSettingsWebClient": "Webasiakasohjelma",
"HeaderSleepTimer": "Uniajastin",
"HeaderStatsLargestItems": "Suurimmat kohteet",
@@ -205,6 +211,7 @@
"HeaderTableOfContents": "Sisällysluettelo",
"HeaderTools": "Työkalut",
"HeaderUpdateAccount": "Päivitä tili",
+ "HeaderUpdateApiKey": "Päivitä API avain",
"HeaderUpdateAuthor": "Päivitä tekijä",
"HeaderUpdateDetails": "Päivitä yksityiskohdat",
"HeaderUpdateLibrary": "Päivitä kirjasto",
@@ -234,6 +241,8 @@
"LabelAllUsersExcludingGuests": "Kaikki käyttäjät vieraita lukuun ottamatta",
"LabelAllUsersIncludingGuests": "Kaikki käyttäjät mukaan lukien vieraat",
"LabelAlreadyInYourLibrary": "Jo kirjastossasi",
+ "LabelApiKeyCreated": "API avain \"{0}\" luotu onnistuneesti.",
+ "LabelApiKeyCreatedDescription": "Varmista, että kopioit API avaimen. Sitä ei näytetä enää tämän jälkeen.",
"LabelApiToken": "Sovellusliittymätunnus",
"LabelAppend": "Lisää loppuun",
"LabelAudioBitrate": "Äänen bittinopeus (esim. 128k)",
@@ -283,6 +292,7 @@
"LabelContinueListening": "Jatka kuuntelua",
"LabelContinueReading": "Jatka lukemista",
"LabelContinueSeries": "Jatka sarjoja",
+ "LabelCorsAllowed": "Salli CORS Origins",
"LabelCover": "Kansikuva",
"LabelCoverImageURL": "Kansikuvan URL-osoite",
"LabelCoverProvider": "Kansikuvan tarjoaja",
From 4a4d4a8f17100ed492bf159ad1c4c3b709c28138 Mon Sep 17 00:00:00 2001
From: Jan
Date: Tue, 30 Sep 2025 21:29:58 +0200
Subject: [PATCH 007/111] Translated using Weblate (German)
Currently translated at 100.0% (1161 of 1161 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
---
client/strings/de.json | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/client/strings/de.json b/client/strings/de.json
index fff1b4e58..d3bd7e4a6 100644
--- a/client/strings/de.json
+++ b/client/strings/de.json
@@ -55,7 +55,7 @@
"ButtonNext": "Vor",
"ButtonNextChapter": "Nächstes Kapitel",
"ButtonNextItemInQueue": "Das nächste Element in der Warteschlange",
- "ButtonOk": "Ok",
+ "ButtonOk": "OK",
"ButtonOpenFeed": "Feed öffnen",
"ButtonOpenManager": "Manager öffnen",
"ButtonPause": "Pausieren",
@@ -75,7 +75,7 @@
"ButtonQuickMatch": "Schnellabgleich",
"ButtonReScan": "Neu scannen",
"ButtonRead": "Lesen",
- "ButtonReadLess": "weniger Anzeigen",
+ "ButtonReadLess": "Weniger Anzeigen",
"ButtonReadMore": "Mehr Anzeigen",
"ButtonRefresh": "Neu Laden",
"ButtonRemove": "Entfernen",
@@ -104,7 +104,7 @@
"ButtonStartM4BEncode": "M4B-Kodierung starten",
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
"ButtonStats": "Statistiken",
- "ButtonSubmit": "Ok",
+ "ButtonSubmit": "Absenden",
"ButtonTest": "Test",
"ButtonUnlinkOpenId": "OpenID trennen",
"ButtonUpload": "Hochladen",
@@ -116,7 +116,7 @@
"ButtonViewAll": "Alles anzeigen",
"ButtonYes": "Ja",
"ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten",
- "ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuche den Titel und oder den Autor zu aktualisieren",
+ "ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuche den Titel und/oder den Autor zu aktualisieren.",
"ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden",
"HeaderAccount": "Konto",
"HeaderAddCustomMetadataProvider": "Benutzerdefinierten Metadatenanbieter hinzufügen",
@@ -138,13 +138,13 @@
"HeaderCustomMessageOnLogin": "Benutzerdefinierte Nachricht für die Anmeldung",
"HeaderCustomMetadataProviders": "Benutzerdefinierte Metadatenanbieter",
"HeaderDetails": "Details",
- "HeaderDownloadQueue": "Download Warteschlange",
+ "HeaderDownloadQueue": "Download-Warteschlange",
"HeaderEbookFiles": "E-Buch-Dateien",
"HeaderEmail": "E-Mail",
"HeaderEmailSettings": "E-Mail-Einstellungen",
"HeaderEpisodes": "Episoden",
"HeaderEreaderDevices": "E-Reader Geräte",
- "HeaderEreaderSettings": "Einstellungen zum Lesen",
+ "HeaderEreaderSettings": "E-Reader-Einstellungen",
"HeaderFiles": "Dateien",
"HeaderFindChapters": "Kapitel suchen",
"HeaderIgnoredFiles": "Ignorierte Dateien",
@@ -436,9 +436,9 @@
"LabelLibraryFilterSublistEmpty": "Keine {0}",
"LabelLibraryItem": "Bibliothekseintrag",
"LabelLibraryName": "Bibliotheksname",
- "LabelLibrarySortByProgress": "Fortschritt: Zuletzt geändert",
+ "LabelLibrarySortByProgress": "Fortschritt: Zuletzt aktualisiert",
"LabelLibrarySortByProgressFinished": "Fortschritt: Beendet",
- "LabelLibrarySortByProgressStarted": "Fortschritt: Angefangen",
+ "LabelLibrarySortByProgressStarted": "Fortschritt: Gestartet",
"LabelLimit": "Begrenzung",
"LabelLineSpacing": "Zeilenabstand",
"LabelListenAgain": "Erneut anhören",
From 161f3cb177fce7e0ef63c292bf7a120203972109 Mon Sep 17 00:00:00 2001
From: B0rax
Date: Tue, 30 Sep 2025 20:43:56 +0200
Subject: [PATCH 008/111] Translated using Weblate (German)
Currently translated at 100.0% (1161 of 1161 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
---
client/strings/de.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/client/strings/de.json b/client/strings/de.json
index d3bd7e4a6..2eaebbcc6 100644
--- a/client/strings/de.json
+++ b/client/strings/de.json
@@ -139,7 +139,7 @@
"HeaderCustomMetadataProviders": "Benutzerdefinierte Metadatenanbieter",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download-Warteschlange",
- "HeaderEbookFiles": "E-Buch-Dateien",
+ "HeaderEbookFiles": "E-Book-Dateien",
"HeaderEmail": "E-Mail",
"HeaderEmailSettings": "E-Mail-Einstellungen",
"HeaderEpisodes": "Episoden",
From 80b0e3546e893a30d76c4a120d6fadb953ad9aa6 Mon Sep 17 00:00:00 2001
From: DR
Date: Tue, 30 Sep 2025 12:52:37 +0200
Subject: [PATCH 009/111] Translated using Weblate (Hebrew)
Currently translated at 72.9% (847 of 1161 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/
---
client/strings/he.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/client/strings/he.json b/client/strings/he.json
index 798299986..9525ca6cd 100644
--- a/client/strings/he.json
+++ b/client/strings/he.json
@@ -348,7 +348,7 @@
"LabelExample": "דוגמה",
"LabelExpandSeries": "הרחב סדרה",
"LabelExpandSubSeries": "הרחב תת סדרה",
- "LabelExplicit": "בוטה",
+ "LabelExplicit": "מפורש",
"LabelExplicitChecked": "בוטה (מסומן)",
"LabelExplicitUnchecked": "לא בוטה (לא מסומן)",
"LabelExportOPML": "ייצוא OPML",
From 6bdf402da8c4f25eac0198ea7023ea235d922ef0 Mon Sep 17 00:00:00 2001
From: SmileFate
Date: Mon, 29 Sep 2025 22:17:58 +0200
Subject: [PATCH 010/111] Translated using Weblate (Turkish)
Currently translated at 100.0% (1161 of 1161 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
---
client/strings/tr.json | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/client/strings/tr.json b/client/strings/tr.json
index be5f5fa7c..d326344e2 100644
--- a/client/strings/tr.json
+++ b/client/strings/tr.json
@@ -116,7 +116,7 @@
"ButtonViewAll": "Tümünü Görüntüle",
"ButtonYes": "Evet",
"ErrorUploadFetchMetadataAPI": "Üst veri alınırken hata oluştu",
- "ErrorUploadFetchMetadataNoResults": "Üst veri bulunamadı. Başlığı veya yazarı güncellemeyi deneyin.",
+ "ErrorUploadFetchMetadataNoResults": "Üst veri bulunamadı. Başlığı veya yazarı güncellemeyi deneyin",
"ErrorUploadLacksTitle": "Başlık girilmesi zorunludur",
"HeaderAccount": "Hesap",
"HeaderAddCustomMetadataProvider": "Özel Üst Veri Sağlayıcısı Ekle",
@@ -259,7 +259,7 @@
"LabelAutoFetchMetadata": "Üst Veriyi Otomatik Getir",
"LabelAutoFetchMetadataHelp": "Yüklemeyi kolaylaştırmak için başlık, yazar ve seri üst verilerini getirir. Ek üst verilerin yüklemeden sonra eşleştirilmesi gerekebilir.",
"LabelAutoLaunch": "Otomatik Başlat",
- "LabelAutoLaunchDescription": "Giriş sayfasına gidildiğinde otomatik olarak kimlik doğrulama sağlayıcısına yönlendir (manuel geçersiz kılma yolu: /login?autoLaunch=0)",
+ "LabelAutoLaunchDescription": "Giriş sayfasına gidildiğinde otomatik olarak kimlik doğrulama sağlayıcısına yönlendir (manuel geçersiz kılma yolu: /login?autoLaunch=0)",
"LabelAutoRegister": "Otomatik Kayıt",
"LabelAutoRegisterDescription": "Giriş yaptıktan sonra yeni kullanıcıları otomatik olarak oluştur",
"LabelBackToUser": "Kullanıcıya Geri Dön",
@@ -449,7 +449,7 @@
"LabelLowestPriority": "En Düşük Öncelik",
"LabelMatchConfidence": "Güvenilirlik",
"LabelMatchExistingUsersBy": "Mevcut kullanıcıları şuna göre eşleştir",
- "LabelMatchExistingUsersByDescription": "Mevcut kullanıcıları bağlamak için kullanılır. Bağlandıktan sonra, kullanıcılar SSO sağlayıcınızdan gelen benzersiz bir kimlikle eşleştirilir.",
+ "LabelMatchExistingUsersByDescription": "Mevcut kullanıcıları bağlamak için kullanılır. Bağlandıktan sonra, kullanıcılar SSO sağlayıcınızdan gelen benzersiz bir kimlikle eşleştirilir",
"LabelMaxEpisodesToDownload": "İndirilecek maks. bölüm sayısı. Sınırsız için 0 kullanın.",
"LabelMaxEpisodesToDownloadPerCheck": "Kontrol başına indirilecek maks. yeni bölüm sayısı",
"LabelMaxEpisodesToKeep": "Saklanacak maks. bölüm sayısı",
@@ -466,7 +466,7 @@
"LabelMissingEbook": "E-kitabı yok",
"LabelMissingSupplementaryEbook": "Ek e-kitabı yok",
"LabelMobileRedirectURIs": "İzin Verilen Mobil Yönlendirme URI'leri",
- "LabelMobileRedirectURIsDescription": "Bu, mobil uygulamalar için geçerli yönlendirme URI'lerinin beyaz listesidir. Varsayılan olan audiobookshelf://oauth'dır, bunu kaldırabilir veya üçüncü taraf uygulama entegrasyonu için ek URI'lerle tamamlayabilirsiniz. Tek giriş olarak bir yıldız işareti (*) kullanmak herhangi bir URI'ye izin verir.",
+ "LabelMobileRedirectURIsDescription": "Bu, mobil uygulamalar için geçerli yönlendirme URL'lerinin beyaz listesidir. Varsayılan olan audiobookshelf://oauth'dır, bunu kaldırabilir veya üçüncü taraf uygulama entegrasyonu için ek URL'lerle tamamlayabilirsiniz. Tek giriş olarak bir yıldız işareti (*) kullanmak herhangi bir URL'ye izin verir.",
"LabelMore": "Daha Fazla",
"LabelMoreInfo": "Daha Fazla Bilgi",
"LabelName": "İsim",
@@ -497,9 +497,9 @@
"LabelNumberOfBooks": "Kitap Sayısı",
"LabelNumberOfChapters": "Bölüm sayısı:",
"LabelNumberOfEpisodes": "Bölüm Sayısı",
- "LabelOpenIDAdvancedPermsClaimDescription": "Uygulama içindeki kullanıcı eylemleri için yönetici olmayan rollere uygulanacak gelişmiş izinleri içeren OpenID talebinin adı (yapılandırılmışsa). Talep yanıtta eksikse, ABS'ye erişim reddedilir. Tek bir seçenek eksikse, 'false' olarak kabul edilir. Kimlik sağlayıcısının talebinin beklenen yapıyla eşleştiğinden emin olun:",
+ "LabelOpenIDAdvancedPermsClaimDescription": "Uygulama içindeki kullanıcı eylemleri için yönetici olmayan rollere uygulanacak gelişmiş izinleri içeren OpenID talebinin adı (yapılandırılmışsa). Talep yanıtta eksikse, ABS'ye erişim reddedilir. Tek bir seçenek eksikse, 'false' olarak kabul edilir. Kimlik sağlayıcısının talebinin beklenen yapıyla eşleştiğinden emin olun:",
"LabelOpenIDClaims": "Gelişmiş grup ve izin atamasını devre dışı bırakmak için aşağıdaki seçenekleri boş bırakın, bu durumda otomatik olarak 'Kullanıcı' grubu atanır.",
- "LabelOpenIDGroupClaimDescription": "Kullanıcının gruplarının bir listesini içeren OpenID talebinin adı. Genellikle 'groups' olarak adlandırılır. (Yapılandırılmışsa), uygulama, bu grupların talepte büyük/küçük harfe duyarsız olarak 'admin', 'user' veya 'guest' olarak adlandırılması koşuluyla, kullanıcının grup üyeliklerine göre rolleri otomatik olarak atayacaktır. Talep bir liste içermelidir ve bir kullanıcı birden fazla gruba aitse, uygulama en yüksek erişim düzeyine karşılık gelen rolü atayacaktır. Hiçbir grup eşleşmezse, erişim reddedilir.",
+ "LabelOpenIDGroupClaimDescription": "Kullanıcının gruplarının bir listesini içeren OpenID talebinin adı. Genellikle 'groups' olarak adlandırılır. (Yapılandırılmışsa), uygulama, bu grupların talepte büyük/küçük harfe duyarsız olarak 'admin', 'user' veya 'guest' olarak adlandırılması koşuluyla, kullanıcının grup üyeliklerine göre rolleri otomatik olarak atayacaktır. Talep bir liste içermelidir ve bir kullanıcı birden fazla gruba aitse, uygulama en yüksek erişim düzeyine karşılık gelen rolü atayacaktır. Hiçbir grup eşleşmezse, erişim reddedilir.",
"LabelOpenRSSFeed": "RSS Akışını Aç",
"LabelOverwrite": "Üzerine Yaz",
"LabelPaginationPageXOfY": "Sayfa {0} / {1}",
@@ -584,7 +584,7 @@
"LabelSetEbookAsSupplementary": "Ek olarak ayarla",
"LabelSettingsAllowIframe": "Iframe içine gömülmesine izin ver",
"LabelSettingsAudiobooksOnly": "Sadece sesli kitaplar",
- "LabelSettingsAudiobooksOnlyHelp": "Bu ayarın etkinleştirilmesi, bir sesli kitap klasörünün içinde olmadıkları sürece e-kitap dosyalarını yoksayar, bu durumda ek e-kitaplar olarak ayarlanırlar.",
+ "LabelSettingsAudiobooksOnlyHelp": "Bu ayarın etkinleştirilmesi, bir sesli kitap klasörünün içinde olmadıkları sürece e-kitap dosyalarını yoksayar, bu durumda ek e-kitaplar olarak ayarlanırlar",
"LabelSettingsBookshelfViewHelp": "Ahşap raflı skeuomorfik tasarım",
"LabelSettingsChromecastSupport": "Chromecast desteği",
"LabelSettingsDateFormat": "Tarih Formatı",
@@ -790,7 +790,7 @@
"MessageConfirmRemoveAuthor": "\"{0}\" yazarını kaldırmak istediğinizden emin misiniz?",
"MessageConfirmRemoveCollection": "\"{0}\" koleksiyonunu kaldırmak istediğinizden emin misiniz?",
"MessageConfirmRemoveEpisode": "\"{0}\" bölümünü kaldırmak istediğinizden emin misiniz?",
- "MessageConfirmRemoveEpisodeNote": "Not: Bu, \"Dosyayı kalıcı olarak sil\" seçeneği işaretlenmedikçe ses dosyasını silmez.",
+ "MessageConfirmRemoveEpisodeNote": "Not: Bu, \"Dosyayı kalıcı olarak sil\" seçeneği işaretlenmedikçe ses dosyasını silmez",
"MessageConfirmRemoveEpisodes": "{0} bölümü kaldırmak istediğinizden emin misiniz?",
"MessageConfirmRemoveListeningSessions": "{0} dinleme oturumunu kaldırmak istediğinizden emin misiniz?",
"MessageConfirmRemoveMetadataFiles": "Kütüphane öğesi klasörlerinizdeki tüm metadata.{0} dosyalarını kaldırmak istediğinizden emin misiniz?",
@@ -823,7 +823,7 @@
"MessageInvalidAsin": "Geçersiz ASIN",
"MessageItemsSelected": "{0} öğe seçildi",
"MessageItemsUpdated": "{0} öğe güncellendi",
- "MessageJoinUsOn": "Bize katılın:",
+ "MessageJoinUsOn": "Bize katılın",
"MessageLoading": "Yükleniyor...",
"MessageLoadingFolders": "Klasörler yükleniyor...",
"MessageLogsDescription": "Kayıtlar /metadata/logs içinde JSON dosyaları olarak saklanır. Çökme kayıtları /metadata/logs/crash_logs.txt içinde saklanır.",
@@ -884,13 +884,13 @@
"MessageRemoveEpisodes": "{0} bölümü kaldır",
"MessageRemoveFromPlayerQueue": "Oynatma sırasından kaldır",
"MessageRemoveUserWarning": "\"{0}\" kullanıcısını kalıcı olarak silmek istediğinizden emin misiniz?",
- "MessageReportBugsAndContribute": "Hataları bildirin, özellikler isteyin ve katkıda bulunun:",
+ "MessageReportBugsAndContribute": "Hataları bildirin, özellikler isteyin ve katkıda bulunun",
"MessageResetChaptersConfirm": "Bölümleri sıfırlamak ve yaptığınız değişiklikleri geri almak istediğinizden emin misiniz?",
- "MessageRestoreBackupConfirm": "Şu tarihte oluşturulan yedeği geri yüklemek istediğinizden emin misiniz:",
+ "MessageRestoreBackupConfirm": "Şu tarihte oluşturulan yedeği geri yüklemek istediğinizden emin misiniz",
"MessageRestoreBackupWarning": "Bir yedeği geri yüklemek, /config konumundaki tüm veritabanının ve /metadata/items & /metadata/authors içindeki kapak resimlerinin üzerine yazacaktır.
Yedekler, kütüphane klasörlerinizdeki hiçbir dosyayı değiştirmez. Sunucu ayarlarını kütüphane klasörlerinizde kapak resmi ve üst veri saklamak için etkinleştirdiyseniz, bunlar yedeklenmez veya üzerine yazılmaz.
Sunucunuzu kullanan tüm istemciler otomatik olarak yenilenecektir.",
"MessageScheduleLibraryScanNote": "Çoğu kullanıcı için, bu özelliği devre dışı bırakıp klasör izleyici ayarını etkin tutmaları önerilir. Klasör izleyici, kütüphane klasörlerinizdeki değişiklikleri otomatik olarak algılayacaktır. Klasör izleyici her dosya sistemi için (NFS gibi) çalışmaz, bu nedenle bunun yerine zamanlanmış kütüphane taramaları kullanılabilir.",
"MessageScheduleRunEveryWeekdayAtTime": "Her {0} günü saat {1}'de çalıştır",
- "MessageSearchResultsFor": "Arama sonuçları:",
+ "MessageSearchResultsFor": "Arama sonuçları",
"MessageSelected": "{0} seçildi",
"MessageSeriesSequenceCannotContainSpaces": "Seri sırası boşluk içeremez",
"MessageServerCouldNotBeReached": "Sunucuya ulaşılamadı",
From a4e8f01f0ec5fdbed819d05e7bfb630b907730e3 Mon Sep 17 00:00:00 2001
From: Milo Ivir
Date: Sun, 5 Oct 2025 00:36:04 +0200
Subject: [PATCH 011/111] Translated using Weblate (Croatian)
Currently translated at 100.0% (1163 of 1163 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
---
client/strings/hr.json | 2 ++
1 file changed, 2 insertions(+)
diff --git a/client/strings/hr.json b/client/strings/hr.json
index 1719b450a..c5cbf66e1 100644
--- a/client/strings/hr.json
+++ b/client/strings/hr.json
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "Neuspješno dodavanje stavki u zbirku",
"ToastCollectionRemoveSuccess": "Zbirka izbrisana",
"ToastCollectionUpdateSuccess": "Zbirka ažurirana",
+ "ToastConnectionNotAvailable": "Veza nije dostupna. Pokušaj ponovo kasnije",
+ "ToastCoverSearchFailed": "Pretraga naslovnice neuspjela",
"ToastCoverUpdateFailed": "Ažuriranje naslovnice nije uspjelo",
"ToastDateTimeInvalidOrIncomplete": "Datum i vrijeme su neispravni ili nepotpuni",
"ToastDeleteFileFailed": "Brisanje datoteke nije uspjelo",
From bbae9acc2d671e62ac8f74314ff9d8598efeac43 Mon Sep 17 00:00:00 2001
From: Kabika82
Date: Sun, 5 Oct 2025 16:23:50 +0200
Subject: [PATCH 012/111] Translated using Weblate (Hungarian)
Currently translated at 100.0% (1163 of 1163 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
---
client/strings/hu.json | 2 ++
1 file changed, 2 insertions(+)
diff --git a/client/strings/hu.json b/client/strings/hu.json
index 1138cee5d..61e9316d9 100644
--- a/client/strings/hu.json
+++ b/client/strings/hu.json
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "A tétel(ek) hozzáadása gyűjteményhez sikertelen",
"ToastCollectionRemoveSuccess": "Gyűjtemény eltávolítva",
"ToastCollectionUpdateSuccess": "Gyűjtemény frissítve",
+ "ToastConnectionNotAvailable": "A kapcsolat nem elérhető. Kérem, próbálkozzon később",
+ "ToastCoverSearchFailed": "A borítók keresése sikertelen",
"ToastCoverUpdateFailed": "A borító frissítése nem sikerült",
"ToastDateTimeInvalidOrIncomplete": "A dátum és az időpont érvénytelen vagy hiányos",
"ToastDeleteFileFailed": "Nem sikerült törölni a fájlt",
From eb00b19457fc95f53f4c1d76236dc2aabd85b490 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=93=D0=BE=D1=80?=
=?UTF-8?q?=D0=BF=D0=B8=D0=BD=D1=96=D1=87?=
Date: Sat, 4 Oct 2025 06:34:30 +0200
Subject: [PATCH 013/111] Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1163 of 1163 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
---
client/strings/uk.json | 2 ++
1 file changed, 2 insertions(+)
diff --git a/client/strings/uk.json b/client/strings/uk.json
index 61fd4160a..fbb736f6d 100644
--- a/client/strings/uk.json
+++ b/client/strings/uk.json
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "Не вдалося додати елемент(и) до колекції",
"ToastCollectionRemoveSuccess": "Добірку видалено",
"ToastCollectionUpdateSuccess": "Добірку оновлено",
+ "ToastConnectionNotAvailable": "З’єднання недоступне. Спробуйте пізніше",
+ "ToastCoverSearchFailed": "Пошук обкладинки не вдався",
"ToastCoverUpdateFailed": "Не вдалося оновити обкладинку",
"ToastDateTimeInvalidOrIncomplete": "Дата й час недійсні або неповні",
"ToastDeleteFileFailed": "Не вдалося видалити файл",
From 2d313851d2fa29f52de9dce55c31ad522e88df9f Mon Sep 17 00:00:00 2001
From: FiendFEARing
Date: Sat, 4 Oct 2025 04:13:17 +0200
Subject: [PATCH 014/111] Translated using Weblate (Chinese (Simplified Han
script))
Currently translated at 100.0% (1163 of 1163 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
---
client/strings/zh-cn.json | 2 ++
1 file changed, 2 insertions(+)
diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json
index 7675ec817..361a74613 100644
--- a/client/strings/zh-cn.json
+++ b/client/strings/zh-cn.json
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "项目添加到收藏夹失败",
"ToastCollectionRemoveSuccess": "收藏夹已删除",
"ToastCollectionUpdateSuccess": "收藏夹已更新",
+ "ToastConnectionNotAvailable": "连接不可用. 请稍后重试",
+ "ToastCoverSearchFailed": "封面搜索失败",
"ToastCoverUpdateFailed": "封面更新失败",
"ToastDateTimeInvalidOrIncomplete": "日期和时间无效或不完整",
"ToastDeleteFileFailed": "删除文件失败",
From 54a5e368c215a0e9ce230e683eea7e942f19a545 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?O=C4=9Fuz=20Ersen?=
Date: Sat, 4 Oct 2025 19:06:52 +0200
Subject: [PATCH 015/111] Translated using Weblate (Turkish)
Currently translated at 100.0% (1163 of 1163 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
---
client/strings/tr.json | 2 ++
1 file changed, 2 insertions(+)
diff --git a/client/strings/tr.json b/client/strings/tr.json
index d326344e2..af64a618d 100644
--- a/client/strings/tr.json
+++ b/client/strings/tr.json
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "Öğe(ler) koleksiyona eklenemedi",
"ToastCollectionRemoveSuccess": "Koleksiyon kaldırıldı",
"ToastCollectionUpdateSuccess": "Koleksiyon güncellendi",
+ "ToastConnectionNotAvailable": "Bağlantı kurulamıyor. Lütfen daha sonra tekrar deneyin",
+ "ToastCoverSearchFailed": "Kapak arama başarısız",
"ToastCoverUpdateFailed": "Kapak güncellemesi başarısız oldu",
"ToastDateTimeInvalidOrIncomplete": "Tarih ve saat geçersiz veya eksik",
"ToastDeleteFileFailed": "Dosya silinemedi",
From 54a996634ee55dd0910fb39fb877590035b04ceb Mon Sep 17 00:00:00 2001
From: Petter Schaug-Pettersen
Date: Mon, 6 Oct 2025 23:33:35 +0200
Subject: [PATCH 016/111] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?=
=?UTF-8?q?an=20Bokm=C3=A5l)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently translated at 90.4% (1052 of 1163 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
---
client/strings/no.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/client/strings/no.json b/client/strings/no.json
index f6db0b66f..1a19cfa1e 100644
--- a/client/strings/no.json
+++ b/client/strings/no.json
@@ -11,7 +11,7 @@
"ButtonApplyChapters": "Bruk kapittel",
"ButtonAuthors": "Forfattere",
"ButtonBack": "Tilbake",
- "ButtonBatchEditPopulateFromExisting": "Opprett fra eksisterende",
+ "ButtonBatchEditPopulateFromExisting": "Fyll ut fra eksisterende",
"ButtonBatchEditPopulateMapDetails": "Legg til detaljer",
"ButtonBrowseForFolder": "Bla gjennom mappe",
"ButtonCancel": "Avbryt",
@@ -941,6 +941,7 @@
"ToastCollectionItemsAddFailed": "Feil med å legge til element(er)",
"ToastCollectionRemoveSuccess": "Samling fjernet",
"ToastCollectionUpdateSuccess": "samlingupdated",
+ "ToastCoverSearchFailed": "Finner ikke bokomslag",
"ToastCoverUpdateFailed": "Oppdatering av bilde feilet",
"ToastDeleteFileFailed": "Kunne ikke slette fil",
"ToastDeleteFileSuccess": "Fil slettet",
From 37dd46d31f0189c5cf44898fbb9ca51c8b6090b2 Mon Sep 17 00:00:00 2001
From: Ahetek
Date: Mon, 6 Oct 2025 21:51:48 +0200
Subject: [PATCH 017/111] Translated using Weblate (Polish)
Currently translated at 88.2% (1026 of 1163 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
---
client/strings/pl.json | 69 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 69 insertions(+)
diff --git a/client/strings/pl.json b/client/strings/pl.json
index 4707efeb8..7ed1551c3 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -476,6 +476,7 @@
"LabelNewestAuthors": "Najnowsi autorzy",
"LabelNewestEpisodes": "Najnowsze odcinki",
"LabelNextBackupDate": "Data kolejnej kopii zapasowej",
+ "LabelNextChapters": "Następny odcinek:",
"LabelNextScheduledRun": "Następne uruchomienie",
"LabelNoApiKeys": "Brak kluczy API",
"LabelNoCustomMetadataProviders": "Brak niestandardowych dostawców metadanych",
@@ -619,6 +620,7 @@
"LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana",
"LabelSettingsTimeFormat": "Format czasu",
"LabelShare": "Udostępnij",
+ "LabelShareDownloadableHelp": "Użytkownicy mogą przy pomocy linka ściągnąć archiwum ZIP pozycji biblioteki",
"LabelShareOpen": "Otwórz udział",
"LabelShareURL": "Link do udziału",
"LabelShowAll": "Pokaż wszystko",
@@ -633,6 +635,7 @@
"LabelStartTime": "Czas rozpoczęcia",
"LabelStarted": "Rozpoczęty",
"LabelStartedAt": "Rozpoczęto",
+ "LabelStartedDate": "Rozpoczęto {0}",
"LabelStatsAudioTracks": "Ścieżki audio",
"LabelStatsAuthors": "Autorzy",
"LabelStatsBestDay": "Najlepszy dzień",
@@ -655,11 +658,14 @@
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
"LabelTagsNotAccessibleToUser": "Znaczniki niedostępne dla użytkownika",
"LabelTasks": "Uruchomione zadania",
+ "LabelTextEditorBulletedList": "Lista punktowana",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Lista numerowana",
"LabelTextEditorUnlink": "Usuń link",
+ "LabelTheme": "Kompozycja",
"LabelThemeDark": "Ciemny",
"LabelThemeLight": "Jasny",
+ "LabelThemeSepia": "Sepia",
"LabelTimeDurationXHours": "{0} godzin",
"LabelTimeDurationXMinutes": "{0} minuty",
"LabelTimeDurationXSeconds": "{0} sekundy",
@@ -682,7 +688,12 @@
"LabelTrackFromFilename": "Ścieżka z nazwy pliku",
"LabelTrackFromMetadata": "Ścieżka z metadanych",
"LabelTracks": "Ścieżki",
+ "LabelTracksMultiTrack": "Wielościeżkowy",
+ "LabelTracksNone": "Brak ścieżek",
+ "LabelTracksSingleTrack": "Pojedyncza ścieżka",
+ "LabelTrailer": "Zwiastun",
"LabelType": "Typ",
+ "LabelUnabridged": "Pełna wersja",
"LabelUndo": "Wycofaj",
"LabelUnknown": "Nieznany",
"LabelUnknownPublishDate": "Nieznana data publikacji",
@@ -695,6 +706,7 @@
"LabelUploaderDragAndDropFilesOnly": "Przeciągnij i upuść pliki",
"LabelUploaderDropFiles": "Puść pliki",
"LabelUploaderItemFetchMetadataHelp": "Automatycznie pobierz tytuł, autora i serie",
+ "LabelUseAdvancedOptions": "Opcje zaawansowane",
"LabelUseChapterTrack": "Użyj ścieżki rozdziału",
"LabelUseFullTrack": "Użycie ścieżki rozdziału",
"LabelUser": "Użytkownik",
@@ -715,8 +727,11 @@
"LabelYourBookmarks": "Twoje zakładki",
"LabelYourPlaylists": "Twoje playlisty",
"LabelYourProgress": "Twój postęp",
+ "MessageAddToPlayerQueue": "Dodaj do kolejki odtwarzania",
"MessageAppriseDescription": "Aby użyć tej funkcji, konieczne jest posiadanie instancji Apprise API albo innego rozwiązania, które obsługuje schemat zapytań Apprise. URL do interfejsu API powinno być całkowitą ścieżką, np., jeśli Twoje API do powiadomień jest dostępne pod adresem http://192.168.1.1:8337 to wpisany tutaj URL powinien mieć postać: http://192.168.1.1:8337/notify.",
+ "MessageAsinCheck": "Upewnij się, że używasz ASIN z poprawnego regionu Audible, nie Amazona.",
"MessageAuthenticationLegacyTokenWarning": "Starsze tokeny API zostaną w przyszłości usunięte. Zamiast nich należy używać kluczy API.",
+ "MessageAuthenticationOIDCChangesRestart": "Zrestartuj serwer aby zastosować zmiany w OIDC.",
"MessageAuthenticationSecurityMessage": "Uwierzytelnianie zostało ulepszone ze względów bezpieczeństwa. Wszyscy użytkownicy muszą się ponownie zalogować.",
"MessageBackupsDescription": "Kopie zapasowe obejmują użytkowników, postępy użytkowników, szczegóły pozycji biblioteki, ustawienia serwera i obrazy przechowywane w /metadata/items & /metadata/authors. Kopie zapasowe nie obejmują żadnych plików przechowywanych w folderach biblioteki.",
"MessageBackupsLocationEditNote": "Uwaga: Zmiana lokalizacji kopii zapasowej nie przenosi ani nie modyfikuje istniejących kopii zapasowych",
@@ -730,6 +745,7 @@
"MessageBookshelfNoResultsForFilter": "Nie znaleziono żadnych pozycji przy aktualnym filtrowaniu \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Brak wyników zapytania",
"MessageBookshelfNoSeries": "Nie masz jeszcze żadnych serii",
+ "MessageBulkChapterPattern": "Ile rozdziałów chcesz dodać przy pomocy tego wzorca numeracji?",
"MessageChapterEndIsAfter": "Koniec rozdziału następuje po zakończeniu audiobooka",
"MessageChapterErrorFirstNotZero": "Pierwszy rozdział musi rozpoczynać się na 0",
"MessageChapterErrorStartGteDuration": "Nieprawidłowy czas rozpoczęcia, musi być krótszy niż długość audiobooka",
@@ -792,6 +808,7 @@
"MessageFeedURLWillBe": "URL kanału: {0}",
"MessageFetching": "Pobieranie...",
"MessageForceReScanDescription": "przeskanuje wszystkie pliki ponownie, jak przy świeżym skanowaniu. Tagi ID3 plików audio, pliki OPF i pliki tekstowe będą skanowane jak nowe.",
+ "MessageHeatmapListeningTimeTooltip": "{0} słucha na {1}",
"MessageImportantNotice": "Ważna informacja!",
"MessageInsertChapterBelow": "Wstaw rozdział poniżej",
"MessageInvalidAsin": "Nieprawidłowy ASIN",
@@ -862,13 +879,34 @@
"MessageResetChaptersConfirm": "Czy na pewno chcesz zresetować rozdziały i cofnąć wprowadzone zmiany?",
"MessageRestoreBackupConfirm": "Czy na pewno chcesz przywrócić kopię zapasową utworzoną w dniu",
"MessageRestoreBackupWarning": "Przywrócenie kopii zapasowej spowoduje nadpisanie bazy danych w folderze /config oraz okładek w folderze /metadata/items & /metadata/authors.
Kopie zapasowe nie modyfikują żadnego pliku w folderach z plikami audio. Jeśli włączyłeś ustawienia serwera, aby przechowywać okładki i metadane w folderach biblioteki, to nie są one zapisywane w kopii zapasowej lub nadpisywane
Wszyscy klienci korzystający z Twojego serwera będą automatycznie odświeżani.",
+ "MessageScheduleLibraryScanNote": "W przypadku większości użytkowników zaleca się pozostawienie tej funkcji wyłączonej i włączenie opcji monitorowania folderów. Monitor folderów automatycznie wykrywa zmiany w folderach biblioteki. Monitor folderów nie działa w przypadku wszystkich systemów plików (np. NFS), dlatego zamiast niego można używać zaplanowanych skanowań biblioteki.",
+ "MessageScheduleRunEveryWeekdayAtTime": "Uruchom w każdy {0} o {1}",
"MessageSearchResultsFor": "Wyniki wyszukiwania dla",
"MessageSelected": "{0} wybranych",
"MessageServerCouldNotBeReached": "Nie udało się uzyskać połączenia z serwerem",
+ "MessageSetChaptersFromTracksDescription": "Ustaw rozdziały, używając każdego pliku audio jako rozdziału, a tytuł rozdziału jako nazwy pliku audio.",
"MessageShareExpirationWillBe": "Czas udostępniania {0}",
"MessageShareExpiresIn": "Wygaśnie za {0}",
"MessageShareURLWillBe": "Udostępnione pod linkiem {0}",
"MessageStartPlaybackAtTime": "Rozpoczęcie odtwarzania \"{0}\" od {1}?",
+ "MessageTaskAudioFileNotWritable": "Plik audio \"{0}\" jest niemodyfikowalny",
+ "MessageTaskCanceledByUser": "Zadanie anulowane przez użytkownika",
+ "MessageTaskDownloadingEpisodeDescription": "Ściąganie odcinka \"{0}\"",
+ "MessageTaskEmbeddingMetadata": "Wbudowywanie medatanych",
+ "MessageTaskEmbeddingMetadataDescription": "Wbudowywanie metadanych do audiobooka \"{0}\"",
+ "MessageTaskEncodingM4b": "Kodowanie M4B",
+ "MessageTaskEncodingM4bDescription": "Konwersja audiobooka \"{0}\" do pojedynczego pliku m4b",
+ "MessageTaskFailed": "Niepowodzenie",
+ "MessageTaskFailedToBackupAudioFile": "Nieudana próba wykonania kopii zapasowego pliku audio \"{0}\"",
+ "MessageTaskFailedToCreateCacheDirectory": "Nie udało się utworzyć katalogu cache",
+ "MessageTaskFailedToEmbedMetadataInFile": "Nie udało się wbudować metadanych do pliku \"{0}\"",
+ "MessageTaskFailedToWriteMetadataFile": "Niepowodzenie zapisania pliku metadanych",
+ "MessageTaskNoFilesToScan": "Brak plików do skanowania",
+ "MessageTaskScanItemsAdded": "Dodano {0}",
+ "MessageTaskScanItemsMissing": "Brakuje {0}",
+ "MessageTaskScanItemsUpdated": "Zaktualizowano {0}",
+ "MessageTaskScanNoChangesNeeded": "Brak zmian",
+ "MessageTaskTargetDirectoryNotWritable": "Brak prawa zapisu do folderu docelowego",
"MessageThinking": "Myślę...",
"MessageUploaderItemFailed": "Nie udało się przesłać",
"MessageUploaderItemSuccess": "Przesłanie powiodło się!",
@@ -926,6 +964,21 @@
"ToastBookmarkRemoveSuccess": "Zakładka została usunięta",
"ToastCollectionRemoveSuccess": "Kolekcja usunięta",
"ToastCollectionUpdateSuccess": "Zaktualizowano kolekcję",
+ "ToastCoverSearchFailed": "Nieudane wyszukiwanie okładki",
+ "ToastCoverUpdateFailed": "Nieudana aktualizacja okładki",
+ "ToastDateTimeInvalidOrIncomplete": "Niepoprawna data i czas",
+ "ToastDeleteFileFailed": "Usunięcie pliku nie powiodło się",
+ "ToastDeleteFileSuccess": "Plik został usunięty",
+ "ToastDeviceAddFailed": "Nieudana próba dodania urządzenia",
+ "ToastDeviceNameAlreadyExists": "Czytnik z taką nazwą już istnieje",
+ "ToastDeviceTestEmailFailed": "NIeudana próba wysłania testowego maila",
+ "ToastDeviceTestEmailSuccess": "Testowy email został wysłany",
+ "ToastEmailSettingsUpdateSuccess": "Ustawienia email zaktualizowane",
+ "ToastEpisodeDownloadQueueClearSuccess": "Wyczyszczono kolejkę epizodów do ściągnięcia",
+ "ToastEpisodeUpdateSuccess": "Zaktualizowano {0} odcinków",
+ "ToastInvalidImageUrl": "Nieprawidłowy URL obrazu",
+ "ToastInvalidUrl": "Nieprawidłowy URL",
+ "ToastInvalidUrls": "Jeden lub więcej URL-i są nieprawidłowe",
"ToastItemCoverUpdateSuccess": "Zaktualizowano okładkę",
"ToastItemDetailsUpdateSuccess": "Zaktualizowano szczegóły",
"ToastItemMarkedAsFinishedFailed": "Nie udało się oznaczyć jako ukończone",
@@ -939,12 +992,28 @@
"ToastLibraryScanFailedToStart": "Nie udało się rozpocząć skanowania",
"ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki",
"ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji",
+ "ToastMatchAllAuthorsFailed": "Nie udało się dopasować wszystkich autorów",
+ "ToastMustHaveAtLeastOnePath": "Musi mieć przynajmniej jedną ścieżkę",
+ "ToastNameEmailRequired": "Nazwa i email są wymagane",
+ "ToastNameRequired": "Imię jest wymagane",
+ "ToastNewApiKeyUserError": "Trzeba wybrać użytkownika",
+ "ToastNewEpisodesFound": "Znaleziono {0} nowych odcinków",
+ "ToastNewUserCreatedFailed": "Nie udało się utworzyć konta: \"{0}\"",
+ "ToastNewUserCreatedSuccess": "Utworzono nowe konto",
+ "ToastNewUserLibraryError": "Trzeba wybrać co najmniej jedną bibliotekę",
+ "ToastNewUserPasswordError": "Hasło jest wymagane, jedynie użytkownik \"root\" może posiadać puste hasło",
+ "ToastNewUserTagError": "Trzeba wybrać chociaż jeden tag",
+ "ToastNewUserUsernameError": "Wprowadź nazwę użytkownika",
+ "ToastNoNewEpisodesFound": "Nie znaleziono nowych odcinków",
+ "ToastNoRSSFeed": "Podcast nie posiada RSS Feed",
+ "ToastNotificationFailedMaximum": "Maks. ilość nieudanych prób musi być >= 0",
"ToastPlaylistCreateFailed": "Nie udało się utworzyć playlisty",
"ToastPlaylistCreateSuccess": "Playlista utworzona",
"ToastPlaylistRemoveSuccess": "Playlista usunięta",
"ToastPlaylistUpdateSuccess": "Playlista zaktualizowana",
"ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu",
"ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony",
+ "ToastPodcastEpisodeUpdated": "Zaktualizowano odcinki",
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
"ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się",
"ToastRemoveItemFromCollectionFailed": "Nie udało się usunąć elementu z kolekcji",
From 3aa95fec11f409c56a611bd1ddcda433178954a7 Mon Sep 17 00:00:00 2001
From: Grzegorz Orlowski
Date: Mon, 6 Oct 2025 21:38:45 +0200
Subject: [PATCH 018/111] Translated using Weblate (Polish)
Currently translated at 88.2% (1026 of 1163 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
---
client/strings/pl.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/client/strings/pl.json b/client/strings/pl.json
index 7ed1551c3..98335a21d 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -709,6 +709,7 @@
"LabelUseAdvancedOptions": "Opcje zaawansowane",
"LabelUseChapterTrack": "Użyj ścieżki rozdziału",
"LabelUseFullTrack": "Użycie ścieżki rozdziału",
+ "LabelUseZeroForUnlimited": "Użyj 0, aby wyłączyć ograniczenia",
"LabelUser": "Użytkownik",
"LabelUsername": "Nazwa użytkownika",
"LabelValue": "Wartość",
From 6e19ad7777769a40eea3365de83ba3291c71b441 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=AE=D1=80=D0=B0=20=D0=9A=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2?=
=?UTF-8?q?=D0=B8=D1=87?=
Date: Sun, 5 Oct 2025 23:43:51 +0200
Subject: [PATCH 019/111] Translated using Weblate (Russian)
Currently translated at 100.0% (1163 of 1163 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
---
client/strings/ru.json | 2 ++
1 file changed, 2 insertions(+)
diff --git a/client/strings/ru.json b/client/strings/ru.json
index 14953379e..8bebb2835 100644
--- a/client/strings/ru.json
+++ b/client/strings/ru.json
@@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "Не удалось добавить элемент(ы) в коллекцию",
"ToastCollectionRemoveSuccess": "Коллекция удалена",
"ToastCollectionUpdateSuccess": "Коллекция обновлена",
+ "ToastConnectionNotAvailable": "Подключение недоступно. Пожалуйста попробуйте позже",
+ "ToastCoverSearchFailed": "Ошибка поиска обложки",
"ToastCoverUpdateFailed": "Не удалось обновить обложку",
"ToastDateTimeInvalidOrIncomplete": "Дата и время указаны неверно или не до конца",
"ToastDeleteFileFailed": "Не удалось удалить файл",
From aec8acbdd73f8297a0de4160fb070537434d7056 Mon Sep 17 00:00:00 2001
From: Jan-Eric Myhrgren
Date: Mon, 6 Oct 2025 09:10:21 +0200
Subject: [PATCH 020/111] Translated using Weblate (Swedish)
Currently translated at 97.2% (1131 of 1163 strings)
Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
---
client/strings/sv.json | 2 ++
1 file changed, 2 insertions(+)
diff --git a/client/strings/sv.json b/client/strings/sv.json
index b8a785c4b..e41dd8e2b 100644
--- a/client/strings/sv.json
+++ b/client/strings/sv.json
@@ -1008,6 +1008,8 @@
"ToastCollectionItemsAddFailed": "Misslyckades med att addera böcker till samlingen",
"ToastCollectionRemoveSuccess": "Samlingen har raderats",
"ToastCollectionUpdateSuccess": "Samlingen har uppdaterats",
+ "ToastConnectionNotAvailable": "Uppkopplingen är inte tillgänglig. Var vänlig försök senare.",
+ "ToastCoverSearchFailed": "Sökningen efter omslag misslyckades",
"ToastCoverUpdateFailed": "Uppdatering av omslag misslyckades",
"ToastDateTimeInvalidOrIncomplete": "Datum och klockslag är felaktigt eller ej komplett",
"ToastDeleteFileFailed": "Misslyckades att radera filen",
From 00c62fa49451f0fc2c3bb65fdeb2eee361bcd9e4 Mon Sep 17 00:00:00 2001
From: mikiher
Date: Wed, 8 Oct 2025 17:42:00 +0300
Subject: [PATCH 021/111] Add a script to build an uncompressed windows
executable
---
package.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/package.json b/package.json
index 34abc60ef..00168f25c 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"client": "cd client && npm ci && npm run generate",
"prod": "npm run client && npm ci && node index.js",
"build-win": "npm run client && pkg -t node20-win-x64 -o ./dist/win/audiobookshelf -C GZip .",
+ "build-win-no-compress": "npm run client && pkg -t node20-win-x64 -o ./dist/win/audiobookshelf .",
"build-linux": "build/linuxpackager",
"docker": "docker buildx build --platform linux/amd64,linux/arm64 --push . -t advplyr/audiobookshelf",
"docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local",
From 29752798f3d1eed348746830a879f0699ff48993 Mon Sep 17 00:00:00 2001
From: advplyr
Date: Wed, 8 Oct 2025 10:34:34 -0500
Subject: [PATCH 022/111] Version bump v2.30.0
---
client/package-lock.json | 4 ++--
client/package.json | 2 +-
package-lock.json | 4 ++--
package.json | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/client/package-lock.json b/client/package-lock.json
index efc8e4436..0d2699ed1 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-client",
- "version": "2.29.0",
+ "version": "2.30.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-client",
- "version": "2.29.0",
+ "version": "2.30.0",
"license": "ISC",
"dependencies": {
"@nuxtjs/axios": "^5.13.6",
diff --git a/client/package.json b/client/package.json
index da046a67f..50fd0b713 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
- "version": "2.29.0",
+ "version": "2.30.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",
diff --git a/package-lock.json b/package-lock.json
index 99415bd9c..8eea3abbd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf",
- "version": "2.29.0",
+ "version": "2.30.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
- "version": "2.29.0",
+ "version": "2.30.0",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",
diff --git a/package.json b/package.json
index 00168f25c..0b4ee4137 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "2.29.0",
+ "version": "2.30.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast server",
"main": "index.js",
From 3e876e3383ece4fdec80a74fb85d13a2a42fb55c Mon Sep 17 00:00:00 2001
From: advplyr
Date: Wed, 8 Oct 2025 14:24:50 -0500
Subject: [PATCH 023/111] Add Turkish to the language option
---
client/plugins/i18n.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js
index 861091757..e4c179016 100644
--- a/client/plugins/i18n.js
+++ b/client/plugins/i18n.js
@@ -29,6 +29,7 @@ const languageCodeMap = {
ru: { label: 'Русский', dateFnsLocale: 'ru' },
sl: { label: 'Slovenščina', dateFnsLocale: 'sl' },
sv: { label: 'Svenska', dateFnsLocale: 'sv' },
+ tr: { label: 'Türkçe', dateFnsLocale: 'tr' },
uk: { label: 'Українська', dateFnsLocale: 'uk' },
'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' },
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
From cafd92e206a07b50121feea67b75ac4959d99fbb Mon Sep 17 00:00:00 2001
From: advplyr
Date: Wed, 8 Oct 2025 14:52:14 -0500
Subject: [PATCH 024/111] Fix item edit modal show next/prev arrows when
opening from Files or Match context menu item #4718
---
client/components/app/BookShelfRow.vue | 4 ++--
client/components/app/LazyBookshelf.vue | 4 ++--
client/components/cards/LazyBookCard.vue | 4 ++--
client/components/widgets/ItemSlider.vue | 4 ++--
client/mixins/bookshelfCardsHelpers.js | 4 ++--
5 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/client/components/app/BookShelfRow.vue b/client/components/app/BookShelfRow.vue
index 082f9fe37..fac89a70b 100644
--- a/client/components/app/BookShelfRow.vue
+++ b/client/components/app/BookShelfRow.vue
@@ -93,10 +93,10 @@ export default {
editAuthor(author) {
this.$store.commit('globals/showEditAuthorModal', author)
},
- editItem(libraryItem) {
+ editItem(libraryItem, tab = 'details') {
var itemIds = this.shelf.entities.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
- this.$store.commit('showEditModal', libraryItem)
+ this.$store.commit('showEditModalOnTab', { libraryItem, tab: tab || 'details' })
},
editEpisode({ libraryItem, episode }) {
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])
diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue
index 854b61b24..4c72d0d78 100644
--- a/client/components/app/LazyBookshelf.vue
+++ b/client/components/app/LazyBookshelf.vue
@@ -232,11 +232,11 @@ export default {
clearFilter() {
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
},
- editEntity(entity) {
+ editEntity(entity, tab = 'details') {
if (this.entityName === 'items' || this.entityName === 'series-books') {
const bookIds = this.entities.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', bookIds)
- this.$store.commit('showEditModal', entity)
+ this.$store.commit('showEditModalOnTab', { libraryItem: entity, tab: tab || 'details' })
} else if (this.entityName === 'collections') {
this.$store.commit('globals/setEditCollection', entity)
} else if (this.entityName === 'playlists') {
diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue
index fbb50bb14..d06f083ba 100644
--- a/client/components/cards/LazyBookCard.vue
+++ b/client/components/cards/LazyBookCard.vue
@@ -788,11 +788,11 @@ export default {
},
showEditModalFiles() {
// More menu func
- this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'files' })
+ this.$emit('edit', this.libraryItem, 'files')
},
showEditModalMatch() {
// More menu func
- this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
+ this.$emit('edit', this.libraryItem, 'match')
},
sendToDevice(deviceName) {
// More menu func
diff --git a/client/components/widgets/ItemSlider.vue b/client/components/widgets/ItemSlider.vue
index 880654a44..5b96e51b4 100644
--- a/client/components/widgets/ItemSlider.vue
+++ b/client/components/widgets/ItemSlider.vue
@@ -132,10 +132,10 @@ export default {
editAuthor(author) {
this.$store.commit('globals/showEditAuthorModal', author)
},
- editItem(libraryItem) {
+ editItem(libraryItem, tab = 'details') {
var itemIds = this.items.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
- this.$store.commit('showEditModal', libraryItem)
+ this.$store.commit('showEditModalOnTab', { libraryItem, tab: tab || 'details' })
},
selectItem(payload) {
this.$emit('selectEntity', payload)
diff --git a/client/mixins/bookshelfCardsHelpers.js b/client/mixins/bookshelfCardsHelpers.js
index fc8a41256..f1571f70e 100644
--- a/client/mixins/bookshelfCardsHelpers.js
+++ b/client/mixins/bookshelfCardsHelpers.js
@@ -118,8 +118,8 @@ export default {
propsData: props,
parent: this,
created() {
- this.$on('edit', (entity) => {
- if (_this.editEntity) _this.editEntity(entity)
+ this.$on('edit', (entity, tab) => {
+ if (_this.editEntity) _this.editEntity(entity, tab)
})
this.$on('select', ({ entity, shiftKey }) => {
if (_this.selectEntity) _this.selectEntity(entity, shiftKey)
From 37beb7b37c8ea27ee86b04a311008786ec25fcd0 Mon Sep 17 00:00:00 2001
From: advplyr
Date: Wed, 8 Oct 2025 15:03:33 -0500
Subject: [PATCH 025/111] Disable chapter editor chapter play button when time
is invalid #4691
---
client/pages/audiobook/_id/chapters.vue | 21 ++++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
diff --git a/client/pages/audiobook/_id/chapters.vue b/client/pages/audiobook/_id/chapters.vue
index cf254165b..4d6c0e41d 100644
--- a/client/pages/audiobook/_id/chapters.vue
+++ b/client/pages/audiobook/_id/chapters.vue
@@ -117,10 +117,10 @@
-
@@ -594,6 +594,14 @@ export default {
this.hasChanges = hasChanges
},
+ getAudioTrackForTime(time) {
+ if (typeof time !== 'number') {
+ return null
+ }
+ return this.tracks.find((at) => {
+ return time >= at.startOffset && time < at.startOffset + at.duration
+ })
+ },
playChapter(chapter) {
console.log('Play Chapter', chapter.id)
if (this.selectedChapterId === chapter.id) {
@@ -608,9 +616,12 @@ export default {
this.destroyAudioEl()
}
- const audioTrack = this.tracks.find((at) => {
- return chapter.start >= at.startOffset && chapter.start < at.startOffset + at.duration
- })
+ const audioTrack = this.getAudioTrackForTime(chapter.start)
+ if (!audioTrack) {
+ console.error('No audio track found for chapter', chapter)
+ return
+ }
+
this.selectedChapter = chapter
this.isLoadingChapter = true
From 2592467d09146aae6d664466eee461fcd62f2298 Mon Sep 17 00:00:00 2001
From: advplyr
Date: Wed, 8 Oct 2025 15:32:37 -0500
Subject: [PATCH 026/111] Fix: Always re-load libraries when changing users
#4694
---
client/pages/login.vue | 1 +
client/store/libraries.js | 6 +++---
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/client/pages/login.vue b/client/pages/login.vue
index ef3827afe..60f31eca5 100644
--- a/client/pages/login.vue
+++ b/client/pages/login.vue
@@ -189,6 +189,7 @@ export default {
require('@/plugins/chromecast.js').default(this)
}
+ this.$store.commit('libraries/setLastLoad', 0) // Ensure libraries get loaded again when switching users
this.$store.commit('libraries/setCurrentLibrary', { id: userDefaultLibraryId })
this.$store.commit('user/setUser', user)
// Access token only returned from login, not authorize
diff --git a/client/store/libraries.js b/client/store/libraries.js
index 62c515ebf..115fb53bf 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -159,7 +159,7 @@ export const actions = {
.$get(`/api/libraries`)
.then((data) => {
commit('set', data.libraries)
- commit('setLastLoad')
+ commit('setLastLoad', new Date())
})
.catch((error) => {
console.error('Failed', error)
@@ -176,8 +176,8 @@ export const mutations = {
setFoldersLastUpdate(state) {
state.folderLastUpdate = Date.now()
},
- setLastLoad(state) {
- state.lastLoad = Date.now()
+ setLastLoad(state, date) {
+ state.lastLoad = date
},
setLibraryIssues(state, val) {
state.issues = val
From 4224f44259da03000a924c3ed382fa358909ecb1 Mon Sep 17 00:00:00 2001
From: Finn Dittmar
Date: Thu, 9 Oct 2025 08:49:13 +0200
Subject: [PATCH 027/111] Remove duplicate (and also wrong byte conversion)
---
client/components/cards/ItemUploadCard.vue | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue
index cb9dd5589..adbf3adbe 100644
--- a/client/components/cards/ItemUploadCard.vue
+++ b/client/components/cards/ItemUploadCard.vue
@@ -143,19 +143,12 @@ export default {
return Math.min(100, Math.round((this.uploadProgress.loaded / this.uploadProgress.total) * 100))
},
uploadProgressText() {
- const loaded = this.formatBytes(this.uploadProgress.loaded)
- const total = this.formatBytes(this.uploadProgress.total)
+ const loaded = this.$bytesPretty(this.uploadProgress.loaded)
+ const total = this.$bytesPretty(this.uploadProgress.total)
return `${this.uploadProgressPercent}% (${loaded} / ${total})`
}
},
methods: {
- formatBytes(bytes) {
- if (bytes === 0) return '0 Bytes'
- const k = 1024
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
- },
setUploadStatus(status) {
this.isUploading = status === 'uploading'
this.uploadFailed = status === 'failed'
From fc06aa2c788fd47957cffda3047eb2ef9f18c8ce Mon Sep 17 00:00:00 2001
From: Frank de Lange
Date: Thu, 9 Oct 2025 16:02:02 +0200
Subject: [PATCH 028/111] Explicitly launch OpenID Connect authentication with
?autoLaunch=1
This change extends OIDC authentication by enabling explicit redirection
to the OAuth provider when navigating to the login page with the manual
override parameter (/login?autoLaunch=1).
Use case: directly launch audiobookshelf from within e.g. Nextcloud using the
external sites app (use something like https://abs.example.org/login?autoLaunch=1
as URL) while keeping the possibility to launch audiobookshelf using its built-in
authentication mechanism. Assuming the username or mail address used in Nextcloud
and audiobookshelf are identical the user will be logged in to his or her account
no matter which method is used.
---
client/pages/login.vue | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/client/pages/login.vue b/client/pages/login.vue
index 60f31eca5..8e5cde098 100644
--- a/client/pages/login.vue
+++ b/client/pages/login.vue
@@ -299,8 +299,8 @@ export default {
}
if (authMethods.includes('openid')) {
- // Auto redirect unless query string ?autoLaunch=0
- if (this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') {
+ // Auto redirect unless query string ?autoLaunch=0 OR when explicity requested through ?autoLaunch=1
+ if ((this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') || this.$route.query?.autoLaunch == '1') {
window.location.href = this.openidAuthUri
}
From feed827223bf78e25f8ca015af57f76d46fecc3e Mon Sep 17 00:00:00 2001
From: advplyr
Date: Fri, 10 Oct 2025 18:00:37 -0500
Subject: [PATCH 029/111] Update settings update endpoint to validate
allowedOrigins is array
---
server/controllers/MiscController.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js
index c779bdd63..490cb27d2 100644
--- a/server/controllers/MiscController.js
+++ b/server/controllers/MiscController.js
@@ -142,6 +142,9 @@ class MiscController {
Logger.warn('Cannot disable iframe when ALLOW_IFRAME is enabled in environment')
return res.status(400).send('Cannot disable iframe when ALLOW_IFRAME is enabled in environment')
}
+ if (settingsUpdate.allowedOrigins && !Array.isArray(settingsUpdate.allowedOrigins)) {
+ return res.status(400).send('allowedOrigins must be an array')
+ }
const madeUpdates = Database.serverSettings.update(settingsUpdate)
if (madeUpdates) {
From a87ea327151fcb7ec67ce981bb6a44878a9ab343 Mon Sep 17 00:00:00 2001
From: advplyr
Date: Mon, 13 Oct 2025 09:50:01 -0500
Subject: [PATCH 030/111] Fix admin user unable to close sessions of other
users #4746
---
server/controllers/SessionController.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js
index 872f635de..7cc8d35e2 100644
--- a/server/controllers/SessionController.js
+++ b/server/controllers/SessionController.js
@@ -339,9 +339,9 @@ class SessionController {
var playbackSession = this.playbackSessionManager.getSession(req.params.id)
if (!playbackSession) return res.sendStatus(404)
- if (playbackSession.userId !== req.user.id) {
- Logger.error(`[SessionController] User "${req.user.username}" attempting to access session belonging to another user "${req.params.id}"`)
- return res.sendStatus(404)
+ if (playbackSession.userId !== req.user.id && !req.user.isAdminOrUp) {
+ Logger.error(`[SessionController] Non-admin user "${req.user.username}" attempting to access session belonging to another user "${req.params.id}"`)
+ return res.sendStatus(403)
}
req.playbackSession = playbackSession
From 4f30cbf2f652362f2fcb4ac1baa1af3104ad9fb5 Mon Sep 17 00:00:00 2001
From: mikiher
Date: Tue, 14 Oct 2025 18:09:32 +0300
Subject: [PATCH 031/111] SearchController: New providers API, query param
validation
---
server/controllers/SearchController.js | 298 ++++++++++++++++++++++---
1 file changed, 267 insertions(+), 31 deletions(-)
diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js
index bb3382f71..72f602d2a 100644
--- a/server/controllers/SearchController.js
+++ b/server/controllers/SearchController.js
@@ -6,6 +6,28 @@ const AuthorFinder = require('../finders/AuthorFinder')
const Database = require('../Database')
const { isValidASIN } = require('../utils')
+// Provider name mappings for display purposes
+const providerMap = {
+ all: 'All',
+ best: 'Best',
+ google: 'Google Books',
+ itunes: 'iTunes',
+ openlibrary: 'Open Library',
+ fantlab: 'FantLab.ru',
+ audiobookcovers: 'AudiobookCovers.com',
+ audible: 'Audible.com',
+ 'audible.ca': 'Audible.ca',
+ 'audible.uk': 'Audible.co.uk',
+ 'audible.au': 'Audible.com.au',
+ 'audible.fr': 'Audible.fr',
+ 'audible.de': 'Audible.de',
+ 'audible.jp': 'Audible.co.jp',
+ 'audible.it': 'Audible.it',
+ 'audible.in': 'Audible.in',
+ 'audible.es': 'Audible.es',
+ audnexus: 'Audnexus'
+}
+
/**
* @typedef RequestUserObject
* @property {import('../models/User')} user
@@ -16,6 +38,100 @@ const { isValidASIN } = require('../utils')
class SearchController {
constructor() {}
+ /**
+ * Validates that multiple parameters are strings
+ * @param {Object} params - Object with param names as keys and values to validate
+ * @param {string} methodName - Name of the calling method for logging
+ * @returns {{valid: boolean, error?: {status: number, message: string}}}
+ */
+ static validateStringParams(params, methodName) {
+ for (const [key, value] of Object.entries(params)) {
+ if (typeof value !== 'string') {
+ Logger.error(`[SearchController] ${methodName}: Invalid ${key} parameter`)
+ return {
+ valid: false,
+ error: {
+ status: 400,
+ message: 'Invalid request query params'
+ }
+ }
+ }
+ }
+ return { valid: true }
+ }
+
+ /**
+ * Validates that a required string parameter exists and is a string
+ * @param {any} value - Value to validate
+ * @param {string} paramName - Parameter name for logging
+ * @param {string} methodName - Name of the calling method for logging
+ * @returns {{valid: boolean, error?: {status: number, message: string}}}
+ */
+ static validateRequiredString(value, paramName, methodName) {
+ if (!value || typeof value !== 'string') {
+ Logger.error(`[SearchController] ${methodName}: Invalid or missing ${paramName}`)
+ return {
+ valid: false,
+ error: {
+ status: 400,
+ message: `Invalid or missing ${paramName}`
+ }
+ }
+ }
+ return { valid: true }
+ }
+
+ /**
+ * Validates and fetches a library item by ID
+ * @param {string} id - Library item ID
+ * @param {string} methodName - Name of the calling method for logging
+ * @returns {Promise<{valid: boolean, libraryItem?: any, error?: {status: number, message: string}}>}
+ */
+ static async fetchAndValidateLibraryItem(id, methodName) {
+ const validation = SearchController.validateRequiredString(id, 'library item id', methodName)
+ if (!validation.valid) {
+ return validation
+ }
+
+ const libraryItem = await Database.libraryItemModel.getExpandedById(id)
+ if (!libraryItem) {
+ Logger.error(`[SearchController] ${methodName}: Library item not found with id "${id}"`)
+ return {
+ valid: false,
+ error: {
+ status: 404,
+ message: 'Library item not found'
+ }
+ }
+ }
+
+ return { valid: true, libraryItem }
+ }
+
+ /**
+ * Maps custom metadata providers to standardized format
+ * @param {Array} providers - Array of custom provider objects
+ * @returns {Array<{value: string, text: string}>}
+ */
+ static mapCustomProviders(providers) {
+ return providers.map((provider) => ({
+ value: provider.getSlug(),
+ text: provider.name
+ }))
+ }
+
+ /**
+ * Static helper method to format provider for client (for use in array methods)
+ * @param {string} providerValue - Provider identifier
+ * @returns {{value: string, text: string}}
+ */
+ static formatProvider(providerValue) {
+ return {
+ value: providerValue,
+ text: providerMap[providerValue] || providerValue
+ }
+ }
+
/**
* GET: /api/search/books
*
@@ -23,18 +139,19 @@ class SearchController {
* @param {Response} res
*/
async findBooks(req, res) {
- const id = req.query.id
- const libraryItem = await Database.libraryItemModel.getExpandedById(id)
const provider = req.query.provider || 'google'
const title = req.query.title || ''
const author = req.query.author || ''
- if (typeof provider !== 'string' || typeof title !== 'string' || typeof author !== 'string') {
- Logger.error(`[SearchController] findBooks: Invalid request query params`)
- return res.status(400).send('Invalid request query params')
- }
+ // Validate string parameters
+ const validation = SearchController.validateStringParams({ provider, title, author }, 'findBooks')
+ if (!validation.valid) return res.status(validation.error.status).send(validation.error.message)
- const results = await BookFinder.search(libraryItem, provider, title, author)
+ // Fetch and validate library item
+ const itemValidation = await SearchController.fetchAndValidateLibraryItem(req.query.id, 'findBooks')
+ if (!itemValidation.valid) return res.status(itemValidation.error.status).send(itemValidation.error.message)
+
+ const results = await BookFinder.search(itemValidation.libraryItem, provider, title, author)
res.json(results)
}
@@ -46,19 +163,23 @@ class SearchController {
*/
async findCovers(req, res) {
const query = req.query
- const podcast = query.podcast == 1
+ const podcast = query.podcast === '1' || query.podcast === 1
+ const title = query.title || ''
+ const author = query.author || ''
+ const provider = query.provider || 'google'
- if (!query.title || typeof query.title !== 'string') {
- Logger.error(`[SearchController] findCovers: Invalid title sent in query`)
- return res.sendStatus(400)
- }
+ // Validate required title
+ const titleValidation = SearchController.validateRequiredString(title, 'title', 'findCovers')
+ if (!titleValidation.valid) return res.status(titleValidation.error.status).send(titleValidation.error.message)
+
+ // Validate other string parameters
+ const validation = SearchController.validateStringParams({ author, provider }, 'findCovers')
+ if (!validation.valid) return res.status(validation.error.status).send(validation.error.message)
let results = null
- if (podcast) results = await PodcastFinder.findCovers(query.title)
- else results = await BookFinder.findCovers(query.provider || 'google', query.title, query.author || '')
- res.json({
- results
- })
+ if (podcast) results = await PodcastFinder.findCovers(title)
+ else results = await BookFinder.findCovers(provider, title, author)
+ res.json({ results })
}
/**
@@ -71,14 +192,16 @@ class SearchController {
async findPodcasts(req, res) {
const term = req.query.term
const country = req.query.country || 'us'
- if (!term) {
- Logger.error('[SearchController] Invalid request query param "term" is required')
- return res.status(400).send('Invalid request query param "term" is required')
- }
- const results = await PodcastFinder.search(term, {
- country
- })
+ // Validate required term
+ const termValidation = SearchController.validateRequiredString(term, 'term', 'findPodcasts')
+ if (!termValidation.valid) return res.status(termValidation.error.status).send(termValidation.error.message)
+
+ // Validate country parameter
+ const validation = SearchController.validateStringParams({ country }, 'findPodcasts')
+ if (!validation.valid) return res.status(validation.error.status).send(validation.error.message)
+
+ const results = await PodcastFinder.search(term, { country })
res.json(results)
}
@@ -90,10 +213,10 @@ class SearchController {
*/
async findAuthor(req, res) {
const query = req.query.q
- if (!query || typeof query !== 'string') {
- Logger.error(`[SearchController] findAuthor: Invalid query param`)
- return res.status(400).send('Invalid query param')
- }
+
+ // Validate query parameter
+ const validation = SearchController.validateRequiredString(query, 'query', 'findAuthor')
+ if (!validation.valid) return res.status(validation.error.status).send(validation.error.message)
const author = await AuthorFinder.findAuthorByName(query)
res.json(author)
@@ -107,15 +230,128 @@ class SearchController {
*/
async findChapters(req, res) {
const asin = req.query.asin
- if (!isValidASIN(asin.toUpperCase())) {
- return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' })
- }
const region = (req.query.region || 'us').toLowerCase()
+
+ // Validate ASIN parameter
+ const asinValidation = SearchController.validateRequiredString(asin, 'asin', 'findChapters')
+ if (!asinValidation.valid) return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' })
+
+ if (!isValidASIN(asin.toUpperCase())) return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' })
+
+ // Validate region parameter
+ const validation = SearchController.validateStringParams({ region }, 'findChapters')
+ if (!validation.valid) res.json({ error: 'Invalid region', stringKey: 'MessageInvalidRegion' })
+
const chapterData = await BookFinder.findChapters(asin, region)
if (!chapterData) {
return res.json({ error: 'Chapters not found', stringKey: 'MessageChaptersNotFound' })
}
res.json(chapterData)
}
+
+ /**
+ * GET: /api/search/providers/podcasts/covers
+ * Get available podcast cover metadata providers
+ *
+ * @param {RequestWithUser} req
+ * @param {Response} res
+ */
+ async getPodcastCoverProviders(req, res) {
+ // Podcast covers only use iTunes
+ const customProviders = await Database.customMetadataProviderModel.findAll({
+ where: {
+ mediaType: 'podcast'
+ }
+ })
+
+ const providers = [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customProviders)]
+
+ res.json({ providers })
+ }
+
+ /**
+ * GET: /api/search/providers/books/covers
+ * Get available book cover metadata providers
+ *
+ * @param {RequestWithUser} req
+ * @param {Response} res
+ */
+ async getBookCoverProviders(req, res) {
+ // Book covers use all book providers
+ const customProviders = await Database.customMetadataProviderModel.findAll({
+ where: {
+ mediaType: 'book'
+ }
+ })
+
+ const providers = [SearchController.formatProvider('best'), ...BookFinder.providers.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customProviders), SearchController.formatProvider('all')]
+
+ res.json({ providers })
+ }
+
+ /**
+ * GET: /api/search/providers/books
+ * Get available book metadata providers
+ *
+ * @param {RequestWithUser} req
+ * @param {Response} res
+ */
+ async getBookProviders(req, res) {
+ const customProviders = await Database.customMetadataProviderModel.findAll({
+ where: {
+ mediaType: 'book'
+ }
+ })
+
+ // Filter out cover-only providers
+ const bookProviders = BookFinder.providers.filter((p) => p !== 'audiobookcovers')
+
+ const providers = [...bookProviders.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customProviders)]
+
+ res.json({ providers })
+ }
+
+ /**
+ * GET: /api/search/providers/podcasts
+ * Get available podcast metadata providers
+ *
+ * @param {RequestWithUser} req
+ * @param {Response} res
+ */
+ async getPodcastProviders(req, res) {
+ const customProviders = await Database.customMetadataProviderModel.findAll({
+ where: {
+ mediaType: 'podcast'
+ }
+ })
+
+ const providers = [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customProviders)]
+
+ res.json({ providers })
+ }
+
+ /**
+ * GET: /api/search/providers/authors
+ * Get available author metadata providers
+ *
+ * @param {RequestWithUser} req
+ * @param {Response} res
+ */
+ async getAuthorProviders(req, res) {
+ const providers = [SearchController.formatProvider('audnexus')]
+ res.json({ providers })
+ }
+
+ /**
+ * GET: /api/search/providers/chapters
+ * Get available chapter metadata providers
+ *
+ * @param {RequestWithUser} req
+ * @param {Response} res
+ */
+ async getChapterProviders(req, res) {
+ const providers = [SearchController.formatProvider('audnexus')]
+ res.json({ providers })
+ }
}
module.exports = new SearchController()
From 1da3ab7fdc819ad71271098bba1f186de058fc7a Mon Sep 17 00:00:00 2001
From: mikiher
Date: Tue, 14 Oct 2025 18:10:12 +0300
Subject: [PATCH 032/111] ApiRouter: New provider API routes
---
server/routers/ApiRouter.js | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index 6446ecc80..c72aa143f 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -283,6 +283,12 @@ class ApiRouter {
this.router.get('/search/podcast', SearchController.findPodcasts.bind(this))
this.router.get('/search/authors', SearchController.findAuthor.bind(this))
this.router.get('/search/chapters', SearchController.findChapters.bind(this))
+ this.router.get('/search/providers/books', SearchController.getBookProviders.bind(this))
+ this.router.get('/search/providers/books/covers', SearchController.getBookCoverProviders.bind(this))
+ this.router.get('/search/providers/podcasts', SearchController.getPodcastProviders.bind(this))
+ this.router.get('/search/providers/podcasts/covers', SearchController.getPodcastCoverProviders.bind(this))
+ this.router.get('/search/providers/authors', SearchController.getAuthorProviders.bind(this))
+ this.router.get('/search/providers/chapters', SearchController.getChapterProviders.bind(this))
//
// Cache Routes (Admin and up)
From ce4ff4f894ea5e924ac64429c3fa8fa772f210d0 Mon Sep 17 00:00:00 2001
From: mikiher
Date: Wed, 15 Oct 2025 09:52:15 +0300
Subject: [PATCH 033/111] Client: Use new server providers API
---
.../modals/BatchQuickMatchModel.vue | 2 +-
client/components/modals/item/tabs/Cover.vue | 4 +-
client/components/modals/item/tabs/Match.vue | 2 +-
.../modals/libraries/EditLibrary.vue | 2 +-
.../modals/libraries/LibrarySettings.vue | 2 +-
client/layouts/default.vue | 24 ++-
client/pages/config/index.vue | 3 +-
client/pages/upload/index.vue | 2 +-
client/store/libraries.js | 2 -
client/store/scanners.js | 171 ++++++------------
10 files changed, 90 insertions(+), 124 deletions(-)
diff --git a/client/components/modals/BatchQuickMatchModel.vue b/client/components/modals/BatchQuickMatchModel.vue
index 8bea68faf..138684b20 100644
--- a/client/components/modals/BatchQuickMatchModel.vue
+++ b/client/components/modals/BatchQuickMatchModel.vue
@@ -88,7 +88,7 @@ export default {
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
- return this.$store.state.scanners.providers
+ return this.$store.state.scanners.bookProviders
},
libraryProvider() {
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
diff --git a/client/components/modals/item/tabs/Cover.vue b/client/components/modals/item/tabs/Cover.vue
index b404a9abb..4ed734b1b 100644
--- a/client/components/modals/item/tabs/Cover.vue
+++ b/client/components/modals/item/tabs/Cover.vue
@@ -133,8 +133,8 @@ export default {
}
},
providers() {
- if (this.isPodcast) return this.$store.state.scanners.podcastProviders
- return [{ text: 'Best', value: 'best' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders, { text: 'All', value: 'all' }]
+ if (this.isPodcast) return this.$store.state.scanners.podcastCoverProviders
+ return this.$store.state.scanners.bookCoverProviders
},
searchTitleLabel() {
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue
index 3faa26b27..36cdd7a8e 100644
--- a/client/components/modals/item/tabs/Match.vue
+++ b/client/components/modals/item/tabs/Match.vue
@@ -319,7 +319,7 @@ export default {
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
- return this.$store.state.scanners.providers
+ return this.$store.state.scanners.bookProviders
},
searchTitleLabel() {
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
diff --git a/client/components/modals/libraries/EditLibrary.vue b/client/components/modals/libraries/EditLibrary.vue
index 083fc5766..1e4d3990a 100644
--- a/client/components/modals/libraries/EditLibrary.vue
+++ b/client/components/modals/libraries/EditLibrary.vue
@@ -74,7 +74,7 @@ export default {
},
providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
- return this.$store.state.scanners.providers
+ return this.$store.state.scanners.bookProviders
}
},
methods: {
diff --git a/client/components/modals/libraries/LibrarySettings.vue b/client/components/modals/libraries/LibrarySettings.vue
index d3b40de95..231cb5ec4 100644
--- a/client/components/modals/libraries/LibrarySettings.vue
+++ b/client/components/modals/libraries/LibrarySettings.vue
@@ -136,7 +136,7 @@ export default {
},
providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
- return this.$store.state.scanners.providers
+ return this.$store.state.scanners.bookProviders
},
maskAsFinishedWhenItems() {
return [
diff --git a/client/layouts/default.vue b/client/layouts/default.vue
index 4b9729248..21c256bd3 100644
--- a/client/layouts/default.vue
+++ b/client/layouts/default.vue
@@ -371,11 +371,25 @@ export default {
},
customMetadataProviderAdded(provider) {
if (!provider?.id) return
- this.$store.commit('scanners/addCustomMetadataProvider', provider)
+ // Refetch the appropriate provider types
+ if (provider.mediaType === 'book') {
+ this.$store.dispatch('scanners/fetchBookProviders')
+ this.$store.dispatch('scanners/fetchBookCoverProviders')
+ } else if (provider.mediaType === 'podcast') {
+ this.$store.dispatch('scanners/fetchPodcastProviders')
+ this.$store.dispatch('scanners/fetchPodcastCoverProviders')
+ }
},
customMetadataProviderRemoved(provider) {
if (!provider?.id) return
- this.$store.commit('scanners/removeCustomMetadataProvider', provider)
+ // Refetch the appropriate provider types
+ if (provider.mediaType === 'book') {
+ this.$store.dispatch('scanners/fetchBookProviders')
+ this.$store.dispatch('scanners/fetchBookCoverProviders')
+ } else if (provider.mediaType === 'podcast') {
+ this.$store.dispatch('scanners/fetchPodcastProviders')
+ this.$store.dispatch('scanners/fetchPodcastCoverProviders')
+ }
},
initializeSocket() {
if (this.$root.socket) {
@@ -612,6 +626,12 @@ export default {
this.$store.dispatch('libraries/load')
+ // Fetch metadata providers
+ this.$store.dispatch('scanners/fetchBookProviders')
+ this.$store.dispatch('scanners/fetchBookCoverProviders')
+ this.$store.dispatch('scanners/fetchPodcastProviders')
+ this.$store.dispatch('scanners/fetchPodcastCoverProviders')
+
this.initLocalStorage()
this.checkVersionUpdate()
diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue
index 3d030bb32..099ae9c4e 100644
--- a/client/pages/config/index.vue
+++ b/client/pages/config/index.vue
@@ -247,7 +247,8 @@ export default {
return this.$store.state.serverSettings
},
providers() {
- return this.$store.state.scanners.providers
+ // Use book cover providers for the cover provider dropdown
+ return this.$store.state.scanners.bookCoverProviders || []
},
dateFormats() {
return this.$store.state.globals.dateFormats
diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue
index eef05b608..84d4d1478 100644
--- a/client/pages/upload/index.vue
+++ b/client/pages/upload/index.vue
@@ -155,7 +155,7 @@ export default {
},
providers() {
if (this.selectedLibraryIsPodcast) return this.$store.state.scanners.podcastProviders
- return this.$store.state.scanners.providers
+ return this.$store.state.scanners.bookProviders
},
canFetchMetadata() {
return !this.selectedLibraryIsPodcast && this.fetchMetadata.enabled
diff --git a/client/store/libraries.js b/client/store/libraries.js
index 115fb53bf..a824b6474 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -131,8 +131,6 @@ export const actions = {
commit('setLibraryIssues', issues)
commit('setLibraryFilterData', filterData)
commit('setNumUserPlaylists', numUserPlaylists)
- commit('scanners/setCustomMetadataProviders', customMetadataProviders, { root: true })
-
commit('setCurrentLibrary', { id: libraryId })
return data
})
diff --git a/client/store/scanners.js b/client/store/scanners.js
index 2d3d465ca..40a8d864b 100644
--- a/client/store/scanners.js
+++ b/client/store/scanners.js
@@ -1,126 +1,73 @@
export const state = () => ({
- providers: [
- {
- text: 'Google Books',
- value: 'google'
- },
- {
- text: 'Open Library',
- value: 'openlibrary'
- },
- {
- text: 'iTunes',
- value: 'itunes'
- },
- {
- text: 'Audible.com',
- value: 'audible'
- },
- {
- text: 'Audible.ca',
- value: 'audible.ca'
- },
- {
- text: 'Audible.co.uk',
- value: 'audible.uk'
- },
- {
- text: 'Audible.com.au',
- value: 'audible.au'
- },
- {
- text: 'Audible.fr',
- value: 'audible.fr'
- },
- {
- text: 'Audible.de',
- value: 'audible.de'
- },
- {
- text: 'Audible.co.jp',
- value: 'audible.jp'
- },
- {
- text: 'Audible.it',
- value: 'audible.it'
- },
- {
- text: 'Audible.co.in',
- value: 'audible.in'
- },
- {
- text: 'Audible.es',
- value: 'audible.es'
- },
- {
- text: 'FantLab.ru',
- value: 'fantlab'
- }
- ],
- podcastProviders: [
- {
- text: 'iTunes',
- value: 'itunes'
- }
- ],
- coverOnlyProviders: [
- {
- text: 'AudiobookCovers.com',
- value: 'audiobookcovers'
- }
- ]
+ bookProviders: [],
+ podcastProviders: [],
+ bookCoverProviders: [],
+ podcastCoverProviders: []
})
export const getters = {
- checkBookProviderExists: state => (providerValue) => {
- return state.providers.some(p => p.value === providerValue)
+ checkBookProviderExists: (state) => (providerValue) => {
+ return state.bookProviders.some((p) => p.value === providerValue)
},
- checkPodcastProviderExists: state => (providerValue) => {
- return state.podcastProviders.some(p => p.value === providerValue)
+ checkPodcastProviderExists: (state) => (providerValue) => {
+ return state.podcastProviders.some((p) => p.value === providerValue)
}
}
-export const actions = {}
-
-export const mutations = {
- addCustomMetadataProvider(state, provider) {
- if (provider.mediaType === 'book') {
- if (state.providers.some(p => p.value === provider.slug)) return
- state.providers.push({
- text: provider.name,
- value: provider.slug
- })
- } else {
- if (state.podcastProviders.some(p => p.value === provider.slug)) return
- state.podcastProviders.push({
- text: provider.name,
- value: provider.slug
- })
+export const actions = {
+ async fetchBookProviders({ commit }) {
+ try {
+ const response = await this.$axios.$get('/api/search/providers/books')
+ if (response?.providers) {
+ commit('setBookProviders', response.providers)
+ }
+ } catch (error) {
+ console.error('Failed to fetch book providers', error)
}
},
- removeCustomMetadataProvider(state, provider) {
- if (provider.mediaType === 'book') {
- state.providers = state.providers.filter(p => p.value !== provider.slug)
- } else {
- state.podcastProviders = state.podcastProviders.filter(p => p.value !== provider.slug)
+ async fetchPodcastProviders({ commit }) {
+ try {
+ const response = await this.$axios.$get('/api/search/providers/podcasts')
+ if (response?.providers) {
+ commit('setPodcastProviders', response.providers)
+ }
+ } catch (error) {
+ console.error('Failed to fetch podcast providers', error)
}
},
- setCustomMetadataProviders(state, providers) {
- if (!providers?.length) return
-
- const mediaType = providers[0].mediaType
- if (mediaType === 'book') {
- // clear previous values, and add new values to the end
- state.providers = state.providers.filter((p) => !p.value.startsWith('custom-'))
- state.providers = [
- ...state.providers,
- ...providers.map((p) => ({
- text: p.name,
- value: p.slug
- }))
- ]
- } else {
- // Podcast providers not supported yet
+ async fetchBookCoverProviders({ commit }) {
+ try {
+ const response = await this.$axios.$get('/api/search/providers/books/covers')
+ if (response?.providers) {
+ commit('setBookCoverProviders', response.providers)
+ }
+ } catch (error) {
+ console.error('Failed to fetch book cover providers', error)
+ }
+ },
+ async fetchPodcastCoverProviders({ commit }) {
+ try {
+ const response = await this.$axios.$get('/api/search/providers/podcasts/covers')
+ if (response?.providers) {
+ commit('setPodcastCoverProviders', response.providers)
+ }
+ } catch (error) {
+ console.error('Failed to fetch podcast cover providers', error)
}
}
-}
\ No newline at end of file
+}
+
+export const mutations = {
+ setBookProviders(state, providers) {
+ state.bookProviders = providers
+ },
+ setPodcastProviders(state, providers) {
+ state.podcastProviders = providers
+ },
+ setBookCoverProviders(state, providers) {
+ state.bookCoverProviders = providers
+ },
+ setPodcastCoverProviders(state, providers) {
+ state.podcastCoverProviders = providers
+ }
+}
From 888190a6be4c103b6eec3e5c0f97f678eda87900 Mon Sep 17 00:00:00 2001
From: mikiher
Date: Wed, 15 Oct 2025 18:28:15 +0300
Subject: [PATCH 034/111] Fix codeQL failures
---
server/controllers/SearchController.js | 25 +++++++++++++------------
server/finders/BookFinder.js | 7 +++++--
server/utils/index.js | 19 +++++++++++++++++++
3 files changed, 37 insertions(+), 14 deletions(-)
diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js
index 72f602d2a..57538d2c0 100644
--- a/server/controllers/SearchController.js
+++ b/server/controllers/SearchController.js
@@ -4,7 +4,7 @@ const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder')
const AuthorFinder = require('../finders/AuthorFinder')
const Database = require('../Database')
-const { isValidASIN } = require('../utils')
+const { isValidASIN, getQueryParamAsString } = require('../utils')
// Provider name mappings for display purposes
const providerMap = {
@@ -139,9 +139,10 @@ class SearchController {
* @param {Response} res
*/
async findBooks(req, res) {
- const provider = req.query.provider || 'google'
- const title = req.query.title || ''
- const author = req.query.author || ''
+ // Safely extract query parameters, rejecting arrays to prevent type confusion
+ const provider = getQueryParamAsString(req.query.provider, 'google')
+ const title = getQueryParamAsString(req.query.title, '')
+ const author = getQueryParamAsString(req.query.author, '')
// Validate string parameters
const validation = SearchController.validateStringParams({ provider, title, author }, 'findBooks')
@@ -164,9 +165,9 @@ class SearchController {
async findCovers(req, res) {
const query = req.query
const podcast = query.podcast === '1' || query.podcast === 1
- const title = query.title || ''
- const author = query.author || ''
- const provider = query.provider || 'google'
+ const title = getQueryParamAsString(query.title, '')
+ const author = getQueryParamAsString(query.author, '')
+ const provider = getQueryParamAsString(query.provider, 'google')
// Validate required title
const titleValidation = SearchController.validateRequiredString(title, 'title', 'findCovers')
@@ -190,8 +191,8 @@ class SearchController {
* @param {Response} res
*/
async findPodcasts(req, res) {
- const term = req.query.term
- const country = req.query.country || 'us'
+ const term = getQueryParamAsString(req.query.term)
+ const country = getQueryParamAsString(req.query.country, 'us')
// Validate required term
const termValidation = SearchController.validateRequiredString(term, 'term', 'findPodcasts')
@@ -212,7 +213,7 @@ class SearchController {
* @param {Response} res
*/
async findAuthor(req, res) {
- const query = req.query.q
+ const query = getQueryParamAsString(req.query.q)
// Validate query parameter
const validation = SearchController.validateRequiredString(query, 'query', 'findAuthor')
@@ -229,8 +230,8 @@ class SearchController {
* @param {Response} res
*/
async findChapters(req, res) {
- const asin = req.query.asin
- const region = (req.query.region || 'us').toLowerCase()
+ const asin = getQueryParamAsString(req.query.asin)
+ const region = getQueryParamAsString(req.query.region, 'us').toLowerCase()
// Validate ASIN parameter
const asinValidation = SearchController.validateRequiredString(asin, 'asin', 'findChapters')
diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js
index a6a6b07e6..6dc90c44f 100644
--- a/server/finders/BookFinder.js
+++ b/server/finders/BookFinder.js
@@ -402,7 +402,8 @@ class BookFinder {
let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus)
// Remove underscores and parentheses with their contents, and replace with a separator
- const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, ' - ')
+ // Use negated character classes to prevent ReDoS vulnerability
+ const cleanTitle = title.replace(/\[[^\]]*\]|\([^)]*\)|{[^}]*}|_/g, ' - ')
// Split title into hypen-separated parts
const titleParts = cleanTitle.split(/ - | -|- /)
for (const titlePart of titleParts) authorCandidates.add(titlePart)
@@ -668,7 +669,9 @@ function cleanTitleForCompares(title, keepSubtitle = false) {
let stripped = keepSubtitle ? title : stripSubtitle(title)
// Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game")
- let cleaned = stripped.replace(/ *\([^)]*\) */g, '')
+ // Use a safe two-pass approach to prevent ReDoS vulnerability
+ let cleaned = stripped.replace(/\([^)]*\)/g, '') // Remove parenthetical content
+ cleaned = cleaned.replace(/\s+/g, ' ').trim() // Clean up any resulting multiple spaces
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
cleaned = cleaned.replace(/'/g, '')
diff --git a/server/utils/index.js b/server/utils/index.js
index 369620276..4421fbade 100644
--- a/server/utils/index.js
+++ b/server/utils/index.js
@@ -277,3 +277,22 @@ module.exports.timestampToSeconds = (timestamp) => {
}
return null
}
+
+/**
+ * Safely extracts a query parameter as a string, rejecting arrays to prevent type confusion
+ * Express query parameters can be arrays if the same parameter appears multiple times
+ * @example ?author=Smith => "Smith"
+ * @example ?author=Smith&author=Jones => null (array detected)
+ *
+ * @param {any} value - Query parameter value
+ * @param {string} defaultValue - Default value if undefined/null
+ * @returns {string|null} String value or null if invalid (array)
+ */
+module.exports.getQueryParamAsString = (value, defaultValue = '') => {
+ // Explicitly reject arrays to prevent type confusion
+ if (Array.isArray(value)) {
+ return null
+ }
+ // Return default for undefined/null, otherwise return the value
+ return value == null ? defaultValue : value
+}
From 3f6162f53c3246963cbbaaa1697043a3119cfd89 Mon Sep 17 00:00:00 2001
From: mikiher
Date: Wed, 15 Oct 2025 18:54:29 +0300
Subject: [PATCH 035/111] CodeQL fix: limit parameter sizes
---
server/finders/BookFinder.js | 9 +++++++--
server/utils/index.js | 16 ++++++++++++----
2 files changed, 19 insertions(+), 6 deletions(-)
diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js
index 6dc90c44f..fe1a61027 100644
--- a/server/finders/BookFinder.js
+++ b/server/finders/BookFinder.js
@@ -385,6 +385,11 @@ class BookFinder {
if (!title) return books
+ // Truncate excessively long inputs to prevent ReDoS attacks
+ const MAX_INPUT_LENGTH = 500
+ title = title.substring(0, MAX_INPUT_LENGTH)
+ author = author?.substring(0, MAX_INPUT_LENGTH) || author
+
const isTitleAsin = isValidASIN(title.toUpperCase())
let actualTitleQuery = title
@@ -402,7 +407,7 @@ class BookFinder {
let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus)
// Remove underscores and parentheses with their contents, and replace with a separator
- // Use negated character classes to prevent ReDoS vulnerability
+ // Use negated character classes to prevent ReDoS vulnerability (input length validated at entry point)
const cleanTitle = title.replace(/\[[^\]]*\]|\([^)]*\)|{[^}]*}|_/g, ' - ')
// Split title into hypen-separated parts
const titleParts = cleanTitle.split(/ - | -|- /)
@@ -669,7 +674,7 @@ function cleanTitleForCompares(title, keepSubtitle = false) {
let stripped = keepSubtitle ? title : stripSubtitle(title)
// Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game")
- // Use a safe two-pass approach to prevent ReDoS vulnerability
+ // Use negated character class to prevent ReDoS vulnerability (input length validated at entry point)
let cleaned = stripped.replace(/\([^)]*\)/g, '') // Remove parenthetical content
cleaned = cleaned.replace(/\s+/g, ' ').trim() // Clean up any resulting multiple spaces
diff --git a/server/utils/index.js b/server/utils/index.js
index 4421fbade..0661d14f8 100644
--- a/server/utils/index.js
+++ b/server/utils/index.js
@@ -286,13 +286,21 @@ module.exports.timestampToSeconds = (timestamp) => {
*
* @param {any} value - Query parameter value
* @param {string} defaultValue - Default value if undefined/null
- * @returns {string|null} String value or null if invalid (array)
+ * @param {number} maxLength - Optional maximum length (defaults to 10000 to prevent ReDoS attacks)
+ * @returns {string|null} String value or null if invalid (array or too long)
*/
-module.exports.getQueryParamAsString = (value, defaultValue = '') => {
+module.exports.getQueryParamAsString = (value, defaultValue = '', maxLength = 1000) => {
// Explicitly reject arrays to prevent type confusion
if (Array.isArray(value)) {
return null
}
- // Return default for undefined/null, otherwise return the value
- return value == null ? defaultValue : value
+ // Return default for undefined/null
+ if (value == null) {
+ return defaultValue
+ }
+ // Reject excessively long strings to prevent ReDoS attacks
+ if (typeof value === 'string' && value.length > maxLength) {
+ return null
+ }
+ return value
}
From 0a82d6a41b5662ee681bdecf1e922824f427fda5 Mon Sep 17 00:00:00 2001
From: mikiher
Date: Fri, 17 Oct 2025 08:11:03 +0300
Subject: [PATCH 036/111] CoverSearchManager: Fix broken podcast cover search
---
server/managers/CoverSearchManager.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/server/managers/CoverSearchManager.js b/server/managers/CoverSearchManager.js
index ddcaa23db..193176766 100644
--- a/server/managers/CoverSearchManager.js
+++ b/server/managers/CoverSearchManager.js
@@ -224,6 +224,9 @@ class CoverSearchManager {
if (!Array.isArray(results)) return covers
results.forEach((result) => {
+ if (typeof result === 'string') {
+ covers.push(result)
+ }
if (result.covers && Array.isArray(result.covers)) {
covers.push(...result.covers)
}
From 0a4de61eff00ae1f4ba102acf4cf2a744192e208 Mon Sep 17 00:00:00 2001
From: Finn Dittmar
Date: Sun, 19 Oct 2025 09:22:12 +0200
Subject: [PATCH 037/111] Chnage Auth Expiry
---
server/auth/TokenManager.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js
index d972b5345..faa6774a3 100644
--- a/server/auth/TokenManager.js
+++ b/server/auth/TokenManager.js
@@ -12,9 +12,9 @@ class TokenManager {
constructor() {
/** @type {number} Refresh token expiry in seconds */
- this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 7 * 24 * 60 * 60 // 7 days
+ this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 30 * 24 * 60 * 60 // 30 days
/** @type {number} Access token expiry in seconds */
- this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 12 * 60 * 60 // 12 hours
+ this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 1 * 60 * 60 // 1 hour
if (parseInt(process.env.REFRESH_TOKEN_EXPIRY) > 0) {
Logger.info(`[TokenManager] Refresh token expiry set from ENV variable to ${this.RefreshTokenExpiry} seconds`)
From 0a8662d1983736ea36fb76a3bfd028f6e2586106 Mon Sep 17 00:00:00 2001
From: mikiher
Date: Sun, 19 Oct 2025 10:53:27 +0300
Subject: [PATCH 038/111] Merge providers API into a single endpoint
---
server/controllers/SearchController.js | 104 +++----------------------
server/routers/ApiRouter.js | 7 +-
2 files changed, 13 insertions(+), 98 deletions(-)
diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js
index 57538d2c0..72d215f34 100644
--- a/server/controllers/SearchController.js
+++ b/server/controllers/SearchController.js
@@ -251,108 +251,28 @@ class SearchController {
}
/**
- * GET: /api/search/providers/podcasts/covers
- * Get available podcast cover metadata providers
+ * GET: /api/search/providers
+ * Get all available metadata providers
*
* @param {RequestWithUser} req
* @param {Response} res
*/
- async getPodcastCoverProviders(req, res) {
- // Podcast covers only use iTunes
- const customProviders = await Database.customMetadataProviderModel.findAll({
- where: {
- mediaType: 'podcast'
- }
- })
+ async getAllProviders(req, res) {
+ const customProviders = await Database.customMetadataProviderModel.findAll()
- const providers = [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customProviders)]
+ const customBookProviders = customProviders.filter((p) => p.mediaType === 'book')
+ const customPodcastProviders = customProviders.filter((p) => p.mediaType === 'podcast')
- res.json({ providers })
- }
-
- /**
- * GET: /api/search/providers/books/covers
- * Get available book cover metadata providers
- *
- * @param {RequestWithUser} req
- * @param {Response} res
- */
- async getBookCoverProviders(req, res) {
- // Book covers use all book providers
- const customProviders = await Database.customMetadataProviderModel.findAll({
- where: {
- mediaType: 'book'
- }
- })
-
- const providers = [SearchController.formatProvider('best'), ...BookFinder.providers.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customProviders), SearchController.formatProvider('all')]
-
- res.json({ providers })
- }
-
- /**
- * GET: /api/search/providers/books
- * Get available book metadata providers
- *
- * @param {RequestWithUser} req
- * @param {Response} res
- */
- async getBookProviders(req, res) {
- const customProviders = await Database.customMetadataProviderModel.findAll({
- where: {
- mediaType: 'book'
- }
- })
-
- // Filter out cover-only providers
const bookProviders = BookFinder.providers.filter((p) => p !== 'audiobookcovers')
- const providers = [...bookProviders.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customProviders)]
+ // Build minimized payload with custom providers merged in
+ const providers = {
+ books: [...bookProviders.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customBookProviders)],
+ booksCovers: [SearchController.formatProvider('best'), ...BookFinder.providers.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customBookProviders), SearchController.formatProvider('all')],
+ podcasts: [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customPodcastProviders)]
+ }
res.json({ providers })
}
-
- /**
- * GET: /api/search/providers/podcasts
- * Get available podcast metadata providers
- *
- * @param {RequestWithUser} req
- * @param {Response} res
- */
- async getPodcastProviders(req, res) {
- const customProviders = await Database.customMetadataProviderModel.findAll({
- where: {
- mediaType: 'podcast'
- }
- })
-
- const providers = [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customProviders)]
-
- res.json({ providers })
- }
-
- /**
- * GET: /api/search/providers/authors
- * Get available author metadata providers
- *
- * @param {RequestWithUser} req
- * @param {Response} res
- */
- async getAuthorProviders(req, res) {
- const providers = [SearchController.formatProvider('audnexus')]
- res.json({ providers })
- }
-
- /**
- * GET: /api/search/providers/chapters
- * Get available chapter metadata providers
- *
- * @param {RequestWithUser} req
- * @param {Response} res
- */
- async getChapterProviders(req, res) {
- const providers = [SearchController.formatProvider('audnexus')]
- res.json({ providers })
- }
}
module.exports = new SearchController()
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index c72aa143f..db04bf5ec 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -283,12 +283,7 @@ class ApiRouter {
this.router.get('/search/podcast', SearchController.findPodcasts.bind(this))
this.router.get('/search/authors', SearchController.findAuthor.bind(this))
this.router.get('/search/chapters', SearchController.findChapters.bind(this))
- this.router.get('/search/providers/books', SearchController.getBookProviders.bind(this))
- this.router.get('/search/providers/books/covers', SearchController.getBookCoverProviders.bind(this))
- this.router.get('/search/providers/podcasts', SearchController.getPodcastProviders.bind(this))
- this.router.get('/search/providers/podcasts/covers', SearchController.getPodcastCoverProviders.bind(this))
- this.router.get('/search/providers/authors', SearchController.getAuthorProviders.bind(this))
- this.router.get('/search/providers/chapters', SearchController.getChapterProviders.bind(this))
+ this.router.get('/search/providers', SearchController.getAllProviders.bind(this))
//
// Cache Routes (Admin and up)
From b01e7570d398b20e300c87a80d61a82aa903041b Mon Sep 17 00:00:00 2001
From: mikiher
Date: Sun, 19 Oct 2025 10:54:26 +0300
Subject: [PATCH 039/111] Remove custom providers from library filterdata
request
---
server/controllers/LibraryController.js | 2 --
1 file changed, 2 deletions(-)
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index e63441f0b..55ef45690 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -221,13 +221,11 @@ class LibraryController {
const includeArray = (req.query.include || '').split(',')
if (includeArray.includes('filterdata')) {
const filterdata = await libraryFilters.getFilterData(req.library.mediaType, req.library.id)
- const customMetadataProviders = await Database.customMetadataProviderModel.getForClientByMediaType(req.library.mediaType)
return res.json({
filterdata,
issues: filterdata.numIssues,
numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
- customMetadataProviders,
library: req.library.toOldJSON()
})
}
From 141211590f15c2ac6485e6ed09e8fe0f57a048f8 Mon Sep 17 00:00:00 2001
From: mikiher
Date: Sun, 19 Oct 2025 11:39:10 +0300
Subject: [PATCH 040/111] Merge provider actions and mutations, add loaded
state
---
client/store/scanners.js | 67 ++++++++++++++++------------------------
1 file changed, 27 insertions(+), 40 deletions(-)
diff --git a/client/store/scanners.js b/client/store/scanners.js
index 40a8d864b..ccf7d9249 100644
--- a/client/store/scanners.js
+++ b/client/store/scanners.js
@@ -2,7 +2,8 @@ export const state = () => ({
bookProviders: [],
podcastProviders: [],
bookCoverProviders: [],
- podcastCoverProviders: []
+ podcastCoverProviders: [],
+ providersLoaded: false
})
export const getters = {
@@ -11,63 +12,49 @@ export const getters = {
},
checkPodcastProviderExists: (state) => (providerValue) => {
return state.podcastProviders.some((p) => p.value === providerValue)
- }
+ },
+ areProvidersLoaded: (state) => state.providersLoaded
}
export const actions = {
- async fetchBookProviders({ commit }) {
+ async fetchProviders({ commit, state }) {
+ // Only fetch if not already loaded
+ if (state.providersLoaded) {
+ return
+ }
+
try {
- const response = await this.$axios.$get('/api/search/providers/books')
+ const response = await this.$axios.$get('/api/search/providers')
if (response?.providers) {
- commit('setBookProviders', response.providers)
+ commit('setAllProviders', response.providers)
}
} catch (error) {
- console.error('Failed to fetch book providers', error)
+ console.error('Failed to fetch providers', error)
}
},
- async fetchPodcastProviders({ commit }) {
- try {
- const response = await this.$axios.$get('/api/search/providers/podcasts')
- if (response?.providers) {
- commit('setPodcastProviders', response.providers)
- }
- } catch (error) {
- console.error('Failed to fetch podcast providers', error)
+ async refreshProviders({ commit, state }) {
+ // if providers are not loaded, do nothing - they will be fetched when required (
+ if (!state.providersLoaded) {
+ return
}
- },
- async fetchBookCoverProviders({ commit }) {
+
try {
- const response = await this.$axios.$get('/api/search/providers/books/covers')
+ const response = await this.$axios.$get('/api/search/providers')
if (response?.providers) {
- commit('setBookCoverProviders', response.providers)
+ commit('setAllProviders', response.providers)
}
} catch (error) {
- console.error('Failed to fetch book cover providers', error)
- }
- },
- async fetchPodcastCoverProviders({ commit }) {
- try {
- const response = await this.$axios.$get('/api/search/providers/podcasts/covers')
- if (response?.providers) {
- commit('setPodcastCoverProviders', response.providers)
- }
- } catch (error) {
- console.error('Failed to fetch podcast cover providers', error)
+ console.error('Failed to refresh providers', error)
}
}
}
export const mutations = {
- setBookProviders(state, providers) {
- state.bookProviders = providers
- },
- setPodcastProviders(state, providers) {
- state.podcastProviders = providers
- },
- setBookCoverProviders(state, providers) {
- state.bookCoverProviders = providers
- },
- setPodcastCoverProviders(state, providers) {
- state.podcastCoverProviders = providers
+ setAllProviders(state, providers) {
+ state.bookProviders = providers.books || []
+ state.podcastProviders = providers.podcasts || []
+ state.bookCoverProviders = providers.booksCovers || []
+ state.podcastCoverProviders = providers.podcasts || [] // Use same as bookCovers since podcasts use iTunes only
+ state.providersLoaded = true
}
}
From 816a47a4bac71ed4415e4923e99980a0150ce049 Mon Sep 17 00:00:00 2001
From: mikiher
Date: Sun, 19 Oct 2025 11:40:40 +0300
Subject: [PATCH 041/111] Remove custom providers from library fetch action
---
client/store/libraries.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/client/store/libraries.js b/client/store/libraries.js
index a824b6474..a6cf1dd39 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -117,7 +117,6 @@ export const actions = {
const library = data.library
const filterData = data.filterdata
const issues = data.issues || 0
- const customMetadataProviders = data.customMetadataProviders || []
const numUserPlaylists = data.numUserPlaylists
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
From 166e0442a0e5763f62d5f82d76543fd13ec50510 Mon Sep 17 00:00:00 2001
From: mikiher
Date: Sun, 19 Oct 2025 11:47:17 +0300
Subject: [PATCH 042/111] Remove providers prefetch, refresh on custom provider
add/remove
---
client/layouts/default.vue | 26 ++++----------------------
1 file changed, 4 insertions(+), 22 deletions(-)
diff --git a/client/layouts/default.vue b/client/layouts/default.vue
index 21c256bd3..75753b214 100644
--- a/client/layouts/default.vue
+++ b/client/layouts/default.vue
@@ -371,25 +371,13 @@ export default {
},
customMetadataProviderAdded(provider) {
if (!provider?.id) return
- // Refetch the appropriate provider types
- if (provider.mediaType === 'book') {
- this.$store.dispatch('scanners/fetchBookProviders')
- this.$store.dispatch('scanners/fetchBookCoverProviders')
- } else if (provider.mediaType === 'podcast') {
- this.$store.dispatch('scanners/fetchPodcastProviders')
- this.$store.dispatch('scanners/fetchPodcastCoverProviders')
- }
+ // Refresh providers cache
+ this.$store.dispatch('scanners/refreshProviders')
},
customMetadataProviderRemoved(provider) {
if (!provider?.id) return
- // Refetch the appropriate provider types
- if (provider.mediaType === 'book') {
- this.$store.dispatch('scanners/fetchBookProviders')
- this.$store.dispatch('scanners/fetchBookCoverProviders')
- } else if (provider.mediaType === 'podcast') {
- this.$store.dispatch('scanners/fetchPodcastProviders')
- this.$store.dispatch('scanners/fetchPodcastCoverProviders')
- }
+ // Refresh providers cache
+ this.$store.dispatch('scanners/refreshProviders')
},
initializeSocket() {
if (this.$root.socket) {
@@ -626,12 +614,6 @@ export default {
this.$store.dispatch('libraries/load')
- // Fetch metadata providers
- this.$store.dispatch('scanners/fetchBookProviders')
- this.$store.dispatch('scanners/fetchBookCoverProviders')
- this.$store.dispatch('scanners/fetchPodcastProviders')
- this.$store.dispatch('scanners/fetchPodcastCoverProviders')
-
this.initLocalStorage()
this.checkVersionUpdate()
From 538a5065a448336bbf29eccf65101728c1f9c53a Mon Sep 17 00:00:00 2001
From: mikiher
Date: Sun, 19 Oct 2025 18:57:27 +0300
Subject: [PATCH 043/111] Update providers users to fetch providers on demand
---
.../modals/BatchQuickMatchModel.vue | 6 ++-
client/components/modals/item/tabs/Cover.vue | 2 +
client/components/modals/item/tabs/Match.vue | 53 ++++++++++++++-----
.../modals/libraries/EditLibrary.vue | 2 +
.../modals/libraries/LibrarySettings.vue | 5 --
client/pages/config/index.vue | 2 +
client/pages/upload/index.vue | 2 +
7 files changed, 52 insertions(+), 20 deletions(-)
diff --git a/client/components/modals/BatchQuickMatchModel.vue b/client/components/modals/BatchQuickMatchModel.vue
index 138684b20..f6bcd9728 100644
--- a/client/components/modals/BatchQuickMatchModel.vue
+++ b/client/components/modals/BatchQuickMatchModel.vue
@@ -96,6 +96,9 @@ export default {
},
methods: {
init() {
+ // Fetch providers when modal is shown
+ this.$store.dispatch('scanners/fetchProviders')
+
// If we don't have a set provider (first open of dialog) or we've switched library, set
// the selected provider to the current library default provider
if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) {
@@ -127,8 +130,7 @@ export default {
this.show = false
})
}
- },
- mounted() {}
+ }
}
diff --git a/client/components/modals/item/tabs/Cover.vue b/client/components/modals/item/tabs/Cover.vue
index 4ed734b1b..be17f9636 100644
--- a/client/components/modals/item/tabs/Cover.vue
+++ b/client/components/modals/item/tabs/Cover.vue
@@ -438,6 +438,8 @@ export default {
mounted() {
// Setup socket listeners when component is mounted
this.addSocketListeners()
+ // Fetch providers if not already loaded
+ this.$store.dispatch('scanners/fetchProviders')
},
beforeDestroy() {
// Cancel any ongoing search when component is destroyed
diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue
index 36cdd7a8e..4b92f6cd8 100644
--- a/client/components/modals/item/tabs/Match.vue
+++ b/client/components/modals/item/tabs/Match.vue
@@ -2,7 +2,7 @@